reproducing cve-2013-2028

Featured image

嗯,最近打 d^3ctf 的时候,就遇到了一个 cgi 的题是复现一个 1-day fast cgi 漏洞,虽然这是我 CTF 生涯中很少见的几次用 1-day 出题,但是感觉复现漏洞能力还是非常关键,而且涉及到供应链安全
然后做网安导作业的时候,遇到了这个复现 cve-2013-2028 的 challenge,嗯,其实去年的时候就遇到了,那个时候刚打完 XCTF FINAL,是在和造编译器的伙伴们 hangout 的时候尝试的,当时并没有跑起来现成的 exp,今年在杰哥的帮助下成功复现,还是学到了很多,在这里记一下

主要源码是参考的 这个仓库 其实个人遇到的主要是两个问题,第一个是如何以正确的 local address, remote address 跑起来该 exp,第二个是如何调试

确定 remote host ip address, listener ip address

通过 docker ps 确定跑起来的 container,然后 docker inspect <container_id> 查看网络配置,找到 IpAddress GateWay 字段,分别表示 remote host ip address 和 listener ip address
alt text

debug

我们发现脚本跑起来以后还是不能反弹 shell,canary 可以爆破出来,猜测是 rop 中有些地址和之前的 release 版本地址不一致,从而产生了 crash
我们思路是在 Docker 里面 gdb -p <pid> attach 上 nginx 的进程上,然后 r 运行到 crash 的位置,看栈上的 ROP chain 是否正确
然后需要分两次爆破 canary 和打 ROP,不然会难以把控 gdb attach 的时机

如图为调试输出,可以看到寄存器和 stack 的情况
alt text

alt text

我们发现是 mprotect 函数的地址不对,它是通过 libc_relative_addr + offset 来计算的,我们调整一下 offset 就行了
具体的指令是

docker ps
docker exec --privileged -it <container_id> /bin/bash
ps aux|grep nginx
docker -p 1
set follow-fork-mode child # 跟踪子进程
b mprotect # 通过输出查看 mprotect 的位置,像是 `Breakpoint 1 at 0x7528de82e870: file ../sysdeps/unix/syscall-template.S, line 84`

analysis

基本分析见这个链接
调用链大概如下

ngx_http_read_discarded_request_body
  ngx_http_discard_request_body_filter
    ngx_http_parse_chunked (vulnerable function)

然后基本的 dataflow 如下

ngx_http_read_discarded_request_body(r):
  loop:
    size = ngx_min(r->headers_in.content_length_n,
                                NGX_HTTP_DISCARD_BUFFER_SIZE) // since this is signed comparison, if content_length_n is bigger than INT64_MAX, size will be negative, so the size will be a negative value

    n = r->connection->recv(r->connection, buffer, size) // size is negative, and will be cast to unsigned type, so it will receive a very large amount of data and cause a stack overflow
    // ......
    rc = ngx_http_discard_request_body_filter(r, &b)

ngx_http_discard_request_body_filter(r,b_ptr):
  if r->headers_in.chunked:
    loop:
      rc = ngx_http_parse_chunked(r, b, rb->chunked)
      if rc == NGX_AGAIN:
        r->headers_in.content_length_n = rb->chunked->length // this can be bigger than INT64_MAX too

ngx_http_parse_chunked(r, b, ctx):
  ctx->size = A_VALUE_BIGGER_THAN_INT64_MAX // vulnerability
  ctx->length = ctx->size + const

reproduce

此外,就是看我们怎么满足到达这些代码所需要的分支条件了(唉 回想起来复现 cve-2023-21768 的时候 也是差不多的操作 笑死)
首先在 ngx_http_discard_request_body_filter 函数里面,需要 ngx_http_parse_chunked 的返回值为 NGX_AGAIN 这就需要我们在 overflow 之前输入的那个 chunk (4096 bytes 的连续字节) 不存在 LF 字符,且 size 字段可以被解析为负数,这个是在脑子里符号执行分析出的 xs

thinking like a pwner

possibility of automated discovery

这个漏洞的类型属于整数溢出,但是可以看到从漏洞点到利用点还是有一些距离,理论上可以通过 taint analysis 来解出,而 CodeQL 可以用来实现有源码的 taint analysis 检测,具体可以类比看我们能否控制 memcpy, memmove 这种函数的 size 的情况,我们去判断 source 和 sink 点,source 为我们解析出 ctx->size 的位置,sink 点为 recv 的地方,用 taint 的方法看 source 和 sink 点之间是否有 dataflow
或者可以直接上 fuzz 来找洞,也是比较通用的方法 (TODO 可以试一下),他有一个经典 http 状态机,可能直接用 afl/REDQUEEN 这种简单的编译策略可能不太能 fuzz 出来感觉,倒是可以上一些对 state 更敏感的 fuzzer 来玩,就像我们组博栋师兄的 statefuzz

integer overflow

就像 ctf 里面一样,总得检查一遍比较是 signed 还是 unsigned 类型,只能说 signed 类型的比较更 conducive to bugs 吧 笑死,这种小地方确实不容易注意到orz
对于这个漏洞,他之所以能让 size 是负数主要是因为解析时是这样写的

ctx->size = ctx->size * 16 + (ch - '0');

该例子可以用 rust 的 Wrapping 方法来检测溢出到负数,像是 wrapping_mul 之类的,见 this link