Finding a RCE in my old TP-Link router
I was eating lunch one day in late December (2025), and was reading an article by Simone Margaritelli about several TP-Link vulnerabilities he found in his IP Camera. After finishing both the article and my toast, I realised two things:
- I had my own TP-Link router collecting dust in a cupboard.
- The firmware blobs for it were stored on an open S3 bucket, which would make it incredibly easy to reverse engineer.
With this in mind, I dusted off my old router and got to work.
Acquiring the firmware
First things first, I downloaded a list of all possible firmware from TP-Link’s S3 bucket.
aws s3 ls s3://download.tplinkcloud.com/ --no-sign-request --recursive --human-readable > tplink_s3_bucket.txt
I found that there were 90 possible different firmware images for my TL-MR6400.
❯ cat tplink_s3_bucket.txt | grep TL-MR6400 | wc -l
90
Luckily, I managed to get it down to only 10 choices by filtering for my region (APAC, meaning Asia-Pacific).
❯ cat tplink_s3_bucket.txt | grep TL-MR6400 | grep -i apac | sort
2020-09-23 TL-MR6400apacv5_1.0.0_0.9.1_up_boot200721_202_1600846650694.bin
2021-04-21 TL-MR6400apacv5_1.1.0_0.9.1_up_boot201207_all__1618997938275.bin
2022-07-11 TL-MR6400apacv5_1.2.0_0.9.1_up_boot211009_all__1657510738338.bin
2022-07-13 TL-MR6400APACv5.2_1.1.0_0.9.1_210803-rel55933__1657706066736.bin
2022-12-19 TL-MR6400APACv5.3_1.4.0_0.9.1_220506-rel57555__1671433970902.bin
2024-08-05 TL-MR6400(APAC)v7_1.0.0_0.9.1_[240424-rel73593]_2024-04-25_16.08.bin
2024-10-10 TL-MR6400(APAC)v7_1.1.0_0.9.1_[240906-rel35335]_2024-09-06_11.26.bin
2024-10-10 TL-MR6400(APAC)v7_1.1.0_0.9.1_[240906-rel35335]_2024-09-06_11.26.bin
2025-11-28 TL-MR6400(APAC)v7_1.2.0_0.9.1_[250903-rel49945]_2025-09-03_19.37.bin
2025-11-28 TL-MR6400(APAC)v7_1.0.0_0.9.1_[250903-rel64246]_2025-09-03_17.59.bin
From here, I just downloaded the firmware that most closely matched the version my router was already running.
❯ aws s3 cp "s3://download.tplinkcloud.com/firmware/TL-MR6400APACv5.3_1.4.0_0.9.1_220506-rel57555__1671433970902.bin" . --no-sign-request
download: s3://download.tplinkcloud.com/firmware/TL-MR6400APACv5.3_1.4.0_0.9.1_220506-rel57555__1671433970902.bin to ./TL-MR6400APACv5.3_1.4.0_0.9.1_220506-rel57555__1671433970902.bin
❯ mv TL-MR6400APACv5.3_1.4.0_0.9.1_220506-rel57555__1671433970902.bin TL-MR6400_MYROUTER.bin
Extraction
Now for the fun part. It was time to extract this firmware and start the reverse engineering process.
Since I was feeling particularly lazy that day, I decided to let binwalk attempt to extract all the relevant data. Luckily for me, it seemed to work just fine.

After digging around in the maze of files generated by binwalk, I found the majority of TP-Link’s custom binaries were contained in squashfs-root/usr/bin/.

In particular, I wanted to target httpd and cli as these represented TP-Link’s frontend admin panel and their optional telnet server (which allows for the same administrative configuration as the WebUI).
Reverse Engineering
To cut a long story short, I checked out the httpd binary and although I found some potential threads to pull at, they turned out to all be dead ends. The really interesting stuff happened when I started to reverse the cli binary.

When looking at the Telnet based Management CLI that TP-Link provides, I found an undocumented command called mdlog prepare. This mdlog command appears to be used to gather diagnostics information about the router and upload it to a user-provided TFTP server.
When mdlog prepare is first run, it will get the IP address of the user connected via telnet.

This is then used to attempt to connect to a TFTP server hosted by the user on its default port (69) and pull a file called conf.json.

From this JSON file, an ultralightweight JSON parser called cJSON is used to extract router.workdir.

And this is where the issues begin. This workdir variable is then combined with a busybox command, which is intended to download a signed diagnosis tool from the TFTP server and then execute it.
The problem with this is that the workdir variable was not being sanitised at all. So, a delimiter character such as ; can be inserted and the router will run the original command, but it will also run a subsequent command of our choosing.
If we choose a subsequent command such as sh, it gives us a full root Linux shell on the router that we can interact with.

The exact workdir value I chose was /tmp/x; sh; #, which makes the full command:
busybox tftp -g -r mdlog_bridge.bin -l /tmp/x; sh; #mdlog_bridge.bin <IP> -b 16384 2>/dev/null
This effectively gets run as three separate commands, with the third command not being executed because it is commented using a hash (#) symbol.
busybox tftp -g -r mdlog_bridge.bin -l /tmp/x
sh
#mdlog_bridge.bin <IP> -b 16384 2>/dev/null
So it just ends up running:
busybox tftp -g -r mdlog_bridge.bin -l /tmp/x
And then once that command runs, it executes:
sh
This gives us a fully interactive root shell.
Timeline (DD/MM/YYYY)
- 21/12/2025 - Found the vulnerability
- 24/12/2025 - Reported to TP-Link
- 12/03/2026 - Vulnerability patched and CVE-2026-3841 issued
- 12/04/2026 - Additional 30-day window from the 90+30 disclosure policy expired
- 30/04/2026 - Blog published
Donations
So far, I have not been paid bug bounties for any of the vulnerabilities I have found.
If you would like to support me, consider buying me a coffee here.