Gaining a full admin shell on a ZyXEL gateway

Writeup-20240727163101076.webp
We have been using an old small business gateway ZyXEL SBG3300 in our computer network. A year ago, it started to reset itself at random times and it was time to replace it. We kept it as a last-resort backup but few weeks ago, we decided to throw it away.

I thought that before throwing it out, it would still be fun to play around with this 10 year old router. I wanted to see what software runs on the router, maybe try flashing a custom firmware on it or to try to run custom code.

In the end, I found a "hidden command" that gives an authenticated user access to a privileged BusyBox shell and allows complete control over the device. This is not a security vulnerability since we need to log into the device to be able to run the command, but it gives us full control over the device as opposed to the limited set of configuration commands that are typically available after login.

This post is about my goal-free exploration that led to this small discovery.

Jacking in

Gaining a full admin shell on a ZyXEL gateway-20240727171018258.webp
From an ordinary user point of view, after buying a wifi router, we want to configure our new device - set wifi name, password, set up the internet access. For this, most users typically use the Web-based configuration management system - connect the router to the computer, type in the IP address into the internet browser and log in using the default credentials.

The underlying system of the router is hidden to the user and the only thing that is accessible is this user-friendly configuration interface. But in the end, routers are just small computers with networking capabilites, and so in theory, they are capable to do much more than what is prescribed by the manufacturer.

One of the ways to escape the comfortable prison of the web interface is to connect to the device using a serial port. As far as I know, most routers have this interface exposed for diagnostics and maintenance but it is usually hidden under the plastic casing of the device.

I opened the case and looked for 4-pin pin connector on the routers motherboard. Then I determined which pin does what in the serial communication using a multimeter. For TTL Serial connection, we need 3 pins - GND, the common ground, transmit pin TX and receive pin RX. I also needed to determine Baudrate of the serial connection. Usually one can guess because there are only few transfer speeds that are actually being used. (9600 and 115200 in my limited experience) But if you have an oscilloscope, you can use it to capture the data signal on the TX pin and then you do not have to guess.
I made slightly embarrassing TikTok video that shows this measurement. I leaned too much into "l33t haxxor" aesthetics and got rightfully criticized in the comments :-)) But hey, it was fun to make and the measurement procedure is shown there correctly:
https://www.tiktok.com/@hackbalservis/video/7149966276104965382
After that, I connected a TTL-to-USB adapter to the correct pins, set the correct baudrate, connected to the router terminal:

$ picocom -b 115200 /dev/ttyUSB0

After switching on the power, I was greeted by the stream of boot debug output, lots of information already there (ZyXEL SBG3300 Bootlog)! For example we can now determine that the router runs on the BCM63168 SoC.

After two minutes, I am met with the quintessential hacker movie scene: the login prompt.

ZyXEL SBG3300
Login: █

I try default user and password, does not work. Then I remember the credentials from when the router was being used and we are in!

ZyXEL SBG3300
Login: admin
Password:
 > █

We are in! ... or are we?

What is the first command you type in an unknown shell? To shine light into the darkness of an unknown system. My first instinct, wired in my muscle memory, is ls.

ZyXEL SBG3300
Login: admin
Password:
 > ls
consoled:error:646.324:processInput:502:unrecognized command ls
 > █

Betrayal! unrecognized command ls? This is clearly not a full-fledged shell! Lets see what we can do here:

 > help
?
help
logout
exit
quit
reboot
adsl
xdslctl
xtm
brctl
cat
loglevel
logdest
virtualserver
ddns
df
...

The commands available in this shell are mostly just for the router configuration - so the same functionality we have in the web configuration system. Although there are some more useful commands such as cat, we miss a lot of the core utilities and shell commands such as ls, cd and we cannot run any executables. So we did not gain control over the router by plugging directly into the serial port! Yet...
It seems to me that I must be missing something, the serial port is there for debugging and programming the device. Surely, during the router development, the programmers did have an access to a full shell through here but they disabled it for the customers. There might be a way to reactivate it somehow.
Lets see how is this limited shell implemented and we might get some ideas how to escape its confinement.

Escape the shell

Lets try a completely different approach. It looks like we cannot examine the device from the inside by connecting to it. But we can try examining it from the outside by analyzing the firmware.
The router manufacturers distribute firmware updates to patch up security vulnerabilities and fix bugs. Customers download the update packages and upload it to their devices though the web interface. In the update binary package, there is a complete image of the system running on the device.

I downloaded the firmware update for SBG3300 and examined it using the tool binwalk, used for analyzing firmware images:

$ binwalk 'V1.01(AADY.9)C0.bin'

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
131072        0x20000         JFFS2 filesystem, big endian

The binwalk utility identified a JFFS2 filesystem inside the binary. Using binwalk -Me I extracted the contents of the filesystem. Now we can look how exactly is the router firmware implemented. At the high level, we see a traditional Linux directory structure:

$ tree -L 2 _V1.01\(AADY.9\)C0.bin.extracted/
_V1.01(AADY.9)C0.bin.extracted/
├── 20000.jffs2
└── jffs2-root
    ├── bin
    ├── cferam.000
    ├── data
    ├── dev
    ├── etc
    ├── filesystems
    ├── home
    ├── lib
    ├── linuxrc -> bin/busybox
    ├── log
    ├── mnt
    ├── opt
    ├── proc
    ├── psk.txt -> /dev/null
    ├── sbin
    ├── sys
    ├── tmp -> /dev/null
    ├── usr
    ├── var
    ├── vmlinux.lz
    └── webs

17 directories, 7 files

Lets see if we can find the program that handles the serial console and that was restraining us before. We search for a file that contains the string "Login:" because that is the exact string that we see after connecting to the router.

$ grep -rl "Login:"
lib/private/libcms_cli.so

This might be it, lets examine libcms_cli.so and see if we find anything interesting there. Just by looking at the filename libcms_cli.so, we see that we have found an interesting file. In the context of routers, CMS is an abbreviation for Configuration Management System and CLI is Command Line Interface.

Before searching for "Login:", I tried searching for different keywords that could be interesting, such as "admin", "passwd", "root", "supervisor". Interestingly, lib/private/libcms_cli.so comes up in the search results most of the time. Nevertheless, I analyzed more files than just libcms_cli.so during this phase of the exploration before I found the hidden commands.

But what does the library do exactly? We do not know. I fired up Ghidra - a reverse engineering software - and opened the libcms_cli.so for disassembly and analysis.

First, I looked at the strings contained in the binary

Gaining a full admin shell on a ZyXEL gateway-20240729175459287.webp Gaining a full admin shell on a ZyXEL gateway-20240729175611315.webp

I found the "Login:" string along with many more that are used for the authentication. Then I looked for other interesting strings and when something caught my eye, I jumped into the code and tried to understand how it works.
At one point during this exploration I found the function that facilitates the command line processing and execution loop. If we write e.g. "reboot" to the terminal, this function takes what we wrote, finds out what needs to be executed and executes it.
Gaining a full admin shell on a ZyXEL gateway-20240729180722583.webp
Interestingly, the user terminal input is handed over to two functions - cli_processCliCmd and then cli_processHiddenCmd. What is processHiddenCmd?? Anything that is called hidden is suddenly hundred times more interesting. I jumped into the function and looked around.
Gaining a full admin shell on a ZyXEL gateway-20240730123810663.webp
The function takes the command name that the user wrote into the terminal. Then it compares this command with 6 "hidden" commands: dumpmem, ebtables, iptables, logread, setmem and sh.
If it is the first five are run as is with the function prctl_runCommandInShellBlocking. The sh command requires some special handling but in the end is also spawned with prctl_spawnProcess.
I tried googling cli_processHiddenCmd and I even found the original code! https://github.com/ad7843/hi/blob/master/cli_cmd.c#L389

So, could that be it? Lets login again and try writing sh:

ZyXEL SBG3300
Login: admin
Password: 
 > ls
consoled:error:659.705:processInput:502:unrecognized command ls
 > sh
~ # ls
bin         etc         log         sbin        var
cferam.000  home        mnt         sys         vmlinux.lz
data        lib         opt         tmp         webs
dev         linuxrc     proc        usr
~ # whoami
supervisor
~ # █

Wow! We have a full shell now, and it is running under the supervisor user which has root privileges.
Now we are truly in!

After googling for some keywords - the hidden commands and function names from libcms_cli.so, I found a few posts that mention the hidden sh command.

  • According to this Russian forum thread, the sh command might be protected by password. The question is whether that password is secure.
  • Googling the hidden commands yields a "confidential" CLI manual from ZyXEL where the commands are described.
  • I also found two Chinese blog posts [1] [2], second of which is very similar to mine, that describes the procedure of finding the hidden commands. On top of this, they also find default login credentials hardcoded into their version of libcms_cli.so and also command injection vulnerability using the ping command from the "visible" commands.
    This is interesting because on my system, there are no hardcoded credentials but there is a less privileged user "zyuser" with default credentials. It cannot run the sh hidden command but it is possible to use the command injection to elevate into root shell:
Login: zyuser
Password: (1234 = default password)
 > ping localhost && sh
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.666 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.420 ms
64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.418 ms
64 bytes from 127.0.0.1: seq=3 ttl=64 time=0.421 ms

--- localhost ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.418/0.481/0.666 ms
~ # whoami
supervisor

The default configuration restricts a remote access for the zyuser so this privilege elevation works only on the serial connection and not on telnet/ssh.

Notes

  • Before reverse engineering the libcms_cli.so, I tried using the limited commands available after login and tried cat /etc/passwd. I found out that besides the admin user, there is also a supervisor user. I had a theory that this user might have the full access and I tried cracking the password hash using John the Ripper password cracker. The admin password succumbed after a minute (we did not have a secure password on the router...), the supervisor password did not budge.
  • Funnily, while reverse engineering various libraries, I found that the passwords for all users are stored in plaintext (base64 encoded) in the file /var/csamu. The supervisor password is 31 characters long, it is regenerated on each reboot and contains random letters and numbers around a fixed substring zyad1234. So John the Ripper didn't stand a chance. I tried logging in as the supervisor with the unencrypted password from csamu file and found that has the same limited shell as the admin account and so this was a complete dead end. Still it is funny that all passwords are stored in plain text on the device.