漏洞介绍
GoAhead是一个开源(商业许可)、简单、轻巧、功能强大、可以在多个平台运行的Web Server,多用于嵌入式系统、智能设备。其支持运行ASP、Javascript和标准的CGI程序。
这个漏洞是CVE-2017-17562漏洞补丁的绕过,攻击者可以利用该补丁没有考虑到的multipart表单控制目标服务器的环境变量,进而劫持LD_PRELOAD来执行任意代码。
漏洞范围
GoAhead =4.x
5.x<=GoAhead<5.1.5
漏洞靶场
使用vulhub的靶场:
使用环境为GoAhead 5.1.4
vulhub-master/goahead/CVE-2021-42342/
启动靶场环境:
1 | docker-compose up -d |
启动完成后,访问http://your-ip:8080/即可看到欢迎页面。访问http://your-ip:8080/cgi-bin/index即可查看到Hello页面,即为CGI执行的结果。
漏洞原理
首先对CVE-2017-17562漏洞补丁进行简要分析。修复主要有2个地方,第1处修改位于cgi.c#cgiHandler函数中:

src/cgi.c#L182-L187
乍一看当参数以LD_开头将会被忽略,所以LD_PRELOAD无法使用。但是深入分析我们发现这里犯了一个非常低级错误。vp来源于s->name.value.string:
1 | vp = strim(s->name.value.string, 0, WEBS_TRIM_START); |
src/cgi.c#L176
看下strim函数定义:

src/runtime.c#L2734-L2756
vp将永远为0,所以上面通过if过滤LD_PRELOAD参数的过程是没有任何意义的。接着往下走,GoAhead会将POST请求的表单变量使用ME_GOAHEAD_CGI_VAR_PREFIX作为前缀,通过函数sfmt进行字符串格式化处理,以保证LD_PRELOAD不会被劫持。这里看起来修复方式非常完善,但是我们深入分析发现,进入第183行if语句处理的前提条件是s->arg的值不为0(初始化状态为0)。
第2处修改位于http.c#addFormVars函数:

src/http.c#L1389-L1421
将arg赋值为1,正好可以满足上面if语句的判断条件。
HTTP处理机制分析
在补丁分析的基础上,为了更好理解漏洞产生的原理,下面我们继续对GoAhead处理HTTP的机制进行深入分析。
0x01 进程初始化
GoAhead进程启动后,会调用http.c#websServer函数完成配置初始化处理:

src/http.c#L3356-L3374
函数websOpen尝试解析route.txt,一个典型的route.txt配置如下图所示:

src/route.txt
websOpen函数根据配置选择启动的模块:

src/http.c#L229-L301

src/cgi.c#L251-L255
websDefineHandler函数用来定义处理不同请求类型的回调函数,比如上面定义CGI的回调处理函数为cgiHandler,前面补丁对比分析时提到过这个函数,它会对参数进行加前缀处理。
回到http.c#websServer,第3426行将调用websListen函数启动HTTP服务,进入该函数:

src/http.c#L624-L674
第644行定义了当HTTP请求来到时,将回调websAccept函数进行处理:

src/http.c#L680-L746
第702行将调用websAlloc为每一个请求分配单独的Webs结构体空间:

src/http.c#L524-L539
initWebs函数完成对Webs结构体初始化的工作:

src/http.c#L360-L431
0x02 HTTP请求状态处理机制
下面我们简要分析一下GoAhead对单次HTTP请求进行处理的流程。GoAhead将HTTP请求分为5个状态:

src/goahead.h#L1856-L1863
对于每一个HTTP请求,GoAhead以事件形式通过readEvent函数进行处理:

src/http.c#L799-L847
进入websPump函数:

src/http.c#L850-L880
- WEBS_BEGIN
在Accept阶段(即WEBS_BEGIN),调用函数parseIncoming:

src/http.c#L883-L951
进入parseHeaders对HTTP头进行检查与解析:

src/http.c#L1132-L1139
根据content-type的不同类型,完成对wp->flags的赋值。
- WEBS_CONTENT
当进入参数处理状态时(即WEBS_CONTENT),websPump将调用processContent进行处理:

src/http.c#L1196-L1237
这里重点讲下文件上传状态WEBS_UPLOAD的处理过程。在upload.c中,默认定义上传文件保存目录为tmp:

src/upload.c#L453-L465
在processUploadHeader中构造此次HTTP请求结构体的uploadTmp值:

src/upload.c#L241, L248

src/osdep.c#L44-L67
因此,websTempFile默认将生成一个/tmp/tmp-0.tmp的临时文件名称,数字是websTempFile中根据count++自增生成的。然后打开临时文件,并将文件句柄赋值给wp->upfd。在processContentData处理上传文件时,将调用writeToFile写入文件:

src/upload.c#L319-L399

src/upload.c#L293-L316
最终将文件内容写入了wp->upfd,也就是保存到了创建的临时文件中。
- WEBS_READY
websPump将调用websRunRequest进行处理:

src/route.c#L139-L183
在这里有两个函数websSetQueryVars和websSetFormVars需要注意:

src/http.c#L1473-L1484

src/http.c#L1459-L1470
这2个函数都调用了addFormVars,在前面对CVE-2017-17562漏洞补丁进行分析的过程中提到过这个函数,addFormVars处理的最后将sp->arg赋值为1,使得cgi.c#cgiHandler会对请求参数进行重命名,从而修复了CVE-2017-17562漏洞。
漏洞原理分析
有了前面对CVE-2017-17562漏洞修复的补丁对比和HTTP请求原理的分析基础,下面的漏洞分析过程就显得非常简单了。
在WEBS_READY提到,GoAhead对POST请求和GET请求提交的参数都会调用addFormVars函数进行处理,将sp->arg赋值为1,从而使得cgi.c#cgiHandler重命名环境变量,但是我们可以看到POST请求调用addFormVars的前提是wp-flags取值为WEB_FORM,回顾WEBS_BEGIN处理过程,当content-type为multipart/form-data时,wp-flags将赋值为WEBS_UPLOAD,也就是说,如果HTTP请求为文件上传类型,参数将不会通过addFormVars处理,此时s->arg取值仍然为0,从而在cgi.c#cgiHandler中将不会使用ME_GOAHEAD_CGI_VAR_PREFIX作为前缀进行修改,而是直接进入了else分支:

src/cgi.c#L173-L195
后面LD_PRELOAD劫持的漏洞触发原理与CVE-2017-17562类似,这里就不重复分析了。
漏洞复现
我们首先需要编译一个动态链接库,而且需要和目标架构相同。所以在实战中,如果对方是一个智能设备,你可能需要交叉编译。因为Vulhub运行在Linux x86_64的机器中,所以我们直接用Linux PC编译即可。动态链接库源码:
1 | #include <unistd.h> |
这样,before_main函数将在程序执行前被调用。编译以上代码:
1 | gcc -s -shared -fPIC ./payload.c -o payload.so |
然后,我们使用这个脚本来发送恶意数据包,复现漏洞:
1 | python poc.py http://target-ip:8080/cgi-bin/index /path/to/payload.so |
可见,我们在动态链接库中编写的劫持代码已经被成功执行:

参考
https://github.com/vulhub/vulhub/blob/master/goahead/CVE-2021-42342/README.zh-cn.md
https://ahmed-belkahla.me/post/2-methods-rce-0-day-in-goahead-webserver-pbctf-2021/