CVE-2018-1160
pwnable.tw最近新上的一题,想尝试下 :p,不过卡住了,后来解决了,记录下
搭建环境
源码下载
1
https://sourceforge.net/projects/netatalk/files/netatalk/3.1.11/
依赖库(可能还要多安装一些,我libdb5.3搞了半天)
1
2
3
4
5
6
7sudo apt install libcrack2-dev
sudo apt install libgssapi-krb5-2
sudo apt install libgssapi3-heimdal
sudo apt install libgssapi-perl
sudo apt-get install libkrb5-dev
sudo apt-get install libtdb5.3-dev
sudo apt-get install libevent-dev编译安装(按着先知的文章来的)
1 | ./configure --with-init-style=debian-systemd --without-libevent --without-tdb --with-cracklib --enable-krbV-uam --with-pam-confdir=/etc/pam.d --with-dbus-daemon=/usr/bin/dbus-daemon --with-dbus-sysconf-dir=/etc/dbus-1/system.d --with-tracker-pkgconfig-version=1.0 |
- 启动服务
1 | sudo systemctl enable avahi-daemon |
启动后会监听548端口
1 | sudo netstat -nap | grep 548 |
编译安装完成后的程序会放在/usr/local/sbin
里
1 | ruan@ubuntu /usr/local/sbin $ ls |
代码分析
漏洞代码
源码目录dsi/Dsi_opensess.c
1 | /* OpenSession. set up the connection */ |
其中的dsi->commands
是字符数组,是用户可控的,故memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
这里存在溢出,最多溢出255个字节
两个关键结构
DSI
1 | typedef struct DSI { |
从DSI
这个结构体可以看出,我们能溢出覆盖的变量有datasize,server_quantum,serverID,clientID,commands
,后面的data字符数组太大了,就覆盖不到后面的变量了,其中的commands
变量比较重要,后面会分析
dsi->header
1 |
|
main.c
首先从main.c(afpd/main.c)开始
这里是处理请求的地方,而这个漏洞进的是LISTEN_FD
分支
1 | for (int i = 0; i < asev->used; i++) { |
dsi_start
于是程序会调用dsi_start
(afpd/main.c):
1 | static afp_child_t *dsi_start(AFPObj *obj, DSI *dsi, server_child_t *server_children) |
dsi_start
又会调用dsi_getsession
:
dsi_getsession
源码目录dsi/Dsi_getsess.c
1 | /*! |
从pid = dsi->proto_open(dsi)
这里继续跟进:
dsi_tcp_open
源码目录dsi/Dsi_tcp.c
1 | /* accept the socket and do a little sanity checking */ |
dsi_tcp_open
声明了变量block
来接收我们发过去的数据
1 | if (0 == (pid = fork()) ) { /* child */ |
然后根据接收的用户数据填充dsi->header
1 | dsi->header.dsi_flags = block[0]; |
填充后在读取dsi->header.dsi_len
长度的command
字符,然后返回到dsi_getsession
函数的后半部分:
1 | switch (dsi->header.dsi_command) { |
根据dsi->header.dsi_command
进行不同的操作,而其中的DSIFUNC_OPEN
分支里的dsi_opensession
函数里存在栈溢出,即开头的那一段代码:
1 | /* parse options */ |
然后一路返回到dsi_start
函数(标箭头处):
1 | static afp_child_t *dsi_start(AFPObj *obj, DSI *dsi, server_child_t *server_children) |
进入afp_over_dsi
函数 ,而afp_over_dsi
函数又调用了dsi_stream_receive
函数
1 | /* set TCP_NODELAY */ |
dsi_stream_receive
1 | /* Receiving DSIWrite data is done in AFP function, not here */ |
此处的dsi_stream_read(dsi, dsi->commands, dsi->cmdlen)
配合前面的溢出,就能造成任意地址读写
poc
1 | from pwn import * |
效果
1 | ► 0x7f7d5adbb82b <dsi_opensession+139> movzx eax, byte ptr [rcx + r9] |
执行完memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
后可以看到
1 | tickle = 0x0, |
我们成功的把commands
覆盖成为了0xdeadbeefcafebabe
,然后就段错误了,
这里的覆盖配合后续的dsi_stream_receive
函数使得我们有了任意地址写的能力,于是乎问题就变成了写哪里,一个残酷的事实是:
1 | ruan@ubuntu /mnt/hgfs/shared/pwntw/netatalk/debug checksec afpd |
开启了ASLR,没泄露地址的话也不知道该写哪里,在这里卡住了,orz,看下自己后续能不能想办法绕过这个
Brute Force Address
时隔多日,到了2月14,众所周知的情人节,甜甜的恋爱什么时候能轮到我.jpg
后来在看Balsn战队HITCON2019_Quals的wp时,看见了netatalk
这题,看了下,和这题好像是一样的,难怪这题在pwnable.tw上才100分
我们知道程序开了ASLR,且是用子进程来处理我们的请求的,而子进程的地址空间和父进程是一样的,而且子进程奔溃了不影响父进程,所以我们可以爆破libc的基地址,爆破的方法是一位一位的覆盖DSI->commands
,如果没段错误就说明爆破正确了,那怎么判断是否段错误了呢
从上面的代码分析可以得出,如果没出现段错误的话,程序会把被我们覆盖掉的server_quantum
返回给我们,我们可以依靠这个来判断程序是否段错误
爆破代码为:
1 | from pwn import * |
这样爆破出来的地址和libc存在一定的偏差,所以我们后续还要根据这个地址来继续爆破libc基地址
1 | [*] Closed connection to 127.0.0.1 port 548 |
Hijack rip
假设我们爆破出了libc的基地址,我们就可以把DSI->commands
覆盖到__free_hook
上方,这样配合后续的dsi_stream_receive
函数就可以覆写__free_hook
,后续触发free
函数即可,看了Balsn战队的wp,又学到了新方法
在这个漏洞中,我们可以像任意地址写入任意个字节,因为if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
,其中的dsi->commands和dsi->cmdlen
都是可控的
wp中选择了把__free_hook
改写为__libc_dlopen_mode+56
处,这里又会判断_dl_open_hook
是否为空:
1 | ► 0x7f24057ea488 <__libc_dlopen_mode+56> mov rax, qword ptr [rip + 0x28a019] |
不为空的话call dlopen_mode
,dl_open_hook
的结构是这样的:
1 |
|
由于_dl_open_hook
这个结构体是在__free_hook
地址后面,所以我们也可以覆盖掉,覆盖为
1 | ► 0x7f2405702a1f <fgetpos64+207> mov rdi, rax <0x7f2405a744b0> |
这个gadget,因为此时rax指向被我们覆盖的_dl_open_hook
:
1 | ────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────── |
在mov rdi,rax;call qword ptr [rax + 0x20]
这句执行之后,我们就跳到了setcontext+53
这个地方
1 | RAX 0x7f2405a744b0 (__vdso_clock_gettime) —▸ 0x7f2405702a1f (fgetpos64+207) ◂— mov rdi, rax |
这样就实现了控制程序执行流的效果,又学到了
触发free函数
触发free函数其实挺容易的,程序的代码中很多地方都用到了free函数,我们可以看这根据cmd
进行不同操作的代码段:
1 | dsi->flags |= DSI_DATA; |
可以看到有一个DSIFUNC_CLOSE
,有close的话,可能就会存在释放资源的操作,所以可以跟进去看看:
1 | static void afp_dsi_close(AFPObj *obj) |
最后有一个dsi_close(dsi)
:
1 |
|
这里就有一个free
了,所以我们只要把我们后续发过去的dsi->header
的dsi_command
设置为1就好:
1 | /* DSI Commands */ |
实际在 LOG(log_note, logtype_afpd, "AFP statistics: %.2f KB read, %.2f KB written", dsi->read_count/1024.0, dsi->write_count/1024.0);
这一句已经触发了free:
1 | ► 0x7f240571b950 <free> push r15 |
至此就结束了,还是学到了蛮多东西的
参考链接:
https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#netatalk
https://medium.com/tenable-techblog/exploiting-an-18-year-old-bug-b47afe54172