Talos Vulnerability Report

TALOS-2024-2020

Wavlink AC3000 internet.cgi set_add_routing() command injection vulnerabilities

January 14, 2025
CVE Number

CVE-2024-39764,CVE-2024-39765,CVE-2024-39763,CVE-2024-39762

SUMMARY

Multiple OS command injection vulnerabilities exist in the internet.cgi set_add_routing() functionality of Wavlink AC3000 M33A8.V5030.210505. A specially crafted HTTP request can lead to arbitrary command execution. An attacker can make an authenticated HTTP request to trigger these vulnerabilities.

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

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

CWE

CWE-77 - Improper Neutralization of Special Elements used in a Command (‘Command Injection’)

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.

When interacting with and configuring the Wavlink AC3000 wifi router, as is typical of most wifi routers, an administrator logs in via some web portal and configures appropriate options via the HTTP interface. In the case of this particular router, and in another somewhat common execution pattern, these HTML pages can invoke .cgi binaries due to how the lighttpd server is configured. Since all of these .shtml and .cgi files are located in the web root, anyone with network access to the device doesn’t actually need to log in to the device to interact with these .cgi files, and it usually is the responsibility of the .cgi binary to check if the authentication is completed successfully. On this device, one will see a check_valid_user() function in each individual .cgi binary which will check the session cookie of the HTTP request to see if it’s coming from a validly logged in user.

Assuming that we’ve passed this check in the internet.cgi binary, we then run into a set of functions that we can call based off of what we pass for the page= parameter in our HTTP POST request. Of the available commands, we focus on the following:

00401a18              else if (strcmp(page_get, "addrouting") == 0)
00401ae4                  set_add_routing(malloc_contlen)

If we provide page=addrouting, we enter the set_add_routing function and our provided POST data is further parsed therein:

00402f78  int32_t set_add_routing(int32_t arg1)

00402fc0      char buff_0x100_1[0x100]
00402fc0      memset(&buff_0x100_1, 0, 0x100)
00402fdc      char buff_0x100_2[0x100]
00402fdc      memset(&buff_0x100_2, 0, 0x100)
00402ff4      web_debug_header()
00403028      int32_t dest_1 = strdup(web_get("dest", arg1, 1))       // [1]
00403038      int32_t dest = dest_1
00403034      if (dest_1 == 0)
0040365c          dest = 0x4075bc
00403064      int32_t hostnet_2 = strdup(web_get("hostnet", arg1, 1)) // [2]
00403074      int32_t hostnet = hostnet_2
00403070      if (hostnet_2 == 0)
00403668          hostnet = 0x4075bc
004030a0      int32_t netmask_1 = strdup(web_get("netmask", arg1, 1)) // [3]
004030b0      int32_t netmask = netmask_1
004030ac      if (netmask_1 == 0)
00403674          netmask = 0x4075bc
004030dc      int32_t gateway_1 = strdup(web_get("gateway", arg1, 1)) // [4]
004030ec      int32_t gateway = gateway_1
004030e8      if (gateway_1 == 0)
00403680          gateway = 0x4075bc
00403118      int32_t interface_1 = strdup(web_get("interface", arg1, 1)) // [5]
00403128      int32_t interface = interface_1
00403124      if (interface_1 == 0)
0040368c          interface = 0x4075bc
00403154      int32_t custom_interface_2 = strdup(web_get("custom_interface", arg1, 1))  // [6]
00403164      int32_t custom_interface = custom_interface_2
00403160      if (custom_interface_2 == 0)
00403698          custom_interface = 0x4075bc
00403190      int32_t comment_2 = strdup(web_get("comment", arg1, 1)) // [7]
004031a0      int32_t comment = comment_2

Immediately upon function entry, the binary reads in the dest [1], hostnet[2], netmask[3], gateway[4], interface[5], custom_interface[6], and comment[7] HTTP POST parameters from our input. Each of these parameters can lead to OS command injection and are described separately below.

CVE-2024-39762 - netmask injection

004030a0      int32_t netmask_1 = strdup(web_get("netmask", arg1, 1))
004030b0      int32_t netmask = netmask_1
// [...]
0040331c          if (sx.d(*netmask) == 0)
00403618              __builtin_strncpy(dest: netmask, src: "255.255.255.", n: 0xc)
0040362c              *(netmask + 0xf) = '5'
00403634              *(netmask + 0xb) = '25'
0040331c          else
0040333c              snprintf(&buff_0x100_1, 0x100, "%s netmask %s", &buff_0x100_1, netmask)  // [8]
// [...]
00403380          if ((sx.d(*gateway) == 0 && sx.d(*interface) == 0) || (sx.d(*gateway) != 0 && sx.d(*interface) == 0))
// [...]
004033f0              label_4033f0:
004033f0              snprintf(&buff_0x100_1, 0x100, "%s dev %s ", &buff_0x100_1, custom_iface_potentially)
00403438              __builtin_strncpy(dest: &buff_0x100_1[strlen(&buff_0x100_1)], src: "2>&1 ", n: 6)
00403444              puts(&buff_0x100_1, 0x20, &buff_0x100_1)
00403460              int32_t popen_result = popen(&buff_0x100_1, "r") // [9]

At [8], our input netmask is input into the buff_0x100_1 stack buffer as part of a command that is then run with popen at [9]. Since there’s no input filtering, we can easily inject arbitrary system commands to be run, resulting in command execution.

CVE-2024-39763 - gateway injection

004030dc      int32_t gateway_1 = strdup(web_get("gateway", arg1, 1))
004030ec      int32_t gateway = gateway_1
// [...]
00403350          if (sx.d(*gateway) == 0)
004035c4              __builtin_strcpy(dest: gateway, src: "0.0.0.0")
00403350          else
00403370              snprintf(&buff_0x100_1, 0x100, "%s gw %s", &buff_0x100_1, gateway) // [10]
// [...]
00403380          if ((sx.d(*gateway) == 0 && sx.d(*interface) == 0) || (sx.d(*gateway) != 0 && sx.d(*interface) == 0))
// [...]
004033f0              label_4033f0:
004033f0              snprintf(&buff_0x100_1, 0x100, "%s dev %s ", &buff_0x100_1, custom_iface_potentially)
00403438              __builtin_strncpy(dest: &buff_0x100_1[strlen(&buff_0x100_1)], src: "2>&1 ", n: 6)
00403444              puts(&buff_0x100_1, 0x20, &buff_0x100_1)
00403460              int32_t popen_result = popen(&buff_0x100_1, "r") // [11]

At [10], our input netmask is input into the buff_0x100_1 stack buffer as part of a command that is then run with popen at [11]. Since there’s no input filtering, we can easily inject arbitrary system commands to be run, resulting in command execution.

CVE-2024-39764 - dest injection

00403028      int32_t dest_1 = strdup(web_get("dest", arg1, 1))  
00403038      int32_t dest = dest_1
// [...]
004032d0          strcat(&buff_0x100_1, dest) // [12]
// [...]
00403380          if ((sx.d(*gateway) == 0 && sx.d(*interface) == 0) || (sx.d(*gateway) != 0 && sx.d(*interface) == 0))
// [...]
004033f0              label_4033f0:
004033f0              snprintf(&buff_0x100_1, 0x100, "%s dev %s ", &buff_0x100_1, custom_iface_potentially)
00403438              __builtin_strncpy(dest: &buff_0x100_1[strlen(&buff_0x100_1)], src: "2>&1 ", n: 6)
00403444              puts(&buff_0x100_1, 0x20, &buff_0x100_1)
00403460              int32_t popen_result = popen(&buff_0x100_1, "r") // [13]

At [12], our input dest is input into the buff_0x100_1 stack buffer as part of a command that is then run with popen at [13]. Since there’s no input filtering, we can easily inject arbitrary system commands to be run, resulting in command execution.

CVE-2024-39765 - custom_interface injection

00403118      int32_t interface_1 = strdup(web_get("interface", arg1, 1))  // [14]
00403128      int32_t interface = interface_1  
00403124      if (interface_1 == 0)
0040368c          interface = 0x4075bc
00403154      int32_t custom_interface_2 = strdup(web_get("custom_interface", arg1, 1))  // [15]
00403164      int32_t custom_interface = custom_interface_2  

// [...]
00403380          if ((sx.d(*gateway) == 0 && sx.d(*interface) != 0) || (sx.d(*gateway) != 0 && sx.d(*interface) != 0))
004033a0              if (strcmp(interface, &data_4071e0) == 0)
004036c0                  custom_iface_potentially = get_wanif_name()
004036bc                  goto label_4033f0
004033c0              if (strcmp(interface, "Custom") != 0)
004036dc                  custom_iface_potentially = get_lanif_name()
004036d8                  goto label_4033f0
004033d4              custom_iface_potentially = custom_interface // [16]
// [...]
00403380          if ((sx.d(*gateway) == 0 && sx.d(*interface) == 0) || (sx.d(*gateway) != 0 && sx.d(*interface) == 0))
// [...]
004033f0              label_4033f0:
004033f0              snprintf(&buff_0x100_1, 0x100, "%s dev %s ", &buff_0x100_1, custom_iface_potentially)
00403438              __builtin_strncpy(dest: &buff_0x100_1[strlen(&buff_0x100_1)], src: "2>&1 ", n: 6)
00403444              puts(&buff_0x100_1, 0x20, &buff_0x100_1)
00403460              int32_t popen_result = popen(&buff_0x100_1, "r") // [17]

At [14] and [15], our inputs interface and custom_interface are read in, and assuming that our interface input is set to “Custom”, then we end up utilizing our custom_interface POST parameter at [16]. Finally our custom_interface input is copied into the buff_0x100_1 stack buffer as part of a command that is then run with popen at [17]. Since there’s no input filtering, we can easily inject arbitrary system commands to be run, resulting in command execution.

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.