Talos Vulnerability Report

TALOS-2023-1864

Tp-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) web interface memory corruption vulnerability

April 9, 2024
CVE Number

CVE-2023-48724

SUMMARY

A memory corruption vulnerability exists in the web interface functionality of Tp-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) v5.1.0 Build 20220926. A specially crafted HTTP POST request can lead to denial of service of the device’s web interface. An attacker can send an unauthenticated HTTP POST 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 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/

CVSSv3 SCORE

7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/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 Wireless Access Point runs various services to manage the access point. One such service is httpd_portal which listens, by default, on ports 80, 443, 22080, 22443, 33443, 44443 and 33080.

A stack-based buffer overflow exists in httpd_portal within the functionality responsible for parsing x-www-form-urlencoded POST parameters. When an inbound POST request is received, it will eventually have the POST parameters extracted within a function at offset 0x40e940 named _http_parser_formArg. An annotated decompilation of this function is included below, for reference.

int _http_parser_formArg(http_request_t* request) {
	char content[0x801];
	char key[0x801];
	char value[0x801];
	int result;

	memset(&content, 0, 0x801);
	memset(&key, 0, 0x801);
	memset(&value, 0, 0x801);

	if (request == NULL) {
		result = -1;
	} else {
		int content_length = request->content_length;
		if (strncmp(&request->content_type, "application/x-www-form-urlencoded", 0x21) != 0) {
			result = -1;
		} else {
			// [1] Receive `content_length` bytes into &content, where `content_length` is 
            //     derived from the Content-Length Header
			int rxd_bytes = _http_partial_recv(request->server, &content, content_length);
			request->content_length -= rxd_bytes;
			char* current_position = &content;
			int num_bytes = rxd_bytes;

A portion of the HTTP request is extracted into the content stack buffer via a function we refer to as _http_partial_recv. The function will try to read content_length (or 0x1000, whichever is less) bytes of the request into content. Notably, this call can be manipulated into overflowing the content buffer, but the limitation on the max size of 0x1000 stops this overflow from extending beyond the end of the key stack buffer.

			while (num_bytes > 0) {
				if (request->num_params >= 0x28) {
					break;
				}
				// [2] Create a pointer to the start of the key
				char* key_start = current_position;
				int key_len = 0;
				// [3] Iterate along the string until '=', is found,
				//     incrementing `key_len` for each character in the key
				while (*current_position != '=') {
					if (num_bytes <= 0) {
						break;
					}
					key_len++;
					current_position++;
					num_bytes--;
				}
				if (i == 0) {
					break;
				}

This is the portion of the function responsible for recovering POST parameter key names. It does so by keeping a copy of a pointer to the start of the key string in key_start (at [2]) and then iterating over each character until it finds ‘=’ or runs out of data (at [3]), incrementing key_len for each acceptable character.

				i--;
				current_position++;
				// [4] Similarly, iterate along the remainder of the string until
				//     the string has ended or '&' is found, denoting the next key/value pair
				char* value_start = current_position;
				int value_len = 0;
				while (*current_position != '&') {
					if (i <= 0) {
						break;
					}
					value_len++;
					current_position++;
					i--;
				}
				if (i > 0) {
					current_position++;
					i--;
				}

Similarly, immediately following a found ‘=’ character, the code will take the same set of steps to recover the value of the POST parameter. Notably, this will calculate a length for the value without regard for whether that length is larger than the allocated 0x801 bytes.

				memset(&key, 0, 0x801);
				memset(&value, 0, 0x801);
				// [6] Finally, copy the key and value into their respective stack buffers, utilizing the
				//     calculated length of the attacker controlled strings, instead of the max size of the buffer
				strncpy(&key, key_start, key_len);
				strncpy(&value, value_start, value_len);
				...
			}
		}
	}
}

Finally, the key and value stack buffers are prepared to receive a copy of the key=value pair by setting their content to \0 (as these buffers are reused between key=value pairs). These memset calls introduce some difficulty in further exploiting this vulnerability, as the layout of the stack means that when key is memset, every byte after the 0x804th byte of content will be set back to \0. This is where the data we intend to use to overflow the strncpy call for value is supposed to live. Working around this to craft an example exploit that fully corrupts the stack appears to be impossible due to these memset calls. However, we can leverage the null-byte padding functionality of strncpy to corrupt the stack frame with zeros and crash the process by attempting to return to 0x00000000. So long as an unauthenticated attacker submits a key=value pair where the value is 0x815 bytes or longer, they can crash the httpd_portal service, which can only be restarted by restarting the entire access point. We believe that for devices being managed by Omada, this would make them unmanagable without a restart, as Omada manages these devices via their web interfaces.

Crash Information

Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────── registers ────
$zero: 0x0       
$at  : 0x7fff1dc0  →  0x00000000
$v0  : 0x0       
$v1  : 0x0       
$a0  : 0x77cc71f8
$a1  : 0x1       
$a2  : 0x2       
$a3  : 0x00474f7a  →  0x00002f64
$t0  : 0x77cc71f8
$t1  : 0x0       
$t2  : 0x1       
$t3  : 0x20697320 (" is "?)
$t4  : 0xfffffffe
$t5  : 0x1       
$t6  : 0x0       
$t7  : 0x7420696e ("t in"?)
$s0  : 0x0       
$s1  : 0x77c8d720
$s2  : 0x7fff6e68  →  0x00000000
$s3  : 0x7fff6f24  →  0x7fff6fd3  →  "httpd_portal"
$s4  : 0x1       
$s5  : 0x00403b28  →  0x3c1c0009
$s6  : 0x0040d7d0  →  <main+0> addiu sp, sp, -32
$s7  : 0x0044e634  →   nop 
$t8  : 0x8       
$t9  : 0x77c108fc
$k0  : 0x400     
$k1  : 0x0       
$s8  : 0x0       
$pc  : 0x0       
$sp  : 0x7ffc8c58
$hi  : 0x8       
$lo  : 0x0       
$fir : 0x0       
$ra  : 0x0       
$gp  : 0x0049a7d0  →  0x00000000
──────────────────────────────────────────────────────────────────────────────── code:mips:MIPS32 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x0
───────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "httpd_portal", stopped 0x0 in ?? (), reason: SIGSEGV
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.