CVE-2024-39754
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.
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
Wavlink AC3000 - https://www.wavlink.com/en_us/product/WL-WN533A8.html
10.0 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE-912 - Hidden Functionality
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.
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
Discovered by Lilith >_> of Cisco Talos.