MrBruh's Epic Blog

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:

  1. I had my own TP-Link router collecting dust in a cupboard.
  2. 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.

tplink_binwalk.avif

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/.

tplink_usr_bin.avif

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.

tplink_command_line_model.avif

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.

tplink_cli_source_one.avif

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.

tplink_cli_source_two.avif

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

tplink_cli_source_three.avif

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.

tplink_cli_source_four.avif

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)

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.

https://ko-fi.com/mrbruhh