Post

Hardware Hacking: From zero to a Pre-Auth Stack Buffer Overflow on Amazon's best-selling router

Hardware Hacking: From zero to a Pre-Auth Stack Buffer Overflow on Amazon's best-selling router

This will be a blog series where we’ll do a deep dive into the Mercusys MB115-4G router, which, as of today (02/17/2026), is the best-selling router on Amazon Spain. In this first post, we’ll cover the workflow from receiving the device to gaining a root shell via UART and extracting the firmware. In the upcoming posts, we’ll analyze the firmware searching for vulnerabilities.

UPDATE - 04/08/2026

Or at least that was the idea. I did find vulnerabilities, but I kept putting off writing up the discovery because I had started investigating other things (mainly the Windows kernel) and ended up forgetting about it. I’ve now added a section at the end explaining the bug I found, now that it’s patched and the 90-day responsible disclosure period has ended :)

1. Some prior reconnaissance

While I was waiting for the router to arrive, I decided to gather as much information as I could, both for the hardware and the firmware.

As for the first, I started looking for the FCC ID, hoping to find some juicy information about the internals of the device and their datasheets. Unfortunately for me, the device wasn’t indexed in the FCC ID database, meaning that this router doesn’t have an FCC certificate and its use and distribution is illegal in the US.

I kept looking for some internal images on other sites like WikiDevi or OpenWrt, but I couldn’t find anything, so I decided to wait for the device to arrive to discover it myself.

Regarding the firmware, I found 2 versions on their support page. These were kind of weird, because there is no indication of which one is the latest version—the dates said one thing but the numbers in the firmware names indicated the other. I decided to download both and compare them.

Firmware versions Figure 1: Firmware versions available in the downloads section

After extracting the SquashFS filesystem with binwalk (I also took a brief look at the kernel and the U-Boot bootloader, but they weren’t my main interest) and comparing both versions with diffing tools (meld and diff), I found that the differences between versions were minimal—only a couple of additions in the web page information to identify and support other router models. As they were the same, I went with version 1.6.0.

The first thing I did was to look for the init process (the first userland process that is loaded after the kernel). I found this in /etc/inittab:

Contents of inittab Figure 2: Contents of the inittab file

  • First line indicates that the first thing the system will do on boot is execute the /etc/init.d/rcS script (we’ll go to that in a second)
  • The second line gives us some interesting info. We have a serial interface ttyS1 in the second serial port of the CPU (first is ttyS0). It has a baud rate of 115200 (pretty standard speed). This will come in handy when we have the physical device to connect to it via UART.

Now, looking at the rcS script we also see some interesting information, I share the most interesting findings here:

  • A comment suggesting that the device uses the MediaTek MT7628 (standard in this price range)
    1
    
    # 7628 watch dog
    
  • Some security-related info:
1
2
3
4
5
6
7
8
9
10
/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
[...]
  • Max perms for everyone in a lot of /var directories
  • Telnet without args (open console for anyone?)
  • The system seems to be restoring the same passwords file at every boot. Let’s have a look at the passwd.bak file:

Contents of passwd.bak Figure 3: Contents of the passwd.bak file

  • dropbear: User for the SSH service
  • nobody: This user exists for processes that should have minimal privileges. However, here it has UID and GID 0 (0:0), meaning it has root privileges. Very dangerous.
  • admin: Even though the name is not root, it acts like it. The password is MD5-crypt ($1) and has no salt.

I cracked the admin password with hashcat using the rockyou.txt wordlist. After a couple of seconds it broke. Guess what, the password is 1234. This may be useful later.

After this initial recon, I decided to wait until I had the router to analyze the firmware that came with it.

P.S.: At the moment of writing this post I’ve discovered that if you change the language of the firmware download page to English, there are two additional firmware images available, both newer than the ones that appeared in the Spanish section. (Pretty odd, huh?)

2. Hardware Analysis

In this section we’ll identify the main components of the router and their functionality.

The first thing I had to do once I got the router was to open it. The device only had two small screws on its back.

Router front view Figure 4: Router front view

Router back view Figure 5: Router back view

After removing them, I used a flat screwdriver to fully open it (I admit it was kind of tricky—maybe I should get a spudger). This is the inside of the device; I’ll explain the components that caught my eye:

Router internals Figure 6: Router internals showing main components

1. Ethernet Transformer:

Ethernet transformer component Figure 7: Ethernet transformer component

A magnetic component located immediately behind the RJ45 ports. It provides galvanic isolation. It protects the internal circuitry from voltage spikes on the network cables and filters electromagnetic noise to ensure data integrity over the Ethernet connection.

2. UART (Serial Console Port):

UART pinout identification Figure 8: UART pinout identification

The physical “backdoor” for developers. By connecting a USB-to-TTL adapter, we can monitor the kernel boot logs and, if unprotected, drop into a root shell directly. If you want to learn more about this protocol, go here: https://www.analog.com/en/resources/analog-dialogue/articles/uart-a-hardware-communication-protocol.html

The UART wasn’t labeled, so I had to identify the different ports using a voltmeter. First I found GND and then I connected the router to power looking for a port with constant voltage (VCC, operating at 3.3 V) and a fluctuating one (TX).

3. SPI Flash Memory:

SPI flash memory chip Figure 9: SPI flash memory chip

This is the router’s “hard drive.” It stores the Firmware. It’s our primary target for extracting the firmware dump for static analysis.

4. 4G LTE Module:

4G LTE module Figure 10: 4G LTE module

A self-contained cellular communication module soldered to the main board. It manages the connection to mobile networks, SIM card authentication, and data transmission.

5. Main EMI Shield (SoC & RAM): An Electromagnetic Interference (EMI) shield. It is a metal cage that covers the most sensitive components. It houses the SoC (System on Chip) and the RAM.

3. Connecting via UART to access the system

Now that we have identified the various components on the PCB, it’s time to come up with a plan to gain a first foothold in the system. As we have discovered a UART interface that seems to be active (because of the test with the voltmeter and the previous firmware reconnaissance) we’ll go with that.

The first thing we need to do is get a good connection to the UART. We can accomplish this in several ways. On this occasion, I decided to solder some pins directly to the interface to use a TTL-to-USB adapter.

Soldered UART pins Figure 11: Soldered UART pins for connection

Now we can use the adapter to monitor the boot logs and interact with the interface. We have to connect the pins properly as shown in the image (notice that VCC is not used, as both the adapter and the PCB have their own power supply). Also remember to change the adapter voltage to 3.3 V; otherwise we could damage some circuit.

UART connection diagram Figure 12: UART connection diagram

TTL-to-USB adapter Figure 13: TTL-to-USB adapter

Now we only need to connect the adapter to the computer, identify the device (/dev/ttyUSB0 in my case) and use screen indicating the essential parameters so both devices understand each other.

1
screen /dev/ttyUSB0 115200

I’ll try with a baud rate of 115200 (the one we saw in the inittab file) and the default config. If we see strange symbols instead of legible characters that means that some parameter is wrong. In that case we could use a logic analyzer to try to decipher the way the protocol is sending the information.

We plug in the router and see boot information come across the screen, confirming that the parameters are right and we are receiving the information properly. We save all the boot logs in a file to look at them later.

Boot logs showing console prompt Figure 14: Boot logs showing console prompt

Among all the boot logs, we can see a message asking us to press the ENTER key to activate the console. Then it asks us for a username and a password. If we try users like root or admin without a valid password we cannot log in.

Failed login attempt Figure 15: Failed login attempt

Let’s try with the credentials we saw in the passwd.bak file:

Successful root login Figure 16: Successful root login

The credentials are valid!! Now we have a shell as root.

4. Dumping the firmware

Now that we have complete access to the system, I want to see if I can extract the firmware without having to use an EEPROM reader. I’m too lazy for that.

Luckily for us, if we type in busybox –list we can see all the utilities available inside busybox. Among them we have tftp. We can use it to dump all the firmware to our local machine without having to connect anything to the PCB.

Reading /dev/mtd* copies the flash partitions exposed by the kernel. On this device, that is enough for static firmware analysis, but it should not be considered necessarily identical to a dump made with an external programmer, which could include data or regions not exposed through the partition map. It also does not capture volatile RAM. The /var directory contains runtime data created after boot, which we can inspect later because we already have root access.

To extract the partitions content the only thing we need is a tftp server running on the host (I used atftpd) and a connection to one of the router LAN ports.

Final setup with UART and LAN connection Figure 17: Final setup with UART and LAN connection

Now let’s enumerate all the system partitions (present in /proc/mtd):

Flash memory partitions Figure 18: Flash memory partitions listing

With all this information, we start the TFTP server on our machine and run the following script on the router.

1
2
3
4
5
6
7
8
9
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!"

And with that, we have just dumped all the flash memory of the router. (I would recommend comparing the MD5 hashes of the dumped memory to ensure no information loss.)

Recap and next steps

This is the end of this first part of the blog series. Here’s what we’ve accomplished:

Security issues found so far:

  • Weak default credentials (admin:1234)
  • Unsalted MD5 password hashes
  • User “nobody” running with UID 0 (root privileges)
  • World-writable directories (777 permissions)

In the upcoming posts, we will dive into the root filesystem looking for potential vulnerabilities in the main binaries, analyze the web interface for security flaws, and attempt to find exploitable vulnerabilities.

UPDATE - 04/08/2026

As I said at the beginning of the post, I don’t really plan on making another full post, since it’s been quite a while since I was working with this target and, having not written anything, I don’t feel capable of continuing the narrative coherently. Therefore, I’ll leave here the Root Cause Analysis of the bug I found in the httpd binary.

CVE-2026-12495. Finding a Pre-Auth Stack Buffer Overflow in the Mercusys MB115-4G

Before getting into the RCA, an honest note: I am writing this quite a while after the original research. I should have written the post when the analysis was still fresh, but as I have mentioned, I moved on to other topics and the details faded. What follows is reconstructed from my memory and from the material I made at that time: the report I sent, the Ghidra screenshots, the Ghidriff output, and the vendor negotiation.

The vulnerability was in /usr/bin/httpd, the web server used by the Mercusys MB115-4G v1.0. The device is MIPS32 little endian, and the vulnerable binary I originally analyzed came from firmware 1.7.0 0.9.1 v0001.0 Build 250414 Rel.59734n (httpd MD5: 7589198845cd709e020614f027144c46). I later confirmed that the same vulnerable logic was still present in the latest firmware I could diff directly, MB115-4G(EU)_V1_1.9.0 Build 20250928 (httpd MD5: 3275a62e1b371060748e579e1e09539d). A newer v1.10 image existed, but the firmware image was encrypted/obfuscated enough that I could not perform the same direct binary diff on it from the materials I had.

The bug was a classic stack-based buffer overflow caused by a mismatch between the size expected by the caller and the size handled by the decryption helper.

The first important detail is that the affected route was reachable before authentication. The routing table registered /cgi/login with g_http_author_all, so this code path was part of the login surface itself.

Pre-authenticatedcgi/login route Figure 19: /cgi/login registered as reachable through g_http_author_all

The request shape was also a little unusual. The login request was a POST, but the encrypted payload was passed through the URL query string in the data parameter, so the body had Content-Length: 0. The sign parameter traveled next to it.

Keep in mind that the data field is first AES encrypted and then base64 encoded.

HTTP request carrying encrypted login data Figure 20: Encrypted login data passed through the data query parameter

Inside http_rpm_login, the handler obtains both values with http_parser_getEnv("sign") and http_parser_getEnv("data"). It then prepares a local destination buffer and calls http_gdpr_decrypt, passing that local 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

The destination in http_rpm_login was only 512 bytes:

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

However, the callee was built around much larger temporary buffers. In http_gdpr_decrypt, the decrypted AES plaintext buffer was 2048 bytes.

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

The root cause is at the final copy. After base64 decoding and AES decryption, http_gdpr_decrypt copies data_len bytes into the output pointer provided by the caller:

1
memcpy(output_buffer, aes_plaintext_buff, data_len);

There is no check that data_len fits in the caller’s 512-byte destination. If attacker-controlled decrypted data exceeds 512 bytes, the copy overflows the http_rpm_login stack frame.

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

From an architectural point of view, this looked like a potential pre-authentication RCE primitive. The affected binary had no meaningful mitigations: no stack canary, no PIE, an executable stack, and RWX segments. So this would be a trivial jump to shellcode, as if we were in the 90’s.

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

A pre-authenticated route feeds attacker-controlled encrypted data into a stack buffer, the copy length comes from the decoded/decrypted input, and the binary does not have the usual protections that would turn the bug into a much harder exploit. In a cleaner input path, overflowing past 512 bytes could overwrite local state and eventually the saved return address.

However, there I faced one issue during my own validation: the HTTP parser appeared to enforce a maximum URL length of roughly 1024 characters. Because the payload was transported in the query string, not in the POST body, that cap limited the amount of plaintext I could deliver through the normal web interface. After base64 and encryption overhead, my reachable plaintext size was around 450 bytes, which was below the 512 bytes required to trigger the stack overflow through that exact path. Mercusys later told me that their internal validation had managed to trigger the overflow and crash httpd; I do not know how they bypassed or avoided the 1024-character limit I observed.

This is why I initially reported it as a serious latent application layer bug rather than presenting a PoC or exploit. The unsafe C code was there, and it was reachable before authentication, but the web parser’s request length behavior appeared to block my standard HTTP path from reaching the vulnerable size. Mercusys’ later result confirmed that the overflow could be triggered, although the demonstrated impact was an httpd crash rather than remote code execution.

To check whether this was an old issue or an already fixed one, I used Ghidriff to compare the original vulnerable httpd with the newest firmware version I could analyze. The diff showed that the vulnerable logic had effectively survived into v1.9.0; no bounds check had been added before the final copy.

Some time after the initial report, Mercusys responded and sent a signed beta firmware named Bugfix_Pre-Auth_Stack-based_Buffer_Overflow_(CWE-121)_MB115-4G(EU)v1_1.9.0_signed.bin. They asked me to verify the fix, so I extracted the firmware, loaded the new httpd in Ghidra, and compared the relevant functions again.

The direct fix was the one expected: the decryption routine received an additional size parameter representing the maximum size of the caller-provided output buffer, and the final copy was guarded by a bounds check before memcpy. In simplified form, the patched logic became:

1
2
3
if (decrypted_len <= output_buffer_size) {
    memcpy(output_buffer, aes_plaintext_buff, decrypted_len);
}

The patch also changed more than the immediate overflow. Mercusys replaced part of the RSA-based login flow with ECC-based decryption and added an HMAC check before processing the message.

In the vulnerable firmware, this path used RSA for the initial payload decryption:

1
2
3
4
5
6
7
8
9
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, that same area had moved to ECC:

1
2
3
4
5
6
7
8
9
10
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 was also visible right after the AES block decryption. In the old flow, successful AES decryption led directly toward the output copy. In the patched firmware, the decrypted message is authenticated before the function reaches the final copy and the new boundary check, guaranteeing integrity and authenticity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
    }

    (...)
}

The last part of the disclosure process was the impact discussion. My position was that the bug represented a potential RCE: pre-authentication input, stack overflow, no protections at all. Mercusys’ internal validation, however, categorized the demonstrated result as an httpd crash rather than confirmed remote code execution, and they proposed a CVSS 4.0 score of 5.3 (Medium).

I accepted that classification for the coordinated disclosure process. After all, I had initially thought it was unexploitable through the path I tested due to the 1024-character limit in the URL, so I didn’t provide any proof of concept/exploit.

Timeline

  • 22-02-2026: Initial report sent to Mercusys
  • 22-04-2026: Response from the vendor. They could not decrypt the encrypted report. I sent it 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

That’s all for now. Hope you found this useful! And remember,

"Do hard things"
This post is licensed under CC BY 4.0 by the author.

Trending Tags