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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$> ls -o
total 80
drwxrwxrwx  2 root  4096 Nov 28  2024 bin
drwxrwxrwx  2 root  4096 Nov 28  2024 bootfs
drwxrwxrwx  2 root  4096 Nov 28  2024 data
drwxrwxrwx  2 root  4096 Nov 28  2024 dev
drwxrwxrwx 35 root  4096 Sep 19  2024 etc
drwxrwxrwx  4 root  4096 Nov 28  2024 fw_data
drwxrwxrwx 42 root 12288 Nov 28  2024 lib
drwxrwxrwx  2 root  4096 Nov 28  2024 mnt
drwxrwxrwx  2 root  4096 Nov 28  2024 overlay
drwxrwxrwx  2 root  4096 Nov 28  2024 proc
drwxrwxrwx  2 root  4096 Nov 28  2024 rom
drwxrwxrwx  2 root  4096 Nov 28  2024 root
drwxrwxrwx  2 root  4096 Nov 28  2024 sbin
drwxrwxrwx  2 root  4096 Nov 28  2024 sys
drwxrwxrwx  2 root  4096 Nov 28  2024 tmp
drwxrwxrwx  2 root  4096 Nov 28  2024 tp_data
drwxrwxrwx  8 root  4096 Sep 19  2024 usr
lrwxrwxrwx  1 root     4 Nov 28  2024 var -> /tmp
drwxrwxrwx  5 root  4096 Nov 28  2024 www

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.

1
2
3
4
5
6
7
DISTRIB_ID='OpenWrt'
DISTRIB_RELEASE='Attitude Adjustment'
DISTRIB_REVISION='unknown'
DISTRIB_CODENAME='attitude_adjustment'
DISTRIB_TARGET='model_bcm_bcm675x/generic'
DISTRIB_DESCRIPTION='OpenWrt Attitude Adjustment 12.09-rc1'
DISTRIB_TAINTS='%t'

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'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
validate_section_dropbear()
{
	uci_validate_section dropbear dropbear "${1}" \
		'PasswordAuth:bool:1' \
		'enable:bool:1' \
		'Interface:string' \
		'GatewayPorts:bool:0' \
		'RootPasswordAuth:bool:1' \
		'RootLogin:bool:1' \
		'rsakeyfile:file' \
		'dsskeyfile:file' \
		'BannerFile:file' \
		'Port:list(port):20001' \
		'SSHKeepAlive:uinteger:300' \
		'IdleTimeout:uinteger:0' \
		'mdns:uinteger:1'
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
config_get_dropbear()
{
	local section=$1
	local section_msg=$2

	config_get_bool PasswordAuth $section PasswordAuth 1
	config_get_bool enable $section enable 1
	config_get Interface $section
	config_get_bool GatewayPorts $section GatewayPorts 0
	config_get_bool RootPasswordAuth $section RootPasswordAuth 1
	config_get_bool RootLogin $section RootLogin 1
# [TRUNCATED]
1
2
3
4
5
6
7
dropbear_instance()
{
	local PasswordAuth enable Interface GatewayPorts \
		RootPasswordAuth RootLogin rsakeyfile \
		dsskeyfile BannerFile Port SSHKeepAlive IdleTimeout \
		mdns ipaddrs EnableLoginDelay FailureMaxTimes FailureLockMinute OnlyIPv4 
# [TRUNCATED]

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.

1
2
3
4
5
{
	"role":"AP",
	"key":"AAAAB3NzaC1yc2EAAAADAQABAAAAQQC/q1ssRADI/lt3+ZqGrkvGFcsqp2YwaDFm4BftZC17yffpRY4IlbgsKRsHvqFpP+ZkWZSE4y6OiT+Kr8eTcQlBAAAAQBfgpKctMhVYBaRavKbMWh17W8/sookDg+sDXgu5fAaUhW9JwgdXPDS8D0cVBk5Hg/fHI54NjtA5wtPSkuETgKkAAAAhAOwtOrktdJ+mszMwnEAsELe8VRfMkXfLKZqflANJ2hCZAAAAIQDPwcpAklQjNe4fhhH6+nkFbBLh/DPqG24Q8bI+k/Ye6Q==",
	"gid":"70303de6-63d9-11e8-a3f6-0000eb367511"
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- v2 (default): SSH encryption excludes insecure algorithms, and the group key is already RSA-2048.
-- v1: SSH encryption excludes insecure algorithms, but some units have not yet updated their group keys to RSA-2048.
--     If an FAP receives a probe from an RE indicating v1, it synchronizes the 2048-bit group key and can quickly revert to v2.
--     v1 transmission logic: If the unit is bound but lacks the 2048-bit group key, it includes v1 in the probe. Only REs perform this check; FAPs missing the key (e.g., after a firmware upgrade) generate it upon startup.
--     v1 reception logic: Writes "v1" to `/tmp/ssh_client_sec` and restarts dropbear. dropbear/libssh configure key exchange algorithms using the legacy method, and dropbear loads the 512-bit RSA key.
--                         The FAP uses `update_groupkey` to synchronize the 2048-bit group key to the RE.
-- v0: Legacy devices exist in the network; SSH encryption and public keys must maintain backward compatibility.
--     Can revert to v2 once legacy devices go offline.
--     v0 transmission logic: Includes v0 in the probe if the unit is unbound or already has the 2048-bit group key, but the network negotiation state is v0.
--     v0 reception logic: Only records the timestamp; no other processing occurs.
-- none (field absent): Indicates a legacy device.
--     none reception logic: Writes "v0" to `/tmp/ssh_client_sec` and restarts dropbear. dropbear/libssh configure key exchange algorithms using the legacy method, and dropbear loads the 512-bit RSA key.
-- Note: Operations here are somewhat asymmetric. For instance, v1 is primarily a local state; transmission checks only the local group info, whereas reception requires writing to `/tmp/ssh_client_sec` to adjust dropbear/libssh algorithms.
--       v0 is a state for devices running new firmware (with 2048-bit group keys) that detect legacy devices on the network;

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
merge_usrconf_to_defconf = function()
  -- function num : 0_22 , upvalues : MD5_KEY, DES_KEY, luci
  local cmd = nil
  cmd = "crytool -r /tmp/default.config.json -m " .. MD5_KEY .. " -d " .. DES_KEY .. " -p default-config >/dev/null 2>&1"
  ;
  ((luci.sys).call)(cmd)
  cmd = "crytool -r /tmp/user.config.json -m " .. MD5_KEY .. " -d " .. DES_KEY .. " -p user-config >/dev/null 2>&1"
  ;
  ((luci.sys).call)(cmd)
  cmd = "jsonmerge /tmp/user.config.json /tmp/default.config.json /tmp/merge.json"
  ;
  ((luci.sys).call)(cmd)
  cmd = "crytool -w /tmp/merge.json -m " .. MD5_KEY .. " -d " .. DES_KEY .. " -p user-config -f 0 >/dev/null 2>&1"
  ;
  ((luci.sys).call)(cmd)
  ;
  ((luci.sys).call)("rm -f /tmp/default.config.json >/dev/null 2>&1")
  ;
  ((luci.sys).call)("rm -f /tmp/user.config.json >/dev/null 2>&1")
  ;
  ((luci.sys).call)("rm -f /tmp/merge.json >/dev/null 2>&1")
end

Where MD5_KEY and DES_KEY were static variables defined as:

1
2
local MD5_KEY = "478DA50BF9E3D2CF8819839D4C061445"
local DES_KEY = "478DA50BF9E3D2CF"

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
get_dft_account = function()
  -- function num : 0_21 , upvalues : os, require, io, get_key
  (os.execute)("nvrammanager -r /tmp/default-config.cry -p default-config >/dev/null 2>&1")
  local cry = require("luci.model.crypto")
  ;
  (cry.dec_file_entry)("/tmp/default-config.cry", "/tmp/default-config.xml")
  local fp = (io.open)("/tmp/default-config.xml", "r")
  if not fp then
    return 
  end
  local dft_cfg = fp:read("*a") or ""
  fp:close()
  ;
  (os.execute)("rm -f /tmp/default-config.xml /tmp/default-config.cry >/dev/null 2>&1")
  dft_cfg = get_key(dft_cfg, "accountmgnt", true)
  local dft_account = {}
  dft_account.username = get_key(dft_cfg, "username")
  dft_account.username_md5 = get_key(dft_cfg, "username_md5")
  dft_account.shadow = get_key(dft_cfg, "shadow")
  dft_account.password_md5 = get_key(dft_cfg, "password_md5")
  dft_account.version = get_key(dft_cfg, "version")
  return dft_account
end

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().

1
2
3
4
5
6
7
local build_nopwd_cmd = function(file, nozlib, enc)
  -- function num : 0_5 , upvalues : enc_nopwd_cmd_nozlib, enc_nopwd_cmd, dec_nopwd_cmd_nozlib, dec_nopwd_cmd, file_opt, nopwd_opt
  local cmd = nil
  cmd = not enc or (nozlib and enc_nopwd_cmd_nozlib) or enc_nopwd_cmd
  cmd = nozlib and dec_nopwd_cmd_nozlib or dec_nopwd_cmd
  return (cmd) % {file and file_opt % file or "", nopwd_opt}
end

Which relies on the following global variables:

1
2
3
4
5
6
7
local cipher = "aes-256-cbc"
local dec_nopwd_cmd = "openssl " .. cipher .. " -d %s %s | openssl zlib -d"
local dec_nopwd_cmd_nozlib = "openssl " .. cipher .. " -d %s %s"
local dft_key = "2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836"
local dft_iv = "360028C9064242F81074F4C127D299F6"
local nopwd_opt = "-K " .. dft_key .. " -iv " .. dft_iv
local file_opt = "-in %q"

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<groupkey2048 name="groupkey2048">
	<count>0</count>
	<key1>
		AAAAB3NzaC1yc2EAAAADAQABAAABAQCcHRHgijTiA+mK+9JrYIvWOKJ5PM6nN5QUT1wdy5q5RiMuH6I/kVYmBFaTJqHgdQH50iBrZBrMMYJ+aChQFlQPGY344Ji6kl8Lb2SJVHdNTdaj1t1LbTdFiMUZEtESPixcWms4PgCyoFxgfPkilZ0pg9sTVFWJD3dQMw7Tjwl</key1>
	<key2>
		cC1+lWsGw1JzjFFBAbBRBhABSYwlriHhWw4N0UTAvUIkzKUgSaozDGOqRi0aXZOC220XwIy+JnU4ralHwiOF+TX3pprPX5sj/Ow6Rq8qmgJaluKHn3VTuMXRstAqjM/ZMUlG5yUaX48TY7eIi9N9OJneohsDob0zrnTCpRnO1KmstAAABAAM2Et57oU5ZNmuHi2LDjo</key2>
	<key3>
		97xJXPWHs2k2Bz6HPBctTm+oVx8k2x8OPLzjcX10JteGVZ5epSRGiDsojknhhVo+mCmk8ZWZ2UMgnOGF3oePgZXDYBubVvO/DoI7vEuBG9adQFf8lH9zjgFRO0BmY9zcjjP4qkfwpDIUPAT8iuG3XDSt5Et711EgZWIaV61XsaOfRuKu053goS3Hz/lGfHtEhaR3TjQ</key3>
	<key4>
		+JxiCcsHAu8w/2u1oPcp/tDyk2ypzFohxtUnhxVCn5ynh8rdEtaH+sIkL+1W4K89XdFvdgyt5RaqU4Y1N2+uxWt3ZvPbqRRGp4B8vUGBglclCfI0pHo2wRdBYEAAACBALW/AqeSmItsHyl2WYahjo8s5bcECfJKFB7zBCTRua0OXCRPE8qTUwJuZtEs0joxxgACI6iC</key4>
	<key5>
		6wZ5FWGtJcowPn2/ME+yBUdpVLrodKE3nmpkD8F2QVSGHBnZ37+K8LsXta3yUIIAbOOppQTrSSecGlAL1XeArpTFbHrcJcon9D+tAAAAgQDb5SAIXGY6sMSisXv4eJgsvLSH1WfoKNc6X8VVCp05uLynmb2RpMBFRpZ/y30qEQYbXr/9CCn7lLc7a4ZQgd/SZNPi5Fw</key5>
	<key6>
		7MGpwsGZI4QAPXxb1+qzI5XDY4kI9YHk6NPtHpc88b2eyQxLpAdnYDVhDeBvjTwtEcoixW50XhmZJgQ==</key6>
</groupkey2048>
1
2
3
4
5
6
7
8
<cloud_account>
	<username>test@tp-link.net</username>
	<username_md5>310bd3debdf0628c0d7db54970fda3b0a26de417eb8083f707770b8121804663</username_md5>
	<shadow>126D8rSh5sjUE</shadow>
	<password_md5>
		U2FsdGVkX18oPN94tUqifrI2DCrgHZqjE8X1HNEa8P9woj506Nbx2EsDG9TNIgsos5LZizrgba3PdlUI7yhjmQ==</password_md5>
	<version>1</version>
</cloud_account>
1
2
3
4
5
6
7
<rsa_mgnt>
	<rsa name="keys">
		<e>010001</e>
		<d>091550E28B45A770B296EDAEEF04E687F3258AB765A22E7CEA9D1BC8EB10BD2A0601A4421D267FD5ED5BF25A7372B67FFAD6D41A81A194B67623617F0A86A28F3727A6EC0E34ACCA4823F486CB3E08D9BBC2D043D62CC943EF898EF7C74CDCD8E9CEA87006019D6464B7B2BA37043D911611580A7A87D862E6BEBE4AD96146B1</d>
		<n>D1E79FF135D14E342D76185C23024E6DEAD4D6EC2C317A526C811E83538EA4E5ED8E1B0EEE5CE26E3C1B6A5F1FE11FA804F28B7E8821CA90AFA5B2F300DF99FDA27C9D2131E031EA11463C47944C05005EF4C1CE932D7F4A87C7563581D9F27F0C305023FCE94997EC7D790696E784357ED803A610EBB71B12A8BE5936429BFD</n>
	</rsa>
</rsa_mgnt>
1
2
3
4
<group_info>
	<gid>1ad4b09e-beb0-11e7-b722-50c7bf2a6709</gid>
	<key>AAAAB3NzaC1yc2EAAAADAQABAAAAQQCeYwFZQmXBDoZCYIlF94nsCgtZUBSjj5YrrAQeGYYXeDPuGn1p4ih5p+wZG14utsut15Fba55fk5RUWckdNdyDAAAAQAi/9nCcauul5kPfemXrnWp1SZg3BNTeV1D1UUrqZQ6BxoXvAyJ20seLyJSkVumnHy6i613qFcO3eo4EUG6/uHkAAAAhAOY20wkmRnlQpgKYjZPD9bFqWJ/3HseiZb17JIGih3EZAAAAIQCwIJjMpr6Pxl1dU/FGDSJEh3CEp2foUvUuvUmciXvh+w==</key>
</group_info>

Some sort of “legality” switch:

1
2
3
4
<cloud_push name="device_legality">
	<illegal>0</illegal>
	<illegal_type>0</illegal_type>
</cloud_push>

And the SSH settings that confirm access to the root account via password:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<dropbear>
	<dropbear>
		<RootPasswordAuth>on</RootPasswordAuth>
		<Port>22</Port>
		<PasswordAuth>on</PasswordAuth>
		<SysAccountLogin>off</SysAccountLogin>
	</dropbear>
	<dropbear2 name="default">
		<RootPasswordAuth>on</RootPasswordAuth>
		<Port>20001</Port>
		<PasswordAuth>on</PasswordAuth>
		<SysAccountLogin>off</SysAccountLogin>
	</dropbear2>
</dropbear>

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.