Talos Vulnerability Report

TALOS-2023-1888

Tp-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) web interface Radio Scheduling stack-based buffer overflow vulnerability

April 9, 2024
CVE Number

CVE-2023-49907,CVE-2023-49910,CVE-2023-49911,CVE-2023-49908,CVE-2023-49912,CVE-2023-49909,CVE-2023-49906,CVE-2023-49913

SUMMARY

A stack-based buffer overflow vulnerability exists in the web interface Radio Scheduling functionality of Tp-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) v5.1.0 Build 20220926. A specially crafted series of HTTP requests can lead to remote code execution. An attacker can make an authenticated HTTP request 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.

Tp-Link N300 Wireless Access Point (EAP115) v5.0.4 Build 20220216
Tp-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) v5.1.0 Build 20220926

PRODUCT URLS

AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) - https://www.tp-link.com/us/business-networking/omada-sdn-access-point/eap225/ N300 Wireless Access Point (EAP115) - https://www.tp-link.com/us/business-networking/ceiling-mount-access-point/eap115/

CVSSv3 SCORE

7.2 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
##### CWE

CWE-121 - Stack-based Buffer Overflow

DETAILS

The EAP225(US) AC1350 Access Point is a wireless access point from TP-Link offering native integration with tp-link Omada Cloud SDN for centralized cloud management and zero-touch provisioning.

The EAP225 and EAP115 Wireless Access Points run various services to manage the access points. One such service is httpd_portal on the EAP225 (httpd on the EAP115), which listens on ports 80, 443, 22080, 22443, 33443, 44443 and 33080. By default, these services run as the root user. The web interfaces exposes a scheduling facility that allows an administrative user to apply a schedule for when specific managed SSIDs and radios should be enabled. This functionality is exposed via the URI /data/scheduler.association.json.

The function assigned to handle POST requests to this URI, named postScheAssocSsidDataJson, is located at offset 0x45c8f0 of the binary httpd_portal when shipped with v5.1.0 Build 20220926 of the EAP225, and at offset 0x42210c of the binary httpd when shipped with v5.0.4 Build 20220216 of the EAP115. While the names differ between access points, and the underlying HTTP servers are slightly different, these two functions are very similar and indicate they likely have a shared ancestor code base. For the initial portion of this report we will be discussing the function as recovered from the EAP225 firmware.

We are most interested in a function that gets dispatched specifically when the POST param operation is any value other than read or load. When using the web interface to store changes to the scheduling, this value is commonly set to save. As we will show below, the value is not very tightly constrained.

0045c8f0  void* postScheAssocSsidDataJson(struct request_t** ptrRequest)
0045c8f0  {
0045c914      int32_t inputParameters = 0;
0045c928      int32_t retval;
0045c928      if (arg1 == 0)
0045c928      {
0045c934          retval = -1;
0045c934      }
0045c948      else
0045c948      {
0045c948          struct request_t* req = *ptrRequest;
0045c960          // [1] Extract the `operation` parameter from the POST body
0045c960          char* operation_param = http_get_param(req, "operation");
0045c978          if (operation_param == 0)
0045c978          {
0045c99c              printf("[HTTPSCHEDULE-ERROR], [%s, %d]operations is NULL\n", "postScheAssocSsidDataJson", 0x67a);
0045c9ac              retval = -1;
0045c9ac          }
0045c9d0          else
0045c9d0          {
0045c9d0              // [2] Check if the `operation` parameter is "read"
0045c9d0              int32_t operation_matches_read = strncmp(operation_param, "read", 4);
0045c9dc              int32_t operation_matches_load;
0045c9dc              if (operation_matches_read != 0)
0045c9dc              {
0045c9fc                  // [3] Check if the `operation` parameter is "load"
0045c9fc                  operation_matches_load = strncmp(operation_param, "load", 4);
0045ca08                  if (operation_matches_load != 0)
0045ca08                  {
0045ca5c                      // [4] If `operation` is not "read" or "load" call the vulnerable function responsible for `saving` the schedule
0045ca5c                      _http_wsche_assocSsidBuildJsonTo(req, _http_wsche_saveAssocSsid(req, &inputParameters), inputParameters);
0045ca6c                      retval = 2;
0045ca6c                  }
0045c9dc              }
0045ca08              if ((operation_matches_read == 0 || (operation_matches_read != 0 && operation_matches_load == 0)))
0045ca08              {
0045ca14                  getScheAssocSsidDataJson(arg1);
0045ca24                  retval = 2;
0045ca24              }
0045c9dc          }
0045c9dc      }
0045ca84      return retval;
0045ca84  }

An appropriately formatted POST request will be passed into _http_wsche_saveAssocSsid, which further extracts the ssid, profile, action and band parameters. When configuring schedules for multiple SSIDs, the parameters for each are passed as newline-delimited lists. For example, when scheduling both 2.4GHz and 5GHz bands to only be enabled on weekdays, the POST request parameters would look like this.

operation: save
ssid:      TP-Link_2.4GHz\nTP-Link_5GHz
band:      2.4GHz\n5GHz
profile:   Weekdays\nWeekdays
action:    1\n1

We note this only so that the future references to a newline delimeter make sense, although they are not necessary for exploitation of this vulnerability.

Below, is an annotated decompilation of the vulnerable http_sche_saveAssocSsid function which is responsible for parsing these parameters and individually saving the configuration for each set of parameters.

0045a810  int32_t _http_wsche_saveAssocSsid(struct request_t* req, int32_t* inputParameters)
0045a810  {
0045a810      // [5] Initialize stack variables to store copies of POST parameters
0045a86c      uint8_t ssid_copy[0x21] = {0};
0045a86c      uint8_t profile_copy[0x20] = {0};
0045a88c      uint8_t action_copy[0x4] = {0};
0045a890      uint8_t band_copy[0x8] = {0};
0045a8b0      uint8_t wrpOpDo_params[0x70] = {0};

First, several stack variables are initialized. These _copy variables will end up holding the individual newline-delimited parameters. wrpOpDo_params is a specifically formatted structure that is passed via unix-socket IPC to a binary responsible for the underlying implementation of radio scheduling.

0045a8c4      int32_t retval;
0045a8c4      if (inputParameters == 0)
0045a8c4      {
0045a8e8          printf("[HTTPSCHEDULE-ERROR], [%s, %d]input parameters is NULL.\n", "_http_wsche_saveAssocSsid", 0x45c);
0045a8f8          retval = -1;
0045a8f8      }
0045a91c      else
0045a91c      {
0045a91c          // [6] Extract the initial `ssid`, `profile`, `action`, and `band` parameters
0045a91c          char* ssid = http_get_param(req, "ssid");
0045a938          char* profile = http_get_param(req, "profile");
0045a954          char* action = http_get_param(req, "action");
0045a970          char* band = http_get_param(req, "band");
0045a97c          if (ssid == NULL || profile == NULL || action == NULL || band == NULL)
0045a97c          {
0045a9d0              printf("[HTTPSCHEDULE-ERROR], [%s, %d]input parameters is NULL.\n", "_http_wsche_saveAssocSsid", 0x466);
0045a9e0              retval = -1;
0045a9e0          }
0045a98c          else
0045a98c          {

At this point ([6]), pointers to the four expected parameters are extracted from the request and their existence is confirmed.

0045adfc              // [7] This endpoint is used to update the schedule for multiple radios, so the values for each parameter
0045adfc              //     are expected to be new-line delimited, and this while loop iterates over each set of parameters
0045adfc              while (true)
0045adfc              {
0045adfc                  if (*ssid != 0 && *profile != 0 && *action != 0 && *band != 0)
0045adfc                  {
0045adfc                      // [8] Reset copy buffers for next iteration of loop
0045aa04                      memset(&ssid_copy, 0, 0x21);
0045aa28                      memset(&band_copy, 0, 8);
0045aa4c                      memset(&profile_copy, 0, 0x20);
0045aa70                      memset(&action_copy, 0, 4);
0045aa70
0045aaa8                      // [9] For each parameter, copy from the current offset of `<param>` into `<param>_copy` until a `\n` or `\0` is found. 
0045aaa8                      //     Given that `strcpy_until` returns the length of the value copied, the pointer in `param` is updated to point
0045aaa8                      //     to the next value to be handled.
0045aaa8                      //     Observe that there are no limits on the lengths of the value being copied, so overflows are straightforward
0045aaa8                      ssid    = &ssid[strcpy_until(ssid, '\n', &ssid_copy)];
0045aad8                      band    = &band[strcpy_until(band, '\n', &band_copy)];
0045ab08                      profile = &profile[strcpy_until(profile, '\n', &profile_copy)];
0045ab38                      action  = &action[strcpy_until(action, '\n', &action_copy)];

The first set of vulnerable function calls appears above, at [9]. A function we refer to as strcpy_until copies string data from the first parameter into the third parameter, until either the delimiter character is found or a null-byte is found. This is done without regard for the size of the destination buffer, and at no point is the length of the input checked. Each of the four parameters (ssid, band, profile, action) can be individually used to corrupt the stack as a result of the strcpy_until calls. strcpy_until returns the number of bytes copied, and that value is used to advance the pointer of each parameter to the next potential value in the new-line delimited list.

0045ab54                      // [10] Dominating corruptor for `ssid` parameter
0045ab54                      int32_t ssid_len = strlen(&ssid_copy);
0045ab7c                      strncpy(&wrpOpDo_params.ssid, &ssid_copy, ssid_len);
0045ab7c
0045ab7c                      // [11] Dominating corruptor for `profile` parameter
0045aba0                      int32_t profile_len = strlen(&profile_copy);
0045abc8                      strncpy(&wrpOpDo_params.profile, &profile_copy, profile_len);
0045abc8                      ...

Of note, the two strncpy calls here that populate the wrpOpDo_params structure can also overflow, and given that they are located further down the stack than the initial _copy variables, they will be the dominating corruptors for the ssid and profile parameters. The remainder of the function simply passes the constructed wrpOpDo_params structure into the IPC mechanism discussed earlier.

An attacker who can successfully submit an authenticated and appropriately malformed POST request to the /data/scheduler.association.json endpoint can cause one or more of several buffers to overflow, corrupting the stack and gaining control of code execution.

CVE-2023-49906 - EAP225 ssid

0045ab7c                      strncpy(&wrpOpDo_params.ssid, &ssid_copy, ssid_len);

CVE-2023-49907 - EAP225 band

0045aad8                      band    = &band[strcpy_until(band, '\n', &band_copy)];

CVE-2023-49908 - EAP 225 profile

0045abc8                      strncpy(&wrpOpDo_params.profile, &profile_copy, profile_len);

CVE-2023-49909 - EAP225 action

0045ab38                      action  = &action[strcpy_until(action, '\n', &action_copy)];

Crash Information

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────── registers ────
$zero: 0x0       
$at  : 0x1       
$v0  : 0xffffffff
$v1  : 0x0       
$a0  : 0x77cc71f8
$a1  : 0x1       
$a2  : 0x1       
$a3  : 0x0047f00f  →  0x005b4854
$t0  : 0x77cc71f8
$t1  : 0x0       
$t2  : 0x1       
$t3  : 0x19999999
$t4  : 0xfffffffe
$t5  : 0x1       
$t6  : 0x0       
$t7  : 0x400     
$s0  : 0x41414141 ("AAAA"?)
$s1  : 0x77c8d720
$s2  : 0x7fff6e58  →  0x00000000
$s3  : 0x7fff6f14  →  0x7fff6fca  →  "/usr/bin/httpd_portal"
$s4  : 0x1       
$s5  : 0x00403b28  →  0x3c1c0009
$s6  : 0x0040d7d0  →  <main+0> addiu sp, sp, -32
$s7  : 0x0044e634  →   nop 
$t8  : 0x8       
$t9  : 0x77c108fc
$k0  : 0x0       
$k1  : 0x0       
$s8  : 0x41414141 ("AAAA"?)
$pc  : 0x41414141 ("AAAA"?)
$sp  : 0x7ffc8740
$hi  : 0x1       
$lo  : 0x0       
$fir : 0x0       
$ra  : 0x41414141 ("AAAA"?)
$gp  : 0x0049a7d0  →  0x00000000
──────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "httpd_portal", stopped 0x41414141 in ?? (), reason: SIGSEGV

CVE-2023-49910 - EAP115 ssid

At this point, it becomes necessary to discuss the differences between the two different compilations of this function. In the EAP115, the two distinct functions discussed previously were in-lined and are one contiguous function, which slightly alters the layout of variables on the stack. There is also a second and much more significant difference, and that is in how the ssid and profile variables are copied into the wrpOpDoParam structure. In the above instance, references are taken and passed directly into the strncpy calls, whereas the EAP115-specific variables are allocated to store these reference pointers. Below is a decompilation of just the distinct portions of the function, as recovered from the EAP115.

                                      // [6] Take copies of pointers to the `ssid` and `profile` fields of the `wrpOpDo_params` structure
00422614                              uint8_t* p_wrpOpDo_ssid = &wrpOpDo_params.ssid;
0042261c                              uint8_t* p_wrpOpDo_profile = &wrpOpDo_params.profile;
00422624                              struct wrpOpDo_param* p_wrpOpDo_params = &wrpOpDo_params;

                                      // [7] This endpoint is used to update the schedule for multiple radios, so the values for each parameter
                                      //     are expected to be new-line delimited, and this while loop iterates over each set of parameters
00422640                              while (true)
00422640                              {
                                          // [8] Reset copy buffers for next iteration of loop
004223d8                                  memset(&ssid_copy, 0, 0x21);
004223f4                                  memset(&band_copy, 0, 0x8);
004223f8                                  memset(&profile_copy, 0, 0x20);
00422410                                  memset(&action_copy, 0, 0x4);
00422410
                                          // [9] For each parameter, copy from the current offset of `param` into `param_copy` until a `\n` or `\0` is found. 
                                          //     This differs slightly in that the pointers are not updated immediately, but later in a portion of the function we did not include
                                          //     Observe that there are no limits on the lengths of the value being copied, so overflows are straightforward
0042240c                                  int32_t ssid_len = strcpy_until(ssid, '\n', &ssid_copy);
00422420                                  int32_t band_len = strcpy_until(band_1, '\n', &band_copy);
00422434                                  int32_t profile_len = strcpy_until(profile, '\n', &profile_copy);
00422448                                  int32_t action_len = strcpy_until(action, '\n', &action_copy);

                                          // [10] Copy the `ssid` and `profile` parameter values into the respective fields of the `wrpOpDo_params` structure
                                          //      as pointed to through `p_wrpOpDo_ssid` and `p_wrpOpDo_profile`
                                          //      Since these pointers point into a structure allocated on the stack, these copies are also buffer overflows
                                          //      and the second strncpy is the dominating overflow, given its position on the stack.
0042247c                                  strncpy(p_wrpOpDo_ssid, &ssid_copy, strlen(&ssid_copy));
004224b0                                  strncpy(p_wrpOpDo_profile, &profile_copy, strlen(&profile_copy));
004224d8                                  wrpOpDo_params.action = atoi(&action_copy);
004224f4                                  ...

Most significantly, the pointers copied at [6] are stored into p_wrpOpDo_ssid and p_wrpOpDo_profile, which are located in the region of the stack following the wrpOpDo structure and the _copy pointers. This allows for certain interesting attacks, where an attacker can craft a write-what-where primitive by corrupting only the pointers stored in p_wrpOpDo_ssid and p_wrpOpDo_profile. The string provided in ssid or profile would then be strncpy‘d into the attacker-controlled destination address. It also complicates a succesful corruption of the return pointer. Successful exploitation of this vulnerability will either require an info-leak to disclose a writable pointer or a very lucky guess to keep the strncpy calls at [10] from crashing the process. Corruption of the return address is then possible, potentially resulting in remote code execution.

CVE-2023-49911 - EAP115 band

00422420                                  int32_t band_len = strcpy_until(band_1, '\n', &band_copy);

CVE-2023-49912 - EAP115 profile

004224b0                                  strncpy(p_wrpOpDo_profile, &profile_copy, strlen(&profile_copy));

CVE-2023-49913 - EAP115 action

00422448                                  int32_t action_len = strcpy_until(action, '\n', &action_copy);
VENDOR RESPONSE

The vendor released new firmware at: https://www.tp-link.com/us/support/download/eap115/v4/#Firmware https://www.tp-link.com/us/support/download/eap225/v3/#Firmware

TIMELINE

2023-12-11 - Vendor Disclosure
2024-04-03 - Vendor Patch Release
2024-04-09 - Public Release

Credit

Discovered by the Vulnerability Discovery and Research team of Cisco Talos.