The rooting, firmware analysis and hardcoded, reset-persistent credentials of the TP-Link TL-841N!

blog.juni-mp4.com · 2026-08-02 · by Long Nguyen ·generic
round 2: rooting & analysing firmware of the TP-Link TL-WR841N

The rooting, firmware analysis and hardcoded, reset-persistent credentials of the TP-Link TL-841N!

Toggle theme

The rooting, firmware analysis and hardcoded, reset-persistent credentials of the TP-Link TL-841N!

2 Aug 2026 Now, let me just preface this with a disclaimer:

… and get used to this puppo, as you may see this a few more times in this post :’).

In light of continuing my bumbling foray into the hardware hacking landscape, I bought a bottom-of-the-line $10 TP-Link TL-841N(EU) router off a random fellow on Facebook Marketplace, in a thrilling, high-stakes deal conducted in the middle of… a nearby mall.

My intention behind such a daring exchange was to practice the end-to-end process of pulling apart an IoT device to poke at it, access debug logs, potentially get a root shell, practice various kinds of firmware extraction (via UART & via on-chip flash memory), and even have a mosey about the filesystem for some potential security vulnerabilities later down the road.

Here’s a short overview of the journey so far, documented below:

1. Cracking it open: Finding the UART & getting connected
   
2. Dumping the firmware (Method 1. - UART & tftp)
   
3. Dumping the firmware (Method 2. - on-chip flashrom chip extraction)
   
4. Un-squashing the root filesystem
   
5. Having a Look-see - Preliminary Snooping/Analysis

Hang around until the end for discovering what kinds of various plaintext, hardcoded creds from the previous owner (censored) could be found on this device… one of which would survive even a full “router reset”.

Yeah. Kinda… no bueno?

Let’s dig in.

---

1. Cracking it open: Finding the UART & getting connected

I began by searching the router model number up on the FCCID to check for its datasheet, to get information on the PCB, voltage levels & the available chips on the board.

Opening it up and finding the very-well-labelled UART ports for serial/debug interface access was straightforward, with one quirk being that the flash was found on the underside of my board - model number `GD25Q64CSIG` ( datasheet).

Finding the UART ports (four "traffic light" holes on right picture) on the shelled router (left)

If your device isn’t as friendly with labelling, you can also use a multimeter to probe each suspected UART port at boot time to determine which pin is which - the device’s `TX` pin should have a fluctuating voltage during boot (due to it transmitting various boot logs in the form of serial `1`s and `0`s, corresponding to fluctuations in voltage), and the `GND` pin should be `0V`.

Probing the suspected UART pins at boot until I found one with a fluctuating voltage level (i.e. not the standard steady 3.3V or 0V for my router), indicating it's likely the \`TX\` port.

My trusty AliExpress USB-to-UART adapter (which I checked to confirm supports both 3.3V and 5V output, so we don’t fry anything) had the following pinout/wire colouring:

# Four-color DuPont terminal wiring
RED: 5V
BLACK: GND
GREEN: TX
WHITE: RX

So, I connected the USB `GND` to the router’s `GND` port (or can just touch grounded metal on the board), USB `RX` to the discovered router’s `TX` port, and USB `TX` to router’s suspected `RX` port (which can be found by trying the remaining two pins, until you get a successful input by spamming a key on your keyboard once connected).

I soldered mine in for a better/persistent connection, & plug-&-play functionality 😜.

Soldering on the \`GND\`, \`RX\` and \`TX\` leads to access the debug interface hands-free!

Once I identified what `/mnt/dev` device my USB-to-UART device was and got connected up with `picocom -b 115200 --logfile bootlog-tplink.txt /dev/tty.usbserial-1210` (using the common baud rate of `115200`, which worked), we can see we’re dropped into a `root` shell once again! Too easy 😅.

(I also attempted to connect my phone to the generated TP-Link Wifi network, which generated the extra logs seen above)

Connecting my computer directly via one of the router’s LAN ports, we can be assigned an IP and access the admin console as expected:

Home sweet home... but how do we get in?

2. Dumping the firmware (Method #1 - `UART` & `tftp`)

So, now I could see & navigate the filesystem, I wanted to dump & transfer it to my local device to perform some preliminary analysis!

Listing the flash partitions at `/proc/mtd`, as well as a few other `mount` details, gives us a good point to start:

Some preliminary details about the router's configured memory partitions

Preparing our receiving TFTP server

So we can transfer the firmware files over, I installed & started a tftp-now server with `tftp-now serve` on my Mac (with `brew`, as am on apple silicon).

NOTE: Ensure to create a folder in your home directory corresponding to where you’re transferring the file from on your router (for me, from `~/var`), otherwise you’ll get a “No such file exists” error when the `tftp` agent attempts to `put` its remote file on your computer (as it does so at the same path that it’s uploading it from, `/var/`).

To solve the (many) errors shown below, I created a temporary `var` folder inside my `~` home directory to match up with the file path the router’s `tftp` wanted to “put” to:

tftp-now server on my Mac

Now, connect your analysis computer to the shelled router's LAN port, & check that it's gets assigned an IP (like \`192.168.0.200\`) so they can talk to one another, despite not having internet access.

Switching back to our UART connection to the router, and checking we can begin to create a dump of the first flash partition with a tool like `cat` (if your router doesn’t have `dd`, like mine).

NOTE: Keep an eye on your router’s storage \- if yours is also full (like mine was - use `df` to check), write the firmware dumps incrementally into the `/var` directory, which is `RAM`\-backed and my largest partition (still only \~6.4 MiB).

To avoid overloading the RAM, for each partition dump, I used the below process:

1. \*\*Dump flash partition with `cat /dev/mtd0 > /var/boot.bin` 2. Transfer it over to the IP address of your receiving analysis laptop with `tftp -p -l /var/boot.bin 192.168.1.100` 3. Delete transferred flash partition with `rm /var/boot.bin`, and repeat.

# Process:
cat /dev/mtd0 > /var/boot.bin 
tftp -p -l /var/boot.bin 192.168.1.100 
rm /var/boot.bin

Repeat above process for each sector, rm'ing as you go

cat /dev/mtd1 > /var/kernel.bin cat /dev/mtd2 > /var/rootfs.bin cat /dev/mtd3 > /var/config.bin cat /dev/mtd4 > /var/romfile.bin cat /dev/mtd5 > /var/rom.bin cat /dev/mtd6 > /var/radio.bin

Tedious, but how it had to be (unless performing an on/off-flash chip extraction, or you have a USB interface on your router to write to).

However, for a CLI quality of life boost as stated in this article, “we can make our lives much easier by uploading our own fully-loaded BusyBox via the existing TFTP client on the device. You may find a precompiled little-endian MIPS BusyBox binary on the official website.” This will give us access to significantly more standard linux commands to use on the router.

Again, well, it should be evident, but…

We’re learning. It worked in the end. Firmware files extracted to my local computer over `tftp` & `UART`!

2B - Dumping the firmware (Method #2 - on-chip `flashrom` chip extraction)

An alternative way of analysing the filesystem is via an on-chip (or off-chip, if you want to desolder a tiny `IC` and have a hot air gun) firmware extraction. For this, I used the " poor man’s flash extraction tool" \- the CH341A.

I first found the flash chip - on the back of the board - and checked it was a flash chip with it’s model number. This also told me that the flash rom voltage is at 3.3V ( datasheet) so the CH341A i have is safe to use. Some chips run at 1.8V, not 3.3V, so before carefully placing the clip on the clip and plugging in the programmer, check your chip’s voltage (via online datasheets or multimeter). If it’s 1.8V, use a voltage adapter (bought with the adapter) to avoid possible damage.

Also, ensure the clamp-side’s red cord is in line with to `PIN 1` of the router’s flash chip, which should be (typically marked with a small circle in the corresponding corner).

The on-chip extraction, with the circle marking where to align the corner of the clip with the red wire leading to it

Beginning of the chip model number often corresponds to type: so for our `25Q64CSIG`, the `25` at the start means we would position our 8 pins over the slots marked as `25 SPI BIOS`, whereas if our chip said `26`, it would go on the `EPROM I2C` side.

After seating the PCB & connected cord in the USB programmer with the RED cord on the top right, as pictured below, double check that the red cord on the clamp-side is attached to `PIN 1` of the router’s flash chip (typically marked with a small circle, as mentioned before).

The on-chip extraction, with the circle marking where to align the corner of the clip with the red wire leading to it

\- \Video explaining the CH341A connection process in detail (Matt Brown)\

Then, simply install & fire up flashrom, and run the following command to extract the firmware!

flashrom --programmer ch341a_spi -r modem.bin

Now we have `modem.bin`, which (being a full flash dump) should contain inside it, the same manually-extracted flash partitions as extracted via `ftpd`. However, to analyse it more meaningfully, we can split it up into the various `boot`, `config`, `rootfs` sectors like before with `dd`.

Manually carving out the flash sectors

Using the memory sizes/addresses found before with `/proc/mtd` when on the router, we can manually carve our `modem.bin` flash dump into the identified sectors for analysis.

The commands run earlier on the router to map out the flash memory sectors

To get our first sector, `mtd0`, which presumably contains U-Boot based on our observed boot logs, we can use `dd` to carve the specified chunk out of `modem.bin`:

dd if=modem.bin of=mtd0-boot.bin bs=1 count=$((0x20000))

Search for strings identifying u-boot

strings -a mtd0-boot.bin | grep -Ei 'u-boot|boot|version|console|baud|mtd|flash'

In this case, my U-boot sector wasn’t initially identified by `binwalk` automatically when I first analysed `modem.bin`, due to likely being a customised, vendor-modified version of U-boot. Hence why I decided to carve out the sectors manually according to the router’s `/proc/mnt` information, just for some additional accuracy and organisation.

Using \`binwalk\` to analyse the dumped firmware's structure

The presence of a customised U-boot was confirmed with `strings` finding `Ralink UBoot Version` in the `mtd1` partition (the presumed linux kernel) when this part was carved out and analysed later on, indicating this was a Ralink\-modified U-Boot version.

Anyway, running the `strings` command again on the extracted suspected U-boot `mtd0` partition confirms it is the part of the flash containing U-Boot, and helps us identify more info about it, including U-Boot’s version number and behaviour.

Using \`strings\` to extract information about the U-Boot sector

I repeated the above process to carve our each sector of the firmware dump, until I had all sectors, `mtd0-mtd6`, for analysis.

# Carving out each sector from full modem.bin flash dump
dd if=modem.bin of=mtd0-boot.bin bs=1 count=$((0x20000))
dd if=modem.bin of=mtd1-kernel.bin bs=1 skip=$((0x020000)) count=$((0x140000))
dd if=modem.bin of=mtd2-rootfs.bin bs=1 skip=$((0x160000)) count=$((0x660000))
dd if=modem.bin of=mtd3-config.bin bs=1 skip=$((0x7C0000)) count=$((0x010000))
dd if=modem.bin of=mtd4-romfile.bin bs=1 skip=$((0x7D0000)) count=$((0x010000))
dd if=modem.bin of=mtd5-rom.bin bs=1 skip=$((0x7E0000)) count=$((0x010000))
dd if=modem.bin of=mtd6-radio.bin bs=1 skip=$((0x7F0000)) count=$((0x010000))

I asked the friendly (?) neighbourhood AI chatbot to help map a visual summary of the chip so far:

0x000000 ┌───────────────────────────┐
         │                           │
         │ mtd0                      │
         │ Custom U-Boot 1.1.3       │
         │ Ralink                    │
         │ 128 KiB                   │
0x020000 ├───────────────────────────┤
         │                           │
         │ mtd1                      │
         │ Linux kernel              │
         │                           │
0x160000 ├───────────────────────────┤
         │                           │
         │ mtd2                      │
         │ SquashFS                  │
         │ 6.6 MiB                   │
         │                           │
0x7C0000 ├───────────────────────────┤
         │ mtd3 config               │ 64 KiB
0x7D0000 ├───────────────────────────┤
         │ mtd4 romfile              │ 64 KiB
0x7E0000 ├───────────────────────────┤
         │ mtd5 rom                  │ 64 KiB
0x7F0000 ├───────────────────────────┤
         │ mtd6 radio                │ 64 KiB
0x800000 └───────────────────────────┘

If we use `binwalk -e` to extract what it can automatically from the original `modem.bin`, it also gives us the uncompressed kernel image (`mtd1`) in the form of `decompressed.bin`, which checks out when looking at its identified magic bytes below:

Verifying that \`binwalk\` detects the linux kernel image on our second \`mtd1\` sector

4. Un-squashing the root filesystem

Another reason that I had to carve out and extract the `mtd2` `squashfs` filesystem manually with `dd` was, once again, because I’m on a mac. And the `sasquatch` command initiated as part of `binwalk`’s extraction process (when extracting a `squashfs` sector) continued to fail for me (despite being installed…),

Oh well. At least we know what to do from above to get and un-squash the `rootfs` for analysis:

# extract the squashfs filesystem directly
dd if=modem.bin of=mtd2-rootfs.bin bs=1 skip=$((0x160000)) count=$((0x660000))

check it's squashfs

file mtd2-rootfs.bin

unsquash it (uses xz) to explore

unsquashfs -no-xattrs mtd2-rootfs.bin
Note: If you get errors like `create_inode: could not create character device squashfs-root/dev/zero, because you're not superuser!`, don’t panic. As the filesystem contains device nodes under `/dev`, your computer (MacOS for example) may not permit a normal user to create those special device files. However, they’re (allegedly) not usually needed for firmware analysis, but you can use pure Linux to `unsquash` this while preserving the device nodes with `sudo`.

(yes, i know this would’ve just “all worked” on linux. i am waiting for my poor mac baby to die before the final leap.)

Comparing the two extraction methods (left done via Method 1, `tftpd`, and right via on-chip extraction, Method 2), we can see nearly identical copies of the filesystem to analyse!

Comparing the results of our two extraction methods

Now, we can proceed to analyse the dumped binaries/unsquashed filesystems using `strings`, `grep`, and `binwalk`, to understand how the router works and perhaps even uncover some juicy vulns later down the track…!

But, I’ll leave the groundbreaking vulnerability reversing to part 2 (or more capable souls than I), as this is getting long, but thanks for sticking along for the ride! We’ve eztracted firmware from a router in two ways; via `tftp` over `UART`, as well as by using `flashrom` for a direct on-flash-chip firmware extraction. \\

But… we should have a peek before we go, shouldn’t we? Just a little?

5. Having a Look-see - Preliminary Snooping/Analysis

NOTE: I am just poking around here to learn more about what is/isn’t exposed & accessible on a fairly typical IoT device that I own and have full authority over. The device was factory reset before being sold to me, and no discovered credentials were used to access any unauthorised material.

Now, to introduce you to the cheap & nasty firmware analyst’s favourite tool: `strings`.

As stated on it’s `man` page, `strings` "…looks for ASCII strings in a binary file, object, or standard input.". So we can quickly parse compiled `.bin` files and the like for a dirty peek at any readable strings that may exist within, which is most likely to be either text the program prints, or plaintext configuration values (depending on compilation type, of course).

Examining the `mtd3-config.bin` first, where router-specific configs should live, with `strings mtd3-config.bin | less`, we can actually find the router’s hard-coded WPS password in plaintext, i.e. the one printed on the back of the router!

Finding the hardcoded plaintext WPS password, located on the bottom of the router

Well, uh, upon further research: we’ve stumbled upon what appears to be a previously discovered “`CVE`” ( CVE-2026-4346), in fact:

“The vulnerability affecting TL-WR850N v3 allows cleartext storage of administrative and Wi-Fi credentials in a region of the devices flash memory while the serial interface remains enabled and protected by weak authentication. An attacker with physical access and the ability to connect to the serial port can recover sensitive information, including the router’s management password and wireless network key. Successful exploitation can lead to full administrative control of the device and unauthorized access to the associated wireless network.”

(Although I’d say, if someone’s got hardware access to your router’s serial interface/flash chip in the first place, you’re already pretty borked. And I wouldn’t call it protected by “weak authentication”… no authentication.)

Another gem uncovered by `strings mtd3-config.bin | grep -ri "pass"` are hardcoded credentials for what appears to be previous PPPoe ISP/router provisioning credentials (the `@unitiair` portion strongly suggesting an ISP/service-provider account).

The hardcoded plaintext PPPoe ISP customer credentials, presumably from the previous router's owner (verified via OSINT)

Oopsies.

And for an even bigger oopsies/yikes, even after performing a router factory reset, these credentials were still accessible via the freshly-dumped `config` partition of the firmware.

Given that I am not the local small business whose email is listed in plaintext in the un-redacted version of the picture above, nor a customer of Unitiair, I don’t believe I should still be seeing this after the router was “reset”.

This means that not even a FACTORY reset of the device (holding down `RESET` button for 10 seconds, and verifying reset was performed by checking boot logs, as detailed in the TP-Link documentation), which most consumers would presume would wipe all user OR ISP-modified settings from it, can truly erase the ISP PPPOe WAN credentials.

Boot logs during the router's factory reset

Comparing the initial firmware dump to the factory-reset firmware dump

So, a factory reset leaves a previous subscriber’s broadband authentication credential recoverable from persistent flash storage., in `Firmware Version: 0.9.1 4.16 v0001.0 Build 170622 Rel.64334n` (the latest for my EU model of the router)

The router's listed firmware version

Whilst you don’t seem to be able to authenticate to this ISP itself (which, luckily for them, requires an account number to login, unlike my ISP which uses email), user/ISP modified credentials stored in plaintext persisting after a reset is… not a great privacy sign for what is considered a “factory reset” device.

Anyway…

Searching for more “sensitive” strings in the extracted `root-fs` (`mtd2`), we get both the router login from `./web/help/RestoreDefaultCfgHelpRpm.htm`, as well as the hashed password of the `admin` OS user.

grep -RniE 'password|passwd|credential|admin' . 2>/dev/null
...
./web/help/RestoreDefaultCfgHelpRpm.htm:20:<li class="admin">Default User Name<B> - admin</B>.</lI>
./web/help/RestoreDefaultCfgHelpRpm.htm:21:<li class="admin_0">Default Password<B> - admin</B>.</lI>...
./etc/passwd.bak:1:admin:$1$$iC.dUsGpxNNJGeOm1dFio/:0:0:root:/:/bin/sh

\`grep\`ping the admin account's password hash

So, we can login to the router admin interface thanks to the golden keys of… `admin:admin`. Of course!

Behind the gates we sneezed on to open...!

Additionally, as we recovered an UNSALTED admin hash from `/etc/shadow`, we can try and crack the `admin` OS user account’s password (whose permissions are mapped to `root`, `GUID=0`, so an account that’s kinda useful):

## Create password hash file
printf '%s\n' '$1$$iC.dUsGpxNNJGeOm1dFio/' > admin.hash

check it with cat

cat admin.hash

download a wordlist (see link) & use with hashcat to crack MD5 hash (easy, as has no salt)

hashcat -m 500 -a 0 admin.hash common.txt --status

Our master account password was, of course, cracked within a few seconds…

Cracked in 1,2,3,4...

`admin:1234` \- fun times. i’m quaking in my boots :’).

well, until next time - see you for the squeakquel!

(Jeanette is my spirit animal)

blog.juni-mp4.com · 35 images · 48 links

Comments (0)

No comments yet