
What We Found!
The teardown was the fun part — but the finding that actually matters shows up when you turn the camera around and look at it from the network side.
While poking at the camera’s services, we stumbled onto a suspicious URL that hands you the live video stream with zero authentication. No login, no token, no digest challenge — just hit the endpoint and the feed opens up. Anyone who knows the path and can reach the device on the network gets a front-row seat to whatever the camera is pointed at.

That would be bad enough on a single unit. The problem is it isn’t a single unit. When we pivoted from “our camera” to “every camera like it on the internet” and searched for that same fingerprint, the exposed endpoint turned up on more than 15,000 cameras reachable online — all of them leaking their streams to anyone who asks.

IP Cameras: A Security Perspective
Tearing down a cheap network camera, byte by byte — from the PCB to a root shell, and what vendors should fix.
Introduction
Internet Protocol (IP) cameras are everywhere — homes, offices, shops, streets. Unlike the old analog cameras that just pumped a video signal down a coax cable, an IP camera is a tiny networked Linux computer with a lens bolted on. It digitizes video, encodes it, and ships it over the network, which is what gives you remote access, crisp image quality, and all the smart features.
That same “tiny Linux computer” nature is exactly why they’re interesting from a security angle. A lot of these devices ship with genuinely bad habits baked in: hardcoded credentials, debug ports left wide open, ancient firmware, plaintext secrets, and management services exposed to the network. Pop one open and you often don’t just get the video feed — you get a foothold on whatever network the camera is sitting on.
In this post I’ll walk through a full teardown of one such camera: what’s inside it, how the software is put together, and how we went from a sealed plastic box to a root shell — via UART, a chip-off firmware dump, some Ghidra, and a writable filesystem the vendor really should have locked down. The goal isn’t to dunk on one product; it’s to show the recurring mistakes so people building and deploying these things can do better.
192.168.1.88, with Telnet and a couple of proprietary services listening out of the box.What’s Inside: Hardware
Before you can attack a device, you have to understand it. Here are the components that matter on the board:

| Component | Part | What it does |
|---|---|---|
| SoC (the brains) | Ingenic T21 | Main video-processing SoC that runs the firmware. Integrates a MIPS XBurst CPU (up to 1 GHz), a built-in ISP for the 1080p/3MP feed, on-chip DDR2 RAM, an audio codec, clock generators, and the usual interfaces (UART, I2C, SPI). It does the computing, the H.264 encoding, and talks to everything else. |
| Wi-Fi module | Unknown | Wireless radio that connects the camera to the local Wi-Fi network. |
| I/O isolation | TF1107 | Solid-state I/O interface. Provides galvanic isolation and buffering between an external DC control signal (alarm input, motion trigger, door sensor) and the SoC’s digital logic, so the chip sees a clean, level-shifted signal instead of being wired straight to noisy/high-voltage lines. |
| UART / serial | Header pins | Debug port used in development and firmware recovery. Frequently left populated and unauthenticated on cheap units — and, as you’ll see, our way in. |
| Flash memory | XMC XM25QH64AHIG | An 8-pin SPI NOR flash, 64 Mbit (8 MB). Holds the bootloader, kernel, root filesystem, app binaries, and config. The SoC boots directly out of this chip. |
| Image sensor | Unknown | Captures the scene and converts light into electrical signals for the ISP to process. |
What’s Inside: Software
On the software side it’s a fairly standard embedded Linux stack:
- Operating system. Embedded Linux living in the SPI flash. Boot happens in three stages: U-Boot (the bootloader) → kernel → filesystem.
- U-Boot brings up the basic hardware (CPU, RAM, clocks) and then loads and starts the kernel.
- Kernel manages hardware, processes, memory, filesystems, networking, and security — the core that makes everything else run.
- Filesystem stores and organizes configs, logs, firmware, and user data so it can be read and written reliably.
- Web interface. A lightweight HTTP server for live view, settings, and firmware updates.
- Streaming server. Encodes and transmits video (H.264/H.265) over RTSP, HTTP, or a proprietary protocol.
- Control software. Motion detection, scheduling, recording, user management, and alarms.
- ONVIF / RTSP support. Standardized streaming and device discovery.
The two filesystems: SquashFS and JFFS2
Almost every device like this splits its storage into two very different filesystems, and understanding the split is what makes the later attack obvious:
- SquashFS — a compressed, read-only filesystem for constrained devices. It holds the main firmware (kernel, root filesystem, binaries) in a tight, compressed form that saves flash and speeds up boot. Because it’s read-only, updating anything in it means reflashing the whole image.
- JFFS2 (Journaling Flash File System v2) — a read-write, log-structured filesystem built for NOR/NAND flash. It holds configuration, logs, and runtime settings. Thanks to wear leveling and safe in-place updates, the camera can change things like network settings, Wi-Fi credentials, and user preferences without corrupting the firmware.
Together they give you the best of both worlds: a stable, tamper-resistant firmware base (SquashFS) plus a place for changeable state (JFFS2). The catch — and this is the recurring theme — is that a writable partition that the read-only firmware trusts and executes from is a gift to an attacker.
The Assessment hands-on
The work broke down into hardware recon, a firmware dump, static analysis, live interaction over UART, and finally persistence/root. I’ll take them roughly in that order, though in practice they feed each other.
Step 1 — Finding the UART pins: TX, RX, VCC, GND
The UART header was the obvious first target, but on this board the four pads weren’t labeled. Before you can talk to a serial port you have to figure out which pad is which. Here’s the method we used — nothing fancy, just a multimeter and patience.
1. Find the candidate pads
Look for a row of 3–4 pads or through-holes near the SoC, often in a neat line and sometimes with a slightly larger via for ground. Four pads in a row is the classic UART tell: VCC, GND, TX, RX (in some order).
2. Identify GND (power OFF)
Set the multimeter to continuity mode. Put one probe on a known ground — a shield can, the negative terminal of a large capacitor, or a mounting hole — and touch each pad with the other. The pad that beeps (near-zero ohms to ground) is GND. Do this with the board unpowered.
3. Identify VCC, TX, and RX (power ON)
Now power the board and measure DC voltage from each remaining pad to GND:
- VCC sits at a steady voltage — typically
3.3 V(sometimes 1.8 V) — and doesn’t move. Don’t wire this to your adapter; you only need it to identify the pad. - TX (camera transmit) idles high, close to VCC, then flickers/drops as the bootloader spews text during power-up. On a cheap voltmeter you’ll literally see the number twitch during the first couple of seconds of boot. That “dancing” pad is TX — it’s the one pushing data out.
- RX (camera receive) usually sits high and steadier than TX (it’s an input, often weakly pulled up, higher impedance). It’s the pad that isn’t clearly VCC and isn’t twitching like TX.
If you have a logic analyzer or scope, this gets faster and less ambiguous: TX shows obvious UART framing at boot, and you can measure the narrowest pulse width to derive the baud rate (baud ≈ 1 / shortest pulse). No scope? 115200 8N1 is the overwhelmingly common default — try it first.

Wiring used (camera ↔ Raspberry Pi 3)
We drove the serial link straight off the Pi’s GPIO header (/dev/serial0):
| Camera pad | Raspberry Pi 3 pin | Pi function |
|---|---|---|
| TX | Pin 10 | GPIO15 / UART RXD |
| RX | Pin 8 | GPIO14 / UART TXD |
| GND | Pin 6 | GND |
| VCC | — | not connected |
Step 2 — Reading the firmware (chip-off)
In parallel with the UART work we pulled the firmware off the flash chip. Having the raw image lets you do all your analysis offline, and it’s the foundation for cracking the bootloader password later.
The target is the XMC XM25QH64AHIG SPI NOR chip. Rather than buy a dedicated programmer, we used a Raspberry Pi 3 as the SPI host and flashrom to read it. You can do this two ways:
- Chip-off — desolder the chip and read it on a socket/adapter. Cleanest signal, no interference from the rest of the board.
- In-circuit (clip) — a SOIC-8 test clip straight onto the chip while it’s still soldered. No desoldering, but you usually have to hold the SoC in reset (or otherwise keep it off the bus) so it isn’t fighting you for the SPI lines. On these tiny boards a clean chip-off is often less hassle than fighting a flaky clip.
Flash → Raspberry Pi 3 wiring
Standard 8-pin SPI NOR pinout. The write-protect (WP), HOLD, and VCC pins all tie to 3.3 V (Pi pin 1):
| Flash pin | Signal | Raspberry Pi 3 pin |
|---|---|---|
| 1 | CS / chip select | Pin 24 (CE0) |
| 2 | DO / MISO | Pin 21 (MISO) |
| 3 | WP → 3.3 V | Pin 1 (3.3 V) |
| 4 | GND | Pin 30 (GND) |
| 5 | DI / MOSI | Pin 19 (MOSI) |
| 6 | CLK | Pin 23 (SCLK) |
| 7 | HOLD → 3.3 V | Pin 1 (3.3 V) |
| 8 | VCC → 3.3 V | Pin 1 (3.3 V) |
With the chip wired up, dump it:
# Read the SPI flash and save it to firmware.bin
sudo flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=1000 -r firmware.bin
Run it twice and diff the two dumps — if they match, you’ve got a clean read. (If flashrom can’t ID the chip, force it with -c "XM25QH64A", and drop the SPI speed if reads are flaky.) That gives you a full binary image of everything on the flash.
Step 3 — Carving the image apart (static analysis)
With firmware.bin in hand, the next job is to break it into its partitions and see what’s inside. We used binwalk to locate the signatures (U-Boot, the Linux kernel, SquashFS/JFFS2), dd to carve out specific regions, and strings to grep for interesting text — paths, credentials, embedded scripts.
The exact partition offsets came from a combination of binwalk output and the memory layout we read straight off the live device over UART (the U-Boot bootargs spell out the mtdparts map). Cross-referencing the two gave us a clean split:
binwalk signatures line up with the partition map from U-Boot.# Split the full firmware into individual partitions with dd
# (offsets/sizes from binwalk + the mtdparts map read over UART)
# Bootloader (U-Boot) 256KB / 0x40000
dd if=firmware.bin of=boot.bin bs=1 skip=$((0x000000)) count=$((0x40000))
# Kernel 1.5MB / 0x180000
dd if=firmware.bin of=kernel.bin bs=1 skip=$((0x40000)) count=$((0x180000))
# Root filesystem 2.25MB / 0x240000
dd if=firmware.bin of=rootfs.bin bs=1 skip=$((0x1C0000)) count=$((0x240000))
# IPC / app binaries 3.5MB / 0x380000
dd if=firmware.bin of=ipc.bin bs=1 skip=$((0x400000)) count=$((0x380000))
# Configuration 512KB / 0x80000 (usually JFFS2)
dd if=firmware.bin of=conf.bin bs=1 skip=$((0x780000)) count=$((0x80000))
Then extract each partition. binwalk -e handles the SquashFS and compressed blobs:
binwalk -e boot.bin # bootloader
binwalk -e kernel.bin # Linux kernel image
binwalk -e rootfs.bin # root filesystem (SquashFS)
binwalk -e conf.bin # runtime config (JFFS2)
binwalk -e ipc.bin # camera app binaries / services
binwalk -e often fails to unpack JFFS2 cleanly. Use Jefferson instead.# Install Jefferson in a virtualenv
sudo apt update
sudo apt install python3-venv python3-pip -y
python3 -m venv jeff_env
source jeff_env/bin/activate
pip install jefferson
# Extract JFFS2 from the config partition (and try the others)
jefferson conf.bin # Wi-Fi / network configs and runtime data
jefferson rootfs.bin
jefferson ipc.bin
deactivate # leave the virtualenv when done
Step 4 — Cracking the U-Boot password with Ghidra
This camera doesn’t just drop you into the bootloader — interrupt the boot and U-Boot asks for a password:
### Please input uboot password: ###
So we loaded the extracted U-Boot binary into Ghidra and worked backward from that prompt string. Following the cross-references to the comparison and memory routines around it, we reverse-engineered the password-check logic and recovered the hardcoded bootloader password sitting right next to that prompt.
With the password in hand, we had full, authenticated access to the U-Boot console on the live device — which is where the fun starts.
Step 5 — Live over UART: from bootloader to root shell
Back on the powered camera, we watched the boot in minicom at 115200 baud:
# Serial console over the Pi's UART
sudo apt install minicom -y
minicom -b 115200 -o -D /dev/serial0
We interrupted autoboot by mashing a key, entered the recovered password at the U-Boot prompt, and landed in the bootloader shell. From there, the classic move: rewrite bootargs to skip the normal init and boot straight to a root shell with init=/bin/sh.
# Bypass init, boot directly to a shell
setenv bootargs 'console=ttyS1,115200n8 mem=44672K@0x0 rmem=20864K@0x2BA0000 \
init=/bin/sh rootfstype=squashfs root=/dev/mtdblock2 rw \
mtdparts=jz_sfc:256k(boot),1536k(kernel),2304k(root),3584k(ipc),512k(conf)'
run bootcmd
bootargs.Once at the shell, we mounted what we needed — the read-only SquashFS root and the writable JFFS2 config partition:
# Bring up the basics
mount -t proc proc /proc
mount -t tmpfs tmpfs /dev
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s && echo "mdev ok"
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
# Inspect the init script
cat /etc/init.d/rcS
# Mount the flash partitions
mount -t squashfs /dev/mtdblock3 /mnt/mtd/ipc # app partition (read-only)
mount -t jffs2 /dev/mtdblock4 /mnt/mtd/ipc/conf # config (read/write)
Step 6 — Persistence & root: the writable-script trap
Here’s where the SquashFS/JFFS2 split bites the vendor. Digging through the writable JFFS2 partition, we found a startup script called Run that gets executed on boot — it’s referenced and kicked off from the read-only SquashFS. In other words: the trusted, read-only firmware hands control to a script sitting on a partition anyone with a shell can write to.
So we did the obvious thing: modified Run to call our own script, which swapped the hashed root password in /etc/passwd for a hash we controlled. Reboot, the system runs our script as root, the password changes, and we own the box — cleanly, persistently, no exploit chain required. Just improper trust in writable storage.
And while we were in there, the filesystem gave up one more prize: the Wi-Fi credentials in plaintext — the SSID and password of the network the camera was joined to, sitting in a config file for anyone with filesystem access to read.
What Vendors Should Fix
None of this required exotic skills or expensive gear — a Raspberry Pi, a multimeter, and free tools. That’s the point. Here’s what would have stopped us:
Lock down (or kill) UART
Debug consoles on shipping devices should be disabled, or gated behind challenge-response auth or hardware fuses. Options range from not populating the header, to using BGA packages that are harder to probe, to disabling the console in the bootloader/kernel config.
Secure the U-Boot environment
Protect U-Boot with a strong, per-device password — not a single hardcoded string baked into every unit’s firmware. Don’t expose sensitive environment variables, and don’t allow insecure bootargs edits (like init=/bin/sh) on production devices.
Sign and/or encrypt the firmware
Flash contents should be signed and, ideally, encrypted, so a chip-off dump doesn’t hand an attacker the whole system — bootloader passwords included. Pair this with secure boot so the SoC refuses to run unsigned images.
Verify filesystem integrity at boot
Use a signed bootloader and hash-check the partitions on startup so tampered SquashFS/JFFS2 content or a modified startup script is detected and rejected. Do this and the whole Run-script trick falls apart — the system simply won’t execute the modified script.
Never store secrets in plaintext
Wi-Fi passwords, user credentials, API keys — none of it belongs on the device in cleartext. Hash what you can, encrypt what you must (keyed to device-unique, hardware-protected material), and store nothing you don’t truly need.
Minimize the attack surface
Turn off services you don’t ship a feature for — Telnet, unused ONVIF, debug daemons — and monitor the network stack for unauthorized access attempts.
Do these, and you turn a 30-minute teardown into a genuinely hard problem. Get them wrong, and a camera meant to watch a room quietly becomes an attacker’s window into the whole network. Better defaults protect the people who bought the thing — which is the entire job.
