
Executive Summary
Consumer routers are the most widely deployed embedded Linux systems on the planet, and they are also among the least scrutinised. This research walks the entire chain on a single device — the Mercusys MB115-4G, which at the time of the original write-up was the best-selling router on Amazon Spain — from ordering the unit to holding a root shell, a full flash dump, and finally a CVE. The first half is pure hardware and firmware methodology: pulling both published firmware images and diffing them, reading /etc/inittab to learn that a serial console exists at 115200 baud before the device even arrives, opening the case, identifying the Ethernet transformer, the SPI flash, the 4G LTE module and an unlabelled UART header, then locating GND, VCC and TX with nothing more sophisticated than a voltmeter. The credentials recovered from a passwd.bak file that the boot script restores on every single boot turn that serial header into an unauthenticated root shell.
The second half is the vulnerability. Inside /usr/bin/httpd, the login endpoint /cgi/login is registered as reachable before authentication. It hands an attacker-supplied, AES-encrypted and base64-encoded blob to a decryption helper that works with a 2048-byte plaintext buffer, then copies the decrypted length into a caller-side stack buffer that is only 512 bytes — with no bounds check whatsoever. The binary is MIPS32 little-endian with no stack canary, no PIE, an executable stack and RWX segments, which is to say the exploitation environment of roughly 1997. The bug became CVE-2026-12495. This post covers the root cause, why the researcher’s own proof of concept was blocked by an unrelated 1024-character URL length cap, how a binary diff proved the flaw had survived untouched into firmware v1.9.0, and what the vendor actually changed in the patch — which turned out to be considerably more than a single bounds check.
1. Reconnaissance Before the Package Arrived
The research began before the hardware was physically in hand. Waiting for a delivery is dead time that can be spent building a model of the target, and for a mass-market consumer device there is usually a surprising amount of material available publicly. Two tracks were pursued in parallel: everything that could be learned about the physical device, and everything that could be learned about the software it ships with.
FCC ID and public teardown databases
The standard opening move for hardware reconnaissance is the FCC ID. Any device sold in the United States that emits radio energy must be certified, and the certification file is public — it routinely includes internal photographs, block diagrams, antenna placement and occasionally component datasheets. For a 4G router, that filing would have handed over the board layout for free.
The MB115-4G was not indexed in the FCC ID database at all. That absence is itself a finding rather than a dead end: no FCC certificate means the device cannot legally be distributed or operated in the US. Follow-up searches on the usual community teardown repositories — WikiDevi and the OpenWrt hardware tables, both of which frequently carry board photos and flash chip part numbers for cheap routers — also came back empty. The internals would have to be mapped by hand.
Two firmware images and no clear “latest”
The firmware side was more productive. The vendor support page offered two downloadable images, and the versioning was contradictory in a way that is worth pausing on: the publication dates suggested one image was newer, while the version numbers embedded in the filenames suggested the opposite. When a vendor cannot unambiguously communicate which build is current, neither can a user — and neither can a defender trying to work out whether a fleet of devices is patched.

Both images were downloaded and unpacked with binwalk, which located and extracted the SquashFS root filesystem from each. The kernel and the U-Boot bootloader were given a quick look but were not the focus — the userland is where the attack surface lives on this class of device. Comparing the two extracted filesystems with meld and diff showed the differences were cosmetic: a handful of additions to the web interface strings so that the same firmware family could identify and support other router models. Since the two were functionally equivalent, version 1.6.0 was chosen for the initial analysis.
From SquashFS to init: reading /etc/inittab
With a root filesystem on disk, the first thing worth reading is the init configuration. init is the first userland process the kernel starts, and its configuration file describes what the system does the moment it finishes booting — including, critically for hardware work, which serial devices it attaches a console to.

Two lines carried the useful information:
- The first line shows that the very first action on boot is executing the
/etc/init.d/rcSscript — the next file to read. - The second line is the prize. There is a serial interface on
ttyS1, the second serial port of the CPU (the first beingttyS0), configured at a baud rate of 115200 — a completely standard speed. This is advance confirmation that a serial console exists and what parameters it expects, before a screwdriver has touched the case.
The rcS script was equally informative. A comment in it names the SoC family outright, which is exactly the sort of detail the missing FCC filing would otherwise have provided:
# 7628 watch dog
The MediaTek MT7628 is the standard SoC in this price bracket — a MIPS24KEc core, which sets expectations for the architecture of every binary that will later be loaded into a decompiler. Further down, the same script contains a cluster of security-relevant operations:
/bin/mkdir -m 0777 -p /var/https
/bin/mkdir -m 0777 -p /var/lock
/bin/mkdir -m 0777 -p /var/log
[...]
cp -p /etc/passwd.bak /var/passwd
[...]
telnetd
[...]
Three separate problems are visible in those few lines:
- Maximum permissions for everyone on a long list of
/vardirectories. Mode0777means any process on the system, at any privilege level, can create, modify and delete files there — including log directories and the HTTPS working directory. telnetdlaunched with no arguments. Telnet is a cleartext protocol with no transport security; starting the daemon with no restrictions raises the obvious question of who is able to reach that console.- The password database is restored from a backup on every boot. The line
cp -p /etc/passwd.bak /var/passwdmeans that whateverpasswd.bakcontains becomes the live account database each time the device powers on. Any credential change a user makes to that file does not survive a reboot — and whatever is baked into the firmware image is permanent.
passwd.bak: the credentials that never change
Given that passwd.bak is authoritative on every boot, it is the obvious next file to open.

Three accounts stand out:
- dropbear — the service account for the Dropbear SSH daemon. Unremarkable in itself, but it confirms an SSH service exists on the device.
- nobody — by universal Unix convention this is the unprivileged account, the identity you drop to when a process should be able to do as little as possible. Here it carries UID 0 and GID 0. That is root. Any process that “drops privileges” to
nobodyon this device drops nothing at all, and any bug that lets an attacker execute code asnobodyyields full system control. This is a serious and entirely gratuitous misconfiguration. - admin — not named root, but functionally equivalent. Its password hash uses MD5-crypt (the
$1$prefix) and, critically, carries no salt. An unsalted hash means precomputation attacks work, identical passwords across every unit of the product line produce identical hashes, and cracking is trivially parallelisable.
The admin hash was fed to hashcat with the rockyou.txt wordlist. It fell in a couple of seconds. The password is 1234. Because rcS restores this file at every boot, that credential is not a “default that the user should change” — it is a permanent property of the firmware. It would prove immediately useful once the serial console was live.
With that, the pre-arrival recon was complete and the analysis paused until the physical unit was available, so that the firmware actually shipped on the device could be examined rather than the one published on the website.
A postscript worth recording, noted at the time of writing the original post: switching the firmware download page from Spanish to English exposes two additional firmware images, both newer than anything offered in the Spanish section. Region-dependent firmware availability on the same product page is an odd distribution practice, and it means that “the latest firmware” is a function of which language you happen to be browsing in.
2. Hardware Analysis
With the device in hand, the goal of this phase is to identify the main components on the board and understand what each one does — both to find an entry point and to know what is worth attacking later. Getting inside was straightforward: two small screws on the back panel.


After removing the screws, a flat screwdriver was used to release the plastic clips and fully separate the shell — an operation the author admits was awkward, and a good argument for keeping a proper spudger in the toolkit rather than risking gouged plastic and slipped blades. With the case open, several components are immediately identifiable.

1. Ethernet transformer

A magnetic component sitting immediately behind the RJ45 ports. Its job is galvanic isolation: it separates the electrical domain of the network cable from the board’s own domain, protecting the internal circuitry from voltage spikes arriving over the wire and filtering electromagnetic noise so that data integrity is preserved across the Ethernet link. Not an attack surface, but knowing what it is prevents wasting time on it.
2. UART (serial console port)

This is the physical “backdoor” left behind for developers. Attaching a USB-to-TTL adapter to these pads makes it possible to watch the kernel boot logs scroll past and, if the console is not properly locked down, to drop straight into a root shell. Readers who want the protocol fundamentals can consult Analog Devices’ primer on UART as a hardware communication protocol.
The header was not silkscreened with any labels, so the pinout had to be determined empirically with a voltmeter. The procedure is the standard one: find GND first (continuity to a known ground plane or shield, with the board unpowered), then apply power and probe the remaining pads looking for one sitting at a constant 3.3 V — that is VCC — and one whose voltage fluctuates as the device boots and emits characters — that is TX. The remaining pad, typically idling high with no traffic, is RX.
3. SPI flash memory

The router’s “hard drive”. This chip holds the bootloader, the kernel, the root filesystem and the configuration partitions — that is, the firmware. It is the primary target for extraction, because a byte-accurate dump of this chip is what enables full static analysis of every binary on the device.
4. 4G LTE module

A self-contained cellular communication module soldered onto the main board. It handles the connection to mobile networks, SIM card authentication and data transmission. From an attack-surface perspective it is effectively a second computer sharing the board — it has its own firmware, its own baseband stack, and it typically talks to the host SoC over a serial or USB link using AT commands.
5. Main EMI shield (SoC and RAM)
The metal cage covering the most sensitive part of the board is an Electromagnetic Interference shield. Underneath it sit the SoC — the MediaTek part the rcS comment already gave away — and the RAM. Shields like this are usually soldered or clipped down and can be removed, but at this stage there was no need: the UART header already offered a path in.
3. Connecting via UART to Access the System
With the components mapped, the next step is establishing a foothold. The UART is the obvious candidate: the voltmeter probing showed activity on the header, and the firmware recon had already confirmed that inittab attaches a console to a serial port at 115200 baud. Two independent sources pointing at the same interface is as strong a signal as this stage of an assessment produces.
A reliable electrical connection comes first. There are several ways to achieve this — spring-loaded pogo pins, test clips, or simply holding jumper wires against the pads and hoping — but the durable option is to solder a pin header directly to the interface and connect a TTL-to-USB adapter to it. That is the route taken here, and it is the right call for any target you expect to reboot dozens of times.

The wiring itself has two rules that matter. First, VCC is deliberately left unconnected: both the adapter and the router board have their own power supplies, and bridging them invites current to flow in directions nobody planned for. Only GND, TX and RX are wired, with TX and RX crossed over between the two devices. Second, the adapter must be switched to 3.3 V logic level before anything is plugged in — feeding 5 V into a 3.3 V SoC pin is a good way to destroy the target permanently.


With the adapter plugged into the host it enumerates as a serial device — /dev/ttyUSB0 in this case — and a terminal program is pointed at it with the parameters both ends must agree on:
screen /dev/ttyUSB0 115200
115200 is the rate inittab disclosed, and the remaining parameters are left at the defaults (8 data bits, no parity, 1 stop bit). If the baud rate or framing is wrong, the terminal fills with garbage instead of readable text; the fallback in that situation is to put a logic analyser on TX and measure the actual bit timing to derive the correct rate, rather than guessing through the standard values.
Powering the router produced clean, legible boot output immediately, confirming the parameters were correct. The full boot log was saved to a file for later analysis — boot logs on embedded devices routinely leak partition layouts, kernel command lines, driver versions and debug messages that are useful long after the initial connection.

Buried in the boot output is a message inviting the user to press ENTER to activate the console, which then presents a username and password prompt. So the console is not wide open — there is at least an authentication gate. Trying the obvious candidates such as root or admin without a valid password gets nowhere.

This is where the pre-arrival firmware work pays for itself. The credentials recovered from passwd.bak and cracked with hashcat — admin / 1234, restored from backup on every boot — are entered instead.

The credentials are valid, and the session is a root shell. The gate on the serial console is only as strong as a static, unsalted, four-digit password baked into every unit of the product line — which is to say, not a gate at all for anyone with five minutes and a screwdriver.
4. Dumping the Firmware
Root access on the running device opens a shortcut. The traditional way to obtain firmware is to desolder the SPI flash chip or clip onto it with a programmer such as a CH341A and read it out externally — reliable, but slow, fiddly and risky for the board. With a root shell already available, there is a far cheaper option.
Running busybox --list enumerates every applet compiled into the device’s BusyBox binary, and among them is tftp. That single applet is enough to move the entire flash contents off the device over the network, with nothing further attached to the PCB.
One methodological caveat deserves emphasis, and the original author is careful to state it. Reading /dev/mtd* copies the flash partitions as the kernel exposes them. On this device that is sufficient for static firmware analysis, but it should not be assumed identical to a dump taken with an external programmer: a hardware read covers the whole chip, including any data or regions that the kernel’s partition map does not expose. It also captures nothing of volatile RAM. The /var directory holds runtime state created after boot, which can be inspected separately — root access is already in hand, so nothing is lost, but the distinction between “everything on the chip” and “everything the kernel chose to show me” is a real one.
The extraction setup needs only a TFTP server running on the host machine (atftpd was used here) and an Ethernet cable from the host to one of the router’s LAN ports.

Before copying anything, the partition table is enumerated from /proc/mtd, which lists every MTD partition with its size, erase block size and name. This tells you how many partitions to iterate over and what each one is supposed to contain — bootloader, kernel, root filesystem, configuration, calibration data and so on.

With the TFTP server listening on the host, the following loop is run on the router. It reads each partition into a temporary file in /tmp, pushes it to the host over TFTP, then deletes the local copy — important, because /tmp on these devices is a small RAM-backed filesystem that would otherwise fill up and hang the extraction partway through.
cd /tmp
for i in 0 1 2 3 4 5 6 7 8; do
echo "Extracting mtd$i..."
cat /dev/mtd$i > mtd$i.bin
#The IP below is the one the router gave to my computer
tftp -p -l mtd$i.bin -r mtd$i.bin 192.168.1.101
rm mtd$i.bin
done
echo "DONE!"
That completes the dump of the router’s entire flash memory without an EEPROM reader. The recommended verification step is to compare MD5 hashes of the on-device partitions against the received files, confirming that nothing was lost or corrupted in transit — TFTP runs over UDP and has no strong integrity guarantees of its own.
Recap: Security Issues Found So Far
Before moving into binary analysis, it is worth consolidating what the hardware and configuration phase alone produced. None of the issues below required a decompiler, a fuzzer or an exploit — they came from reading configuration files and cracking one hash.
| Issue | Where it was found | Why it matters |
|---|---|---|
Weak default credentials (admin / 1234) | /etc/passwd.bak, cracked with hashcat + rockyou.txt | Grants a root shell over the serial console on every unit of the product line |
| Unsalted MD5 password hashes | /etc/passwd.bak ($1$ prefix, no salt) | Enables precomputation and makes identical passwords identifiable across devices |
User nobody running with UID 0 | /etc/passwd.bak | The conventional “least privilege” account is fully privileged, so privilege dropping is a no-op |
| World-writable directories (mode 0777) | /etc/init.d/rcS | Any process can tamper with logs and working directories under /var |
| Password file restored on every boot | cp -p /etc/passwd.bak /var/passwd in rcS | Credential changes do not persist; the baked-in password is permanent |
telnetd started with no arguments | /etc/init.d/rcS | A cleartext administrative protocol with no visible access restrictions |
The original plan from here was a follow-up post analysing the root filesystem binaries, the web interface and any exploitable memory-safety bugs. In practice the author moved on to other research (primarily Windows kernel work) and the write-up stalled — but the vulnerability hunting did happen. What follows is the root cause analysis added to the original post on 4 August 2026, once the bug was patched and the 90-day responsible disclosure window had closed.
CVE-2026-12495: A Pre-Auth Stack Buffer Overflow in the Mercusys MB115-4G
An honest framing note carried over from the original: this analysis was written up a considerable time after the research itself. The author’s own assessment is that it should have been documented while fresh, and what follows is reconstructed from the surviving material — the vulnerability report that was sent to the vendor, the Ghidra screenshots taken at the time, the Ghidriff output from the binary diff, and the vendor correspondence. That provenance is worth stating, because it is exactly the kind of context that gets lost when a finding is summarised into a CVE entry.
Target and affected builds
The vulnerability lives in /usr/bin/httpd, the web server on the Mercusys MB115-4G v1.0. The device is MIPS32 little-endian. The binary originally analysed came from firmware 1.7.0 0.9.1 v0001.0 Build 250414 Rel.59734n, with httpd MD5 7589198845cd709e020614f027144c46.
The same vulnerable logic was later confirmed present in the newest firmware that could be diffed directly, MB115-4G(EU)_V1_1.9.0 Build 20250928, with httpd MD5 3275a62e1b371060748e579e1e09539d. A newer v1.10 image existed, but that firmware image was encrypted or obfuscated to a degree that made the same direct binary comparison impossible with the materials available.
The bug itself is a textbook stack-based buffer overflow (CWE-121), produced by a size mismatch between what the calling function allocates and what the decryption helper is willing to write.
The affected route is reachable before authentication
The single most important property of any web-facing memory corruption bug is whether an attacker has to log in first. Here they do not. The routing table registers /cgi/login with g_http_author_all, meaning this code path is part of the login surface itself and is served to unauthenticated clients by design — it has to be, since it is the endpoint you use to authenticate.

An unusual request shape
The login request is a POST, but the encrypted payload does not travel in the body. It is passed through the URL query string in a data parameter, alongside a sign parameter, leaving the request with Content-Length: 0. This detail looks like a curiosity at first and turns out to be the single most consequential constraint on exploitation, for reasons covered further down.
The data field is AES encrypted and then base64 encoded, so what reaches the vulnerable code is attacker-controlled only after a decode-and-decrypt round trip — which is precisely why the length check that should exist further down is easy for a developer to overlook.

Caller and callee disagree about buffer size
Inside http_rpm_login, the handler pulls both values out of the request with http_parser_getEnv("sign") and http_parser_getEnv("data"). It then prepares a local destination buffer on its own stack frame and calls http_gdpr_decrypt, passing that stack buffer as the output argument.

That destination buffer is 512 bytes. Note what is missing from the call: the caller never tells the callee how large the output buffer is. The size is simply an assumption shared between two functions, enforced by nothing.

The callee, meanwhile, is built around far larger temporaries. In http_gdpr_decrypt, the buffer that receives the decrypted AES plaintext is 2048 bytes — four times the size of the destination it will eventually copy into.

The root cause: an unchecked memcpy
Everything converges on one line. After base64 decoding and AES decryption have completed, http_gdpr_decrypt copies data_len bytes into the output pointer it was handed by the caller:
memcpy(output_buffer, aes_plaintext_buff, data_len);
There is no check that data_len fits in the caller’s 512-byte destination. data_len is derived from the attacker’s decoded and decrypted input, and the source buffer can hold up to 2048 bytes. Any decrypted payload larger than 512 bytes therefore overflows the http_rpm_login stack frame — overwriting adjacent locals first, then saved registers, then the saved return address.

No mitigations at all
Architecturally this reads as a pre-authentication remote code execution primitive, and the binary’s build configuration does nothing to argue otherwise. httpd ships with no stack canary, no PIE, an executable stack and RWX segments. Each of those is individually significant; together they remove every obstacle that would normally stand between a stack overflow and arbitrary execution.
- No stack canary — the saved return address can be overwritten without detection, so there is no guard value to leak or forge.
- No PIE — the binary loads at a fixed address, so gadget and function addresses are known constants rather than something that must be leaked at runtime.
- Executable stack and RWX segments — shellcode can simply be placed in memory and jumped to. No ROP chain, no return-to-libc, none of the machinery modern exploitation requires.
As the original phrases it, this would be a trivial jump to shellcode, as if we were in the ’90s.

To restate the whole chain in one sentence: a pre-authenticated route feeds attacker-controlled encrypted data into a stack buffer, the copy length is taken from the decoded and decrypted input rather than from the destination’s capacity, and the binary lacks every protection that would otherwise turn this into a hard exploitation problem. In a cleaner input path, pushing past 512 bytes would overwrite local state and eventually the saved return address.
Why the researcher could not weaponise it
Validation ran into an obstacle in an unrelated part of the stack. The HTTP parser appeared to enforce a maximum URL length of roughly 1024 characters. Because the payload is transported in the query string rather than the POST body — the unusual request shape noted earlier — that cap directly limits how much plaintext can be delivered through the normal web interface.
After accounting for AES and base64 expansion overhead, the reachable plaintext size worked out to roughly 450 bytes, comfortably below the 512 bytes needed to reach the end of the destination buffer and begin corrupting the frame. The vulnerable code was demonstrably there and demonstrably reachable without credentials, but the transport imposed its own ceiling.
This is why the issue was initially reported as a serious latent application-layer bug rather than accompanied by a working proof of concept or exploit. Mercusys subsequently reported that their own internal validation had managed to trigger the overflow and crash httpd; how they bypassed or sidestepped the 1024-character limit observed during external testing is not known. That gap is a useful reminder that a parser-imposed constraint discovered from the outside is a description of one path, not a proof that no path exists — alternate encodings, chunked handling, or an internal code path that reaches the same function with a different length source can all reopen a case that looked closed.
Binary diffing: the bug survived into v1.9.0
Before reporting, it was worth establishing whether this was an old flaw already silently fixed in a later release. Ghidriff was used to diff the original vulnerable httpd against the newest firmware version available for analysis. The result: the vulnerable logic had survived intact into v1.9.0, with no bounds check added ahead of the final copy. The bug was live, not historical.
The vendor patch — and what else changed
Some time after the initial report, Mercusys responded with a signed beta firmware named Bugfix_Pre-Auth_Stack-based_Buffer_Overflow_(CWE-121)_MB115-4G(EU)v1_1.9.0_signed.bin and asked for verification of the fix. The firmware was extracted, the new httpd loaded into Ghidra, and the relevant functions compared once more.
The direct fix was the expected one. The decryption routine gained an additional parameter carrying the maximum size of the caller-provided output buffer — closing the information gap that caused the bug in the first place — and the final copy was placed behind a bounds check. In simplified form:
if (decrypted_len <= output_buffer_size) {
memcpy(output_buffer, aes_plaintext_buff, decrypted_len);
}
The patch went considerably further than the immediate overflow, however. Mercusys also replaced part of the RSA-based login flow with ECC-based decryption and added an HMAC check before the message is processed — changes that address the authenticity and integrity of the payload, not just its length.
In the vulnerable firmware, the initial payload decryption on this path used RSA:
memset(auStack_81c, 0, 0x800);
memset(acStack_201c, 0, 0x800);
/* Vulnerable firmware: RSA-based decrypt path */
iVar1 = http_rsa_decrypt(param_3, auStack_81c, 0x81, 0);
if (iVar1 == -1) {
__format = "[%s %d]#Msg: http_rsa_decrypt failed\n";
uVar3 = 0x4cc;
}
In the patched firmware, the same region had migrated to elliptic-curve cryptography, with an extra 0x41-byte buffer cleared alongside the existing two:
memset(acStack_818, 0, 0x800);
memset(acStack_2018, 0, 0x800);
memset(auStack_205c, 0, 0x41);
/* Patched firmware: migration to elliptic-curve cryptography */
iVar1 = http_ecc_decrypt(param_3, acStack_818, 1);
uVar3 = 0x526;
if (iVar1 == 0) {
(...)
}
The HMAC change is visible immediately after the AES block decryption. In the old flow, a successful AES decryption led straight toward the output copy — the decrypted bytes were trusted purely because they decrypted. In the patched flow, the message is authenticated first, and only then does execution reach the final copy and its new boundary check, guaranteeing both integrity and authenticity of the payload:
iVar1 = aes_tmp_decrypt_buf_nopadding_new(
auStack_1018,
auStack_1818,
&local_2060,
param_1 + 0x88,
param_1 + 0xa9
);
if (iVar1 == 0) {
/* New integrity verification */
iVar1 = http_check_HMAC(param_1 + 0x88, auStack_205c, auStack_1818);
if (iVar1 != 0) {
__format = "[%s %d]#Msg: cgi_gdpr check HMAC fail\n";
uVar3 = 0x54f;
goto LAB_0041f4fc;
}
(...)
}
Read together, the three changes form a coherent defence-in-depth response rather than a minimal patch: a stronger key exchange primitive, an authenticity check that rejects tampered payloads before they are parsed, and a length check that makes the copy safe even if everything upstream is bypassed. Vendors do not always respond to a single memory-safety report this thoroughly, and it is worth crediting when they do.
The impact discussion
The final phase of the disclosure was a disagreement about severity, which is where most coordinated disclosures end up. The researcher’s position was that the bug represented potential RCE: unauthenticated input, a stack overflow, and a binary with no protections whatsoever. Mercusys’ internal validation classified the demonstrated result as an httpd crash rather than confirmed remote code execution, and proposed a CVSS 4.0 score of 5.3 (Medium).
That classification was accepted for the coordinated disclosure process. The reasoning is candid and defensible: the researcher had initially assessed the bug as unexploitable through the tested path because of the 1024-character URL limit, and no proof of concept or working exploit was supplied. Severity scoring rewards demonstrated impact, and without a PoC there was little basis to argue the number upward — even though the underlying code pattern, on a binary with an executable stack and no ASLR, is about as favourable to an attacker as memory corruption gets.
Disclosure Timeline
| Date | Event |
|---|---|
| 22-02-2026 | Initial report sent to Mercusys |
| 22-04-2026 | Response from the vendor. They could not decrypt the encrypted report. It was sent again |
| 12-05-2026 | Vendor sent the patch for researcher feedback |
| 17-06-2026 | INCIBE reserved the ID CVE-2026-12495 |
| 03-08-2026 | Patch: MB115-4G(EU)_V1_1.11.0 released to the public |
| 04-08-2026 | Post updated with the vulnerability writeup |
The two-month gap between the initial report and the first vendor response is worth noting, as is its cause: the vendor was unable to decrypt the encrypted report and it had to be resent. Encrypted reporting channels are good practice, but a PGP key that the recipient cannot actually use turns a security contact into a black hole. Verifying that the vendor can read an encrypted submission — ideally with a trivial test message before the real report — saves weeks.
Key Takeaways
- Firmware recon before the hardware arrives pays for itself. Reading
inittabfrom a downloaded image supplied the serial port and baud rate, andpasswd.baksupplied the credentials that turned the UART header into a root shell. The physical work was reduced to confirming what the firmware had already disclosed. - An unlabelled UART header is not protection. A voltmeter, a known ground and a few minutes of probing recover the pinout — GND by continuity, VCC by constant 3.3 V, TX by fluctuating voltage during boot.
- A static password baked into the firmware image is not a credential, it is a constant. Because
rcSrestorespasswd.bakon every boot,admin/1234is a permanent property of every unit shipped, not a default a user can meaningfully change. - UID 0 on the
nobodyaccount silently voids privilege separation. Every “we drop to nobody” claim in the codebase becomes meaningless, and any code execution as that user is code execution as root. - Root on the device beats a hardware programmer for firmware extraction — BusyBox
tftpplus/dev/mtd*dumps the flash over a LAN cable — but the two are not equivalent. A kernel-mediated dump only shows what the partition map exposes, and captures nothing of RAM. - The bug class here is a contract mismatch, not a coding slip. A function that writes into a caller-supplied pointer without being told the destination’s capacity is an accident waiting for the right input; the patch fixed it by passing the size explicitly, which is the correct structural remedy.
- Missing mitigations turn a medium-severity crash into a plausible RCE. No canary, no PIE, executable stack and RWX segments on an internet-adjacent MIPS daemon in 2026 is a build-configuration failure independent of any individual bug.
- An external constraint that blocks your PoC is not proof of non-exploitability. The 1024-character URL cap stopped the researcher’s path; the vendor’s own testing still triggered the overflow.
Defensive Recommendations
- Update MB115-4G units to v1.11.0 or later. This is the public release containing the CVE-2026-12495 fix. Check the English-language download page as well as your local one — firmware availability differs by page language on this vendor’s site.
- Treat consumer 4G routers as untrusted network segments. Place them behind your own firewall, never expose their management interface to the WAN or cellular side, and do not rely on the device’s own access control as a security boundary.
- Never ship a password database inside a firmware image, and never restore one on boot. Credentials should be generated per device at first boot and stored in a writable configuration partition. If a default must exist, make it per-unit and printed on the label, and force a change before the device routes traffic.
- Audit every account for UID 0. A one-line check over an extracted root filesystem (
awk -F: '$3==0' etc/passwd) catches thenobody-as-root class of misconfiguration. Extend the same check topasswd.bakand any other credential file the boot scripts touch. - Build network-facing binaries with mitigations enabled. Stack protector, PIE, RELRO, non-executable stack and no RWX segments are compiler and linker flags, not architecture work. Add a CI gate that runs
checksecagainst every shipped binary and fails the build when a protection is missing. - Ban unbounded copies into caller-supplied buffers. Any helper that writes through an output pointer must take the destination capacity as a parameter. Grep the codebase for
memcpy,strcpyandsprintfwhere the length derives from decoded or decrypted input, and treat every hit as a finding until proven otherwise. - Authenticate before you parse. The patched flow verifies an HMAC over the decrypted message before touching it. Decryption is not authentication — treating “it decrypted successfully” as evidence of trustworthiness is the assumption that let attacker-controlled length reach a raw copy.
- Disable or remove
telnetdand lock down the serial console. Cleartext administration daemons should not ship enabled. Where a console is required for support, gate it behind a per-device secret rather than a firmware-wide one. - Verify your vulnerability-disclosure inbox actually works. Two months of this timeline were lost because the vendor could not decrypt the report. Test your PGP key and monitored address on a dummy submission periodically.
Conclusion
Nothing in this research required exotic equipment or novel technique — a screwdriver, a voltmeter, a USB-to-TTL adapter, binwalk, hashcat, Ghidra and Ghidriff. What it required was patience and the discipline to read configuration files carefully before touching hardware. The result was a root shell on the best-selling router of a major online marketplace, a complete firmware dump, and a pre-authentication memory corruption bug in a binary compiled without a single exploitation mitigation. The vendor’s patch was genuinely thorough, adding a bounds check, migrating to ECC and introducing HMAC verification, and the CVE was assigned and released within a normal disclosure window. But the underlying lesson is about the baseline: devices at this price point ship with hardcoded credentials restored on every boot, a nobody account running as root, and network daemons built as if the last thirty years of exploit mitigation research never happened. The bug was patched; the class of decision that produced it has not been.
Original text: “Hardware Hacking: From zero to a Pre-Auth Stack Buffer Overflow on Amazon’s best-selling router” by Rotce at rotcee.github.io.


