Gaining SSH Access to TP-Link RE200 Wi-Fi Range Extender

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.

  1. AES key: 2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836
  2. 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:

  1. squashfs-root/usr/lib/lua/luci/controller/admin/firmware.lua – responsible for the backup/restore process.
  2. 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:

  1. At some stage the script calculates md5sum checksum of the product.
  2. The nvrammanager reads partition data to file.
  3. luci.model.crypto is used.
  4. File is decrypted (dec_file_entry).
  5. LUA script converts decrypted xmlToFile.
  6. Removes two files for security reasons (according to this): accountmgnt and cloud_config.
  7. LUA script converts fileToXml.
  8. 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

6 thoughts on “Gaining SSH Access to TP-Link RE200 Wi-Fi Range Extender

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

  2. 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?

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

Leave a Reply

Your email address will not be published. Required fields are marked *