NEWS

Human Attacker Exploits Marimo RCE, Reaches SSH Bastion in Eight Seconds

Sysdig traced a human attacker from Marimo RCE to SSH bastion access in 8 seconds: no AI, just hand-built tradecraft.

Dylan H.

Security Engineer

September 15, 2026
8 min read
Human Attacker Exploits Marimo RCE, Reaches SSH Bastion in Eight Seconds

A Human Operator Matches Machine-Speed Exploitation

The Sysdig Threat Research Team (TRT) has published a forensic breakdown of a real intrusion that answers a question defenders have been asking all year: once AI-assisted attackers can chain exploitation and lateral movement in seconds, can a skilled human still keep up without help? In this case, the answer was yes. Sysdig traced a single operator moving from an unauthenticated WebSocket shell on a vulnerable Marimo notebook server to authenticated SSH access on an internal bastion host in eight seconds — using a hand-written Python toolkit, with no AI assistance detected anywhere in the chain.

The underlying flaw is CVE-2026-39987, the critical pre-authentication remote code execution vulnerability in Marimo that CosmicBytez Labs previously covered as a standalone advisory after it landed on CISA's KEV catalog. This piece focuses on what Sysdig observed after that initial foothold — the attacker's tradecraft, tooling, and pace — because it reframes how defenders should think about detection-and-response speed in the post-AI-attacker era.


Incident Summary

FieldDetails
VulnerabilityCVE-2026-39987 — unauthenticated PTY shell via Marimo's /terminal/ws WebSocket endpoint
Affected VersionsMarimo up to and including 0.20.4; fixed in 0.23.0
ResearcherSysdig Threat Research Team (TRT)
Session Length~9 hours (12:52 – 21:50 UTC), 850+ interactive commands
Time-to-Bastion (staged run)8 seconds — fresh WebSocket connection to SSH bastion authentication
Tooling5 hand-written Python scripts, staged via base64 to /tmp/; boto3, Paramiko, an AsyncSSH-style listener
AI InvolvementNone detected — no LLM fingerprints, no public offensive tooling
Attacker InfrastructureSource 172.236.12.17 (London); callback 45.79.187.72 (Cedar Knolls, NJ) — both on AS63949 (Akamai Connected Cloud / Linode)

What Happened: Two Sessions, One Chain

Sysdig's telemetry shows the intrusion wasn't a single lucky burst of speed — it was the payoff of methodical preparation. The operator worked in (at least) two distinct sessions against the same compromised Marimo instance:

  • Session one (starting 12:52:18 UTC) was reconnaissance and toolkit-building: scanning the internal subnet, writing and testing Python scripts directly on the compromised host, and validating that a harvested AWS key could reach the target bastion.
  • Session two (starting 18:54:31 UTC) reused what session one had built. By the time the operator opened a third, fresh WebSocket connection at 18:57:22 UTC, the entire credential-theft-to-bastion-login chain fired from a single backgrounded command — completing in eight seconds because every dependency was already staged on disk.

Attack Timeline (UTC)

TimeEvent
~28 hrs priorFirst harvested AWS credential validated (visible later in CloudTrail)
12:52:18Initial WebSocket connection to /terminal/ws from 172.236.12.17
12:54:13First interactive command — TCP sweep of the internal RFC1918 subnet
16:00 – 16:30Five base64-encoded Python scripts written to /tmp/
16:50:40Script calls AWS Secrets Manager GetSecretValue via boto3
16:51:45Retrieved SSH key tested against the bastion host
18:54:31New WebSocket session opens
18:54:45AWS Secrets Manager call fires 14 seconds after session open
18:56:32 – 18:56:50EC2 enumeration attempts (all denied by IAM)
18:57:22Fresh WebSocket session opens
18:57:30SSH bastion authentication observed — 8 seconds later
20:13 – 20:32AsyncSSH-style listener deployed for follow-on access
21:50:14Operator disconnects

The vulnerability itself made this possible: Marimo's /terminal/ws WebSocket endpoint exposed a fully interactive PTY shell but, unlike other WebSocket routes in the same application, skipped the validate_auth() call that would have required credentials. Any client that opened a connection got a shell as the Marimo process user — no login, no token, no exploit chain beyond "connect." Marimo shipped the fix in version 0.23.0.


The Toolkit: Five Hand-Written Scripts, Staged for Speed

Sysdig recovered five Python scripts the attacker wrote and refined over several hours, each deployed with echo '<base64-blob>' | base64 -d > /tmp/<name>.py — a technique that sidesteps shell-quoting problems and keeps the actual script contents out of plaintext bash_history.

  • Script 1 — Initial Secrets Manager retrieval, with region fallback across us-east-1, us-west-2, eu-west-1, ap-southeast-1, and us-east-2.
  • Script 2 — A refined retrieval routine adding key persistence (chmod 0600), JSON parsing, and a fallback to ListSecrets if direct access was denied.
  • Script 3 — A standard-library-only reverse shell built from socket, os.dup2, and subprocess, with error handling and a 10-second timeout.
  • Script 4 — SSH connection logic via Paramiko, with detection for RSAKey, Ed25519Key, and ECDSAKey, plus a netcat listener on port 4444.
  • Script 5 — A consolidated "chain" module tying the previous four together into the single command that ran in 8 seconds (Sysdig only partially recovered its contents).

The operator also probed an EC2 Instance Connect fallback — attempting to push an SSH public key using a placeholder instance ID (i-0000000000000000) after enumeration attempts were denied. AWS blocked it. Sysdig also found evidence the attacker pursued two separate credential paths in parallel: one harvested from environment variables/files on the host, and a second pulled from the application's own Redis backend, each tested against different IAM identities.


No AI Fingerprints — and a Trap That Didn't Work

What makes this report notable isn't the exploit itself; it's what Sysdig didn't find. There was no LLM-generated code, no agentic tool-calling pattern, and no publicly available offensive framework anywhere in the session — just four-plus hours of manual scripting, testing, and debugging by a person who evidently knew AWS IAM and SSH key handling well. Sysdig had also planted a prompt-injection probe inside the compromised Marimo container as a tripwire for AI-driven operators; the human attacker didn't take the bait, which the researchers noted is exactly the kind of judgment call that trips up agentic attack tooling.

Sysdig's own framing of the finding:

"Eight seconds is the kind of speed we expect to see in AI-assisted attacks. This operator got there on skill alone."

The team went further, arguing that the two attacker profiles — human and AI-driven — don't fail the same way, which matters directly for detection engineering:

"A skilled human will outmaneuver an LLM-driven operator on evasion and judgement alone... defenses built for one will likely miss the actions of the other."


Why This Matters for Defenders

The headline number — eight seconds — is a useful shock value, but the real takeaway is about the assumptions built into a lot of current detection logic. Many "time to detect / time to respond" targets, and a good chunk of anomaly-based tooling, were tuned around the idea that fast correlates with automated, and that automated activity looks different from a human at a keyboard (typos, pauses, trial-and-error). This case shows a human operator who eliminated all of that by doing the trial-and-error offline, in an earlier session, then executing the finished chain as a single pre-staged command. The activity that actually hit the bastion host looked instantaneous and clean — nothing about its speed alone would flag it as either "bot" or "human."


Detection & Defensive Recommendations

Indicators of Compromise

  • Source IP 172.236.12.17; callback/C2 IP 45.79.187.72
  • File artifacts: /tmp/chain.py, /tmp/full_chain.py, /tmp/bastion_key (mode 0600), /tmp/chain.log, /tmp/chain_output.txt, /tmp/callback.log
  • Behavioral markers: a base64-decode-then-execute pattern feeding straight into python3; boto3 region-fallback iteration; Paramiko SSHClient connections originating from non-SSH binaries; an early, out-of-place pip3 install boto3

Cloud and runtime detection

  • In CloudTrail, alert on secretsmanager:GetSecretValue calls from newly-seen principals followed by AccessDeniedException retries across multiple regions within about 60 seconds — the region-fallback pattern is a strong signal on its own.
  • In GuardDuty, watch for UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS findings tied to notebook or data-science compute.
  • Apply container runtime policies that flag drift and unexpected outbound egress from notebook/Jupyter-style containers.
  • Restrict egress from notebook services to only the endpoints they legitimately need (package registries, model APIs) — a bastion-bound SSH connection or an unfamiliar HTTPS callback from a notebook container should stand out immediately.

Remediation priority

  1. Upgrade Marimo to version 0.23.0 or later on every instance — this flaw is already confirmed under active exploitation and sits on CISA's KEV catalog.
  2. Put the /terminal/ws endpoint behind authentication, or disable the terminal feature entirely if it isn't required.
  3. Audit every credential-storage surface reachable from a notebook host: environment variables, systemd unit files, ~/.aws, .env files, and any connected data backend (Redis, in this case).
  4. Scope Secrets Manager and IAM permissions available to notebook hosts down to least privilege — dual-path credential harvesting only works if both paths lead somewhere useful.
  5. Rotate any credentials that were reachable from an internet-exposed Marimo instance, treating them as compromised regardless of whether exploitation is confirmed.
  6. Hunt specifically for staged toolkits: unexplained /tmp/*.py files, base64-decoded script artifacts, and SSH private key material written outside expected paths.

References