CVE-2023-49907,CVE-2023-49910,CVE-2023-49911,CVE-2023-49908,CVE-2023-49912,CVE-2023-49909,CVE-2023-49906,CVE-2023-49913
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.
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
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/
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
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.
0045ab7c strncpy(&wrpOpDo_params.ssid, &ssid_copy, ssid_len);
0045aad8 band = &band[strcpy_until(band, '\n', &band_copy)];
0045abc8 strncpy(&wrpOpDo_params.profile, &profile_copy, profile_len);
0045ab38 action = &action[strcpy_until(action, '\n', &action_copy)];
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
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.
00422420 int32_t band_len = strcpy_until(band_1, '\n', &band_copy);
004224b0 strncpy(p_wrpOpDo_profile, &profile_copy, strlen(&profile_copy));
00422448 int32_t action_len = strcpy_until(action, '\n', &action_copy);
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
2023-12-11 - Vendor Disclosure
2024-04-03 - Vendor Patch Release
2024-04-09 - Public Release
Discovered by the Vulnerability Discovery and Research team of Cisco Talos.