CVE-2018-1160 netatalk越界漏洞复现及分析
前言
去年pwnable.tw新上的题目,当时没做出来,今年来解决一下历史遗留问题。
环境搭建
参考Netatalk CVE-2018–1160 越界写漏洞分析这篇文章即可,注意要多安装两个库sudo apt install -y libdb++-dev libdb-dev
。安装启动即可,不过鉴于我是做pwnable.tw的题,把libatalk.so.18
替换一下,使用./afpd -d -F ./afp.conf
启动本地的afpd,看下此时的配置文件,端口为5566,因此connect过去即可和其进行交互。
1 | [Global] |
漏洞分析
Netatalk
是AppleTalk的开源实现方案,可以用作文件服务器来实现文件共享,afpd是其中的一个组件,看名字就知道它是一个类似于httpd的拿来做通信的部分,源码目录的./etc/afpd
子目录为相关源码树。看下其中的main函数,前面都是初始化的部分,当请求的类型为LISTEN_FD
时会调用dsi_start
函数.
1 |
|
跟进去看下dsi_getsession
,关键的调用如下,函数调用dsi->proto_open
这个函数指针创建了一个新的进程,根据下面的pid号也可以大概看出来是fork出来的,父进程里负责创建新的server监听,创建成功后会使用*childp = child
将传入的childp
指针赋值并返回0,从而不会进入调用函数的child == NULL
处理逻辑。
1 | switch (pid = dsi->proto_open(dsi)) { /* in libatalk/dsi/dsi_tcp.c */ |
反之,子进程使用原先的端口进行交互,根据读取到的dsi->header.dsi_command
确定不同的分支跳转,赋值部分在proto_open
函数指针调用处,该指针指向dsi_tcp_open
函数,首先通过dsi_stream_read
函数读取用户的socket输入到block,再通过memcpy对header赋值。在dsi_getsession
初始化返回后,对于子进程来说,当我们赋值的dsi->header.dsi_command=0x04(DSIFUNC_OPEN)
时,会调用dsi_opensession()
打开这个session,且*childp
被置为空。
1 | switch (dsi->header.dsi_command) { |
1 | //这里补充一下关键的数据结构 |
继续看dsi_opensession
函数,对dsi->commands[i++]
进行判断,当dsi->commands[0]=1(DSIOPT_ATTNQUANT)
时,进入的分支会对dsi->attn_quantum
这个4字节的整数进行赋值,memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
然而赋值的长度为用户可控值dsi->commands[i]
,dis->commands是一个char*类型的变量,因此至多拷贝0xff
字节数据到该变量中,造成越界写,观察一下dsi的数据结构,我们可以覆写datasize/server_quantum/serverID/clientID/commands/data[DSI_DATASIZ]
这些成员,其中server_quantum
会回传回来。
1 | /* OpenSession. set up the connection */ |
尝试拿poc打一下产生了崩溃,这是因为我们覆写了commands指针,后续memcpy的时候产生非法指针访问错误。
1 | # -*- coding: UTF-8 -*- |
1 | RDX: 0x0 |
漏洞利用
信息泄露
这个CVE的发现者在NAS上写了个exp,对应的binary编译的时候没有开PIE,因此通过一个利用链可以直接rce,不过我们手头这个是在ubuntu18.04开了PIE编译的,地址泄露是所有利用的开始,这一点也是去年卡住我的原因,今年再回来看这道题的时候搜了下资料,找到了ruan
师傅和hitcon2019 qual
的exp,终于弄明白了破局之道。答案就是侧信道,这一点我也在TSCTF2020出了道题考别人,结果到自己就想不起来了2333。afpd和apache很相似,都是每次接收到一个请求就会fork出一个新的进程来处理,因此进程的地址空间是不变的,刚才我们提到了发送正常包的时候返回数据里包含有server_quantum
,因此我们可以使用部分写的方式,单字节修改dsi->commands
,每次修改之后查看返回包,当发生crash时不会有数据包,反之数据包中包含有我们提前设置的特殊值,据此我们可以得到一个合法的地址值。再根据这个地址同libc基址的偏移关系(偏移固定)计算得到libc_base,爆破的代码如下:
1 | from pwn import * |
从任意地址写到RCE
源码中搜一下对于dsi->commands
的引用,可以找到一处dsi_stream_receive
函数的赋值dsi_stream_read(dsi, dsi->commands, dsi->cmdlen)
,函数的注释表明这是一个对于dsi->commands赋值的函数,查看一下对于这个函数的引用,在afp_over_dsi
函数中有个循环会调用此函数,读取完毕后若cmd=2(DSIFUNC_CMD)
,则以dsi->commands[0]
为索引,err = (*afp_switch[function])(obj,(char *)dsi->commands, dsi->cmdlen,(char *)&dsi->data, &dsi->datalen);
调用afp_switch函数表的指针。
1 | /*! |
这个函数指针数组成员如下,默认为未登录状态,因此只能使用下面的函数指针,当用户登陆之后该表会被重新赋值为另一个数组。当idx=0时函数指针为NULL,在函数中不会进行调用而直接break掉,再找下这个函数的调用,好巧不巧这就是dsi_start
中对于子进程处理的函数,因此必然会调用到这个函数。
那么到现在我们可以先通过memcpy覆写dsi->commands指针,再通过dsi_stream_read
实现任意地址写。
1 | AFPCmd *afp_switch = preauth_switch; |
有了任意地址写之后我们要如何控制执行流呢,按照CTF的思路看如果可以控制__free_hook
以及free的对象就可以任意命令执行,但是在真实的漏洞环境里free的对象往往是不可控的,这里介绍一下hitcon的解法,实际上我们之前也用过类似的操作。
- 覆写
__free_hook
为__libc_dlopen_mode+56
- 覆写
_dl_open_hook
为_dl_open_hook+8
,_dl_open_hook+8
为fgetpos64+207
的这个magic_gadget,mov rdi,rax ; call QWORD PTR [rax+0x20]
,此时因为rdi指向dl_open_hook。我们可以将dl_open_hook+0x20处修改为setcontext+53,从而实现任意函数执行。 - 在dl_open_hook后面布置sigFrame,最终触发err时的free,调用system(cmd)执行反弹shell
exp.py
服务器offset和环境相关,可以再写个循环爆破偏移,我懒得调偏移了,所以只给个本地的exp
1 | from pwn import * |