This article explains the process of identifying and exploiting a known flaw on Zyxel USG devices, taking into consideration the following CVE:
An authentication bypasss vulnerability in the web-based management interface of Zyxel USG/Zywall series firmware versions 4.35 through 4.64 and USG Flex, ATP, and VPN series firmware versions 4.35 through 5.01, which could allow a remote attacker to execute arbitrary commands on an affected device. - CVE Mitre.
Currently, there is no published exploit available for this vulnerability, so we decided to delay publishing this blog post.
Furthermore, this blog post aims to show how to find such vulnerability in two different ways:
First, let’s introduce the target to the reader, Zyxel Usg.
According to the website, it’s a firewall solution designed for small and medium-sized businesses with plenty of features1. Under the hood, the device is powered by a Cavium (now Maxwell) Octeon3 Big Endian MIPS64 SoC.
Unfortunately, Ghidra and QEMU do not fully support this specific architecture. At least IDA Pro seems to support it.
The firmware can be downloaded from Zyxel’s official website, and to extract it, you can’t use the binwalk tool. In this case, there is an obstacle, the firmware is encrypted, so you need to find a method to bypass this protection. There is currently no publication explaining the firmware decryption process, and it is not the purpose of the document to explain how we managed to decompress it. Once extracted, you will have access to the classic LINUX filesystem layout.
The device presents many interesting things inside, such as geoblocking features, anti-botnet logic, Kaspersky antivirus, HTTP parser implemented in the kernel, etc. Keep in mind that more features mean a larger attack surface, but for now, we will restrict our analysis to the webserver since the vulnerability seems there.
The installed web server is an apache HTTPd, with custom CGI binaries, written in C and Python: that’s precisely the right place to look for weakness.
First of all, let’s start by looking at the Apache httpd’s configuration file, /etc/service_conf/httpd.conf
.
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
#LoadModule auth_pam_module modules/mod_auth_pam.so
#LoadModule php4_module modules/libphp4.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule auth_zyxel_module modules/mod_auth_zyxel.so
Include /etc/service_conf/httpd_zld.conf
TypesConfig conf/mime.types
DefaultType text/plain
<IfModule mod_mime_magic.c>
MIMEMagicFile conf/magic
</IfModule>
DirectoryIndex weblogin.cgi
AuthZyxelRedirect /
AuthZyxelSkipPattern /images/ /weblogin.cgi /I18N.js /language
AuthZyxelSkipUserPattern 127.0.0.1:10443 /images/ /I18N.js /language /weblogin.cgi /access.cgi /setuser.cgi /grant_access.html /user/ /cgi-bin/ /RevProxy/ /Exchange/ /exchweb/ /public/ /Socks/ /CnfSocks/ /cifs/ /uploadcifs/ /epc/ /CnfEpc/ /frame_access.html /dummy.html
ScriptAlias /cgi-bin/ "/usr/local/apache/cgi-bin/"
As we can see from the above lines, there is a custom apache module mod_auth_zyxel.so
, where they define AuthZyxelRedirect
/ AuthZyxelSkipPattern
and ‘AuthZyxelSkipUserPattern’ directives. By looking at these lines, we deduce the core endpoint reachable before the authentication. We are talking about weblogin.cgi
.
The vulnerability we are looking for is an authentication bypass with command injection.
So let’s look for the entry points in the code with the following characteristics:
By observing httpd.conf we can quickly narrow the circle.
The flaw will likely be in:
authtok
cookie. This code runs on every request.Now, let’s understand how the login process is implemented by analyzing weblogin.cgi.
The library responsible for parsing the CGI request coming from STDIN is libgcgi, an ancient library. You can find the sources on github2 to get function signatures to improve analysis.
Here is the list of functions prone to executing commands with potential user input under control:
// Libraries functions
execv
execl
popen
// Internal wrappers
__execv
__get_ret_exec
get_ret_exec
Now we can adopt two different approaches to find the vulnerability.
The scenario becomes more evident, and something changes in these functions. Specifically, the user input regex filtering improved in the need_twofa_admin
.
Why the strpbrk
is not sufficient?
Simply, it doesn’t check for spaces and double quotes in the username.
To visualize better what might go wrong, let’s test this with python:
def vulnerable_check(username: str):
cmd = "/bin/zysh -p 110 -e \"configure terminal _two-factor-auth admin-access _auth_need user %s\""
if any(ch in ";`\'*!%^|&#$" for ch in username):
print ("FAIL, you used a forbidden character")
else:
print (f"OK looks good, i will execute: {cmd % username}")
Intended usage:
vulnerable_check('admin')
-> OK looks good, i will execute: /bin/zysh -p 110 -e "configure terminal _two-factor-auth admin-access _auth_need user admin"
Close the current command by injecting a double quote:
vulnerable_check('admin"')
-> OK looks good, i will execute: /bin/zysh -p 110 -e "configure terminal _two-factor-auth admin-access _auth_need user admin""
Inject a new command after closing the current one:
vulnerable_check('admin" -e "injection')
-> OK looks good, i will execute: /bin/zysh -p 110 -e "configure terminal _two-factor-auth admin-access _auth_need user admin" -e "injection"
Well, ok, we found and exploited the vulnerability. Let’s move on now and understand how we could’ve seen it in an automated way with Joern4.
Joern is a platform for analyzing source code, bytecode, and binary executables. It generates code property graphs (CPGs), a graph representation of code for cross-language code analysis. Code property graphs are stored in a custom graph database. This allows code to be mined using search queries formulated in a Scala-based domain-specific query language. Joern is developed with the goal of providing a useful tool for vulnerability discovery and research in static program analysis.
You can look at the documentation to get more information. 5
Let’s imagine we don’t know anything about the code injection vulnerability path. How hard is it to find it by modeling the vulnerable code pattern with joern?
The first step is to import the decompiled code into joern, and run the interprocedural data flow analysis commands:
importCode("./src/vuln-weblogin.cgi.c")
// ossdataflow: Layer to support the OSS lightweight data flow tracker
run.ossdataflow
Note that it is easy to forget the line run.ossdataflow
, beware that the data dependency will not be populated, so all the reachable functions won’t return anything!
Next, we have to identify our inputs (the sources), and the functions which execute the command passed as input (the sinks).
// Our input will be copied in the buffer pointed by ret:
gcgiReturnType gcgiFetchStringNext(char *field,char *ret,int max);
We can easily do it in joern with this single line of code
def src = cpg.method("gcgiFetchStringNext").callIn.argument(2)
def sink_exec = cpg.method.name(".*exec.*").callIn.argument // all the arguments
def sink_popen = cpg.method.name("popen").callIn.argument(1) // restrict to argument 1
def sink_system = cpg.method.name("system").callIn.argument(1) // restrict to argument 1
sink_exec.reachableByFlows(src).map( _.elements.map( n => (n.lineNumber.get,n.astParent.code) )).l
This blog post shows how it is possible to identify a vulnerability in two ways: starting from its patch and exploring new tools to identify new vulnerabilities with a modern approach.
In CYS4, we care a lot about research; this is a small example.
If something was not clear enough, don’t hesitate to contact us.