TP-Link TL-WR840N router
TP-Link TL-WR840N — our target

Part 1: Acquiring the firmware

TP-Link firmwares are easy to get as they are free to download online. But that is a bit too easy isn't it? Is there another way to get it?

Serial port on the router PCB
Serial port identified on the PCB

Most of these devices have a serial port for debugging. Connecting to it should be trivial using a USB to UART adapter:

USB to UART adapter
USB to UART adapter for serial communication

Using this, you could potentially interrupt the boot sequence and enter a low-level shell that would allow you to read the EEPROM chip at different offsets. But I wanted to try something new:

Desoldered EEPROM chip
EEPROM chip desoldered from the PCB

Desoldering the EEPROM that contains the firmware was my way. I had just recently acquired a TL866II+, and wanted to have fun with it! The chip perfectly fits in the 200mm SOP8 adapter:

EEPROM in TL866II+ programmer
EEPROM chip in the TL866II+ programmer

Before being able to read from it, we need to identify which kind of chip it is:

EEPROM chip markings showing GD25Q32
Giga Device GD25Q32 — AH1903 25Q32CS1G

We can read AH1903 25Q32CS1G, and the logo is Giga Device. We then dump the firmware using the Linux fork of the minipro tool:

Bash
minipro -p GD25Q32 -r firmware.bin
Found TL866II+ 04.2.86 (0x256)
Chip ID OK: 0xC84016
Reading Code...  6.99Sec  OK

Part 2: Reversing the firmware

The file is 4MB large, and contains the following signatures:

Bash
binwalk firmware.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
-----------------------------------------------
53488         0xD0F0          U-Boot version string, "U-Boot 1.1.3 (Jun 14 2018)"
66048         0x10200         LZMA compressed data, properties: 0x5D
1048576       0x100000        Squashfs filesystem, little endian, version 4.0,
                              compression:xz, size: 2955905 bytes, 610 inodes
4063248       0x3E0010        XML document, version: "1.0"

We are mostly interested in the squashfs, as it contains the router's filesystem. All the binaries are compiled for MIPS, the most common architecture for routers.

The /etc folder contains several interesting files. Notably, two encrypted XML files: default_config.xml and reduced_data_model.xml. By searching for these filenames inside the whole squashfs, we get one interesting match: libcmm.so.

Compiled code referencing encrypted files might be actually decrypting them, so we disassemble the shared object library using Ghidra:

C
int dm_decryptFile(uint param_1, undefined4 param_2, uint param_3, int param_4)
{
  int iVar1;
  char acStack40 [8];
  int local_20;
  
  memcpy(acStack40, &encryption_key, 8);
  if (param_3 < param_1) {
    cdbg_printf(8, "dm_decryptFile", 0xb83,
      "Buffer exceeded, decrypt buf size is %u, but dm file size is %u",
      param_3, param_1);
    local_20 = 0;
  }
  else {
    local_20 = cen_desMinDo(param_2, param_1, param_4, param_3, acStack40, 0);
    // ...
  }
  return local_20;
}

If that ain't luck... We have all the symbols! The code is copying an 8-byte array in a local buffer (DES key sizes are 7+1 for parity) and calling cen_desMinDo from libcutil.so, a wrapper for DES encryption functions.

We quickly get the results using:

Bash
openssl enc -d -des-ecb -nopad -K XXXXXXXXXXXXXXXX -in default_config.xml > default_config_decrypted.xml

In this XML file, we find loot:

XML
<StorageService>
  <UserAccount instance=1>
    <Enable val=1 />
    <Username val=admin />
    <Password val=admin />
    <X_TP_SupperUser val=1 />
  </UserAccount>

Another interesting thing in the /etc folder: shadow doesn't exist. The passwords are stored the old way, in the passwd file:

Bash
admin:$1$$iC.dUsGpxNNJGeOm1dFio/:0:0:root:/:/bin/sh
dropbear:x:500:500:dropbear:/var/dropbear:/bin/sh
nobody:*:0:0:nobody:/:/bin/sh

Let's see what hashcat thinks about this:

Bash
hashcat -a 0 -m 500 hash /usr/share/wordlists/rockyou.txt

$1$$iC.dUsGpxNNJGeOm1dFio/:1234

Status...........: Cracked
Hash.Name........: md5crypt, MD5 (Unix)
Time.Started.....: Wed Oct 28 14:10:47 2020 (1 sec)

admin:1234 is the root's password of the router.

Part 3: Dropbear

Another interesting user is dropbear — a lightweight SSH server for embedded systems. By looking for dropbear-related data, we find in libcmm.so the string /var/tmp/dropbear/dropbearpwd. The file doesn't exist in the squashfs, so it must be created during startup:

C
undefined4 setDropbearLogin(int conf_obj)
{
  // ...
  __stream = fopen("/var/tmp/dropbear/dropbearpwd", "wb+");
  if (__stream != (FILE *)0x0) {
    fprintf(__stream, "username:%s\n", conf_obj + 0x22);
    password = (char *)(conf_obj + 0x32);
    length = strlen(password);
    cen_md5MakeDigest(dest, password, length);
    // ... hex encode the MD5 hash ...
    fprintf(__stream, "password:%s\n", dest);
    fclose(__stream);
  }
  return uVar1;
}

The function opens dropbearpwd, writes the username, hashes the password with MD5 via cen_md5MakeDigest from libcutil.so, and writes the hex-encoded hash.

After tracing all calls to dm_setObj (135 cross-references), the credentials come from a global data model populated from the XML configuration file we decrypted earlier:

Call graph showing credential flow from XML to dropbear
Call graph: credentials flow from default_config.xml to dropbear initialization

Part 4: Command injections

This all starts with the prominent use of system(), which is widely discouraged. But system itself is not the problem — it is the util_execSystem function that relies on it, and that function has 495 cross-references throughout the code.

C
  memset(command_line, 0, 0x200);
  iVar1 = vsnprintf(command_line, 0x1ff, cmd, &local_res8);
  // No sanitization whatsoever
  if (0 < iVar1) {
    do {
      local_22c = system(command_line);
      // ...
    } while (waitpid);

The buffer is zeroed, then snprintf'd from the command passed as parameter, and straight from there — with no sanitization — passed to system().

Here is a good example of a potentially injectable situation:

C
void oal_ipt_addBridgeIsolationRules(undefined4 param_1)
{
  util_execSystem("oal_ipt_addBridgeIsolationRules",
    "iptables -t filter -I BRIDGE_ISOLATION -i br+ -o %s -j DROP", param_1);

param_1 could contain the name of a network interface entered from the web interface. While being directly concatenated, it could contain executable bash code to gain root on the device.

Part 5: Going further

There is no real conclusion to draw from this study. I bought this router mostly to have fun with it and that's exactly what I did. Passwords are weak, firmware is not encrypted, and so on — but you get what you pay for.

Many things are left unchecked:

  1. What is before the bootloader? (part of it seems to contain hardware configuration)
  2. Kernel is 2.6 — is there some TP-Link proprietary additions to it?
  3. Module tp_domain.ko tries to contact dead domains: tplinklogin.net — why?
  4. Find an exploitable command injection using util_execSystem

A little while after writing this article, I stumbled upon Pierre Kim's blog, which shows very similar vulnerabilities. I suspect the code is significantly similar.

We contacted TP-Link and created CVE-2020-36178 related to the command injection issue. A specific mention to TP-Link's security team is deserved — they are very serious and quick to respond.

References

Stay classy netsecurios.


← Back to articles

Need a security audit or tailored cybersecurity support?

Explore our services →