Prelude
I recently came under the possession of a TP-Link Deco XE5300 Mesh Wi-Fi Router and though I don’t have much use for it, I decided that since it was the only router in my collection that wasn’t yet EoL, there might be some use in poking around at its firmware. (And because of the Broadcom chipset and Wi-Fi 6E capability, neither the OpenWrt or DD-WRT people have looked at it)
Because I wasn’t interested in bypassing signing checks or accidentally bricking the device, I decided against modifying the firmware and trying to upload it to the router. Instead I downloaded the latest firmware for my hardware version from their website and got to work.
Extracting Files
The firmware image downloads as a .ZIP file, which contains 3 files.
Archive: XE75_XE5300v1--up-ver1-4-5-P1[20241129-rel44718].zip
Length Date Time Name
--------- ---------- ----- ----
0 2024-12-04 10:25 XE75_XE5300v1--up-ver1-4-5-P1[20241129-rel44718]/
442346 2023-12-13 11:20 XE75_XE5300v1--up-ver1-4-5-P1[20241129-rel44718]/GPL License Terms.pdf
231408 2020-02-13 20:19 XE75_XE5300v1--up-ver1-4-5-P1[20241129-rel44718]/How to upgrade TP-Link Deco Whole Home Wi-Fi System.pdf
35392982 2024-12-03 10:10 XE75_XE5300v1--up-ver1-4-5-P1[20241129-rel44718]/XE75_XE5300v1--up-ver1-4-5-P1[20241129-rel44718].bin
--------- -------
36066736 4 files
The .bin is the only interesting file here, but since this is a binary blob, I decided to use binwalk on it to find all the embedded files within. Luckily, it did find a lot of Device Tree Blobs and a squashFs filesystem appended to the end (offset 0x548B6C). This was quite fortunate since TP-Link has started encrypting their firmware binaries in newer versions and making it much harder to reverse engineer. Researchers have already found ways to decrypt these new versions, but it requires additional steps that I’m glad to avoid.
With the filesystem expanded, it became clear that this was a full Linux-like system.
| |
This is initially intimidating, but we can eliminate bootfs, data, dev, mnt, overlay, proc, root, sys, and tp_data because they’re empty, var for being a symlink, and both rom and /tmp since they each only contain one file of no importance. This leaves us with 7 folders to sift through.
There are 4 main file types used on this system:
- Plaintext (configuration, Lua scripts, Bash scripts, etc.)
- ELF binaries (ARM, some proprietary, some not)
- Encrypted blobs (
/fw_data) - Lua bytecode (version 5.1, modified)
For the ELFs, you’ll have to use your favorite reverse engineering tool to decompile them, but they’re not obfuscated and most functions can be identified by using the source code of the libraries they use. They are all ARM architecture, but Qemu can run these on any host architecture.
The encrypted blobs are a mystery which we’ll look at later, and the Lua bytecode is pretty strange since the last Lua 5.1 release was in 2014, but reading /etc/openwrt_release immediately answers the latter mystery.
| |
Turns out the firmware is built entirely off of OpenWrt 12.09, which explains the “GPL Code” section of the TP-Link download center site. Those of you who have experience in the router firmware space may have already identified it, but OpenWrt makes frequent use of Lua for a lot of its components, specifically a modified version of Lua 5.1. Knowing this makes it possible to decompile the Lua bytecode files almost perfectly back into their source code using a tool called luadec. However, because OpenWrt has modified Lua’s opcodes, we need to use luadec-openwrt, which provides instructions on how to build luadec so it can decompile OpenWrt Lua files.
Poking Around
Since plaintext is the easiest to deal with and read, we can take a look at some of the more interesting configuration files, and because it’s Linux we can look in familiar spots.
Immediately looking at /etc/passwd showed that there were a few users, but only root was set to a valid login shell (/bin/ash), so naturally I checked /etc/shadow to see if you could log in as root.
root:$1$deco$d12d45CZve2tQoidyZWiB.:17603:0:99999:7:::
daemon:*:0:0:99999:7:::
ftp:*:0:0:99999:7:::
network:*:0:0:99999:7:::
nobody:*:0:0:99999:7:::
Shockingly, yes, you could. $1$ corresponds to MD5, deco is the salt, and d12d45... is the hash. I tried cracking it with rockyou and some basic brute force patterns but had no luck.
It’s possible this is an old configuration account that was used during testing, maybe they turned off password authentication?
config dropbear
option PasswordAuth 'on'
option RootPasswordAuth 'on'
option Port '20001'
# option BannerFile '/etc/banner'
| |
| |
| |
Nope. No less than 4 separate places where PasswordAuth, RootPaswordAuth, and RootLogin are all enabled in dropbear config and startup files.
I can only assume that someone at TP-Link created this password and now has it on file somewhere. The good news is that SSH isn’t accessible on WAN, but the bad news is that SSH is still accessible locally and if KimWolf has taught us anything, it’s that internal attacks can still happen and the last thing you’d want is someone having root shell access on your router.
There’s also a few interesting files that are unencrypted in the fw_data/ folder, including a partition table, hardware IDs, and product information. In the /fw_data/manu_data/product-info file, there’s a base64-encoded RSA key in binary blob format.
vendor_name:TP-LINK
vendor_url:www.tp-link.com
product_name:XE75
product_type:HOMEWIFISYSTEM
language:US
product_ver:1.0.0
product_id:58457510
special_id:55530000
hw_id:12FA5BBBB757762A8DA29428270BA277
oem_id:64C34DA29684B77242870A7E5640042D
country:US
key:AAAAB3NzaC1yc2EAAAADAQABAAAAQQCeYwFZQmXBDoZCYIlF94nsCgtZUBSjj5YrrAQeGYYXeDPuGn1p4ih5p+wZG14utsut15Fba55fk5RUWckdNdyDAAAAQAi/9nCcauul5kPfemXrnWp1SZg3BNTeV1D1UUrqZQ6BxoXvAyJ20seLyJSkVumnHy6i613qFcO3eo4EUG6/uHkAAAAhAOY20wkmRnlQpgKYjZPD9bFqWJ/3HseiZb17JIGih3EZAAAAIQCwIJjMpr6Pxl1dU/FGDSJEh3CEp2foUvUuvUmciXvh+w==
gid:1ad4b09e-beb0-11e7-b722-50c7bf2a6709
And another (different) key in /fw_data/user_data/group-info file.
| |
The product-info key is used as the default “group key” for all the mesh nodes to communicate between each other (and with the mobile app) before fresh certs have been generated. Essentially, all communications with the main router and mesh APs use public-key SSH on port 20001 for firmware upgrades, telemetry, and configuration changes (and port 20002 over UDP with tmpsvr and tmpcli). There are a few different communication types best summarized in a comment left in /lib/sync-server/scripts/probe (machine translated from Chinese).
| |
All the “communicators” (the router, APs, mobile app) have the same 2048-bit RSA private-key that gets distributed when a new device is setup. The default key gets replaced almost immediately, but it does present a (narrow) attack window in a factory-fresh state, and since it’s the same private key used throughout, a compromise of any one of the communicators will compromise the entire network’s security.
Fortunately, the rest of the application seems like standard Luci-based administration over (regrettably) non-SLL/TLS HTTP communication, mostly based on OpenWrt design principles.However, I was still interested in what data was contained within the encrypted blobs of fw_data.
Decrypting fw_data
Knowing that something had to decrypt these files during runtime, finding what mechanism does this was the first priority.
Because the encrypted blobs are in the fw_data/ folder, I searched for “fw_data” and got hits on a few shell scripts and two binary files, crytool and nvrammanager. I figured crytool had something to do with cryptography, and even found references to it in some of the Lua bytecode. Decompiling all the Lua scripts (with our modified luadec) revealed this invocation of crytool:
| |
Where MD5_KEY and DES_KEY were static variables defined as:
| |
This seems like a smoking gun, but checking for further references of this function reveal that this is old code no longer used or invoked by any of the other functions. In fact, I’m fairly certain that crytool was the old decryption tool used before it was replaced by newer methods. Those newer methods I found by noticing that nvrammanager will copy files out of the fw_data/ directory given a partition name.
Usage: nvrammanager [OPTIONS1] [OPTIONS2]
[OPTIONS1]
-r, --read=FILE Read partition to FILE
-w, --write=FILE Write FILE to partition
-e, --erase Erase partition
-s, --show Show partition table
-u, --upgrade=FILE Upgrade firmware from FILE
-c --check=FILE Check whether the firmware is valid
-f, --factory Force reset to factory config after upgrade
-h, --help Display usage
-i, --init setup system and profile info to directory /tmp/
[OPTIONS2]
-p, --partition=PTN_NAME Partition name. It's needed when OPTIONS1(-r or -w or -e) exists.
--reserve=bytes Reserve space for partition
Checking for its invocation in scripts reveals that it’s used to move around these firmware blobs (and write to them for updates and factory resets). It does no encryption/decryption. There are a lot of uses of nvrammanager throughout the system, so to focus my search I looked at the default-config file (located at /fw_data/default_config). I immediately noticed a few commands using nvrammanager to copy this file to the /tmp directory as a .cry file. The decompiled /usr/lib/lua/luci/sys/config.lua file contained this snippet:
| |
The part I’m most interested in is the cry.dec_file_entry function, which is clearly decrypting the file into XML. It’s located in /usr/lib/lua/luci/model/crypto.lua.
dec_file_entry() is a wrapper around two functions, dec_file() if using OpenSSL and wolfssl_enc_dec_file() if using WolfSSL. I know OpenSSL is used much more often, so following the dec_file() function leads to a call to popen on a string generated by build_nopwd_cmd().
| |
Which relies on the following global variables:
| |
This chain forms the command openssl aes256-cbc -d -in {file_name} -K 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836 -iv 360028C9064242F81074F4C127D299F6 | openssl zlib -d which uses AES256-CBC decryption on the hardcoded key and IV values, followed by zlib decompression.
The four encrypted blobs (default_config, profile, user_data/device-config, user_data/user-config) are all large XML files with configuration information. Most of it is standard default settings (like Wi-Fi SSID’s and parental controls), but there appears to be even more hardcoded default keys.
| |
| |
| |
| |
Some sort of “legality” switch:
| |
And the SSH settings that confirm access to the root account via password:
| |
Version Audit
Firmware bundles like these are often woefully out-of-date in their packages, and because this system employs so many, it felt appropriate to audit a few of the more popular tools it employs. The firmware was released in December of 2024, so that’ll be the target to meet.
- OpenSSL:
OpenSSL 1.0.2d 9 Jul 2015 - libc:
uClibc-ng 1.0.43(Apr 5 2023) - dropbear:
Dropbear v2019.78 - curl:
curl 7.40.0 (arm-openwrt-linux-uclibc) libcurl/7.40.0(Jan 8 2015) - BusyBox:
BusyBox v1.22.1(Released Jan 19 2014) - Linux:
Linux/arm 4.19.151(Released Oct 14 2020)
The libc is the most “up-to-date”, but the worst offenders (OpenSSL, dropbear) are by far the most critical to security, both with numerous published CVE’s (and future ones) that could seriously compromise the security of the router.
Conclusions
The root login is a glaring issue that should be addressed, especially since there’s already robust public-key authentication systems built in that leave no reason for password authentication to be enabled. There’s also several packages over a decade out of date in a router that is not EoL (as of June 2026).
The fact OpenWrt is used for such a large portion of this project yet does not have support from TP-Link to develop an official OpenWrt port is disappointing. Understandably, this is mostly due to the use of the Broadcom chipset. Broadcom is notorious for keeping its drivers closed-source and away from public repositories, but a larger commitment by TP-Link towards open source projects would go a long way towards trusting the security of their routers.