Talos Vulnerability Report

TALOS-2024-2034

Wavlink AC3000 wctrls static login vulnerability

January 14, 2025
CVE Number

CVE-2024-39754

SUMMARY

A static login vulnerability exists in the wctrls functionality of Wavlink AC3000 M33A8.V5030.210505. A specially crafted set of network packets can lead to root access. An attacker can send packets to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Wavlink AC3000 M33A8.V5030.210505

PRODUCT URLS

Wavlink AC3000 - https://www.wavlink.com/en_us/product/WL-WN533A8.html

CVSSv3 SCORE

10.0 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-912 - Hidden Functionality

DETAILS

The Wavlink AC3000 wireless router is predominately one of the most popular gigabit routers in the US, in part due to both its potential wireless and wired speed capabilities and extremely low price point (costing at the time of this writing ~$60 USD). Among the configuration options, it’s also able to act as a standalone wireless gateway, a basic network router, or a wireless repeater.

Among the network services running on the Wavlink AC3000, we see a curious UDP service running on port 36338:

udp        0      0 0.0.0.0:36338           0.0.0.0:*                           5928/wctrls

To examine the functionality and purpose of this service, let us start from where this service reads in bytes:

0040090c    do
00400934        bytes_recvd = recvfrom(sockfd: fd, buf: &first_2_byes, len: 2, flags: 0, src_addr: &srcaddr, addrlen: &addrlen)  // [1]
00400944        bytectr = bytectr + bytes_recvd
00400940        if (bytes_recvd s<= 0)
00400940            break
0040090c    while (bytectr == 1)
00400940    if (bytes_recvd s<= 0)
0040094c        rfds = &read_fds
00400940    else
00400954        rfds = &read_fds
00400950        if (bytectr == 2)
00400970            rfds = &read_fds
00400968            if (zx.d(first_2_byes.b - 0x11) u< 7)   // [2]
00400974                char inpbuf[0x10]
00400974                inpbuf[0].d = 0
00400978                inpbuf[4].d = 0
0040097c                inpbuf[8].d = 0
00400980                inpbuf[0xc].d = 0
00400984                int32_t bytes_read = 0
004009c0                int32_t recvfrom_ret
004009c0                do
004009a8                    recvfrom_ret = recvfrom(sockfd: fd, buf: &inpbuf, len: 0x10, flags: 0, src_addr: &srcaddr, addrlen: &addrlen) // [3]
004009b0                    bytes_read = bytes_read + recvfrom_ret
004009b8                    if (recvfrom_ret s< 0)
004009b8                        break
004009c0                while (bytes_read s< 0x10)
004009b8                if (recvfrom_ret s< 0)
004009c8                    break
004009c8                if (bytes_read != 0x10)
004009c8                    break
004009d0                int32_t put_into_next_funcs
004009d0                if (mode_flag == 0x2301)
00400a24                    rfds = &read_fds
00400a20                    if (do_the_enc_dec_etc(fd, srcaddr: &srcaddr, inpbuf: &inpbuf, mode_flag: 0x2301, addr_to_prev_stack: &put_into_next_funcs) != 0) // [4]
00400a2c                        mode_flag = 0x2302
004009d0                else  // hardcoded...
004009d8                    if (mode_flag != 0x2302)
004009d8                        break
004009f4                    mode_flag = 0x2301
004009f0                    do_the_enc_dec_etc(fd, srcaddr: &srcaddr, inpbuf: &inpbuf, mode_flag: 0x2302, addr_to_prev_stack: &put_into_next_funcs)  // [5]
00400a00                    rfds = &read_fds

The wctrls service first waits for 2 bytes [1], the first of which must be from 0x11 to 0x17 [3]. Assuming this occurs, wcrols then reads in 0x10 bytes, and passes it into the do_the_enc_dec_etc function at [4] since our state machine mode_flag always starts out as 0x2301. Continuing within do_the_enc_dec_etc:

00402a50  int32_t do_the_enc_dec_etc(int32_t fd, void* srcaddr, char* inpbuf, int32_t mode_flag, char* addr_to_prev_stack)
00402aa4      char send_stack_buf[0x20]
00402aa4      __builtin_memset(s: &send_stack_buf, c: 0, n: 0x20)
00402ab0      int32_t retval
00402ab0      char buf
00402ab0      if (mode_flag == 0x2301)
00402b44          p3_aes_funcs(inpbuf: "<redacted>", outputbuf: &send_stack_buf, key: "<redacted>") // [6]
00402b6c          if (memcmp(&send_stack_buf, inpbuf, 0x10) != 0) 
00402b78              retval = 0
00402b6c          else
00402b94              uint32_t ctr = 0
00402ba0              srand(time(0))
00402bf0              do                 // [7]
00402bb8                  int32_t rand = random()
00402bc8                  void* $a0 = &addr_to_prev_stack[ctr]
00402bcc                  ctr = ctr + 1
00402bdc                  int32_t $v1_3 = rand s/ 9
00402bf4                  *$a0 = rand.b - (($v1_3 << 3).b + $v1_3.b) + 0x30
00402bf0              while (ctr != 0x10)
00402c08              buf = 0x13
00402c10              char len_to_send = ctr.b  // should be 0x10
00402c0c              p3_aes_funcs(inpbuf: addr_to_prev_stack, outputbuf: &send_stack_buf[0x10], key: "<redacted>") // [8]
00402c30              sendto(sockfd: fd, buf: &buf, len: 2, flags: 0, dest_addr: srcaddr, addrlen: ctr) 
00402c48              sleep(1)
00402c6c              sendto(sockfd: fd, buf: &send_stack_buf[0x10], len: sx.d(len_to_send), flags: 0, dest_addr: srcaddr, addrlen: ctr) // [9]
00402c7c              retval = 1

The 0x10 bytes that we send are compared against an encrypted buffer that is generated at [6]. AES-CBC key and IV have been redacted from the output. The resulting buffer is memcp’ed against our input, and assuming this check passes, the binary then generates a random 0x10 length ASCII string of all digits within the loop at [7], which will be used as an encryption key in the future. The wctrls server then encrypts this generated ASCII key via the same method as before [8], and sends us the encrypted message at [9]. Importantly after this, the mode flag is changed to 0x2302, and we have to send the server another 2 byte packet and 0x10 byte packet since we return back up to main(). Assuming we’ve sent two new packets, we then hit the branch at [5] and enter do_the_enc_dec_etc with a mode_flag of 0x2302 instead:

00402a50  int32_t do_the_enc_dec_etc(int32_t fd, void* srcaddr, char* inpbuf, int32_t mode_flag, char* addr_to_prev_stack)
00402aa4      char send_stack_buf[0x20]
00402aa4      __builtin_memset(s: &send_stack_buf, c: 0, n: 0x20)
00402ab0      int32_t retval
00402ab0      char buf
00402ab0      if (mode_flag == 0x2301)
// [...]
00402ab0      else
00402ac0          sub_4029c4(inpbuf, output: &send_stack_buf[0x10], prevstack: addr_to_prev_stack) // [10]
00402ad4          int32_t* cmd_ptr = &command_list
00402ad8          int32_t ctr_1 = 0
00402b04          do
00402aec              retval = memcmp(&send_stack_buf[0x10], *cmd_ptr, 0x10)
00402af8              if (retval == 0)
00402c88                  (*((ctr_1 << 3) + 0x415284))()
00402ca0                  buf = 0x17
00402cac                  char var_47_1 = 0x10
00402cc8                  sendto(sockfd: fd, buf: &buf, len: 2, flags: 0, dest_addr: srcaddr, addrlen: 0x10)
00402ce4                  printf("exec %s\n", *(0x415280 + (ctr_1 << 3)))
00402cf4                  retval = 1
00402cf0                  break
00402b00              ctr_1 = ctr_1 + 1
00402b08              cmd_ptr = &cmd_ptr[2]
00402b04          while (ctr_1 != 3)
00402b2c      return retval

This branch of the code takes our 0x10 length input and decrypts it at [10] by using the forementioned ASCII key that it sent to us in the previous step. The resulting string is then memcmp‘ed against a set of three strings listed in the command_list struct:

00415280  char* command_list = 0x4030c8 {"TELNETD_#_TEMP#*"}
00415284  char* data_415284 = telnet_checker()
00415288  char* data_415288 = 0x4030dc {"TELNET_#_ETERNAL"}
0041528c  char* data_41528c = eternal_telnet()
00415290  char* data_415290 = 0x4030f0 {"KILL_#_MONITOR#*"}
00415294  char* data_415294 = kill_crond_and_monitor()

Assuming that we match any one of these strings, we then enter the corresponding function. While hopefully appropriately named, let us look at the telnet_checker function to see what can be done via wctrls:

00401984  int32_t telnet_checker()

0040199c      if (is_telnet_running == 1)
004019bc          return puts("telnet already boot") __tailcall
004019ac      return system("telnetd -p 2323&") __tailcall

As clearly shown, telnet can indeed be enabled via this UDP service. Likewise, the eternal_telnet function not only starts the telnet service, but sets an nvram variable such that telnet is always enabled, even after reboot. By itself, this remote management interface can seem problematic, but in combination with another issue on the device, we have automatic root login to any WAVLINK AC3000, even over WAN:

cpio-root/sbin$ grep -r "rootws"
internet.sh:    echo "rootws::0:0:Adminstrator:/:/bin/sh" >> /etc/passwd
internet.sh:    echo "rootws:x:0:rootws" >> /etc/group
internet.sh:    chpasswd.sh rootws <redacted>
storage.sh:             echo "rootws::0:0:Adminstrator:/:/bin/sh" >> /etc/passwd
storage.sh:             echo "rootws:x:0:rootws" >> /etc/group
storage.sh:             chpasswd.sh rootws "<redacted>"

There exists a static admin login of rootws (password redacted) on the device that are always present, even after factory reset. As far as we are aware there was no where that iptables rules were added to prevent access to udp port 36338, so we can summarize the above vulnerability as a WAN-accessible remote management admin interface with static credentials.

TIMELINE

2024-07-25 - Initial Vendor Contact
2024-07-29 - Requesting reply from vendor
2024-07-30 - Vendor confirms receipt
2024-07-30 - Vendor Disclosure
2024-07-30 - Vendor confirms receipt
2024-09-02 - Status update request sent
2024-10-15 - Status update request. Upcoming expiration date announced.
2024-10-22 - Vendor replies product has been discontinued, but patches are being worked on
2024-11-04 - Status update request for patch release dates
2024-11-12 TALOS advisory release date announced
2025-01-14 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.