熬夜一天+通宵一天,终于解出来了,激动的心,颤抖的手
拿到附件,常规操作 file 一下,发现居然是一个带符号、带调试信息的 ELF
1 |
|
在 22.04 中,直接执行会报错:
1 |
|
按照作者的文档,apt 安装好依赖,再执行依然报错。谷歌一下这个报错,再结合 IDA 看到的 print_version
里 g_writeln("xrdp-sesman %s", "0.9.18");
,不难发现附件是 xrdp 中的 xrdp-sesman。
为了尽可能接近远程环境,我选择 clone xrdp 仓库,本地编译并 make install
,然后用附件替换掉 /usr/local/sbin/xrdp-sesman
。编译时确实遇到了 openssl 相关的错误,在网上找到的解决方案:
1 2 3 4 5 6 7 8 9 |
|
0.9.18 的 xrdp-sesman 有 CVE-2022-23613,实际上一搜跳出来的就是出题人曾经的分析文章,通读下来可以知道,利用方式主要是堆溢出覆盖函数指针。文章使用本地提权的方式,先在本地写好要执行的文件,使用堆喷布置参数,利用 plt 中的 g_execvp
、g_execlp3
执行命令。
由于出题人已经介绍过 xrdp 源码、重要结构体,下文不再提及这些内容
简单尝试一下会发现,服务非常容易打挂,并且平台容器没有重启机制,甚至还有频率限制——于是会出现:开容器,十秒打挂,等一分钟后销毁容器,等一分钟后再开新容器。基于这些考虑,不得不放弃堆喷,转而寻找更稳定高效的利用。
首先肯定是需要一个能覆盖函数指针的方法。经过简单地风水(我是新建连接 0、1,关闭连接 0、1,新建连接 2 作为后续使用),可以找到一个 trans
结构体,其 self->in_s->end
地址低于结构体本身,也就是产生的溢出可以覆盖结构体本身。(我认为覆盖自身的情况只需要一个连接,会更稳定)
接下来开始物色覆盖指针的目标。在翻阅 plt 的时候,发现除了出题人介绍过的两个 exec 家族的封装,还有一个 popen
是高价值目标。它需要给定两个字符串参数(popen("cmd", "r")
),于是开始查看每个函数指针的使用情况,主要是以下三种:
1 2 3 |
|
self->sck
是结构体的第一个参数。也就是说,对于 trans_can_recv
,有 *self == self->sck
的情况,显然不能满足传递两个字符串。
对于 trans_recv
,可以放心地布置第一个参数为字符串。但 self->in_s->end
正常是指向要写入的位置,内容不太可控。在 trans_data_in
下面有一个 init_stream
可以把指针重置到开头,但重置之后需要等到下次轮询,才能回到使用其他函数指针的位置,每次轮询开始之前,会对 sck
进行 select
无法顺利通过。也就是说,如果想控制 trans_recv
的第二个参数,就无法控制第一个。
最后的希望是 trans_data_in
。虽然它只有一个参数(而且是可控的),但是寄存器里有其他的值。调试一下会发现,此时第二个参数 RSI 指向的是 self->in_s->data
的位置(或者它的附近),也是可控的!
如果想要调用到 trans_data_in
,还有一个前置条件,read_so_far == self->header_size
,这个计算一下数据长度,不难实现。
于是我们可以开始布置溢出参数,这个时候会发现,trans_data_in
指针十分靠前,在结构体的偏移是 0x18 —— 这意味着第一个参数的字符串最多只能长 24。作为对比,第二个参数只需要简简单单一个 "r"
,却有上千字节的空间。
我一度以为在不出网环境下,24 字节无法完成利用(先 kill server,然后用 Python 监听 3350 端口,做一个 Bind Shell)。此时尚是第二天夜晚,题目还是零解的情况,错过这个机会有点可惜。
后来我又去寻觅了一个任意长度的命令执行,但是只有在 gdb 下断点的时候才能实现。总之,在调试长命令的时候,我才注意到,子进程继承了 socket 连接,可以直接向 7 号 fd 输出,回头看 popen
才发现也可以,于是最后 ls -al >&7
、cat flag >&7
两条命令解决(当然还只是本地)。贴 exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
其中 in_s
的覆盖不是必要的,去掉的话 header size 要做相应调整。这里覆盖的原因是,这个 exp 是从下面没走通的路改出来的(
出题人在群里提到远程奇奇怪怪的问题,其实我找到的任意长度的执行也是因为这个无法实现。因为挺有意思(实际上也复杂得多),我也介绍一下这个利用。
我准备把指针换成 session_start_fork
里的 gadget 片段:
1 2 3 4 5 6 7 8 9 10 |
|
如果用 trans
的任意函数指针跳到这里,只有 RBX 会被使用,RBX 即 trans
结构体的地址。这段 gadget 从 trans->addr
取了一个指针,作为 sh -c
的命令执行。也就是说,如果我们在内存中写入一条命令,并且知道命令的地址,就可以实现任意长度的代码执行了。
那么往哪里写呢?基本上写内存就 trans_recv
一条路。如果我们提前覆盖 self->in_s
,让其 end 本身在 rw 段,其指向也在 rw 段(因为写完内存会有 self->in_s->end += read_bytes;
,修改指针)。
唯一已知的 rw 段是:0x410000 - 0x411000
,这个段里主要是 GOT 表和全局变量,并没有指向段内的指针。
如果是堆,需要泄露地址。修改 trans->wait_s
为几个全局变量(存的是堆指针),可以在 trans_send_waiting
中向 socket 泄漏很多内存,但是泄露之后有一个 free
的检查无法通过。
于是我还是回到了 0x410000
的考虑,这里有的指针还是比较接近段内的。比如 0x410000
是 0x40fde0
,一大堆没有解析的 GOT 表基本指向 0x403...
。
我希望使用 self->in_s->end += read_bytes;
来调整指针,举例来说,如果把 0x410000
的值增大一点点,它就落在了 rw 段内。我需要前面那个能覆盖自身的 trans
,覆盖 self->in_s
到 0x410000-8
,这样 self->in_s->end
就对上了 0x410000
。如果我写入的长度 read_bytes
落在 0x220 ~ 0x1220 之间,那么我就有了一个确定地址的、可写的指针。当然,这个 trans
还需要能覆盖之后一个 trans
的 in_s
,使其数据写在这个确定地址上。
坏消息是,in_s->data
本身有 0x2000 的空间,如果要产生溢出,数据的长度是远远大于 0x1220 的。因此目光就转向了改 GOT 上。把 0x403000
改到 0x410000
,需要 0xd000 ~ 0xe000 的长度。并且我们还得控制:对于被覆盖的第二个 trans
,只能覆盖到它的 in_s
,不能破坏 trans_recv
指针——不然没得读了(trans_recv
是 libcommon 里的函数。程序本身的 plt 里面似乎没有可以替代它的)。于是一通风水,最后终于实现了在 gdb 打断点情况下的任意长度执行。
那为什么不打断点就不行呢?我切换了打断点的思路,把条件断点打在 trans_recv
之后,条件是 read_bytes > 8
,8 是 header 的长度。这时,我发现 trans_recv
根本没有收齐我的 0xd000。那么原因也呼之欲出了,打了断点时,大量的数据有时间慢慢进入缓冲区,然后一次读出;不打断点,就会变成读多少是多少。此外,一旦溢出开始覆盖结构体自身,in_s
、trans_recv
等都会变化,没有第二次续传的机会。我本来还想试试用 g_sleep
,后来也作罢了。
如果抓包会发现,虽然理论上 TCP 支持将近 0x10000 的长度,这些数据还是会被分到多个 TCP 报文中。也许使用 raw socket 可以解决这个问题?其实我曾经在 IOT pwn 上遇到类似的情况,当时捣鼓过一番,最后的选择是——重新堆风水,让溢出的数据包别那么大。现在这个数据的长度是定死的,就不是风水的问题了。
出题人提到的问题“本机docker打通后,docker导出镜像,放远程直接导入镜像,打不通”,多半也是因为 TCP 分片的原因。
前面提到,我解决了 popen
的本地利用问题(gdb 不用下断点也可以!),远程还没通。这个数据包的长度大约 0x2500,对于 TCP 来说还是比较大的。鉴于前面的绝大多数字节都是在为溢出做准备,他们实际上可以分开来发送,只需要最后几百字节是一次性溢出的即可。
把之前 exp 发送 payload 的几行稍微改一下,拆成两段发送,最后就成了
1 2 3 4 |
|
FLAG:KCTF{ee43d769-ac1d-4f2e-82b3-9167ff484c8e}
[2022冬季班]《安卓高级研修班(网课)》月薪两万班招生中~
最后于 21小时前 被tkmk编辑 ,原因: