CVE-2021-42342 Goahead 环境变量注入漏洞分析
2022-1-10 00:0:0 Author: bestwing.me(查看原文) 阅读量:56 收藏

漏洞背景

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

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

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

漏洞分析

环境搭建

参考 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 [email protected] -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/[email protected] (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/[email protected] (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的处理

参考

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

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


文章来源: https://bestwing.me/CVE-2021-42342-Goahead.html
如有侵权请联系:admin#unsafe.sh