从附件所给的Readme以及IDA分析可以看出所给attachment是个xrdp-sesman
,版本为0.9.18。
直接googl xrdp rce,很容搜到 CVE-2022-23613,并且受影响的版本为0.9.18及以前,找到相关的patch commit:
再结合IDA分析所给attachment中的sesman_data_in
函数,没有size <= 8
的判断,显然是未patch的状态:
可以确定这题考察的就是CVE-2022-23613
的利用了。
从github上直接下0.9.18的源码,然后降级openssl,在本地编译一个xrdp,解决lib的问题,即可正常跑起来。
简单来说,xrdp-sesman
会在本地监听3350端口(/etc/xrdp/sesman.ini):
每次有一个socket连接进来,就会分配一个trans
结构体:
然后进行相应的初始化:
简单来说,就是设置sockfd
,type1
,status
等,并且为in_s
和out_s
分配空间,记录client的ip address和port,后面就进入到server和client的socket通信逻辑中了。
在通信过程中,server端接收的数据包格式为:
因此接收一个完整的数据包过程分为两步,第一步接收8 bytes,然后根据size字段计算出后续payload的长度为(size - 8)
问题在于如果header的字段size < 8
,那么to_read
就会整数溢出。
且由于这里涉及的size都是符号整数,因此如果设置header_size = 0x80000000
,那么to_read = 0x80000000 - 8 = 0x7ffffff8
,从而导致heap-based OOB write。
由于每次连接,server端都会创建一个trans
结构体,因此可以创建多个连接,使得在self->in_s->end
后面喷上一堆trans
的结构体,且该结构体中有很多可以利用的成员,包括函数指针。
第一想法就是是否能够通过劫持trans
结构体完成地址泄露.
通过分析trans_check_wait_objs()
发现,server除了每次读完数据之后,还有一个额外的动作,即如果trans->wait_s
如果不为NULL的话,会把缓冲区中的数据发送给client。
虽然正常情况下,trans->wait_s = NULL
,但是可以通过OOB write对其进行劫持,设置trans->wait_s->p
和trans->wait_s->end
,就能把两者之间的数据发送给client。
但是这里存在一个问题,trans->wait_s
是一个struct stream *
:
需要在某个地址已知的地方布置一个stream
结构体,然后再将trans->wait_s
劫持到这个结构体上才能达到目的。
此时由于binary没有PIE,唯一知道的地址就是程序的地址,因此我只能先构造出一个任意地址写的原语,在bss上伪造一个stream
结构体。
因此把目标选为trans->in_s
,这个stream
结构体存放server接收缓冲区的地址信息,根据self->trans_recv(self, self->in_s->end, to_read);
,self->in_s
是在堆上动态分配出来的,因此有可能可以通过OOB write将其劫持,实现任意地址写。
然而调试过程中发现,in_s
chunk总是落在trans
chunk之后,意味着在写到in_s
结构体之前,必然会先破坏trans
结构体。
因此这里需要进行堆风水,让in_s->end
跳过trans
结构体,落在in_s
上。
很自然的想法就是首先通过OOB write,将in_s->end
挪到trans + 0x20
的位置上,因为前0x20 bytes的内容都是已知的。
然后通过断开连接,将这个trans
给释放掉,这样继续OOB的时候,由于in_s->end
此时落在chunk + 0x20的位置,不会破坏free chunk的metadata,因此可以继续OOB write,让其很安全地跨过trans
结构体,落在in_s
前。
最后再进行连接,将这个trans
重新分配出来,这样再OOB write,就能劫持到in_s
结构体了。
不过需要注意的是,内存的分配都是通过g_malloc
进行的:
虽然源码里都是malloc
,但是实际上g_malloc(xx, 0)
最后都被优化为calloc
,因为calloc
不会从tcache中分配chunk,因此这里还需要通过7次连接加断开将相应的tcache填满,才能使得后来释放的trans
能够重新分配回来。
这个阶段结束,就完成对trans->in_s
的劫持了:
之后通过conns[11]
(对应被劫持的trans
)发送数据给server,就能在0x410c00开始的位置布置数据了,即可以伪造任意的stream
的结构体。
为了完成地址泄露,还是需要回到劫持trans->wait_s
的路上去。
但是此时buffer指针已经落在trans
后面了,如果想要继续OOB write劫持后面的trans
结构体的话,会破坏堆上的free chunk。
因此只能将当前OOB write的trans
释放再重新取回,从而重置in_s->end
指针位置,使得其能够在不破坏chunk结构的情况下,劫持到trans->wait_s
。
目前为止,通过在bss上构造一个wait_s
结构体用来泄露got表,以及一个合法的in_s
保证劫持wait_s
的过程中程序的正常执行,即可完成对got表内容的泄露,从而获取到libc的地址。
最后一步显然就是通过劫持trans
中的函数指针来getshell了。
最直接的目标就是trans->trans_recv
,因为在调用trans->trans_recv
时,第三个参数为to_read
(edi
寄存器),它是由header_size
间接控制的,因此只要结合mov rsp, rdx; ret
的gadget,将栈迁移到bss上进行rop就行了。
至于ropchain,前面已经可以在bss上写数据了,因此布置一条rop链也就轻而易举了。
只要执行`system("/bin/sh 1>&7 0>&7")就可以愉快地getshell了。
整体来说堆布局还是比较稳定的,不过显然是一次性exp,第二次再打堆布局就变了,得重启靶机环境。
__int64 print_version()
{
g_writeln(
"xrdp-sesman %s"
,
"0.9.18"
);
g_writeln(
" The xrdp session manager"
);
g_writeln(
" Copyright (C) 2004-2020 Jay Sorg, Neutrino Labs, and all contributors."
);
g_writeln(
" See https://github.com/neutrinolabs/xrdp for more information."
);
g_writeln(
"%s"
, "");
g_writeln(
" Configure options:"
);
return
g_writeln(
"%s"
,
" \n"
);
}
__int64 print_version()
{
g_writeln(
"xrdp-sesman %s"
,
"0.9.18"
);
g_writeln(
" The xrdp session manager"
);
g_writeln(
" Copyright (C) 2004-2020 Jay Sorg, Neutrino Labs, and all contributors."
);
g_writeln(
" See https://github.com/neutrinolabs/xrdp for more information."
);
g_writeln(
"%s"
, "");
g_writeln(
" Configure options:"
);
return
g_writeln(
"%s"
,
" \n"
);
}
struct trans
{
tbus sck;
/
*
socket handle
*
/
int
mode;
/
*
1
tcp,
2
unix socket,
3
vsock
*
/
int
status;
int
type1;
/
*
1
listener
2
server
3
client
*
/
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;
/
*
used to test
for
exit
*
/
struct stream
*
wait_s;
char addr[
256
];
char port[
256
];
int
no_stream_init_on_data_in;
int
extra_flags;
/
*
user defined
*
/
struct ssl_tls
*
tls;
const char
*
ssl_protocol;
/
*
e.g. TLSv1, TLSv1.
1
, TLSv1.
2
, unknown
*
/
const char
*
cipher_name;
/
*
e.g. AES256
-
GCM
-
SHA384
*
/
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;
};
struct trans
{
tbus sck;
/
*
socket handle
*
/
int
mode;
/
*
1
tcp,
2
unix socket,
3
vsock
*
/
int
status;
int
type1;
/
*
1
listener
2
server
3
client
*
/
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;
/
*
used to test
for
exit
*
/
struct stream
*
wait_s;
char addr[
256
];
char port[
256
];
int
no_stream_init_on_data_in;
int
extra_flags;
/
*
user defined
*
/
struct ssl_tls
*
tls;
const char
*
ssl_protocol;
/
*
e.g. TLSv1, TLSv1.
1
, TLSv1.
2
, unknown
*
/
const char
*
cipher_name;
/
*
e.g. AES256
-
GCM
-
SHA384
*
/
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;
};
in_trans
=
trans_create(
self
-
>mode,
self
-
>in_s
-
>size,
self
-
>out_s
-
>size);
in_trans
-
>sck
=
in_sck;
in_trans
-
>type1
=
TRANS_TYPE_SERVER;
in_trans
-
>status
=
TRANS_STATUS_UP;
in_trans
-
>is_term
=
self
-
>is_term;
g_strncpy(in_trans
-
>addr,
self
-
>addr,
sizeof(
self
-
>addr)
-
1
);
g_strncpy(in_trans
-
>port,
self
-
>port,
sizeof(
self
-
>port)
-
1
);
g_sck_set_non_blocking(in_sck);
if
(
self
-
>trans_conn_in(
self
, in_trans) !
=
0
)
{
trans_delete(in_trans);
}
in_trans
=
trans_create(
self
-
>mode,
self
-
>in_s
-
>size,
self
-
>out_s
-
>size);
in_trans
-
>sck
=
in_sck;
in_trans
-
>type1
=
TRANS_TYPE_SERVER;
in_trans
-
>status
=
TRANS_STATUS_UP;
in_trans
-
>is_term
=
self
-
>is_term;
g_strncpy(in_trans
-
>addr,
self
-
>addr,
sizeof(
self
-
>addr)
-
1
);
g_strncpy(in_trans
-
>port,
self
-
>port,
sizeof(
self
-
>port)
-
1
);
g_sck_set_non_blocking(in_sck);
if
(
self
-
>trans_conn_in(
self
, in_trans) !
=
0
)
{
trans_delete(in_trans);
}
| version(
4
bytes) | size(
4
bytes, be) | payload (size
-
8
) |
| version(
4
bytes) | size(
4
bytes, be) | payload (size
-
8
) |
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
)
{
/
/
接收数据,把都进来的数据写到
self
-
>in_s
-
>end 开始的位置,并更新其值
read_bytes
=
self
-
>trans_recv(
self
,
self
-
>in_s
-
>end, to_read);
if
(read_bytes
=
=
-
1
)
{
if
(g_tcp_last_error_would_block(
self
-
>sck))
{
/
*
ok, but shouldn't happen
*
/
}
else
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
if
(
self
-
>si !
=
0
)
{
self
-
>si
-
>cur_source
=
cur_source;
}
return
1
;
}
}
else
if
(read_bytes
=
=
0
)
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
if
(
self
-
>si !
=
0
)
{
self
-
>si
-
>cur_source
=
cur_source;
}
return
1
;
}
else
{
self
-
>in_s
-
>end
+
=
read_bytes;
}
}
/
/
目前已经读到的字节数
read_so_far
=
(
int
) (
self
-
>in_s
-
>end
-
self
-
>in_s
-
>data);
/
/
已经读到需要的字节数,进入 sesman_data_in 进行处理
if
(read_so_far
=
=
self
-
>header_size)
{
if
(
self
-
>trans_data_in !
=
0
)
{
rv
=
self
-
>trans_data_in(
self
);
/
/
sesman_data_in()
if
(
self
-
>no_stream_init_on_data_in
=
=
0
)
{
init_stream(
self
-
>in_s,
0
);
}
}
}
if
(
self
-
>si !
=
0
)
{
self
-
>si
-
>cur_source
=
cur_source;
}
}
if
(trans_send_waiting(
self
,
0
) !
=
0
)
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
return
1
;
}
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
)
{
/
/
接收数据,把都进来的数据写到
self
-
>in_s
-
>end 开始的位置,并更新其值
read_bytes
=
self
-
>trans_recv(
self
,
self
-
>in_s
-
>end, to_read);
if
(read_bytes
=
=
-
1
)
{
if
(g_tcp_last_error_would_block(
self
-
>sck))
{
/
*
ok, but shouldn't happen
*
/
}
else
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
if
(
self
-
>si !
=
0
)
{
self
-
>si
-
>cur_source
=
cur_source;
}
return
1
;
}
}
else
if
(read_bytes
=
=
0
)
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
if
(
self
-
>si !
=
0
)
{
self
-
>si
-
>cur_source
=
cur_source;
}
return
1
;
}
else
{
self
-
>in_s
-
>end
+
=
read_bytes;
}
}
/
/
目前已经读到的字节数
read_so_far
=
(
int
) (
self
-
>in_s
-
>end
-
self
-
>in_s
-
>data);
/
/
已经读到需要的字节数,进入 sesman_data_in 进行处理
if
(read_so_far
=
=
self
-
>header_size)
{
if
(
self
-
>trans_data_in !
=
0
)
{
rv
=
self
-
>trans_data_in(
self
);
/
/
sesman_data_in()
if
(
self
-
>no_stream_init_on_data_in
=
=
0
)
{
init_stream(
self
-
>in_s,
0
);
}
}
}
if
(
self
-
>si !
=
0
)
{
self
-
>si
-
>cur_source
=
cur_source;
}
}
if
(trans_send_waiting(
self
,
0
) !
=
0
)
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
return
1
;
}
static
int
sesman_data_in(struct trans
*
self
)
{
int
version;
int
size;
/
/
解析
8
字节的头部数据,更新header_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)
{
LOG(LOG_LEVEL_ERROR,
"sesman_data_in: bad message size"
);
return
1
;
}
self
-
>header_size
=
size;
self
-
>extra_flags
=
1
;
}
/
/
解析完整的数据包,并做相应的处理
else
{
/
*
process message
*
/
struct sesman_con
*
sc
=
(struct sesman_con
*
)
self
-
>callback_data;
self
-
>in_s
-
>p
=
self
-
>in_s
-
>data;
if
(scp_process(
self
, sc
-
>s) !
=
SCP_SERVER_STATE_OK)
{
LOG(LOG_LEVEL_ERROR,
"sesman_data_in: scp_process_msg failed"
);
return
1
;
}
/
*
reset
for
next
message
*
/
self
-
>header_size
=
8
;
self
-
>extra_flags
=
0
;
init_stream(
self
-
>in_s,
0
);
/
*
Reset
input
stream pointers
*
/
}
return
0
;
}
static
int
sesman_data_in(struct trans
*
self
)
{
int
version;
int
size;
/
/
解析
8
字节的头部数据,更新header_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)
{
LOG(LOG_LEVEL_ERROR,
"sesman_data_in: bad message size"
);
return
1
;
}
self
-
>header_size
=
size;
self
-
>extra_flags
=
1
;
}
/
/
解析完整的数据包,并做相应的处理
else
{
/
*
process message
*
/
struct sesman_con
*
sc
=
(struct sesman_con
*
)
self
-
>callback_data;
self
-
>in_s
-
>p
=
self
-
>in_s
-
>data;
if
(scp_process(
self
, sc
-
>s) !
=
SCP_SERVER_STATE_OK)
{
LOG(LOG_LEVEL_ERROR,
"sesman_data_in: scp_process_msg failed"
);
return
1
;
}
/
*
reset
for
next
message
*
/
self
-
>header_size
=
8
;
self
-
>extra_flags
=
0
;
init_stream(
self
-
>in_s,
0
);
/
*
Reset
input
stream pointers
*
/
}
return
0
;
}
if
(trans_send_waiting(
self
,
0
) !
=
0
)
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
return
1
;
}
int
trans_send_waiting(struct trans
*
self
,
int
block)
{
struct stream
*
temp_s;
int
bytes;
int
sent;
int
timeout;
int
cont;
timeout
=
block ?
100
:
0
;
cont
=
1
;
while
(cont)
{
/
/
self
-
>wait_s不为
0
if
(
self
-
>wait_s !
=
0
)
{
temp_s
=
self
-
>wait_s;
if
(g_tcp_can_send(
self
-
>sck, timeout))
{
bytes
=
(
int
) (temp_s
-
>end
-
temp_s
-
>p);
/
/
通过socket向client发送数据
sent
=
self
-
>trans_send(
self
, temp_s
-
>p, bytes);
if
(sent >
0
)
{
temp_s
-
>p
+
=
sent;
if
(temp_s
-
>source !
=
0
)
{
temp_s
-
>source[
0
]
-
=
sent;
}
if
(temp_s
-
>p >
=
temp_s
-
>end)
{
self
-
>wait_s
=
temp_s
-
>
next
;
free_stream(temp_s);
}
}
else
if
(sent
=
=
0
)
{
return
1
;
}
else
{
if
(!g_tcp_last_error_would_block(
self
-
>sck))
{
return
1
;
}
}
}
else
if
(block)
{
/
*
check
for
term here
*
/
if
(
self
-
>is_term !
=
0
)
{
if
(
self
-
>is_term())
{
/
*
term
*
/
return
1
;
}
}
}
}
else
{
break
;
}
cont
=
block;
}
return
0
;
}
if
(trans_send_waiting(
self
,
0
) !
=
0
)
{
/
*
error
*
/
self
-
>status
=
TRANS_STATUS_DOWN;
return
1
;
}
int
trans_send_waiting(struct trans
*
self
,
int
block)
{
struct stream
*
temp_s;
int
bytes;
int
sent;
int
timeout;
int
cont;
timeout
=
block ?
100
:
0
;
cont
=
1
;
while
(cont)
{
/
/
self
-
>wait_s不为
0
if
(
self
-
>wait_s !
=
0
)
{
temp_s
=
self
-
>wait_s;
if
(g_tcp_can_send(
self
-
>sck, timeout))
{
bytes
=
(
int
) (temp_s
-
>end
-
temp_s
-
>p);
/
/
通过socket向client发送数据
sent
=
self
-
>trans_send(
self
, temp_s
-
>p, bytes);
if
(sent >
0
)
{
temp_s
-
>p
+
=
sent;
if
(temp_s
-
>source !
=
0
)
{
temp_s
-
>source[
0
]
-
=
sent;
}
if
(temp_s
-
>p >
=
temp_s
-
>end)
{
self
-
>wait_s
=
temp_s
-
>
next
;
free_stream(temp_s);
}
}
else
if
(sent
=
=
0
)
{
return
1
;
}
else
{
if
(!g_tcp_last_error_would_block(
self
-
>sck))
{
return
1
;
}
}
}
else
if
(block)
{
/
*
check
for
term here
*
/
if
(
self
-
>is_term !
=
0
)
{
if
(
self
-
>is_term())
{
/
*
term
*
/
return
1
;
}
}
}
}
else
{
break
;
}
cont
=
block;
}
return
0
;
}
struct stream
{
char
*
p;
char
*
end;
char
*
data;
int
size;
int
pad0;
/
*
offsets of various headers
*
/
char
*
iso_hdr;
char
*
mcs_hdr;
char
*
sec_hdr;
char
*
rdp_hdr;
char
*
channel_hdr;
/
*
other
*
/
char
*
next_packet;
struct stream
*
next
;
int
*
source;
};
struct stream
{
char
*
p;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!