Gaining SSH access to TP-Link RE200 device by exploiting the fact that TP-Link encryption keys are store on its firmware.
This story started with me getting a TP-Link repeater for my loft so that I could provide wireless coverage to my smart boiler. I wish the boiler came with an RJ45 connector port, but it didn’t.
And since all devices that connect to the homelab network (and stay connected for a while) get scanned by OpenVAS, I received a report saying that an insecure SSH server has been detected. How about that?
Available, but Unavailable, SSH Server
I thought I’d have a look. I did what everyone would have done in this case – ran an Nmap scan.
Nmap scan revealed an old Dropbear server running on the repeater:
$ nmap -A -p T:22 re200.hl.test Host is up (0.0063s latency). PORT STATE SERVICE VERSION 22/tcp open ssh Dropbear sshd 2016.74 (protocol 2.0) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Furthermore, SSH audit provided several CVEs that applied to the Dropbear server in question:
$ git clone https://github.com/jtesta/ssh-audit.git $ cd ./ssh-audit $ ./ssh-audit.py re200.hl.test # general (gen) banner: SSH-2.0-dropbear_2016.74 (gen) software: Dropbear SSH 2016.74 (gen) compatibility: OpenSSH 6.4-6.6, Dropbear SSH 2013.62-2014.66 (gen) compression: disabled # security (cve) CVE-2018-15599 -- (CVSSv2: 5.0) remote users may enumerate users on the system (cve) CVE-2017-9079 -- (CVSSv2: 4.7) local users can read certain files as root (cve) CVE-2017-9078 -- (CVSSv2: 9.3) local users may elevate privileges to root under certain conditions [...]
Unfortunatelly, attempting to connect to the SSH server to obtain shell access using Web UI credentials did not work:
PTY allocation request failed on channel 0 shell request failed on channel 0
After a bit on online search, I’ve found the following statement on TP-Link’s website:
“SSH Services on the TP-Link products are only available for TP-Link apps. Other SSH clients cannot access to TP-Link products or adjust their settings with command lines. So please rest assured that the SSH will never cause any safety issues on your device.”
Rest assured.
Using Exploit Database
Having such an old version of Dropbear installed meant that there were likely vulnerabilities to be found.
Where do you turn to for known exploits? The Exploit Database. After searching for various CVEs and TP-Link related issues, I’ve come across this remote code execution exploit:
https://www.exploit-db.com/exploits/50962
While it did not apply my device, it shed light on a new attack vector – a backup and restore functionality.
It turned out that TP-Link RE200 provided a Web UI option to create and download a configuration backup file, which could then be modified locally, and uploaded back to the device in order to overwrite SSH server configuration settings.
There was one caveat though: the backup file was encrypted (its type was data), because making it plaintext would have been too easy.
The exploit code on the website is a Python script that’s not too difficult to read. The script logs into the router using a login form, downloads a config file using a backup operation, decrypts and decompresses the config file, then splits the decrypted data file into two (an MD5 checksum and a tar archive), extracts the tar archive, decrypts and decompresses each bin file from the tar archive. At this point a plaintext XML file is available that contains configuration settings. Simple. The process is reversed in order to generate an encrypted config file and restore it to the router using a restore operation. The exploit contains TP-Link router’s encryption keys. Marvellous.
The Search for TP-Link RE200 Backup Encryption Keys
I wanted to work out the location of the encryption keys for the repeater, and whether the keys would be the same as for the router exploit provided by Exploit Database.
TL;DR
The keys were the same.
- AES key: 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836
- IV: 360028C9064242F81074F4C127D299F6
Download RE200 V2 Firmware
I’ve downloaded the device firmware so that I could have a look at what happens when a new backup is created using the Web UI.
$ mkdir ./tp-link-re200 && cd ./tp-link-re200 $ curl -fsSL -o RE200_V2_221012_RC.zip https://static.tp-link.com/upload/firmware/2022/202210/20221019/RE200_V2_221012_RC.zip $ md5sum ./RE200_V2_221012_RC.zip c07fbff37efb98b620c62e592193e144 ./RE200_V2_221012_RC.zip $ unzip ./RE200_V2_221012_RC.zip $ ls -1 GPL License Terms.pdf 'How to upgrade TP-LINK Wireless Range Extender(tplinkrepeater.net version)-NEW.pdf' RE200_V2_221012_RC.zip 're200v2_up-ver1-1-9-P1[20221012-rel7572].bin'
Inside the zip file we have manuals and a bin file with firmware, which can be extracted using binwalk
:
$ binwalk --extract ./re200v2_up-ver1-1-9-P1\[20221012-rel7572\].bin
We can see a squashfs-root
directory which is an unencrypted filesystem that would normally be deployed to our repeater’s flash memory:
$ ls -1 ./_re200v2_up-ver1-1-9-P1\[20221012-rel7572\].bin.extracted/ 192C5 192C5.7z EE8B5.squashfs squashfs-root
Putting a Detective’s Hat On
In order to create a backup file, we have to log into the Web UI, navigate to System Tools > Backup & Restore > Backup and press the backup button.
If we open up a web browser’s Developer Tools panel, we will see the application making a POST request to /cgi-bin/luci/;stok=token/admin/firmware
. Internet tells us that Luci is the main web administration utility for OpenWrt (an embedded operating systems that TP-Link use on their devices).
Let us change to the squashfs-root
directory and see where this request is referenced in:
$ cd ./_re200v2_up-ver1-1-9-P1\[20221012-rel7572\].bin.extracted/squashfs-root/ $ grep "/admin/firmware" -r ./ ./www/webpages/pages/userrpm/basic.html: url: $.su.url("/admin/firmware?form=upgrade")//"./data/firmware.set.json" ./www/webpages/pages/userrpm/firmwareUpgrade.html: var UPGRADE_URL_NEW = $.su.url("/admin/firmware?form=upgrade"); ./www/webpages/pages/userrpm/backupRestore.html: var BACKUP_URL_NEW = $.su.url("/admin/firmware?form=config"); ./www/webpages/index.html: url: $.su.url("/admin/firmware?form=upgrade") ./www/webpages/url_to_json/url_to_json_ycf.txt:/cgi-bin/luci/;stok=12345/admin/firmware?form=upgrade firmware.set.json ./www/webpages/url_to_json/url_to_json_ycf.txt:/cgi-bin/luci/;stok=12345/admin/firmware?form=config system.backup.json ./usr/lib/opkg/info/luci-apps.list:/usr/lib/lua/luci/controller/admin/firmware.lua
We can see that it’s referenced in backupRestore.html
file, there is a backup function called when we press a button:
$("#backup").button({ text: $.su.CHAR.BACKUP.BACKUPBTN, cls: "submit", handler: function(){ backup_proxy.write({operation:'check'}, function(data){ $("#backup-setting").form('submit', {operation:'backup'}); }); } });
According to OpenWRT documentation, LuCI installation directory is /usr/lib/lua/luci
, and admin modules are located in controller/admin
.
There are a couple of lua files in there that are of interest, namely:
- squashfs-root/usr/lib/lua/luci/controller/admin/firmware.lua – responsible for the backup/restore process.
- squashfs-root/usr/lib/lua/luci/model/crypto.lua – responsible for handling encryption/decryption.
Both files are compiled using Lua’s 5.1 bytecode format.
$ file ./usr/lib/lua/luci/controller/admin/firmware.lua ./usr/lib/lua/luci/controller/admin/firmware.lua: Lua bytecode, version 5.1 $ file ./usr/lib/lua/luci/model/crypto.lua ./usr/lib/lua/luci/model/crypto.lua: Lua bytecode, version 5.1
I tried reversing Lua’s bytecode by using luadec
but it resulted in an “bad header in precompiled chunk” error. According to the Internet, somebody wrote a fully working Lua 5.1 bytecode parser.
Using strings
yielded positive results.
$ strings ./usr/lib/lua/luci/controller/admin/firmware.lua [...] md5_product_name /tmp/product_name_md5_file [...] backup_restore [...] nvrammanager -r /tmp/backup/ori-backup-user-config.bin -p user-config >/dev/null 2>&1 luci.model.crypto dec_file_entry /tmp/backup/ori-backup-user-config.bin /tmp/backup/ori-backup-user-config.xml mkdir -p /tmp/backupcfg xmlToFile /tmp/backupcfg accountmgnt cloud_config rm -f /tmp/backupcfg/config/ rm -f /tmp/backup/ori-backup-user-config.bin /tmp/backup/ori-backup-user-config.xml fileToXml /tmp/backupcfg/config enc_file_entry rm -rf /tmp/backupcfg /tmp/backup/ori-backup-user-config.xml tar -cf /tmp/ori-backup-userconf.bin -C /tmp/backup . >/dev/null 2>&1 [...]
While the sequences of printable characters is a bit tricky to read, we can identify some kind of a pattern:
- At some stage the script calculates md5sum checksum of the product.
- The
nvrammanager
reads partition data to file. luci.model.crypto
is used.- File is decrypted (dec_file_entry).
- LUA script converts decrypted xmlToFile.
- Removes two files for security reasons (according to this): accountmgnt and cloud_config.
- LUA script converts fileToXml.
- File is encrypted (enc_file_entry).
We know that the backup file is encrypted and compressed. It does not seem to be archived though. What we don’t know is the encryption function and its key. Let us have a look at luci.model.crypto
.
$ strings ./usr/lib/lua/luci/model/crypto.lua [...] aes-256-cbc openssl zlib -e %s | openssl -e %s openssl -d %s %s | openssl zlib -d -e %s %s -d %s %s -in %q -k %q -kfile /etc/secretkey 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836 360028C9064242F81074F4C127D299F6 -iv crypt_used_openssl [...]
Lua script uses OpenSSL with zlib support in CBC mode to encrypt the backup file. The AES key and IV parameters can be seen in the strings command output above.
Decrypt TP-Link config.bin Backup File to XML
Download the config.bin
backup file using Web UI. It’s going to be encrypted data.
$ file ./config.bin ./config.bin: data
If we look on GitHub, we will find several repositories containing TP-Link backup decryption scripts. For example, somebody wrote one here.
To decrypt the file, we are going to use openssl
with zlib support. It’s available on Rocky 8 (OpenSSL 1.1.1k) and Rocky 9 (OpenSSL 3.0.7) out of the box.
Decrypt and decompress the backup file:
$ openssl aes-256-cbc -d \ -K 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836 \ -iv 360028C9064242F81074F4C127D299F6$AES \ -in config.bin | openssl zlib -d -out config.bin.decrypted
The first 16 bytes will be MD5 checksum of our product, therefore we can skip them.
$ dd if=config.bin.decrypted of=config.bin.decrypted-16b bs=1 skip=16 2>/dev/null
Decrypt again to get the XML file (thanks to bin2xml.sh):
$ openssl aes-256-cbc -d \ -K 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836 \ -iv 360028C9064242F81074F4C127D299F6$AES \ -in config.bin.decrypted-16b | openssl zlib -d -out config.xml
The result should be an XML file:
$ file ./config.xml ./config.xml: XML 1.0 document, ASCII text
Update XML Config to Enable SSH Server Access
The following lines should be present in the XML file:
<dropbear> <RootPasswordAuth>on</RootPasswordAuth> <SysAccountLogin>off</SysAccountLogin> <Port>22</Port> <PasswordAuth>on</PasswordAuth> </dropbear>
What do we need to add to the XML config file in order to enable SSH access? The answer lies in the Dropbear’s SysVinit file squashfs-root/etc/init.d/dropbear
.
If we look at the init script, we will find a function called dropbear_start()
with the following boolean logic:
config_get_bool val "${section}" RemoteSSH 0 [ "${val}" -eq 1 ] && append args "-L"
What it does is it sets an -L flag if RemoteSSH is true. According to Dropbear documentation, the flag allows us to “enable SSH session login”.
Make sure to keep SysAccountLogin as off as this allows to use the web server account login (where password is your web interface password).
Edit the file and add the following line:
<RemoteSSH>on</RemoteSSH>
So that the config file reads:
<dropbear> <RootPasswordAuth>on</RootPasswordAuth> <SysAccountLogin>off</SysAccountLogin> <Port>22</Port> <PasswordAuth>on</PasswordAuth> <RemoteSSH>on</RemoteSSH> </dropbear>
Save changes.
Encrypt XML Config to Binary File and Restore
Do note that in this section, we use the same file names as before, but in reverse order.
First of all, encrypt the modified XML file to create a config.bin.decrypted-16b
file:
$ cat config.xml | openssl zlib | openssl aes-256-cbc \ -K 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836 \ -iv 360028C9064242F81074F4C127D299F6$AES \ -out config.bin.decrypted-16b
Then, we need to generate an MD5 header for our product name, which is a string “RE200”, in a binary format (16 bytes):
$ echo -n "RE200" | md5sum | cut -d' ' -f 1 | xxd -r -p > md5header.bin
Concatenate MD5 binary file md5header.bin
and config.bin.decrypted-16b
file into config.bin.decrypted
:
$ cat md5header.bin config.bin.decrypted-16b > config.bin.decrypted
Finally, encrypt config.bin.decrypted
file to create the final config.bin
that will be restored to RE200:
$ openssl zlib -in config.bin.decrypted | openssl aes-256-cbc \ -K 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836 \ -iv 360028C9064242F81074F4C127D299F6$AES \ -out config.bin
We can now restore the modified config.bin
using TP-Link’s Web UI.
Profit
SSH into the device using the root user to obtain shell access. The root password is your web interface password.
$ ssh [email protected] BusyBox v1.22.1 (2022-10-10 06:42:02 PDT) built-in shell (ash) Enter 'help' for a list of built-in commands. MM NM MMMMMMM M M $MMMMM MMMMM MMMMMMMMMMM MMM MMM MMMMMMMM MM MMMMM. MMMMM:MMMMMM: MMMM MMMMM MMMM= MMMMMM MMM MMMM MMMMM MMMM MMMMMM MMMM MMMMM' MMMM= MMMMM MMMM MM MMMMM MMMM MMMM MMMMNMMMMM MMMM= MMMM MMMMM MMMMM MMMM MMMM MMMMMMMM MMMM= MMMM MMMMMM MMMMM MMMM MMMM MMMMMMMMM MMMM= MMMM MMMMM, NMMMMMMMM MMMM MMMM MMMMMMMMMMM MMMM= MMMM MMMMMM MMMMMMMM MMMM MMMM MMMM MMMMMM MMMM= MMMM MM MMMM MMMM MMMM MMMM MMMM MMMM MMMM$ ,MMMMM MMMMM MMMM MMM MMMM MMMMM MMMM MMMM MMMMMMM: MMMMMMM M MMMMMMMMMMMM MMMMMMM MMMMMMM MMMMMM MMMMN M MMMMMMMMM MMMM MMMM MMMM M MMMMMMM M M M --------------------------------------------------------------- For those about to rock... (Attitude Adjustment, unknown) --------------------------------------------------------------- @OpenWrt:/root$ id
Wonderful work mate. Well explained ! I was expecting same results with my RE220 V1 but unfortunately not working, tried to downgrade firmware and reset but no luck. ‘admin’ still gives pty channel 0 error and ‘root’ says permission denied. I used binwalk to compare files and code is practically the same with the RE200 V2. Even used the firmware tag in the config to run a sed command to modify root passwd in shadow but still nothing. Guess it was patched on the RE220.
Thanks so much Tenac!
Nice tutorial. I have an issue though. I have the Archer C6 v2 (EU), I am able to SSH to the router, but admin (WebUI password) does not have root access. Logging with SSH as root does not work:
[email protected].1.1: Permission denied (publickey,password)
Do you have any idea what to do?
Have you tried logging in as
admin
?Hello, I tried this method on my RE200 V4, but unfortunately, it didn’t work. I can’t log in with root and the web interface password; I can only log in with the admin user and the web interface password, but when logging in, it says “Server refused to allocate pty” and it doesn’t work. Any solution?
Hi Joaquin, your hardware version RE200 V4 is different compared to mine RE200 V2, meaning that firmware will also likely be different.