
Machine speed, hold the AI: hand-rolled marimo CVE-2026-39987 exploit
Sysdig's Threat Research Team documents a hand-coded (non-LLM) exploitation of CVE-2026-39987, a pre-authentication RCE in marimo notebooks caused by a missing validate_auth() check on the /terminal/ws WebSocket endpoint. The operator chained credential theft from environment variables and Redis, AWS Secrets Manager retrieval, and SSH pivoting to a bastion host in eight seconds, ignored a planted prompt-injection honeypot meant to detect LLM agents, and left behind reusable base64-encoded boto3/paramiko toolkits in /tmp. The post includes full script reproductions, IOCs (IPs, file paths, AS63949/Linode infrastructure), CloudTrail/GuardDuty detection guidance, and remediation steps (patch to marimo 0.23.0, restrict the terminal endpoint, scope Secrets Manager access).
What to do
The summary already has the mechanism, the toolkit, and the high-level IOCs. What it omits is the version boundary, the urgency, and a set of detection signatures precise enough to hunt on — plus one distinct finding (a failed EC2 Instance Connect path) that isn't a footnote to the credential chain.
Version boundary and urgency. All marimo versions up to and including 0.20.4 are affected; the fix ships in 0.23.0 (PR #9098 wires the missing validate_auth() into /terminal/ws). This CVE has been on CISA KEV for months, with a federal remediation deadline of May 7, 2026 — already past as of this writing. The page's remediation list also adds: assume full exposure of .env, the data layer, and environment variables if /terminal/ws was reachable at any point since April 8, and rotate accordingly.
Detection signatures (the page's actual value — hunt on chain shape, not typing style):
- CloudTrail:
secretsmanager:GetSecretValuefrom a principal that has never historically called Secrets Manager, returningAccessDeniedException, followed by retries across multiple regions in under a minute. - The region-fallback order is deterministic:
["us-west-2", "eu-west-1", "ap-southeast-1", "us-east-2"]in exactly that sequence after a us-east-1 failure. - GuardDuty:
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWScovers the replayed key from an IP outside normal egress. - On-host behavioral:
echo '<base64>' | base64 -d > /tmp/<name>.pyfollowed within seconds bynohup python3 /tmp/<name>.py;paramiko.SSHClient.connect(..., username="root")from a non-SSH-client process;pip3 install boto3early in the session (the marimo container ships without boto3). - Egress: block outbound TCP from notebook containers except what's required; reverse-shell ports 4444, 1337, 1234, 8080, 9001, 31337 should never appear.
The EC2 Instance Connect fingerprint
A finding the summary doesn't carry: alongside the Secrets Manager chain, a second independent script attempted a parallel route — enumerate instances via DescribeInstances, then push an attacker key via SendSSHPublicKey (EC2 Instance Connect) for a Secrets-Manager-free SSH path. It failed with an instructive bug: when enumeration returned UnauthorizedOperation, the instance-ID variable was never populated, so SendSSHPublicKey fired against the placeholder i-0000000000000000 and AWS rejected it as malformed before IAM evaluation.
This yields a high-fidelity fingerprint: three denied EC2 enumeration calls followed by SendSSHPublicKey against the placeholder ID — automation running without an enumeration guard. It's also the page's evidence of human-built redundancy (two independent routes to the bastion), a contingency-planning behavior the profiled LLM agents didn't exhibit.
Preconditions worth acting on
The operator harvested two distinct AWS keys from two surfaces — the process environment and the application's Redis backend — mapping to different IAM users. Removing one surface would not have closed the path. Audit /proc/<pid>/environ, systemd EnvironmentFile, ~/.aws/credentials, ~/.aws/config, the app .env, and any runtime secret store. Then scope secretsmanager:GetSecretValue: the IAM user on a notebook host should never have read access to a deploy-bastion SSH key.
Caveats
The eight-second chain (WebSocket open 18:57:22 → bastion SSH auth 18:57:30) was only possible after ~4 hours of in-session toolkit building; the speed is the replay, not the development. Script 5 (the consolidated full chain) was only partially captured — the command stream cut off after the imports. The non-LLM attribution is behavioral (ignored a planted prompt-injection marker seen twice, iterative debugging, hand-typed quoting variation), not proof.
Organisations named here
- 172.236.12.17
- 45.79.187.72
- AS63949 Akamai Connected Cloud
- NKAbuse
- PR #9098
CVEs named here
- CVE-2026-39987
Technologies named here
- Falco
- marimo
- Falco Feeds
- AWS Secrets Manager
- Boto3
- AWS EC2 Instance Connect
- Paramiko
- nc
- Sysdig Secure
- AWS GuardDuty
- AWS CloudTrail
- AWS
- Python
- Redis
Techniques named here
- Pre-authentication WebSocket RCE exploitation
- Credential harvesting from compromised host
- AWS credential pivot / cloud credential replay
- Region-fallback discovery
- Base64-encoded script staging
- Standard-library reverse shell
- SSH key-type brute detection
- EC2 Instance Connect key push
- Prompt-injection resistance / probe evasion
- Same-provider infrastructure reuse
Threat actors named here
- Unnamed hand-rolled/skilled human operator
- NKAbuse RAT campaign
- SCARLETEEL
Also named here
- UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS
Related in Threat Intel
Reverse Engineering the Six Stages of MacSync Stealer and RAT
AI-Coded Malware | Analyzing Vibe-Coded AD Enumeration | Huntress
