core-jmp core-jmpdeath of core jump

Hardware Hacking a Best-Selling 4G Router: UART Root, Firmware Dump and CVE-2026-12495

A complete hardware-to-firmware walkthrough of the Mercusys MB115-4G, the best-selling router on Amazon Spain: pre-purchase firmware recon, PCB teardown, UART pinout discovery with a voltmeter, a root shell from hardcoded credentials, and a full flash dump over TFTP without an EEPROM programmer. The second half is the root cause analysis of CVE-2026-12495, a pre-authentication stack buffer overflow in the device httpd binary caused by a 2048-byte decryption buffer being copied into a 512-byte caller stack buffer with no bounds check, on a MIPS binary built with no canary, no PIE and an executable stack.

oxfemale August 13, 2026 31 min read 93 reads
Export PDF
Hardware Hacking a Best-Selling 4G Router: UART Root, Firmware Dump and CVE-2026-12495
Original text: “Hardware Hacking: From zero to a Pre-Auth Stack Buffer Overflow on Amazon’s best-selling router”Rotce, rotcee.github.io (published Feb 17, 2026; updated Aug 4, 2026). Figures, code listings and decompiler output below are reproduced verbatim with attribution captions.

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.

Firmware versions available in the downloads section
Figure 1: Firmware versions available in the downloads section. Source: original article.

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.

Contents of inittab
Figure 2: Contents of the inittab file. Source: original article.

Two lines carried the useful information:

  • The first line shows that the very first action on boot is executing the /etc/init.d/rcS script — 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 being ttyS0), 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 /var directories. Mode 0777 means any process on the system, at any privilege level, can create, modify and delete files there — including log directories and the HTTPS working directory.
  • telnetd launched 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/passwd means that whatever passwd.bak contains 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.

Contents of passwd.bak
Figure 3: Contents of the passwd.bak file. Source: original article.

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 nobody on this device drops nothing at all, and any bug that lets an attacker execute code as nobody yields 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.

Router front view
Figure 4: Router front view. Source: original article.
Router back view
Figure 5: Router back view. Source: original article.

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.

Router internals
Figure 6: Router internals showing main components. Source: original article.

1. Ethernet transformer

Ethernet transformer component
Figure 7: Ethernet transformer component. Source: original article.

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)

UART pinout identification
Figure 8: UART pinout identification. Source: original article.

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

SPI flash memory chip
Figure 9: SPI flash memory chip. Source: original article.

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

4G LTE module
Figure 10: 4G LTE module. Source: original article.

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.

Soldered UART pins
Figure 11: Soldered UART pins for connection. Source: original article.

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.

UART connection diagram
Figure 12: UART connection diagram. Source: original article.
TTL-to-USB adapter
Figure 13: TTL-to-USB adapter. Source: original article.

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.

Boot logs showing console prompt
Figure 14: Boot logs showing console prompt. Source: original article.

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.

Failed login attempt
Figure 15: Failed login attempt. Source: original article.

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.

Successful root login
Figure 16: Successful root login. Source: original article.

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.

Final setup with UART and LAN connection
Figure 17: Final setup with UART and LAN connection. Source: original article.

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.

Flash memory partitions
Figure 18: Flash memory partitions listing. Source: original article.

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.

IssueWhere it was foundWhy it matters
Weak default credentials (admin / 1234)/etc/passwd.bak, cracked with hashcat + rockyou.txtGrants 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.bakThe conventional “least privilege” account is fully privileged, so privilege dropping is a no-op
World-writable directories (mode 0777)/etc/init.d/rcSAny process can tamper with logs and working directories under /var
Password file restored on every bootcp -p /etc/passwd.bak /var/passwd in rcSCredential changes do not persist; the baked-in password is permanent
telnetd started with no arguments/etc/init.d/rcSA cleartext administrative protocol with no visible access restrictions
Consolidated view of the configuration-level findings from the first phase. Compiled from the original article.

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.

Pre-authenticated /cgi/login route registered with g_http_author_all
Figure 19: /cgi/login registered as reachable through g_http_author_all. Source: original article.

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.

HTTP request carrying encrypted login data
Figure 20: Encrypted login data passed through the data query parameter. Source: original article.

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.

Call from http_rpm_login to http_gdpr_decrypt
Figure 21: http_rpm_login retrieving sign and data, then calling http_gdpr_decrypt. Source: original article.

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.

512-byte decrypted_payload buffer in http_rpm_login
Figure 22: The caller-side decrypted_payload[512] stack buffer. Source: original article.

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.

2048-byte AES plaintext buffer in http_gdpr_decrypt
Figure 23: The callee-side aes_plaintext_buff[2048] buffer. Source: original article.

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.

Unchecked memcpy into caller-provided output buffer
Figure 24: The unchecked memcpy from a 2048-byte plaintext buffer into a 512-byte caller buffer. Source: original article.

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.

Binary protections missing from httpd
Figure 25: httpd built without the mitigations that would normally make stack exploitation harder. Source: original article.

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

DateEvent
22-02-2026Initial report sent to Mercusys
22-04-2026Response from the vendor. They could not decrypt the encrypted report. It was sent again
12-05-2026Vendor sent the patch for researcher feedback
17-06-2026INCIBE reserved the ID CVE-2026-12495
03-08-2026Patch: MB115-4G(EU)_V1_1.11.0 released to the public
04-08-2026Post updated with the vulnerability writeup
Coordinated disclosure timeline for CVE-2026-12495. Source: original article.

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 inittab from a downloaded image supplied the serial port and baud rate, and passwd.bak supplied 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 rcS restores passwd.bak on every boot, admin/1234 is a permanent property of every unit shipped, not a default a user can meaningfully change.
  • UID 0 on the nobody account 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 tftp plus /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 the nobody-as-root class of misconfiguration. Extend the same check to passwd.bak and 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 checksec against 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, strcpy and sprintf where 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 telnetd and 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.

oxfemale Vulnerability research, reverse engineering, and exploit development.
// Discussion