Flooding Dropper — WEL1DROPPER npm Slopsquatting Campaign (August 2026)
require() · cross-platform dropper · DNS TXT payload fallback channel · Sliver on Linux, reflective loader on Windows, LaunchAgent on macOSExecutive Summary
Between 2026-08-04 and 2026-08-07 a single threat actor published roughly 800 to 850 malicious packages to the npm registry. OpenSourceMalware tracks the downloader as WEL1DROPPER; Sonatype Research Labs tracks the surrounding campaign as Flooding Dropper under sonatype-2026-005660 (846 components at time of publication, CWE-506, CVSS 8.7). The package names appear to be AI-hallucinated typo-squats — "AI slopsquatting" — the kind of plausible-but-nonexistent dependency name a coding assistant invents and a developer then installs.
The control most teams rely on does not apply here. These packages carry no preinstall or postinstall lifecycle hook. The README instructs the developer to load the library with require("<package>"), and that single import detonates the chain. npm config set ignore-scripts true, install-script sandboxes, and install-time scanners all miss this by design. Execution happens whenever the application imports the dependency — during development, testing, CI, or production start-up.
What the chain does
- Import triggers stage 1.
index.jspresents a harmless fake mobile SDK (init(),version(),configure()), then ends withtry { require("./_helpers"); } catch (_) {}._helpers.jscalls its ownrun()immediately. - Host fingerprint. WEL1DROPPER reads the OS and CPU architecture and maps the victim to one of four payload paths:
/pkg/package(Linux x64),/pkg/package-arm64(Linux ARM64),/pkg/loader_mac(macOS universal),/pkg/package.exe(Windows). - Primary delivery. It shuffles three Cloudflare Workers hosts and issues an IPv4-forced HTTPS GET with a 15-second timeout and the User-Agent
node-fetch/2.6. A response is accepted only on HTTP 200 with more than 1,000 bytes. There is no signature check, no pinning, no expected hash — whatever the server returns is treated as an executable. - DNS fallback — the distinctive signal. If all three HTTPS hosts fail, it queries a TXT record at
c.<domain>, parses the answer as a chunk count between 1 and 2,000, then walks numbered TXT records (0.<domain>,1.<domain>, …), joins the strings and Base64-decodes them into a binary. This is not DNS service discovery — it is a second full payload channel that survives an HTTPS blocklist. - Disk and detached launch. An 8-hex-character random ID names the file:
/var/tmp/.cache_<8 hex>on Unix (chmod +x, launched via/bin/sh -c "… &") or%TEMP%\dotnet_diag_<8 hex>.exeon Windows (launched viacmd.exe /c start /b). Both are detached with stdio ignored. A rate-limit marker (/tmp/.analytics_state,%TEMP%\analytics_state) suppresses re-runs for 21,760 seconds. - Stage 2, per platform. Windows: patches ETW and AMSI, checks for debuggers/VMs/sandboxes/security products, copies itself under
%AppData%, persists via a Registry Run key and a scheduled task, then downloads an encrypted payload from/pkg/update_win.exe, decrypts it and executes it reflectively in memory. macOS: anti-debug and VMware checks, memory-size sandbox heuristic, fetches/pkg/beacon_mac.binthrough five XOR-hidden proxy hosts, writes under~/.local/share/runtime, installs~/Library/LaunchAgents/com.apple.windowserver.helper.plist. Linux: statically linked, non-PIE, UPX 3.96-packed ELF that pulls further binaries from a Cloudflare Worker, reportedly ending in a Sliver implant.
Why this pack scopes developer workstations and CI runners equally
Nothing in the chain needs interactive use. A build agent that runs npm ci and then starts the application imports the dependency exactly as a laptop does, and a build agent typically holds better credentials: npm publish tokens, GitHub tokens, cloud roles, signing keys, deployment secrets. Both primary sources say the same thing — treat a confirmed import as host compromise, rotate every credential reachable from that host, and rebuild.
Two anti-signature properties that shape the detection strategy
- Payload polymorphism. Sonatype observed the packages carrying "slightly modified payloads" — different URL function and variable names producing identical behavior. File hashes are single-sample facts, not campaign-wide ones.
- Account and name churn. Each disposable npm account publishes only a handful of packages. Early names interpolate
bigopsandbnplwith a35.x.yversion pattern, but Sonatype already sees the convention drifting. A name blocklist is yesterday's control.
Consequence: the durable detections in this pack are the behavioral ones — the numbered-TXT-record channel (Q3, Q4), a Node process spawning a shell or downloader (Q6), a Node-parented write-then-execute in a temp directory (Q12), and the platform persistence writes (Q9, Q10, Q11). The atomic indicators in §4 and §10 are for retro-hunting the current wave, not for standing coverage.
Attribution
OpenSourceMalware assesses probable Russian origin from the wel1[.]ru TXT-record infrastructure and the presence of Russian financial-institution domains (tcsbank[.]ru, cloudpayments[.]ru) inside the macOS payload, and assesses the campaign as a likely evolution of the Moika dependency-confusion campaign (250+ npm packages, April/May 2026) on shared tradecraft: the oob string in file and server names, focus on Russian financial institutions and mobile payments, fake-telemetry camouflage, and similar kill switches. Treat attribution as an assessment, not a fact — it changes nothing about the detections below.
Source Review & Web Hunter Notes
Six sources were fetched and saved verbatim to FloodingDropper-WEL1DROPPER-npm-Hunt-sources/. Every atomic indicator shipped in §4 and §10 traces back to one of those snapshots. Nothing was carried forward from memory or inference.
| # | Tier | Source | Key finding | Carry forward |
|---|---|---|---|---|
| 01 | PRIMARY | OpenSourceMalware — "Russian AI Slopsquatting Publishes 700+ Malicious NPM Packages" (2026-08-06) | Full reverse-engineering writeup. Source of every host, hash, dropped-file path, persistence artifact and the DNS TXT chunk protocol. Names the active loader as _helpers.js and lib/telemetry.js as an unimported 80 KB decoy. | Yes — all indicators |
| 03 | PRIMARY | Sonatype Research Labs — "'Flooding Dropper' Campaign Hits npm With Nearly 850 Malicious Packages" (2026-08-05) | Campaign scale (846 components), sonatype-2026-005660, CWE-506 / CVSS 8.7, package-name and 35.x.y version correlation, payload polymorphism, disposable-account distribution. Sole source for the Windows stage-2 behavior: ETW/AMSI patching, AppData self-copy, Run key + scheduled task, encrypted /pkg/update_win.exe executed reflectively. | Yes — Windows stage 2, campaign scoping |
| 02 | TIER 2 | The Hacker News — "Nearly 800 Malicious npm Packages Deliver Cross-Platform RAT and Infostealer" (2026-08-07) | Independent corroboration of the Cloudflare Workers hosts, the four DNS payload domains, the WEL1DROPPER name, and the Sonatype Windows findings. Confirms the Moika lineage assessment. | Yes — corroboration |
| 05 | TIER 2 | DevOps.com — "'Flooding Dropper' Is Hitting npm With a Tidal Wave of Malicious Packages" (2026-08-07) | Confirms the ~850 count, the bigops-backend / bigops-api / dolyame-boxy-desktop-bnpl-card-gallery names, Jorge Cardona's attribution of the wider campaign, and npm's 96.6% share of Q2 2026 malicious-package volume. | Partial — package names, scale |
| 06 | TIER 2 | CyberPress — "Russian AI Slopsquatting Campaign Floods npm With Malicious Packages Targeting Developers" | Restates the OSM findings; notes the campaign growing beyond 1,000 packages and the shared "oob" infrastructure naming with Moika. | Partial — scale trend only |
| 04 | TIER 3 | CyberTechWorld aggregator repost of the THN article | Truncated syndicated copy. Contains no independent technical content and no indicators. | No — snapshot retained for completeness, not cited |
Prompt-injection screening
All six snapshots were screened for text addressing an automated reader — instructions to run commands, to fetch a further URL "for the real IOCs", to skip a validation step, or claims of authorization. No injection attempt was found. The only pattern-matches were the strings "AI & LLM Governance" in Sonatype's site navigation menu, which is product marketing, not instruction. No source was dropped on injection grounds.
Where the sources disagree, and what this pack shipped
- Which file holds the live downloader. Secondary coverage says
lib/telemetry.js"contains the same downloader logic". The primary analysis is more precise and this pack follows it:lib/telemetry.jsis a ~80 KB fake-telemetry decoy that the entry point never imports and that carries no hardcoded infrastructure; the live path is the much smaller_helpers.js. Both files are hashed in §10, but the hunt logic targets_helpers.jsbehavior. - Stage-1 vs stage-3 payload names.
update_win.exeandbeacon_mac.binare later-stage paths, not what stage 1 fetches. Stage 1 requests/pkg/package,/pkg/package-arm64,/pkg/loader_mac,/pkg/package.exeand renames them to.cache_<hex>/dotnet_diag_<hex>.exeon disk. Queries Q7 and Q8 hunt the on-disk names, which is what endpoint telemetry actually records. - Example package name. The primary analysis dissects
checkout-mobile-bnpl@35.6.9; Sonatype's entry point wasbigops-backend. Both are the same campaign — Q13 covers both plus the two additional Sonatype names. - Sliver is reported, not confirmed. OpenSourceMalware states explicitly that it has not confirmed the Linux final stage is Sliver; THN reports it as fact citing Sonatype. This pack treats Sliver as a strong lead and hunts the UPX-packed-ELF staging behavior (Q8, Q10) rather than Sliver protocol signatures.
- Package count. 788 (OSM tracking list), 846 (Sonatype components), "nearly 800" (THN), "nearly 850" (DevOps.com), "beyond 1,000" (CyberPress trend note). The pack cites the range rather than one number.
Known gaps in the intel
- The final Windows and macOS beacons were unavailable for analysis in both primary sources. There is no confirmed C2 protocol, no beacon interval, no confirmed exfiltration destination. No exfil indicator is shipped, because none exists in any source.
- The port used by the five
package-proxyhosts could not be recovered from strings. The request template ishttps://%s:%d%s, so the connection may carry an explicit non-443 port. No port IOC is shipped. - The Linux ELF is UPX-packed with its section header table removed; its inner behavior was not statically recoverable. The visible
/proc/self/exestring is the UPX loader reading its own image and is explicitly not an indicator. - The macOS payload's reaction to each detected analysis artifact (exit, sleep, suppress network) was not recovered.
nexus[.]tcsbank[.]ru,repo-linux[.]tcsbank[.]ruandalertmanager[.]cloudpayments[.]ruare XOR-hidden in the macOS binary behind ahttps://%s/healthformatter. The primary source states static analysis does not establish whether they are attacker-owned, compromised, or decoys. They ship asno_actioninvestigation leads only — never as detect or block rows.- The full package list (788 names) lives on OpenSourceMalware and GitHub and was not enumerated into this pack. Use the Sonatype
sonatype-2026-005660advisory or the OSM list as the authoritative name inventory; §7 gives the lockfile sweep.
Hunt Brief & Attack Chain
Each row is a step an analyst can prove or disprove from telemetry. The hunt-angle column names the query that covers it.
| # | Step | Detail | Telemetry | Hunt angle |
|---|---|---|---|---|
| 1 | Slopsquat package enters the dependency graph | AI-hallucinated name installed by a developer or resolved by a build. Early names interpolate bigops / bnpl, versions in the 35.x.y range; convention is already drifting. | ProcessRollup2 command lines of npm/npx/yarn/pnpm/bun; lockfiles and SBOMs (non-EDR) | Q13 + §7 lockfile sweep |
| 2 | require() detonates stage 1 | No lifecycle hook. index.js tail-loads _helpers.js, which self-invokes run(). Nothing distinguishes this from a normal module load at the process level. | None directly — GAP | Inferred from steps 3–6; see §15 |
| 3 | Kill-switch / rate-limit check | Reads env vars and a marker file (/tmp/.analytics_state, %TEMP%\analytics_state) to skip re-running within 21,760 s. | FileOpenInfo, NewExecutableWritten (low fidelity for a plain marker file) | §7 filesystem sweep |
| 4 | OS + architecture fingerprint | Selects one of four payload paths. Pure in-process logic in Node. | None directly — GAP | — |
| 5 | Primary payload fetch over HTTPS | Shuffles three Cloudflare Workers hosts, IPv4-forced GET, 15 s timeout, UA node-fetch/2.6, accepts only HTTP 200 with >1,000 bytes. No hash or signature verification. | DnsRequest, NetworkConnectIP4; proxy logs | Q2 (known hosts), Q5 (any Node → *.workers.dev) |
| 6 | DNS TXT fallback payload channel | TXT c.<domain> returns a chunk count 1–2,000, then TXT 0.<domain> … TXT n-1.<domain>; strings are joined and Base64-decoded to a binary. Per-platform domains under dl.wel1[.]ru. The macOS loader adds a session/host label: c.<id>.dl.wel1[.]ru. | DnsRequest, SuspiciousDnsRequest; resolver logs | Q1 (domain), Q3 (chunk-label shape, any domain), Q4 (volume anomaly) |
| 7 | Payload written to a temp directory | 8-hex random ID. Unix: /var/tmp/.cache_<8 hex>, chmod +x. Windows: %TEMP%\dotnet_diag_<8 hex>.exe. Names chosen to look boring among real temp files. | PeFileWritten, NewExecutableWritten, ELFFileWritten | Q7, Q8, Q12 |
| 8 | Detached execution | Unix: /bin/sh -c "/var/tmp/.cache_<id> &". Windows: cmd.exe /c start /b %TEMP%\dotnet_diag_<id>.exe. Detached with stdio ignored, so killing the Node parent does not stop it. | ProcessRollup2, SyntheticProcessRollup2 | Q6, Q7, Q8 |
| 9a | Windows stage 2 — evasion | Patches ETW and AMSI functions; checks for debuggers, VMs, sandboxes and security products. | Falcon in-product AMSI/ETW-tamper detections; not a discrete queryable event in the CQL data model — PARTIAL GAP | Q14 (proxy: Node-descended LOLBins) + §9 IOA-3; see §15 |
| 9b | Windows stage 2 — persistence | Self-copy under %AppData%, then both a Registry Run key value and a scheduled task. | AsepValueUpdate, RegGenericValueUpdate, ScheduledTaskRegistered | Q9, Q10 |
| 9c | Windows stage 3 — reflective load | Downloads encrypted /pkg/update_win.exe, decrypts and executes it in memory. No conventional write-and-launch from disk. | Memory-scan / behavioral only — GAP | §9 IOA-4; see §15 |
| 10a | macOS stage 2 — evasion | Looks for lldb, debugserver, dtrace, frida, Wireshark and VMware Fusion artifacts; imports ptrace/sysctlbyname; queries hw.memsize as a low-memory sandbox heuristic. | ProcessRollup2 (weak); file-existence probes are not logged | Partial — §15 |
| 10b | macOS stage 2 — persistence | Writes ~/.local/share/runtime, ~/.local/share/runtime/.lock, ~/.local/share/runtime/com.apple.runtime, plus ~/Library/LaunchAgents/com.apple.windowserver.helper.plist, then launchctl load -w. The plist runs at login with KeepAlive and a 60 s throttle, stdout/stderr to /dev/null. Neither com.apple.runtime nor com.apple.windowserver.helper is a real Apple component in those locations. | CriticalFileModified, NewExecutableWritten, ProcessRollup2 | Q11 |
| 10c | macOS stage 3 — beacon fetch | Retrieves /pkg/beacon_mac.bin from five XOR-obfuscated (key 0x9c) package-proxy.* Workers hosts, possibly on an explicit non-443 port; DNS TXT fallback via dl.wel1[.]ru with a session label. Also probes http://127.0.0.1:4444/health — loopback, so a check for another local component, not external C2. | DnsRequest, NetworkConnectIP4 | Q2, Q3 |
| 11 | Linux stage 2/3 — Sliver staging | Statically linked non-PIE x86-64 ELF, UPX 3.96, section header table stripped. Pulls further binaries from oob-worker.cf99-9b3.workers[.]dev, reportedly ending in a Sliver implant (unconfirmed by the primary source). | ELFFileWritten, ProcessRollup2, NetworkConnectIP4 | Q8, Q10 |
| 12 | Credential exposure | Both primary sources: treat a confirmed import as host compromise. npm tokens, GitHub tokens, cloud credentials, signing keys and deployment secrets reachable from the host are exposed. | Identity / cloud audit logs (out of EDR scope) | §14 containment |
Hunt hypotheses, ordered by fidelity
| ID | Hypothesis | MITRE | Events required | Expected FP source | Conf |
|---|---|---|---|---|---|
| H1 | A host in scope resolved a name under wel1[.]ru. There is no legitimate enterprise reason for this. | T1071.004, T1105 | DnsRequest | Analyst research browsing; sandbox detonation VMs | HIGH |
| H2 | A host resolved one of the eight known Cloudflare Workers staging or proxy hosts. | T1102, T1105 | DnsRequest | Analyst research; the hosts may already be sinkholed | HIGH |
| H3 | A Node process issued TXT lookups whose leftmost label is c or a small integer — the chunked-payload protocol — against any domain, not only wel1[.]ru. This survives infrastructure rotation. | T1071.004, T1132.001 | DnsRequest | Some CDN and reverse-DNS-ish schemes use numeric leftmost labels | HIGH |
| H4 | A file named dotnet_diag_<8 hex>.exe, update_win.exe, .cache_<8 hex> or beacon_mac.bin was written or executed. These names exist only in this campaign. | T1105, T1036.005 | PeFileWritten, NewExecutableWritten, ELFFileWritten, ProcessRollup2 | None expected | HIGH |
| H5 | A macOS host has a LaunchAgent or ~/.local/share/runtime artifact impersonating an Apple component. | T1543.001, T1036.005 | CriticalFileModified, ProcessRollup2 | Genuine third-party agents under ~/Library/LaunchAgents | HIGH |
| H6 | A Node process made an unusual volume of TXT lookups in a short window — the chunk walk is 1 to 2,000 sequential queries. | T1071.004 | DnsRequest | Node-based DNS tooling; DKIM/SPF/ACME validators; service-discovery libraries | MEDIUM |
| H7 | A Node process wrote a new executable into a temp directory and something executed it shortly after. | T1105, T1059 | PeFileWritten, ELFFileWritten, ProcessRollup2 | node-gyp, prebuild-install, esbuild/swc/sharp binary downloads, Electron builders, Playwright/Puppeteer browser installs | MEDIUM |
| H8 | A Registry Run value or a scheduled task points at an executable in %TEMP% or %AppData% on a host with developer tooling. | T1547.001, T1053.005 | AsepValueUpdate, RegGenericValueUpdate, ScheduledTaskRegistered | Squirrel/Electron auto-updaters, Teams, Slack, Zoom, VS Code updater, Dropbox — all legitimately persist from AppData | MEDIUM |
| H9 | A Node process spawned a shell, a downloader or a persistence utility. | T1059.003, T1059.004, T1105 | ProcessRollup2 | Very common in build tooling — high on developer and CI hosts | MEDIUM |
| H10 | A Node process resolved a *.workers.dev host not on the known list — a candidate for the next staging wave. | T1102 | DnsRequest | Legitimate SaaS and APIs hosted on Cloudflare Workers | LOW-MED |
| H11 | A known Flooding Dropper package name appears in an npm/node command line on a workstation or runner. | T1195.002 | ProcessRollup2 | Substring collisions with legitimate bnpl-related packages | MEDIUM |
Consolidated IOC Table
Read the Action column before importing anything. Payload polymorphism means the seven hashes are single-sample facts, not campaign coverage. The three tcsbank / cloudpayments hosts are XOR-hidden strings whose ownership the primary source explicitly could not establish — they are investigation leads, and blocking them risks breaking traffic to legitimate Russian financial services. Every value below appears verbatim in a saved snapshot; see §10 for the trace.
| Type | Value | Conf | Action | Context | Expiry |
|---|---|---|---|---|---|
| domain | wel1[.]ru | HIGH | Block + detect | Campaign apex. Hosts the TXT-record payload channel. No legitimate use. | 2027-02-08 |
| domain | dl[.]wel1[.]ru | HIGH | Block + detect | Second DNS delivery domain found in the macOS loader; queried with a session/host label. | 2027-02-08 |
| domain | sdk[.]dl[.]wel1[.]ru | HIGH | Block + detect | DNS TXT payload domain — Linux x64. | 2027-02-08 |
| domain | ext[.]dl[.]wel1[.]ru | HIGH | Block + detect | DNS TXT payload domain — Linux ARM64. | 2027-02-08 |
| domain | pkg[.]dl[.]wel1[.]ru | HIGH | Block + detect | DNS TXT payload domain — macOS. | 2027-02-08 |
| domain | net[.]dl[.]wel1[.]ru | HIGH | Block + detect | DNS TXT payload domain — Windows. | 2027-02-08 |
| domain | oob-worker[.]cf103-070[.]workers[.]dev | HIGH | Block + detect | Stage-1 HTTPS staging host (1 of 3, shuffled). | 2027-02-08 |
| domain | oob-worker[.]cf102-baf[.]workers[.]dev | HIGH | Block + detect | Stage-1 HTTPS staging host (2 of 3). | 2027-02-08 |
| domain | oob-worker[.]cf99-9b3[.]workers[.]dev | HIGH | Block + detect | Stage-1 staging host (3 of 3), and the host the Linux ELF pulls its next binaries from. | 2027-02-08 |
| domain | package-proxy[.]cf5oobworker[.]workers[.]dev | HIGH | Block + detect | macOS stage-3 beacon proxy, XOR key 0x9c. | 2027-02-08 |
| domain | package-proxy[.]cf6oobworker[.]workers[.]dev | HIGH | Block + detect | macOS stage-3 beacon proxy. | 2027-02-08 |
| domain | package-proxy[.]cf7oobworker[.]workers[.]dev | HIGH | Block + detect | macOS stage-3 beacon proxy. | 2027-02-08 |
| domain | package-proxy[.]cf8oobworker[.]workers[.]dev | HIGH | Block + detect | macOS stage-3 beacon proxy. | 2027-02-08 |
| domain | package-proxy[.]cf11oobworker[.]workers[.]dev | HIGH | Block + detect | macOS stage-3 beacon proxy. | 2027-02-08 |
| domain | nexus[.]tcsbank[.]ru | LEAD | Monitor only | XOR-hidden behind a https://%s/health formatter in the macOS binary. Ownership NOT established — may be legitimate, compromised, or decoy. Do not block. | Re-assess 2026-09-08 |
| domain | repo-linux[.]tcsbank[.]ru | LEAD | Monitor only | As above. Investigation lead, not an indicator. | Re-assess 2026-09-08 |
| domain | alertmanager[.]cloudpayments[.]ru | LEAD | Monitor only | As above. Investigation lead, not an indicator. | Re-assess 2026-09-08 |
| sha256 | 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3 | HIGH | Prevent | Linux x86-64 second stage — statically linked, non-PIE, UPX 3.96-packed ELF. | 2027-02-08 |
| sha256 | c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00 | HIGH | Prevent | macOS universal Mach-O second stage (x86-64 + ARM64) — the loader that installs the LaunchAgent. | 2027-02-08 |
| sha256 | 94ef6b1c4a9d31f78f446d053048bcef34fd88f4376a1a46f7f777a9e9c83a29 | MED | Detect | _helpers.js — the live stage-1 downloader. Single sample; payloads are polymorphic across packages. | 2027-02-08 |
| sha256 | a3e2ffb440b779d30da3ff282affd649731088e8570df7b1aa72742d995b782c | MED | Detect | lib/telemetry.js — the ~80 KB fake-telemetry decoy. Not imported by the entry point. | 2027-02-08 |
| sha256 | b74c5675725911c62091bdf40714df760cc2af7a88360d21065f4e1c878aa8f0 | MED | Detect | index.js — fake SDK facade with the tail require("./_helpers"). | 2027-02-08 |
| sha256 | e2650e9aa2f924433ba422857b22ee7c5996b5ad306f3f903283f6a13e248935 | MED | Detect | package.json of the analyzed sample. Note it contains no lifecycle hook. | 2027-02-08 |
| sha256 | 0fc30f82e1fa5e51a6c0c43f3ed7f13592ea731cb331e43a4d085df60a4db8b6 | MED | Detect | README.md — the file that social-engineers the developer into calling require(). | 2027-02-08 |
| filename | dotnet_diag_<8 hex>.exe | HIGH | Hunt (Q7) | Windows staged payload in %TEMP%. Pattern, not a fixed name — hunt, do not import. | Durable |
| filename | .cache_<8 hex> | HIGH | Hunt (Q8) | Unix staged payload in /var/tmp, chmod +x then detached-launched. | Durable |
| filename | update_win.exe | HIGH | Hunt (Q7) | Windows stage-3 encrypted payload path (/pkg/update_win.exe), decrypted and reflectively loaded — may never touch disk. | Durable |
| filename | beacon_mac.bin | HIGH | Hunt (Q8) | macOS stage-3 beacon path (/pkg/beacon_mac.bin). | Durable |
| path | ~/Library/LaunchAgents/com.apple.windowserver.helper.plist | HIGH | Hunt (Q11) | macOS persistence. Not a legitimate Apple artifact in this location. | Durable |
| path | ~/.local/share/runtime/com.apple.runtime, ~/.local/share/runtime/.lock | HIGH | Hunt (Q11) | macOS payload install directory and lock file, masquerading as an Apple component. | Durable |
| path | /tmp/.analytics_state, %TEMP%\analytics_state | HIGH | Hunt (§7) | Rate-limit marker, not analytics. Presence indicates stage 1 ran on this host. | Durable |
| ua | node-fetch/2.6 | MED | Proxy hunt | Stage-1 User-Agent. Common in benign Node code — only useful correlated with a staging host or an unusual destination. | Durable |
| package | checkout-mobile-bnpl (@35.6.9), bigops-backend, bigops-api, dolyame-boxy-desktop-bnpl-card-gallery | HIGH | Purge + hunt (Q13) | Named samples out of ~800–850. Use the OSM list / sonatype-2026-005660 for the full inventory. | 2026-11-08 |
| pattern | npm names interpolating bigops / bnpl, versions 35.x.y | MED | Triage signal | Sonatype correlation heuristic. Explicitly labelled non-durable by its author — the convention is already drifting. | Re-assess weekly |
| loopback | http://127.0.0.1:4444/health | CONTEXT | Do not block | The only literal IP in either native sample. Loopback, so a probe for another local component — not external C2. Listed to stop it being mis-imported as an indicator. | — |
Affected Surface & Telemetry Matrix
This campaign hits build agents as hard as laptops. Scope both, and scope them the same way. Anything that runs node against a public registry is in the blast radius, including container build stages and ephemeral runners whose telemetry may not reach the sensor at all.
| Surface | Exposure | Required telemetry | Priority | Gap risk |
|---|---|---|---|---|
| Windows developer workstations | Full chain: dropper → ETW/AMSI patch → AppData self-copy → Run key + scheduled task → reflective stage 3. Highest-value credentials sit here. | ProcessRollup2, PeFileWritten, NewExecutableWritten, DnsRequest, NetworkConnectIP4, AsepValueUpdate, RegGenericValueUpdate, ScheduledTaskRegistered | P1 | Low — full sensor coverage typical |
| Windows / Linux CI runners & build agents | Same chain, plus the best credential set in the estate: npm publish tokens, GitHub tokens, cloud roles, signing keys. Ephemeral runners may execute the whole chain and be destroyed before anything is reviewed. | All of the above, plus registry proxy logs and runner job logs | P1 | High — sensors are frequently not installed on ephemeral runners; container build stages are often invisible |
| macOS developer workstations | Universal Mach-O covering Intel and Apple Silicon. LaunchAgent persistence, anti-analysis, a third beacon stage. | ProcessRollup2, CriticalFileModified, NewExecutableWritten, DnsRequest, NetworkConnectIP4 | P1 | Medium — verify DNS telemetry is actually flowing from macOS sensors before trusting Q1/Q3 on this platform |
| Linux developer workstations & servers | x64 and ARM64 payloads. UPX-packed ELF staging toward a reported Sliver implant. | ProcessRollup2, ELFFileWritten, DnsRequest, NetworkConnectIP4 | P1 | Medium — DnsRequest coverage on Linux is sensor-version dependent; confirm before relying on Q1/Q3/Q4 |
| Containers built from a poisoned lockfile | The dependency ships inside the image. Every runtime instance imports it. Nothing about the running container looks unusual at pull time. | Falcon Container sensor; image SBOM diff; registry scan | P2 | High — build-stage execution frequently has no EDR at all |
| Internal npm mirror / proxy cache | Holds retained copies of pulled packages. Removing the dependency from a project does not evict the cache; the next resolve re-serves it. | Nexus / Artifactory / Verdaccio access and quarantine logs | P2 | Medium — coverage depends on whether a proxy is mandatory |
| Recursive DNS resolvers | The TXT chunk channel is visible here even when the endpoint sensor is not. On a fleet without complete endpoint DNS telemetry this is the single best vantage point. | Resolver query logs with QTYPE and client IP | P2 | Medium — many estates log NXDOMAIN only, or drop QTYPE |
| Egress proxy | Sees the node-fetch/2.6 User-Agent and the Workers hosts. Useful when the endpoint fetch predates sensor deployment. | Proxy access logs with UA and full host | P3 | High on hosts that egress without a proxy |
| AI coding assistants in the SDLC | The delivery mechanism. Slopsquatting exists because assistants hallucinate plausible package names that developers then install unchecked. | Assistant audit logs where available; code-review records | P3 | High — usually no telemetry at all |
Scope your hunt before you run it, not after. Every query below is written fleet-wide on purpose. If your estate is large, add a host-group filter as the second stage rather than narrowing the logic — the point of Q3, Q6, Q11 and Q12 is that they fire on hosts you did not expect to have Node on them.
MITRE ATT&CK Mapping
| Tactic | Technique | ID | Observed behavior | Coverage |
|---|---|---|---|---|
| Initial Access | Supply Chain Compromise: Compromise Software Dependencies and Development Tools | T1195.001 | ~800–850 slopsquatted npm packages published across many disposable accounts. | Q13, §7 |
| Resource Development | Stage Capabilities: Upload Malware | T1608.001 | Payloads staged on three rotating Cloudflare Workers hosts plus five macOS beacon proxies. | Q2, Q5 |
| Execution | User Execution: Malicious File | T1204.002 | README instructs the developer to require() the package; the import self-invokes run(). | Q13 (proxy) |
| Execution | Command and Scripting Interpreter: Windows Command Shell | T1059.003 | cmd.exe /c start /b %TEMP%\dotnet_diag_<id>.exe | Q6, Q7 |
| Execution | Command and Scripting Interpreter: Unix Shell | T1059.004 | /bin/sh -c "/var/tmp/.cache_<id> &" | Q6, Q8 |
| Execution | Command and Scripting Interpreter: JavaScript | T1059.007 | _helpers.js executes the full downloader inside the Node runtime. | Q6, Q12 |
| Persistence | Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder | T1547.001 | Windows stage 2 writes a Run key value pointing at its AppData self-copy. | Q9 |
| Persistence | Scheduled Task/Job: Scheduled Task | T1053.005 | Windows stage 2 registers a scheduled task in addition to the Run key. | Q10 |
| Persistence | Create or Modify System Process: Launch Agent | T1543.001 | ~/Library/LaunchAgents/com.apple.windowserver.helper.plist, loaded with launchctl load -w, KeepAlive with a 60 s throttle. | Q11 |
| Defense Evasion | Impair Defenses: Disable or Modify Tools | T1562.001 | Windows stage 2 patches AMSI functions to blind script scanning. | PARTIAL Q14, IOA-3 |
| Defense Evasion | Impair Defenses: Indicator Blocking | T1562.006 | Windows stage 2 patches ETW to suppress event generation. | PARTIAL Q14, IOA-3 |
| Defense Evasion | Reflective Code Loading | T1620 | Encrypted /pkg/update_win.exe decrypted and executed in memory, never written as a conventional executable. | GAP IOA-4 |
| Defense Evasion | Obfuscated Files or Information: Software Packing | T1027.002 | Linux ELF packed with UPX 3.96, statically linked, section header table stripped. | Q8, Q10 |
| Defense Evasion | Obfuscated Files or Information: Encrypted/Encoded File | T1027.013 | macOS binary hides its infrastructure strings with single-byte XOR, key 0x9c. | Static-analysis only |
| Defense Evasion | Masquerading: Match Legitimate Name or Location | T1036.005 | dotnet_diag_<hex>.exe, .cache_<hex>, com.apple.runtime, com.apple.windowserver.helper, and the fake-telemetry SDK facade. | Q7, Q8, Q11 |
| Defense Evasion | Virtualization/Sandbox Evasion: System Checks | T1497.001 | VMware Fusion and VMware Tools artifact checks; hw.memsize queried as a low-memory sandbox heuristic; Windows sandbox and security-product checks. | PARTIAL |
| Defense Evasion | Debugger Evasion | T1622 | macOS payload probes for lldb, debugserver, dtrace, frida, Wireshark; imports ptrace and sysctlbyname. | PARTIAL |
| Discovery | System Information Discovery | T1082 | OS and CPU-architecture fingerprint selects one of four payload paths. | GAP — in-process |
| Command and Control | Application Layer Protocol: DNS | T1071.004 | Numbered TXT records under dl.wel1[.]ru carry the payload; c.<domain> returns the chunk count. | Q1, Q3, Q4 |
| Command and Control | Web Service: Bidirectional Communication | T1102.002 | Cloudflare Workers used as the primary staging and beacon-proxy layer. | Q2, Q5 |
| Command and Control | Ingress Tool Transfer | T1105 | Stage-2 binaries fetched over HTTPS or reassembled from DNS, written to temp and executed. | Q7, Q8, Q12 |
| Command and Control | Data Encoding: Standard Encoding | T1132.001 | TXT chunk strings joined and Base64-decoded into an executable buffer. | Q3, Q4 |
| Command and Control | Fallback Channels | T1008 | DNS TXT delivery activates only when all three HTTPS staging hosts fail — surviving an HTTPS-only blocklist is the design goal. | Q1, Q3, Q4 |
| Credential Access | Unsecured Credentials: Credentials In Files | T1552.001 | Assessed, not observed: the final beacon was unavailable, but both primary sources direct rotation of npm/GitHub/cloud/signing credentials reachable from the host. | §14 |
Reading the coverage column honestly. Four rows are marked GAP or PARTIAL. T1082 and step 2 of the chain (the require() itself) happen entirely inside the Node process and produce no distinct telemetry — they are covered only by inference from what happens next. T1620 reflective loading is the most consequential gap: if Windows stage 3 never touches disk, no file-write query will ever see it. That is what §9 IOA-4 and Falcon's in-product memory scanning are for, and it is why §14 treats a confirmed stage-1 execution as compromise regardless of what stage 3 telemetry shows.
Native Audit-Log & Filesystem Hunts (non-CQL)
Run these where EDR coverage is thin — ephemeral runners, container build stages, unmanaged laptops — and to answer the question the CQL cannot: was the package ever resolved into this project at all?
7.1 · Dependency-graph sweep (run first)
The authoritative name inventory is the OpenSourceMalware campaign list and Sonatype's sonatype-2026-005660 advisory. Pull it, then sweep every lockfile, SBOM, cache and image layer. A dependency can be present transitively without appearing in any package.json.
# 1. Direct + transitive presence, per project
npm ls --all --json 2>/dev/null | grep -iE '"(checkout-mobile-bnpl|bigops-backend|bigops-api|dolyame-boxy-desktop-bnpl-card-gallery)"'
# 2. Lockfile sweep across every repo on disk (the transitive case)
find . -name package-lock.json -o -name npm-shrinkwrap.json -o -name yarn.lock -o -name pnpm-lock.yaml \
| xargs grep -lniE 'bigops|dolyame-boxy|checkout-mobile-bnpl'
# 3. The Sonatype correlation heuristic: bigops/bnpl names on a 35.x.y version.
# Non-durable by its author's own statement -- use as a triage signal, then
# confirm each hit against the authoritative campaign list.
find . -name package-lock.json | xargs grep -niE '"(bigops|.*-bnpl-)[^"]*":[[:space:]]*\{' -A2 | grep -i '"version": "35\.'
# 4. The npm cache and any internal mirror retain copies after removal
npm cache ls 2>/dev/null | grep -iE 'bigops|dolyame-boxy|checkout-mobile-bnpl'
ls ~/.npm/_cacache 2>/dev/null && grep -rl 'bigops' ~/.npm/_cacache/index-v5 2>/dev/null | head
7.2 · Did stage 1 actually run? (the marker file)
The rate-limit marker is written on the first run and is the cheapest proof of execution on a host. It is a plain file, so endpoint file telemetry may not have captured it — check the disk directly.
# Unix -- marker, staged payload, and the macOS install directory
ls -la /tmp/.analytics_state /var/tmp/.cache_* 2>/dev/null
find /var/tmp /tmp "$HOME" -maxdepth 2 -name '.cache_????????' -type f 2>/dev/null -exec ls -la {} \;
# Windows (PowerShell)
Get-ChildItem -Path $env:TEMP -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq 'analytics_state' -or $_.Name -match '^dotnet_diag_[0-9a-f]{8}\.exe$' } |
Select-Object FullName, Length, CreationTimeUtc, LastWriteTimeUtc
7.3 · macOS persistence and payload artifacts
# The LaunchAgent -- neither name is a real Apple component in these locations ls -la ~/Library/LaunchAgents/com.apple.windowserver.helper.plist 2>/dev/null launchctl list 2>/dev/null | grep -i 'com.apple.windowserver.helper' # The fake runtime install directory ls -la ~/.local/share/runtime/ 2>/dev/null # expect .lock and com.apple.runtime # Fleet-wide: every user LaunchAgent that claims an Apple identifier. # Apple ships its agents from /System/Library and /Library, never ~/Library -- # so an com.apple.* plist under a user's LaunchAgents is worth reading in full. for u in /Users/*; do ls "$u/Library/LaunchAgents/" 2>/dev/null | grep -i '^com\.apple\.' | sed "s|^|$u: |" done
7.4 · Windows persistence
# Run keys pointing anywhere user-writable
foreach ($hive in 'HKCU','HKLM') {
foreach ($k in 'Software\Microsoft\Windows\CurrentVersion\Run',
'Software\Microsoft\Windows\CurrentVersion\RunOnce') {
Get-ItemProperty -Path "${hive}:\$k" -ErrorAction SilentlyContinue |
Select-Object -Property * -Exclude PS* |
ForEach-Object { $_.PSObject.Properties } |
Where-Object { $_.Value -match 'AppData|\\Temp\\|dotnet_diag_|update_win' } |
Select-Object @{n='Hive';e={$hive}}, @{n='Key';e={$k}}, Name, Value
}
}
# Scheduled tasks whose action runs from a user-writable path
Get-ScheduledTask | ForEach-Object {
$exec = $_.Actions | Where-Object { $_.Execute } | Select-Object -ExpandProperty Execute
if ($exec -match 'AppData|\\Temp\\|dotnet_diag_|update_win') {
[pscustomobject]@{ Task = $_.TaskName; Path = $_.TaskPath; Execute = $exec }
}
}
7.5 · Resolver and proxy logs (best vantage point on thin-telemetry hosts)
- Any query for a name ending in
wel1[.]ru, over the full retention window. There is no benign reason for this. Include NXDOMAIN answers — a sinkholed or taken-down domain still proves the host tried. - TXT queries whose leftmost label is
cor a small integer, in a burst from one client — the chunk walk is up to 2,000 sequential lookups. - Queries for the eight Cloudflare Workers hosts in §4.
- Proxy: requests carrying
User-Agent: node-fetch/2.6to any*.workers.devhost. Correlate with the requesting host and user before escalating — this UA is common in benign Node code. - Registry proxy (Nexus / Artifactory / Verdaccio): every request for a package name on the campaign list, including requests that 404'd — a 404 still tells you a developer or a build tried to resolve it.
7.6 · CI/CD job archaeology
Ephemeral runners are the worst case: the whole chain can execute and the host can be destroyed before anyone looks. The job log usually outlives the runner.
- Search build logs across the retention window for the campaign package names and for
bigops/dolyame-boxy/-bnpl-substrings. - Diff the resolved dependency tree of recent builds against the previous known-good build. An unexplained new transitive dependency added after 2026-08-04 is the signal.
- Check whether any runner image bakes a lockfile or a warm
node_modules— if so, the image itself needs rebuilding, not just the projects. - Enumerate what each affected runner could reach: npm publish tokens, GitHub App or PAT scopes, cloud role trust policies, signing keys, deployment secrets. That list is the rotation scope in §14.
CrowdStrike LogScale CQL Hunt Queries
No query carries an in-query time filter. Set the window in the Falcon console instead — @timestamp compared against an expression such as now() - N is rejected by the parser and would stop every query on line 1. Start with 30 days: publishing began around 2026-08-04, but an import can happen long after the install, so the first-execution date is not the install date. For Q1, Q2 and Q7/Q8 go to full retention — those are zero-false-positive indicator lookups and there is no cost to widening them.
Run order matters. Q1 → Q2 → Q7 → Q8 → Q11 first: they are the high-confidence, low-noise indicator hunts, and a hit on any of them converts this from a hunt into an incident. Only then run the behavioral set (Q3, Q4, Q6, Q12, Q14), which needs a baseline to be readable. Q5 and Q13 are discovery queries — expect to tune them before they are useful.
The single highest-value query in this pack. The campaign's DNS fallback channel lives entirely under wel1[.]ru, and no legitimate enterprise workload resolves it. This deliberately matches the apex and every subdomain — including the session-labelled form the macOS loader uses (c.<session-id>.dl.wel1[.]ru) — rather than enumerating the four known payload domains, so it survives the actor adding a fifth.
// HUNT: WEL1DROPPER DNS payload-channel resolution (wel1.ru, all depths)
// MITRE: T1071.004, T1008, T1105 | CONF: high FP: low COST: low
// REQUIRES: DnsRequest (verify macOS/Linux sensor DNS telemetry is flowing)
// FALSE POSITIVES: none expected. An analyst researching this report from a
// managed host is the only realistic benign hit -- confirm against the user.
#event_simpleName=/^(DnsRequest|SuspiciousDnsRequest)$/
| DomainName=/(^|\.)wel1\.ru$/i
| table([@timestamp, aid, ComputerName, UserName, ContextBaseFileName, ContextProcessId, DomainName, RequestType, DnsResponseType], limit=2000)
| sort(field=@timestamp, order=asc)
Triage: any hit is an incident, not a finding. Pivot on aid into Q7/Q8 for the staged payload, Q9/Q10/Q11 for persistence. A hit with ContextBaseFileName of node confirms stage 1 ran in-process. An NXDOMAIN answer still proves the host tried — the domain may already be sinkholed or taken down.
Three oob-worker.* hosts serve the stage-1 payload (shuffled per attempt); five package-proxy.* hosts were recovered by XOR-decoding the macOS binary and serve the stage-3 beacon_mac.bin. A hit on a package-proxy host is materially worse than a hit on an oob-worker host: it means the macOS chain reached its third stage.
// HUNT: Flooding Dropper Cloudflare Workers staging + macOS beacon proxies
// MITRE: T1102.002, T1608.001, T1105 | CONF: high FP: low COST: low
// REQUIRES: DnsRequest
// FALSE POSITIVES: none expected -- these are dedicated attacker Workers hosts.
#event_simpleName=/^(DnsRequest|SuspiciousDnsRequest)$/
| DomainName=/^(oob-worker\.cf(103-070|102-baf|99-9b3)|package-proxy\.cf(5|6|7|8|11)oobworker)\.workers\.dev$/i
| stage := if(DomainName=/^package-proxy\./i, then="stage3-macos-beacon", else="stage1-payload")
| table([@timestamp, aid, ComputerName, UserName, ContextBaseFileName, DomainName, stage], limit=2000)
| sort(field=@timestamp, order=asc)
Triage: as Q1. Also check the egress proxy for the matching HTTPS request — a node-fetch/2.6 User-Agent against one of these hosts, with a response over 1,000 bytes, means the payload was delivered rather than merely resolved.
The most durable detection in the pack. Q1 and Q2 die the moment the actor registers a new domain; the protocol does not change so easily. WEL1DROPPER asks for c.<domain> to learn the chunk count, then walks 0.<domain>, 1.<domain> … up to 1,999. This query looks for that leftmost-label shape coming from a Node process against any domain at all, and requires both halves of the protocol — the count query and at least three numbered chunks — before reporting.
// HUNT: DNS TXT chunked-payload protocol from a Node process (any domain)
// MITRE: T1071.004, T1132.001, T1008 | CONF: high FP: low COST: medium
// REQUIRES: DnsRequest with ContextBaseFileName populated
// FALSE POSITIVES: a handful of CDN and sharded-service schemes use numeric
// leftmost labels. Requiring the 'c.' count query AND 3+ numbered siblings on
// the SAME parent domain is what removes them -- a CDN never does both.
// TUNING: if a benign parent domain recurs, exclude it by name at the parent
// stage, e.g. add: | parent!=/(^|\.)your-cdn\.example\.com$/i
// Do NOT relax the chunk_labels threshold -- 3 is already conservative
// against a real chunk walk that runs to as many as 2,000 lookups.
#event_simpleName=/^(DnsRequest|SuspiciousDnsRequest)$/
| ContextBaseFileName=/^node(\.exe)?$/i
| DomainName=/^(?<label>c|\d{1,4})\.(?<parent>[a-z0-9][a-z0-9\-]*\.[a-z0-9\-\.]+)$/i
| kind := if(label=/^c$/i, then="count", else="chunk")
| groupBy([aid, ComputerName, parent], function=[
count(as=lookups),
count(field=label, distinct=true, as=distinct_labels),
collect([kind], limit=5),
min(@timestamp, as=first_seen),
max(@timestamp, as=last_seen)
])
| chunk_labels := distinct_labels - 1
| chunk_labels >= 3
| sort(field=lookups, order=desc, limit=200)
Triage: read parent first. If it ends in wel1.ru, escalate immediately. If it is an unfamiliar domain, you may have found the campaign's next infrastructure — pivot the parent domain fleet-wide and submit it. lookups in the hundreds indicates a completed payload reassembly.
The volume complement to Q3. A payload of any real size takes hundreds of TXT lookups to reassemble, and that burst is visible even if the label shape is obfuscated in a future variant. This is the query that catches the next version of the channel.
// HUNT: anomalous TXT-record query volume from node / node.exe
// MITRE: T1071.004, T1008 | CONF: medium FP: medium COST: medium
// REQUIRES: DnsRequest with RequestType populated
// NOTE: RequestType is the numeric DNS QTYPE; 16 = TXT.
// (!) validate in tenant -- confirm your sensor version populates RequestType
// numerically and not as a string before trusting the =16 filter.
// FALSE POSITIVES: Node tooling that legitimately reads TXT records --
// ACME/Let's Encrypt DNS-01 challenge clients, DKIM/SPF/DMARC validators,
// service-discovery libraries, and some feature-flag SDKs.
// TUNING: exclude your known DNS-01 and mail-validation hosts by aid, and
// exclude the parent domains they legitimately query, e.g. add after the
// groupBy: | !in(field=ComputerName, values=["acme-runner-01","mx-verify-02"])
// Baseline for a week before setting the threshold -- 50 is a starting point,
// not a tuned value.
#event_simpleName=DnsRequest
| ContextBaseFileName=/^node(\.exe)?$/i
| RequestType=16
| groupBy([aid, ComputerName, UserName], function=[
count(as=txt_lookups),
count(field=DomainName, distinct=true, as=distinct_names),
collect([DomainName], limit=25),
min(@timestamp, as=first_seen),
max(@timestamp, as=last_seen)
])
| txt_lookups > 50
| sort(field=txt_lookups, order=desc, limit=200)
Triage: a genuine chunk walk shows high txt_lookups with distinct_names almost equal to it (each numbered label is a unique name) and all names sharing one parent. A DKIM validator shows the opposite: many lookups across a handful of repeated names. Read collect(DomainName) to tell them apart in one glance.
A discovery query, not a detection. Cloudflare Workers hosts plenty of legitimate SaaS, so this will be noisy — but the actor has already burned eight workers.dev hosts and shows every sign of registering more. Run it once, build the allowlist, then keep it as a scheduled low-priority hunt where only genuinely new hosts surface.
// HUNT: node-initiated resolution of any workers.dev host (discovery)
// MITRE: T1102.002 | CONF: low-med FP: high COST: medium
// REQUIRES: DnsRequest
// FALSE POSITIVES: high by design. Legitimate SaaS, APIs, feature-flag services
// and internal tooling are commonly hosted on Cloudflare Workers.
// TUNING: this query is only useful once. Run it, review every host, then
// convert the benign ones into a standing exclusion and keep the remainder as
// the alert. Replace the placeholder below with your reviewed allowlist:
// | !in(field=DomainName, values=["your-approved-app.workers.dev"])
// Also consider restricting to hosts where first_seen is inside the last 7d --
// an established host that has resolved for months is not this campaign.
#event_simpleName=/^(DnsRequest|SuspiciousDnsRequest)$/
| ContextBaseFileName=/^node(\.exe)?$/i
| DomainName=/\.workers\.dev$/i
| known := if(DomainName=/^(oob-worker\.cf(103-070|102-baf|99-9b3)|package-proxy\.cf(5|6|7|8|11)oobworker)\.workers\.dev$/i, then="KNOWN-FLOODING-DROPPER", else="unreviewed")
| groupBy([DomainName, known], function=[
count(as=hits),
count(field=aid, distinct=true, as=hosts),
collect([ComputerName], limit=15),
min(@timestamp, as=first_seen),
max(@timestamp, as=last_seen)
])
| sort(field=first_seen, order=desc, limit=300)
Triage: sort by first_seen descending and read the top of the list — a Workers host that appeared for the first time in the last week, on a small number of developer or CI hosts, is the shape you are looking for. A host resolving from hundreds of machines for months is business software.
Stage 1 ends by shelling out: cmd.exe /c start /b on Windows, /bin/sh -c "… &" on Unix. On a developer or CI host Node spawns shells constantly, so raw volume is useless — the discriminators are the arguments. This query scores each child rather than filtering it away, so you can sort the signal to the top instead of guessing at an exclusion list.
// HUNT: node-parented shell / downloader / persistence-utility execution
// MITRE: T1059.003, T1059.004, T1059.007, T1105 | CONF: medium FP: high COST: medium
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2
// FALSE POSITIVES: very high on developer and CI hosts. Build tooling shells out
// constantly -- node-gyp, husky git hooks, npm run-script, nx/turbo task
// runners, Playwright and Puppeteer browser installs, Electron builders.
// TUNING: do not exclude by child process name -- that deletes the detection.
// Filter on the score instead: the campaign's launch lines carry a detached
// background marker AND a temp-directory path AND a masquerading filename.
// Start at susp_score >= 2 and only exclude a specific ParentCommandLine
// (e.g. your CI's known build wrapper) once you have read it.
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| ParentBaseFileName=/^node(\.exe)?$/i
| FileName=/^(cmd|powershell|pwsh|sh|bash|zsh|dash|curl|wget|osascript|launchctl|schtasks|reg|chmod)(\.exe)?$/i
| detached := if(CommandLine=/(start\s+\/b|&\s*$|nohup|setsid|disown)/i, then=1, else=0)
| tempdir := if(CommandLine=/(\\Temp\\|\bTEMP\b|\/var\/tmp\/|\/tmp\/|\.local\/share\/runtime)/i, then=1, else=0)
| masquerade:= if(CommandLine=/(dotnet_diag_[0-9a-f]{8}|\.cache_[0-9a-f]{8}|update_win|beacon_mac|com\.apple\.(runtime|windowserver\.helper)|analytics_state)/i, then=1, else=0)
| susp_score := detached + tempdir + masquerade
| susp_score >= 1
| table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, susp_score, detached, tempdir, masquerade, CommandLine, ParentCommandLine], limit=1000)
| sort(field=susp_score, order=desc)
Triage: susp_score of 3 is the campaign's exact launch signature and should be treated as confirmed. Score 2 with a temp path is worth reading in full. Score 1 on detached alone is usually benign build tooling — that is the tier to baseline away, and the reason the score is exposed as its own column rather than folded into the filter.
The dropper renames whatever it fetched to %TEMP%\dotnet_diag_<8 hex>.exe — a name chosen to read as a .NET diagnostics artifact. The [0-9a-f]{8} anchor is what keeps this precise: a real Microsoft diagnostics tool is not named with eight random hex characters. update_win.exe is the stage-3 path; it is included because it may be written to disk on some variants even though the analyzed sample loads it reflectively.
// HUNT: WEL1DROPPER Windows staged payload -- write and execute
// MITRE: T1105, T1036.005, T1059.003 | CONF: high FP: low COST: low
// REQUIRES: ProcessRollup2, SyntheticProcessRollup2, PeFileWritten, NewExecutableWritten
// FALSE POSITIVES: none expected. The 8-hex-character suffix is the campaign's
// own randomiser; no shipping Microsoft tool matches this pattern.
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2|PeFileWritten|NewExecutableWritten)$/
| FileName=/^(dotnet_diag_[0-9a-f]{8}|update_win)\.exe$/i or TargetFileName=/(dotnet_diag_[0-9a-f]{8}|update_win)\.exe$/i or CommandLine=/(dotnet_diag_[0-9a-f]{8}\.exe|update_win\.exe|start\s+\/b\s+%TEMP%)/i
| artifact := if(TargetFileName=/dotnet_diag_[0-9a-f]{8}\.exe$/i, then="stage2-written", else="stage2-or-3-executed")
| table([@timestamp, aid, ComputerName, UserName, artifact, FileName, ImageFileName, TargetFileName, SHA256HashData, ParentBaseFileName, GrandparentBaseFileName, CommandLine, ParentCommandLine], limit=1000)
| sort(field=@timestamp, order=asc)
Triage: confirmed compromise. Capture SHA256HashData — payload polymorphism means your sample is probably not one of the seven hashes in §10, and submitting it extends coverage for everyone. Check GrandparentBaseFileName to prove the Node ancestry, then go straight to Q9 and Q10 for persistence and §14 for containment.
The Linux and macOS equivalent of Q7. The dropper writes /var/tmp/.cache_<8 hex>, chmods it executable, and launches it detached. The leading dot hides it from a plain ls, and .cache_ reads as an ordinary cache artifact — the eight hex characters are what make the pattern unambiguous. beacon_mac.bin is the macOS stage-3 payload name.
// HUNT: WEL1DROPPER Unix staged payload -- write, chmod and detached execute
// MITRE: T1105, T1036.005, T1059.004 | CONF: high FP: low COST: low
// REQUIRES: ProcessRollup2, SyntheticProcessRollup2, ELFFileWritten, NewExecutableWritten
// FALSE POSITIVES: none expected. Real cache files are not named with an
// 8-hex-character suffix under /var/tmp and then chmod +x'd.
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2|ELFFileWritten|NewExecutableWritten)$/
| FileName=/^(\.cache_[0-9a-f]{8}|beacon_mac\.bin)$/i or TargetFileName=/(\.cache_[0-9a-f]{8}|beacon_mac\.bin)$/i or CommandLine=/(\.cache_[0-9a-f]{8}|beacon_mac\.bin)/i
| artifact := if(TargetFileName=/\.cache_[0-9a-f]{8}$/i, then="stage2-written", else="stage2-or-3-executed")
| table([@timestamp, aid, ComputerName, UserName, artifact, FileName, ImageFileName, TargetFileName, SHA256HashData, ParentBaseFileName, GrandparentBaseFileName, CommandLine, ParentCommandLine], limit=1000)
| sort(field=@timestamp, order=asc)
Triage: confirmed compromise. On Linux, capture the hash and check it against 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3; if it differs, you have a fresh sample of a polymorphic payload — submit it. A UPX-packed, statically linked, non-PIE ELF under /var/tmp is the shape to confirm, and the reported end state is a Sliver implant, so treat outbound sessions from that host as C2 until proven otherwise.
Windows stage 2 copies itself under %AppData% and persists with both a Run key and a scheduled task — belt and braces, so finding one means looking for the other (Q10). The campaign-specific filenames are scored separately from the generic user-writable-path signal, because the filenames are certainty and the path alone is merely suspicious.
// HUNT: Run/RunOnce value written pointing into AppData or Temp
// MITRE: T1547.001, T1036.005 | CONF: medium FP: medium COST: low
// REQUIRES: AsepValueUpdate or RegGenericValueUpdate
// FALSE POSITIVES: a real population of legitimate software persists from
// AppData by design -- Squirrel/Electron auto-updaters, Microsoft Teams,
// Slack, Zoom, Discord, Dropbox, OneDrive, the VS Code updater.
// TUNING: keep the campaign-filename tier (match_kind=campaign-filename)
// unfiltered -- it has no benign population. Baseline and exclude only the
// generic tier by RegValueName, e.g. add:
// | !in(field=RegValueName, values=["com.squirrel.Slack.Slack","OneDrive","Discord"])
// Excluding by path prefix instead would delete the detection, because the
// malware persists from the same AppData tree those products use.
#event_simpleName=/^(AsepValueUpdate|RegGenericValueUpdate)$/
| RegObjectName=/\\CurrentVersion\\Run(Once)?$/i
| RegStringValue=/(dotnet_diag_[0-9a-f]{8}|update_win\.exe|\\AppData\\|\\Temp\\)/i
| match_kind := if(RegStringValue=/(dotnet_diag_[0-9a-f]{8}|update_win\.exe)/i, then="campaign-filename", else="generic-user-writable-path")
| table([@timestamp, aid, ComputerName, UserName, match_kind, RegObjectName, RegValueName, RegStringValue], limit=1000)
| sort(field=match_kind, order=asc)
Triage: match_kind=campaign-filename is confirmed compromise — escalate without further review. For the generic tier, the question is whether a vendor you actually deploy owns that RegValueName; if the value name is random or meaningless, read the target executable's hash and signer.
The second half of the Windows persistence pair. Run this alongside Q9 and correlate on aid: a host with both a Run value and a scheduled task pointing into the same user-writable tree, created within minutes of each other, is this campaign's exact persistence signature and is far more specific than either finding alone.
// HUNT: scheduled task whose action runs from AppData or Temp
// MITRE: T1053.005, T1036.005 | CONF: medium FP: medium COST: low
// REQUIRES: ScheduledTaskRegistered
// FALSE POSITIVES: auto-updaters again -- Google Update, Edge/Chrome updaters,
// Adobe, Electron app updaters and some IDE toolchains register tasks that
// run from user-writable paths.
// TUNING: keep the campaign-filename tier unfiltered. For the generic tier,
// baseline and exclude by TaskName once you have confirmed the owner, e.g.
// | !in(field=TaskName, values=["GoogleUpdateTaskMachineUA","MicrosoftEdgeUpdateTaskMachineUA"])
// Correlating this query's aid set against Q9's is a better discriminator
// than any exclusion list: legitimate updaters rarely do both at once.
#event_simpleName=ScheduledTaskRegistered
| TaskExecutable=/(dotnet_diag_[0-9a-f]{8}|update_win\.exe|\\AppData\\|\\Temp\\)/i
| match_kind := if(TaskExecutable=/(dotnet_diag_[0-9a-f]{8}|update_win\.exe)/i, then="campaign-filename", else="generic-user-writable-path")
| table([@timestamp, aid, ComputerName, UserName, match_kind, TaskName, TaskExecutable, TaskAuthor], limit=1000)
| sort(field=match_kind, order=asc)
Triage: as Q9. TaskAuthor is the fastest tell — a legitimate updater task is authored by the vendor's installer account or SYSTEM; this campaign's task is authored by the logged-on developer.
The macOS loader installs ~/Library/LaunchAgents/com.apple.windowserver.helper.plist and stages its payload at ~/.local/share/runtime/com.apple.runtime with a .lock sibling, then activates it with launchctl load -w. Both names impersonate Apple components; neither is a legitimate Apple artifact in a user's home directory. That masquerade is the detection: Apple ships its own agents from /System/Library and /Library, never from ~/Library, so any com.apple.* plist under a user LaunchAgents directory is anomalous on its own.
// HUNT: macOS LaunchAgent masquerading as an Apple component + fake runtime dir
// MITRE: T1543.001, T1036.005 | CONF: high FP: low COST: low
// REQUIRES: CriticalFileModified, NewExecutableWritten, ProcessRollup2
// FALSE POSITIVES: near zero for the com.apple.* tier -- Apple does not ship
// LaunchAgents into a user's home directory. Third-party agents under
// ~/Library/LaunchAgents are common but use their own vendor identifiers and
// do not match the com.apple. prefix this query anchors on.
#event_simpleName=/^(CriticalFileModified|NewExecutableWritten|ProcessRollup2|SyntheticProcessRollup2)$/
| TargetFileName=/(Library\/LaunchAgents\/com\.apple\.|\.local\/share\/runtime\/(\.lock|com\.apple\.runtime)|com\.apple\.windowserver\.helper\.plist)/i or CommandLine=/(launchctl\s+(load|bootstrap)\s+-w?.*com\.apple\.windowserver\.helper|\.local\/share\/runtime\/com\.apple\.runtime)/i
| confidence := if(TargetFileName=/com\.apple\.windowserver\.helper\.plist$/i, then="campaign-exact", else="apple-masquerade-anomaly")
| table([@timestamp, aid, ComputerName, UserName, confidence, TargetFileName, FileName, SHA256HashData, ParentBaseFileName, CommandLine], limit=1000)
| sort(field=confidence, order=asc)
Triage: campaign-exact is confirmed compromise. For apple-masquerade-anomaly, read the plist: this campaign's version sets KeepAlive, throttles restarts to 60 s, and redirects stdout and stderr to /dev/null. Removal is not sufficient — see §14, the payload under ~/.local/share/runtime must go too, and a third-stage beacon may already have been fetched.
The name-independent version of Q7 and Q8. If a future wave renames the dropped file, the behavior survives: a Node process writes a fresh executable into a world-writable temp directory. This is the query that still works when every string in this pack has been rotated.
// HUNT: node writes a new executable into a temp directory (name-independent)
// MITRE: T1105, T1036 | CONF: medium FP: medium COST: medium
// REQUIRES: PeFileWritten, NewExecutableWritten, ELFFileWritten with ContextBaseFileName
// FALSE POSITIVES: a genuine and large benign population on developer and CI
// hosts -- node-gyp and prebuild-install fetching native modules, esbuild /
// swc / sharp / better-sqlite3 binary downloads, Electron and Playwright and
// Puppeteer runtime installs, and Cypress binary caching.
// TUNING: exclude the package-manager cache and build-output trees, which is
// where all of the benign population lands, and keep the volatile temp dirs:
// | !in(field=TargetDirectoryName, values=["node_modules","dist","build"])
// A better discriminator than any exclusion list is correlation -- join this
// result set against Q1/Q2/Q3 on aid. A node-written temp binary on a host
// that also resolved campaign infrastructure needs no further tuning.
#event_simpleName=/^(PeFileWritten|NewExecutableWritten|ELFFileWritten)$/
| ContextBaseFileName=/^node(\.exe)?$/i
| TargetFileName=/(\\AppData\\Local\\Temp\\|\\Windows\\Temp\\|\/var\/tmp\/|\/tmp\/|\/private\/var\/folders\/)/i
| hidden_name := if(TargetFileName=/(\\|\/)\.[^\\\/]+$/i, then=1, else=0)
| hex_suffix := if(TargetFileName=/[0-9a-f]{8}(\.exe)?$/i, then=1, else=0)
| susp_score := hidden_name + hex_suffix
| table([@timestamp, aid, ComputerName, UserName, susp_score, hidden_name, hex_suffix, TargetFileName, TargetDirectoryName, SHA256HashData, FileSize, ContextBaseFileName], limit=1000)
| sort(field=susp_score, order=desc)
Triage: susp_score=2 (a dot-prefixed name ending in eight hex characters) is the campaign's exact on-disk shape. Build tooling writes descriptively named binaries into cache directories; it does not write hidden, randomly named ones into /var/tmp.
Endpoint-side coverage for the dependency question, complementing the lockfile sweep in §7.1. Only four package names are publicly named out of roughly 800–850, so this cannot be a complete inventory — the heuristic tiers (bigops, -bnpl-, dolyame) are Sonatype's correlation signals, explicitly labelled non-durable by their author because the naming convention is already drifting.
// HUNT: Flooding Dropper package names invoked via a Node package manager
// MITRE: T1195.001, T1204.002 | CONF: medium FP: medium COST: low
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2
// FALSE POSITIVES: the heuristic tier only. 'bnpl' is a real industry term
// (buy-now-pay-later) and legitimate packages use it; 'bigops' may collide
// with internal tooling. The four named-sample matches have no benign form.
// TUNING: treat match_tier=named-sample as confirmed and never exclude it.
// For the heuristic tier, resolve each hit against the authoritative campaign
// list (OpenSourceMalware, or Sonatype advisory sonatype-2026-005660) before
// escalating, then exclude confirmed-benign names, e.g.
// | !in(field=CommandLine, values=["*@acme/bnpl-checkout*"])
// Re-run this query weekly against a refreshed name list -- the actor is
// publishing from disposable accounts faster than any static list ages well.
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| FileName=/^(npm|npx|node|yarn|pnpm|bun|corepack)(\.exe|-cli\.js)?$/i
| CommandLine=/(checkout-mobile-bnpl|bigops-backend|bigops-api|dolyame-boxy-desktop-bnpl-card-gallery|bigops-|-bnpl-|dolyame)/i
| match_tier := if(CommandLine=/(checkout-mobile-bnpl|bigops-backend|bigops-api|dolyame-boxy-desktop-bnpl-card-gallery)/i, then="named-sample", else="heuristic-bigops-bnpl-dolyame")
| table([@timestamp, aid, ComputerName, UserName, match_tier, FileName, CommandLine, ParentBaseFileName, ParentCommandLine], limit=1000)
| sort(field=match_tier, order=asc)
Triage: a named-sample hit means the package was resolved on this host — go immediately to §7.2 to establish whether it was ever imported, which is the difference between a cleanup and an incident. Remember that a transitive dependency never appears on a command line at all, so a clean result here does not clear the host; §7.1 does.
Partial coverage for the ETW/AMSI-patching and persistence behavior in Windows stage 2. Falcon's data model has no discrete queryable event for an in-process AMSI or ETW patch — that is detected in-product, not by CQL (see §15 and §9 IOA-3). What CQL can see is the surrounding tree: a persistence or scripting utility running two levels below a Node process. On a developer host this is unusual enough to be worth reading every hit.
// HUNT: persistence / scripting LOLBins in a Node-descended process tree
// MITRE: T1547.001, T1053.005, T1562.001, T1562.006 | CONF: medium FP: medium COST: medium
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2 with GrandparentBaseFileName
// NOTE: this is a PROXY for the AMSI/ETW patching reported in Windows stage 2.
// Falcon detects in-process AMSI/ETW tampering in-product; it is not a
// discrete CQL-queryable event. Pair this with the in-product detections
// rather than treating a clean result here as absence of tampering.
// FALSE POSITIVES: build tooling that shells out two levels deep -- installer
// scripts, husky hooks invoking reg/schtasks, and CI wrappers.
// TUNING: exclude your known build wrappers by GrandparentBaseFileName once
// confirmed, e.g. | GrandparentBaseFileName!=/^(agent|runner)\.exe$/i
// Do not exclude powershell.exe -- it is the most likely stage-2 child.
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| GrandparentBaseFileName=/^node(\.exe)?$/i
| FileName=/^(reg|schtasks|powershell|pwsh|rundll32|regsvr32|wscript|cscript|mshta|bitsadmin|certutil)\.exe$/i
| encoded := if(CommandLine=/(-e|-en|-enc|-enco|-encod|-encode|-encoded|-encodedc|-encodedcommand|-w\s+hidden|-nop|frombase64string)/i, then=1, else=0)
| persist := if(CommandLine=/(CurrentVersion\\Run|\/create\s|schtasks.*\/tn)/i, then=1, else=0)
| tempref := if(CommandLine=/(\\AppData\\|\\Temp\\|dotnet_diag_[0-9a-f]{8}|update_win)/i, then=1, else=0)
| susp_score := encoded + persist + tempref
| table([@timestamp, aid, ComputerName, UserName, susp_score, encoded, persist, tempref, FileName, CommandLine, ParentBaseFileName, GrandparentBaseFileName], limit=1000)
| sort(field=susp_score, order=desc)
Triage: any hit with tempref=1 is very likely this campaign. persist=1 should immediately be cross-checked against Q9 and Q10 on the same aid. A score of 0 with a plain reg query is usually build tooling.
Pivot queries (host-scoped, for after a hit)
Replace REPLACE_WITH_AID with the agent ID from any hit above. These are the three questions triage always asks next.
// P1 -- full process tree on the suspect host, Node-related only #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | aid="REPLACE_WITH_AID" | ParentBaseFileName=/^(node|npm|npx|yarn|pnpm|bun|sh|bash|zsh|cmd|powershell)(\.exe)?$/i | table([@timestamp, UserName, ParentBaseFileName, FileName, CommandLine, SHA256HashData], limit=2000) | sort(field=@timestamp, order=asc) // P2 -- every external destination the host reached, newest first #event_simpleName=/^(DnsRequest|SuspiciousDnsRequest|NetworkConnectIP4)$/ | aid="REPLACE_WITH_AID" | groupBy([DomainName, RemoteAddressIP4, RemotePort], function=[count(as=hits), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)]) | sort(field=first_seen, order=desc, limit=500) // P3 -- fleet-wide blast radius for a hash recovered from Q7/Q8/Q12 #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2|PeFileWritten|NewExecutableWritten|ELFFileWritten)$/ | SHA256HashData="REPLACE_WITH_SHA256" | groupBy([aid, ComputerName, UserName], function=[count(as=hits), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)]) | sort(field=first_seen, order=asc, limit=500)
CrowdStrike Custom IOA Recommendations
Four of the fourteen queries are worth promoting from hunt to detection. The rest stay investigate-only: they are either discovery queries that need a per-tenant allowlist first (Q5, Q13) or scored behavioral hunts whose value is in the ranking, not in a binary verdict (Q3, Q4, Q6, Q12, Q14).
| ID | Rule | Type | Pattern | Action | Severity | Source query |
|---|---|---|---|---|---|---|
| IOA-1 | WEL1DROPPER staged payload execution — Windows | Process Creation | Image filename matches dotnet_diag_[0-9a-f]{8}\.exe or update_win\.exe; grandparent image name matches node\.exe | Block & terminate | CRITICAL | Q7 |
| IOA-2 | WEL1DROPPER staged payload execution — macOS / Linux | Process Creation | Image filename matches \.cache_[0-9a-f]{8} or beacon_mac\.bin; parent command line contains a detached background launch | Block & terminate | CRITICAL | Q8 |
| IOA-3 | Node-descended persistence utility on a developer endpoint | Process Creation | Grandparent image name matches node\.exe; image name matches (reg|schtasks)\.exe; command line references AppData, Temp or a campaign filename | Detect (monitor first) | HIGH | Q14 |
| IOA-4 | macOS LaunchAgent impersonating an Apple component | File Creation | Target file matches Library/LaunchAgents/com\.apple\..*\.plist under a user home directory | Detect | HIGH | Q11 |
Deploy IOA-1 and IOA-2 in monitor mode for 48 hours before switching to block. Both patterns are specific enough that a false positive is unlikely, but a blocking rule on a build fleet is a production-availability decision, not just a security one. IOA-3 should stay in detect indefinitely on hosts that run CI jobs — the benign population there is real.
What no IOA can cover. Windows stage 3 decrypts /pkg/update_win.exe and executes it reflectively in memory (T1620). There is no file creation and no conventional process launch to key a Custom IOA on. Coverage for that step comes from Falcon's in-product memory scanning and AMSI/ETW-tamper detections, not from anything in this pack. Confirm those are enabled in your prevention policy, and read §15 before assuming the chain is covered end to end.
Custom IOC import
Import the CSV in §10 into Falcon IOC Management. Set the two native-payload hashes to prevent; leave the five package-file hashes on detect — they are text files from a single analyzed sample and Sonatype documented the payloads as polymorphic across packages, so preventing on them buys little and risks blocking a re-used benign file hash. The three tcsbank / cloudpayments hosts are imported as no_action deliberately: their ownership was never established.
Machine-Readable IOC Appendix
Every atomic value below appears verbatim in a saved source snapshot under FloodingDropper-WEL1DROPPER-npm-Hunt-sources/. Values that could not be traced to a fetched source do not appear here in any form — there is no exfiltration endpoint, no beacon port and no C2 IP address in this pack, because no source contains one.
type,value,action,severity,expiration,description,tags
domain,wel1.ru,detect,critical,2027-02-08,WEL1DROPPER DNS TXT payload channel apex,campaign:FloodingDropper
domain,dl.wel1.ru,detect,critical,2027-02-08,WEL1DROPPER DNS delivery domain macOS loader session-labelled,campaign:FloodingDropper
domain,sdk.dl.wel1.ru,detect,critical,2027-02-08,WEL1DROPPER DNS TXT payload domain Linux x64,campaign:FloodingDropper
domain,ext.dl.wel1.ru,detect,critical,2027-02-08,WEL1DROPPER DNS TXT payload domain Linux ARM64,campaign:FloodingDropper
domain,pkg.dl.wel1.ru,detect,critical,2027-02-08,WEL1DROPPER DNS TXT payload domain macOS,campaign:FloodingDropper
domain,net.dl.wel1.ru,detect,critical,2027-02-08,WEL1DROPPER DNS TXT payload domain Windows,campaign:FloodingDropper
domain,oob-worker.cf103-070.workers.dev,detect,critical,2027-02-08,Stage1 HTTPS payload staging host 1 of 3,campaign:FloodingDropper
domain,oob-worker.cf102-baf.workers.dev,detect,critical,2027-02-08,Stage1 HTTPS payload staging host 2 of 3,campaign:FloodingDropper
domain,oob-worker.cf99-9b3.workers.dev,detect,critical,2027-02-08,Stage1 staging host 3 of 3 and Linux ELF next-stage source,campaign:FloodingDropper
domain,package-proxy.cf5oobworker.workers.dev,detect,critical,2027-02-08,macOS stage3 beacon_mac.bin proxy XOR key 0x9c,campaign:FloodingDropper
domain,package-proxy.cf6oobworker.workers.dev,detect,critical,2027-02-08,macOS stage3 beacon_mac.bin proxy XOR key 0x9c,campaign:FloodingDropper
domain,package-proxy.cf7oobworker.workers.dev,detect,critical,2027-02-08,macOS stage3 beacon_mac.bin proxy XOR key 0x9c,campaign:FloodingDropper
domain,package-proxy.cf8oobworker.workers.dev,detect,critical,2027-02-08,macOS stage3 beacon_mac.bin proxy XOR key 0x9c,campaign:FloodingDropper
domain,package-proxy.cf11oobworker.workers.dev,detect,critical,2027-02-08,macOS stage3 beacon_mac.bin proxy XOR key 0x9c,campaign:FloodingDropper
domain,nexus.tcsbank.ru,no_action,informational,2026-09-08,LEAD ONLY XOR-hidden health-check host ownership not established do not block,campaign:FloodingDropper
domain,repo-linux.tcsbank.ru,no_action,informational,2026-09-08,LEAD ONLY XOR-hidden health-check host ownership not established do not block,campaign:FloodingDropper
domain,alertmanager.cloudpayments.ru,no_action,informational,2026-09-08,LEAD ONLY XOR-hidden health-check host ownership not established do not block,campaign:FloodingDropper
sha256,7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3,prevent,critical,2027-02-08,Linux x86-64 second stage UPX 3.96 statically linked non-PIE ELF,campaign:FloodingDropper
sha256,c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00,prevent,critical,2027-02-08,macOS universal Mach-O second stage x86-64 and ARM64 LaunchAgent installer,campaign:FloodingDropper
sha256,94ef6b1c4a9d31f78f446d053048bcef34fd88f4376a1a46f7f777a9e9c83a29,detect,high,2027-02-08,_helpers.js live stage1 downloader single sample payloads are polymorphic,campaign:FloodingDropper
sha256,a3e2ffb440b779d30da3ff282affd649731088e8570df7b1aa72742d995b782c,detect,medium,2027-02-08,lib/telemetry.js 80KB fake telemetry decoy not imported by entry point,campaign:FloodingDropper
sha256,b74c5675725911c62091bdf40714df760cc2af7a88360d21065f4e1c878aa8f0,detect,medium,2027-02-08,index.js fake SDK facade with tail require of _helpers,campaign:FloodingDropper
sha256,e2650e9aa2f924433ba422857b22ee7c5996b5ad306f3f903283f6a13e248935,detect,medium,2027-02-08,package.json of analyzed sample contains no lifecycle hook,campaign:FloodingDropper
sha256,0fc30f82e1fa5e51a6c0c43f3ed7f13592ea731cb331e43a4d085df60a4db8b6,detect,medium,2027-02-08,README.md that social-engineers the developer into calling require,campaign:FloodingDropper
# Not importable as atomic IOCs -- hunt these with the queries in section 8:
# filename pattern dotnet_diag_[0-9a-f]{8}.exe (Windows staged payload, Q7)
# filename pattern .cache_[0-9a-f]{8} (Unix staged payload, Q8)
# filename update_win.exe (Windows stage 3 path, Q7)
# filename beacon_mac.bin (macOS stage 3 path, Q8)
# path ~/Library/LaunchAgents/com.apple.windowserver.helper.plist (Q11)
# path ~/.local/share/runtime/com.apple.runtime and .lock (Q11)
# marker file /tmp/.analytics_state and %TEMP%\analytics_state (section 7.2)
# user-agent node-fetch/2.6 (proxy hunt, section 7.5)
# Deliberately NOT an indicator: 127.0.0.1:4444 is loopback -- a probe for
# another local component, not external C2. Do not import it.
wel1.ru dl.wel1.ru sdk.dl.wel1.ru ext.dl.wel1.ru pkg.dl.wel1.ru net.dl.wel1.ru oob-worker.cf103-070.workers.dev oob-worker.cf102-baf.workers.dev oob-worker.cf99-9b3.workers.dev package-proxy.cf5oobworker.workers.dev package-proxy.cf6oobworker.workers.dev package-proxy.cf7oobworker.workers.dev package-proxy.cf8oobworker.workers.dev package-proxy.cf11oobworker.workers.dev
Block wel1.ru as a wildcard, not just the four named payload domains — the macOS loader inserts a session label the report could not enumerate. Log every blocked query: a block that fires is a compromised host.
7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3 Linux x86-64 second stage (UPX 3.96) c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00 macOS universal Mach-O second stage 94ef6b1c4a9d31f78f446d053048bcef34fd88f4376a1a46f7f777a9e9c83a29 _helpers.js (live stage-1 downloader) a3e2ffb440b779d30da3ff282affd649731088e8570df7b1aa72742d995b782c lib/telemetry.js (80 KB decoy) b74c5675725911c62091bdf40714df760cc2af7a88360d21065f4e1c878aa8f0 index.js (fake SDK facade) e2650e9aa2f924433ba422857b22ee7c5996b5ad306f3f903283f6a13e248935 package.json (no lifecycle hook) 0fc30f82e1fa5e51a6c0c43f3ed7f13592ea731cb331e43a4d085df60a4db8b6 README.md (require() social engineering)
# Publicly named samples (4 of roughly 800-850). NOT a complete inventory. checkout-mobile-bnpl # analyzed sample, version 35.6.9 bigops-backend # Sonatype entry point into the campaign bigops-api dolyame-boxy-desktop-bnpl-card-gallery # Sonatype correlation heuristics -- explicitly labelled NON-DURABLE by their # author. Use to triage, then confirm against the authoritative list. name contains bigops name contains -bnpl- name contains dolyame version matches 35.x.y # Authoritative inventories (fetch fresh, do not rely on this pack): # OpenSourceMalware campaign package list # Sonatype advisory sonatype-2026-005660 (846 components, CWE-506, CVSS 8.7)
# Windows %TEMP%\dotnet_diag_<8 hex>.exe staged payload %TEMP%\analytics_state rate-limit marker (proves stage 1 ran) cmd.exe /c start /b %TEMP%\dotnet_diag_<id>.exe detached launch HKCU or HKLM ...\CurrentVersion\Run value into AppData or Temp scheduled task action executable under AppData or Temp /pkg/update_win.exe stage-3 path (decrypted, loaded in memory) # Linux /var/tmp/.cache_<8 hex> staged payload, chmod +x /tmp/.analytics_state rate-limit marker /bin/sh -c "/var/tmp/.cache_<id> &" detached launch # macOS /var/tmp/.cache_<8 hex> staged loader ~/.local/share/runtime/ payload install directory ~/.local/share/runtime/.lock ~/.local/share/runtime/com.apple.runtime ~/Library/LaunchAgents/com.apple.windowserver.helper.plist launchctl load -w '<path>/com.apple.windowserver.helper.plist' /pkg/beacon_mac.bin stage-3 beacon path # Stage-1 payload paths requested from the staging hosts /pkg/package Linux x64 /pkg/package-arm64 Linux ARM64 /pkg/loader_mac macOS universal /pkg/package.exe Windows
{
"campaign": "Flooding Dropper",
"malware": "WEL1DROPPER",
"aliases": ["AI slopsquatting npm campaign"],
"tracking": {"sonatype": "sonatype-2026-005660", "cwe": "CWE-506", "cvss": 8.7},
"disclosed": "2026-08-05",
"pack_version": "1.0",
"pack_date": "2026-08-08",
"assessed_predecessor": "Moika dependency-confusion campaign (April-May 2026, 250+ npm packages)",
"ecosystem": "npm",
"package_count_reported": "788-846 (sources vary; trend note reports 1000+)",
"execution_trigger": "require() of the package; NO preinstall/postinstall hook",
"platforms": ["windows", "macos", "linux-x64", "linux-arm64"],
"indicators": {
"domains_block": [
"wel1.ru", "dl.wel1.ru", "sdk.dl.wel1.ru", "ext.dl.wel1.ru",
"pkg.dl.wel1.ru", "net.dl.wel1.ru",
"oob-worker.cf103-070.workers.dev", "oob-worker.cf102-baf.workers.dev",
"oob-worker.cf99-9b3.workers.dev",
"package-proxy.cf5oobworker.workers.dev", "package-proxy.cf6oobworker.workers.dev",
"package-proxy.cf7oobworker.workers.dev", "package-proxy.cf8oobworker.workers.dev",
"package-proxy.cf11oobworker.workers.dev"
],
"domains_lead_only_do_not_block": [
"nexus.tcsbank.ru", "repo-linux.tcsbank.ru", "alertmanager.cloudpayments.ru"
],
"sha256": [
"7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3",
"c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00",
"94ef6b1c4a9d31f78f446d053048bcef34fd88f4376a1a46f7f777a9e9c83a29",
"a3e2ffb440b779d30da3ff282affd649731088e8570df7b1aa72742d995b782c",
"b74c5675725911c62091bdf40714df760cc2af7a88360d21065f4e1c878aa8f0",
"e2650e9aa2f924433ba422857b22ee7c5996b5ad306f3f903283f6a13e248935",
"0fc30f82e1fa5e51a6c0c43f3ed7f13592ea731cb331e43a4d085df60a4db8b6"
],
"filename_patterns": [
"dotnet_diag_[0-9a-f]{8}.exe", ".cache_[0-9a-f]{8}",
"update_win.exe", "beacon_mac.bin"
],
"packages_named": [
"checkout-mobile-bnpl", "bigops-backend", "bigops-api",
"dolyame-boxy-desktop-bnpl-card-gallery"
],
"user_agent": "node-fetch/2.6"
},
"not_indicators": {
"127.0.0.1:4444": "loopback health probe for another local component, not C2",
"/proc/self/exe": "UPX loader reading its own image; explicitly not an IOC"
},
"attck": [
"T1195.001", "T1608.001", "T1204.002", "T1059.003", "T1059.004", "T1059.007",
"T1547.001", "T1053.005", "T1543.001", "T1562.001", "T1562.006", "T1620",
"T1027.002", "T1027.013", "T1036.005", "T1497.001", "T1622", "T1082",
"T1071.004", "T1102.002", "T1105", "T1132.001", "T1008", "T1552.001"
],
"intel_gaps": [
"final Windows and macOS beacons unavailable for analysis",
"no confirmed C2 protocol, beacon interval or exfiltration destination",
"package-proxy port not recoverable from strings",
"Linux ELF inner behavior not statically recoverable (UPX-packed)",
"Sliver attribution reported by secondary sources, NOT confirmed by the primary analysis"
]
}
Detection Validation Gates
Run these before you trust a clean result. A query that returns nothing because the telemetry is absent looks identical to a query that returns nothing because the threat is absent — these gates tell the two apart.
| Gate | Check | How | If it fails |
|---|---|---|---|
| G1 | DNS telemetry is flowing from every platform | Run #event_simpleName=DnsRequest | groupBy([event_platform], function=[count(as=n), count(field=aid, distinct=true, as=hosts)]) over 24 h and confirm Windows, Mac and Linux all appear with a plausible host count. | Q1–Q5 are blind on the missing platform. Fall back to resolver logs (§7.5) and raise sensor coverage as an action item. |
| G2 | RequestType is populated numerically | #event_simpleName=DnsRequest | groupBy([RequestType], function=[count(as=n)]). You should see 16 among the values. | Q4 silently returns nothing. Change the filter to whatever your tenant uses for TXT, or drop Q4 and rely on Q3. |
| G3 | ContextBaseFileName is populated on DNS events | #event_simpleName=DnsRequest | ContextBaseFileName=/^node(\.exe)?$/i | groupBy([ComputerName], function=[count(as=n)]) on a host you know runs Node. | Q3, Q4 and Q5 lose their process attribution and become fleet-wide domain hunts. Remove the ContextBaseFileName stage and accept the extra noise. |
| G4 | Node is actually visible in process telemetry | #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | FileName=/^node(\.exe)?$/i | groupBy([ComputerName], function=[count(as=n)]). Compare the host count against your known developer and CI population. | Q6, Q12, Q13 and Q14 are scoped to a fraction of the estate. Identify which build fleets have no sensor — those are the hosts this campaign will land on unseen. |
| G5 | Registry and scheduled-task telemetry is on | Confirm AsepValueUpdate, RegGenericValueUpdate and ScheduledTaskRegistered each return events in the last 24 h. | Q9 and Q10 return nothing regardless of infection state. Persistence must then be found with the §7.4 PowerShell sweep instead. |
| G6 | Field names resolve in your tenant | Run each query with the final table() removed. Any field your tenant does not populate shows as empty rather than erroring. | Replace the field or drop it from the projection. RequestType, DnsResponseType, TaskAuthor and TargetDirectoryName are the most tenant-variable fields used here. |
| G7 | Positive-control test — DNS chunk protocol | From a lab host with Node installed, run a script that issues TXT lookups for c.example-lab.yourdomain.test then 0., 1., 2., 3. of the same parent. Q3 must return the parent domain. | Q3 is not working. Check G1 and G3 first, then confirm your DNS events carry the full query name and not just the registrable domain. |
| G8 | Positive-control test — staged payload naming | On a lab host, copy any benign executable to %TEMP%\dotnet_diag_deadbeef.exe (or /var/tmp/.cache_deadbeef) and run it. Q7 or Q8 must return it. | Q7/Q8 are not working. Most likely the file-write event is not enabled or FileName is carrying a full path rather than a basename in your tenant. |
| G9 | Baselines captured before tuning | Record the 7-day result counts for Q4, Q5, Q6, Q12, Q13 and Q14 before adding any exclusion. | Without a baseline, every exclusion is a guess and you cannot tell later whether a quiet query is tuned or broken. |
| G10 | Dependency sweep actually covered everything | Confirm §7.1 ran against every repo, every lockfile, the npm cache, the internal mirror, and container image layers — not just the projects someone remembered. | A transitive dependency in an unreviewed repo keeps re-infecting after cleanup. This is the most common way this campaign survives remediation. |
The gate that matters most is G10. Both primary sources make the same point in different words: removing the dependency is necessary and not sufficient, and by the time the package is discovered the package is no longer the main problem. A clean endpoint hunt across a fleet whose lockfiles were never swept proves very little.
Hardening — Tiered & Framework-Cited
The control everyone reaches for first does not work here. npm config set ignore-scripts true, --ignore-scripts in CI, and install-script sandboxing all defend against lifecycle hooks. These packages have none. Execution happens on require(), which is a normal application operation you cannot disable. Keep the ignore-scripts controls — they are good for other campaigns — but do not count them as coverage for this one. The controls that actually bite are egress restriction, execution restriction in temp paths, and getting a malware-aware proxy in front of the registry.
| Control | Why it stops this chain | Authority | Verify |
|---|---|---|---|
Block the 14 campaign hosts at DNS and egress. Wildcard *.wel1.ru, not just the four named payload domains. Log every blocked query. | Kills both delivery channels — the HTTPS staging fetch and the DNS TXT fallback. A blocked query is also a free detection: only a compromised host generates one. | MITRE M1037 (Filter Network Traffic), M1031 (Network Intrusion Prevention) | Playbook D, step 4. Then run Q1 and Q2 and confirm any hits now show blocked/NXDOMAIN answers. |
| Import the §10 CSV into Falcon IOC Management. Two native-payload hashes to prevent, five package-file hashes to detect, three lead hosts to no_action. | Stops the two analyzed second-stage binaries from executing anywhere in the fleet, immediately. | MITRE M1040 (Behavior Prevention on Endpoint) | Falcon console → IOC Management → filter tag campaign:FloodingDropper; expect 24 entries. |
| Confirm Falcon prevention policy has Suspicious Process Blocking, Script-Based Execution Monitoring, and Advanced Memory Scanning enabled on developer and CI host groups. | Advanced memory scanning is the only coverage that exists for the reflectively loaded Windows stage 3 (T1620). Nothing in this pack's CQL can see it. | MITRE M1040; CrowdStrike prevention-policy guidance | Prevention policy → compare the developer/CI policy against your standard workstation policy; they are frequently weaker. |
| Force all DNS through internal resolvers with full query logging including QTYPE, and block outbound 53/853/DoH endpoints from endpoints. | The TXT chunk channel is only visible if you log TXT queries. If a developer host can reach a public resolver directly, the entire DNS half of this pack is blind. | MITRE M1037; CIS Controls v8 §4.9, §13.10; NIST SP 800-81 (Secure DNS Deployment) | From a developer host, query any public resolver directly by address (nslookup -type=txt example.com <public-resolver>) — it must fail. Then repeat against the internal resolver and confirm the query appears in its log with QTYPE=TXT. |
| Run the §7.1 dependency sweep across every repo, lockfile, SBOM, npm cache, internal mirror and container image layer. Treat any hit as an incident, not a cleanup ticket. | This is the only control that finds the transitive case. Both primary sources say removing the dependency is necessary and not sufficient. | MITRE M1051 (Update Software); CIS Controls v8 §16.4, §16.6 | Gate G10. Record which repos were swept, so the ones that were not are visible. |
| Control | Why it stops this chain | Authority | Verify |
|---|---|---|---|
Mandatory malware-aware registry proxy. Nexus Firewall, Artifactory Curation, or equivalent, with quarantine-on-unknown. Block direct egress to registry.npmjs.org from CI runners so the proxy cannot be bypassed. | Sonatype identified this campaign through exactly this control surface. A quarantine policy on newly published, low-download packages stops a 48-hour-old slopsquat before a developer ever imports it. | MITRE M1035 (Limit Access to Resource Over Network); CIS Software Supply Chain Security Guide §2 (Source), §3 (Build Pipelines) | From a runner: curl -sI https://registry.npmjs.org/ must fail; npm ci against the proxy must succeed. |
Deny execution from user-writable temp paths. WDAC or AppLocker on Windows (%TEMP%, %LOCALAPPDATA%\Temp); noexec,nosuid,nodev on /tmp and /var/tmp on Linux. | Directly breaks step 7-to-8 of the chain on both platforms. The dropper has no fallback write location — it hardcodes %TEMP% and /var/tmp. | MITRE M1038 (Execution Prevention); CIS Microsoft Windows Benchmark §18 (AppLocker/WDAC); CIS Distribution Independent Linux Benchmark §1.1.2–1.1.5 | Playbooks A and B. Post-deploy, re-run Q7/Q8 and confirm any staged payload now shows a blocked execution rather than a process start. |
| Default-deny egress for CI runners. Allow only the registry proxy, the artifact store, the VCS and the deployment target. Nothing else, including DNS to external resolvers. | A runner that cannot reach *.workers.dev or resolve wel1.ru cannot complete stage 1 no matter what it imports. This is the highest-leverage control for the surface with the best credentials. | MITRE M1030 (Network Segmentation), M1037; CIS Software Supply Chain Security Guide §3.3 | From a runner: curl -m 5 https://example.workers.dev must fail. Re-run Q5 after a week and confirm the runner population has dropped out of the results. |
| Windows attack-surface reduction: enable the ASR rules for "Block executable files from running unless they meet a prevalence, age, or trusted list criterion" and "Block process creations originating from PSExec and WMI commands", and enable RunAsPPL. | The prevalence/age rule is a direct counter to a payload downloaded seconds ago from a Worker. RunAsPPL raises the cost of the credential theft that follows. | MITRE M1038, M1040, M1043 (Credential Access Protection); Microsoft Security Baseline for Windows 11; CIS Microsoft Windows Benchmark §18.9 | Playbook A, step 3. Get-MpPreference | Select-Object -ExpandProperty AttackSurfaceReductionRules_Ids. |
macOS: deploy a configuration profile that inventories and alerts on user LaunchAgents, and baseline ~/Library/LaunchAgents across the fleet. | Apple never ships agents into a user home directory, so a com.apple.* plist there is anomalous by construction — a cheap, durable signal that outlives this campaign's specific filename. | MITRE M1018 (User Account Management), M1047 (Audit); CIS Apple macOS Benchmark §2 and §5 | Playbook C. Run the §7.3 fleet loop and confirm the inventory is empty or fully explained. |
| Rotate to short-lived, workload-scoped credentials in CI (OIDC federation) and remove long-lived npm and cloud tokens from runner environments. | Changes the outcome rather than the odds. Stage 1 landing on a runner that holds only a 15-minute OIDC token is a very different incident from one that holds a publish-scoped npm token. | MITRE M1027 (Password Policies), M1026 (Privileged Account Management); CIS Controls v8 §5.2, §6.5 | Audit runner environment variables and secret stores for any static NPM_TOKEN / cloud key; the target is zero. |
Keep ignore-scripts enforced anyway — npm ci --ignore-scripts in CI and ignore-scripts=true in the org .npmrc. | Does NOT stop this campaign. Listed explicitly so nobody records it as coverage. It remains correct hygiene against the much larger population of lifecycle-hook malware. | MITRE M1042 (Disable or Remove Feature or Program); CIS Software Supply Chain Security Guide §3.2 | npm config get ignore-scripts returns true. Then confirm in your risk register that this control is not credited against WEL1DROPPER. |
| Control | Why it stops this chain | Authority | Verify |
|---|---|---|---|
| Ephemeral, network-segmented CI runners rebuilt from a golden image per job. | Removes persistence as a concept on the surface that matters most. Run key, scheduled task and LaunchAgent all become irrelevant if the host does not survive the job. | MITRE M1030, M1053 (Data Backup) for image integrity; CIS Software Supply Chain Security Guide §3.1 | Confirm runner uptime metrics show single-job lifetimes and that no runner image ships a warm node_modules or a baked lockfile. |
Internal-first namespace policy: scope all first-party packages (@yourorg/) and configure the registry so unscoped names never resolve externally without review. | Attacks the slopsquatting mechanism at the root. A hallucinated bare package name cannot resolve if bare names are not resolvable, and it also closes the dependency-confusion path the predecessor Moika campaign used. | MITRE M1035; CIS Software Supply Chain Security Guide §2.4; npm scoped-package guidance | From a workstation: npm install some-nonexistent-package-name must fail closed against the proxy rather than reaching the public registry. |
| AI-assistant dependency policy: require that any package name suggested by a coding assistant is validated against the internal allowlist before it enters a branch, and make new-dependency additions a blocking review item. | This campaign exists because assistants invent plausible package names and developers install them unchecked. No endpoint control addresses the human step; a review gate does. | ⚠ best-practice, no formal benchmark — MITRE M1017 (User Training) is the closest mapping; CIS Software Supply Chain Security Guide §2 covers the review requirement generically | Sample recent PRs that add a dependency and confirm each carries an explicit reviewer sign-off on the package name. |
| Continuous SBOM diffing with alerting on any new transitive dependency. | The transitive case is how this survives cleanup. A diff on every build turns "we removed the package" into a verifiable claim. | MITRE M1051; CIS Controls v8 §16.4; NIST SP 800-218 (SSDF) PS.3.2, PW.4.1 | Introduce a benign new transitive dependency in a test branch and confirm the pipeline flags it. |
| Treat developer workstations and build agents as tier-0-adjacent: same EDR policy, same logging, same credential hygiene as production. | Both primary sources direct rotation of npm, GitHub, cloud, signing and deployment credentials after a confirmed import. If those credentials sit on hosts with a weaker policy than the systems they control, the policy is inverted. | MITRE M1026, M1018; CIS Controls v8 §4.1, §5.4, §6.8 | Compare the developer/CI Falcon policy, log retention and credential lifetime against the production baseline; there should be no gap. |
Deployable Playbooks
Pilot every playbook on a small ring before fleet deployment. Playbooks A and B change what is allowed to execute; on a developer or build fleet that is a production-availability decision. Every step below carries its prerequisites, reboot requirement and rollback — if a step has no undo, that is stated explicitly and justified.
Playbook A · Windows — deny execution from temp paths and enable ASR
Prerequisites: Windows 10 1903+ or Windows 11; Microsoft Defender ASR available (works alongside Falcon); local admin or a GPO/Intune deployment channel; an AppLocker or WDAC policy already in enforcement or audit mode. Reboot required: no for ASR rules; yes for a new AppLocker policy to take effect reliably (or restart the Application Identity service). Rollback: given per step.
# --- Step 1. Start in AUDIT mode. Never deploy this in enforce first. -----
# Rollback: set the rules back to 0 (Disabled) -- see step 4.
# Block executables that lack prevalence/age/trust (01443614-cd74-433a-b99e-2ecdc07bfc25)
Add-MpPreference -AttackSurfaceReductionRules_Ids 01443614-cd74-433a-b99e-2ecdc07bfc25 `
-AttackSurfaceReductionRules_Actions AuditMode
# Block process creations from PSExec and WMI (d1e49aac-8f56-4280-b9ba-993a6d77406c)
Add-MpPreference -AttackSurfaceReductionRules_Ids d1e49aac-8f56-4280-b9ba-993a6d77406c `
-AttackSurfaceReductionRules_Actions AuditMode
# --- Step 2. Measure the audit population for 7 days ----------------------
# Rollback: none required -- this step only reads.
Get-WinEvent -LogName 'Microsoft-Windows-Windows Defender/Operational' -MaxEvents 5000 |
Where-Object { $_.Id -in 1121,1122 } |
Select-Object TimeCreated, Id, @{n='Path';e={$_.Properties[4].Value}} |
Group-Object Path | Sort-Object Count -Descending | Select-Object -First 40
# --- Step 3. Promote to Enabled once the audit population is understood ---
# Rollback: re-run with -AttackSurfaceReductionRules_Actions AuditMode.
Set-MpPreference -AttackSurfaceReductionRules_Ids 01443614-cd74-433a-b99e-2ecdc07bfc25 `
-AttackSurfaceReductionRules_Actions Enabled
Set-MpPreference -AttackSurfaceReductionRules_Ids d1e49aac-8f56-4280-b9ba-993a6d77406c `
-AttackSurfaceReductionRules_Actions Enabled
# --- Step 4. FULL ROLLBACK for steps 1 and 3 ------------------------------
Set-MpPreference -AttackSurfaceReductionRules_Ids 01443614-cd74-433a-b99e-2ecdc07bfc25 `
-AttackSurfaceReductionRules_Actions Disabled
Set-MpPreference -AttackSurfaceReductionRules_Ids d1e49aac-8f56-4280-b9ba-993a6d77406c `
-AttackSurfaceReductionRules_Actions Disabled
# --- Step 5. Verify -------------------------------------------------------
# Rollback: none required -- read-only.
$p = Get-MpPreference
for ($i = 0; $i -lt $p.AttackSurfaceReductionRules_Ids.Count; $i++) {
[pscustomobject]@{ Rule = $p.AttackSurfaceReductionRules_Ids[$i]
Action = $p.AttackSurfaceReductionRules_Actions[$i] }
}
AppLocker deny rule for the staged-payload path. Prerequisites: Application Identity service running; an existing AppLocker policy (this MERGES into it — never deploy as a standalone policy or you will replace the fleet's rule set). Reboot required: recommended, or restart AppIDSvc. Rollback: given inline.
<!-- Save as deny-temp-exec.xml. Deny rules win over allow rules in AppLocker. -->
<AppLockerPolicy Version="1">
<RuleCollection Type="Exe" EnforcementMode="AuditOnly">
<FilePathRule Id="7f3b1d20-0c4e-4a11-9f2b-5c8d61a04e77"
Name="DENY execute from user TEMP (Flooding Dropper)"
Description="WEL1DROPPER stages %TEMP%\dotnet_diag_<8hex>.exe"
UserOrGroupSid="S-1-1-0" Action="Deny">
<Conditions><FilePathCondition Path="%OSDRIVE%\Users\*\AppData\Local\Temp\*.exe" /></Conditions>
</FilePathRule>
</RuleCollection>
</AppLockerPolicy>
# Deploy (MERGE, do not replace):
Set-AppLockerPolicy -XmlPolicy .\deny-temp-exec.xml -Merge
Restart-Service AppIDSvc
# Promote to enforcement only after reviewing audit events 8003/8004:
# change EnforcementMode="AuditOnly" to "Enabled" and re-merge.
# ROLLBACK -- remove just this rule, leaving the rest of the policy intact:
$pol = Get-AppLockerPolicy -Effective -Xml
$xml = [xml]$pol
$node = $xml.SelectSingleNode("//FilePathRule[@Id='7f3b1d20-0c4e-4a11-9f2b-5c8d61a04e77']")
if ($node) { $node.ParentNode.RemoveChild($node) | Out-Null }
$xml.Save("$env:TEMP\applocker-rollback.xml")
Set-AppLockerPolicy -XmlPolicy "$env:TEMP\applocker-rollback.xml"
Restart-Service AppIDSvc
# If the policy came from GPO, remove the rule at the GPO instead -- a local
# rollback will be overwritten at the next policy refresh.
Playbook B · Linux — noexec on /tmp and /var/tmp
Prerequisites: root; systemd-based distribution; confirm no build tooling executes from these paths first — some package managers, installers and CI caches legitimately do, and this change will break them. Run the pre-flight check in step 1 for a full week before proceeding. Reboot required: no if you use mount -o remount; the fstab entry makes it survive reboot. Rollback: step 5, effective immediately without reboot.
# --- Step 1. PRE-FLIGHT: does anything legitimately execute from /tmp? ---- # Run for a week before changing anything. Rollback: none required, read-only. sudo auditctl -w /tmp -p x -k tmp_exec_probe sudo auditctl -w /var/tmp -p x -k tmp_exec_probe sudo ausearch -k tmp_exec_probe --start week-ago -i 2>/dev/null | grep -oP 'exe="\K[^"]+' | sort | uniq -c | sort -rn # Remove the probe when finished: sudo auditctl -W /tmp -p x -k tmp_exec_probe # --- Step 2. Back up fstab (this IS the rollback artifact) ---------------- sudo cp -a /etc/fstab "/etc/fstab.bak.$(date +%Y%m%d)" # --- Step 3. Apply. If /tmp is already a separate mount, add the options. # If it is not, create a tmpfs mount for it. grep -qE '^\S+\s+/tmp\s' /etc/fstab \ && sudo sed -i -E 's|^(\S+\s+/tmp\s+\S+\s+)([^ ]+)|\1\2,noexec,nosuid,nodev|' /etc/fstab \ || echo 'tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,size=2G 0 0' | sudo tee -a /etc/fstab grep -qE '^\S+\s+/var/tmp\s' /etc/fstab \ && sudo sed -i -E 's|^(\S+\s+/var/tmp\s+\S+\s+)([^ ]+)|\1\2,noexec,nosuid,nodev|' /etc/fstab \ || echo '/tmp /var/tmp none rw,noexec,nosuid,nodev,bind 0 0' | sudo tee -a /etc/fstab sudo mount -o remount /tmp && sudo mount -o remount /var/tmp sudo systemctl daemon-reload # --- Step 4. Verify -- both must show noexec, and the test must fail ------ findmnt -no TARGET,OPTIONS /tmp /var/tmp printf '#!/bin/sh\necho reached\n' > /tmp/.exec_test && chmod +x /tmp/.exec_test /tmp/.exec_test 2>&1 | grep -q 'Permission denied' && echo "PASS: noexec enforced" || echo "FAIL: still executable" rm -f /tmp/.exec_test # --- Step 5. ROLLBACK -- immediate, no reboot ----------------------------- sudo cp -a "/etc/fstab.bak.$(date +%Y%m%d)" /etc/fstab sudo mount -o remount,exec /tmp && sudo mount -o remount,exec /var/tmp sudo systemctl daemon-reload findmnt -no TARGET,OPTIONS /tmp /var/tmp # confirm noexec is gone
Playbook C · macOS — LaunchAgent baseline and campaign artifact removal
Prerequisites: admin on the target Mac; Full Disk Access for the shell if run under MDM. Do not run the removal half until the host has been isolated and triage evidence is captured — deleting the plist destroys the timeline. Reboot required: no; launchctl bootout takes effect immediately. Rollback: the inventory step is read-only; the removal step is deliberately irreversible and is preceded by an evidence copy.
# --- Step 1. INVENTORY (read-only, safe to run fleet-wide) ----------------
# Rollback: none required.
for u in /Users/*; do
[ -d "$u/Library/LaunchAgents" ] || continue
for p in "$u/Library/LaunchAgents/"*.plist; do
[ -e "$p" ] || continue
lbl=$(/usr/libexec/PlistBuddy -c 'Print :Label' "$p" 2>/dev/null)
prg=$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:0' "$p" 2>/dev/null)
case "$lbl" in com.apple.*) flag="APPLE-MASQUERADE" ;; *) flag="third-party" ;; esac
printf '%s\t%s\t%s\t%s\n' "$flag" "$u" "$lbl" "$prg"
done
done | sort
# Apple ships agents from /System/Library and /Library ONLY. Any APPLE-MASQUERADE
# row is anomalous and must be explained before it is dismissed.
# --- Step 2. CAPTURE EVIDENCE before touching anything --------------------
# Rollback: none required -- this only copies.
EV="$HOME/fd-evidence-$(date +%Y%m%d%H%M%S)"; mkdir -p "$EV"
cp -a ~/Library/LaunchAgents/com.apple.windowserver.helper.plist "$EV/" 2>/dev/null
cp -a ~/.local/share/runtime "$EV/runtime" 2>/dev/null
shasum -a 256 "$EV/runtime/com.apple.runtime" 2>/dev/null | tee "$EV/hashes.txt"
ls -la@ ~/.local/share/runtime/ > "$EV/listing.txt" 2>/dev/null
# --- Step 3. REMOVE (only after isolation and step 2) ---------------------
# Rollback: NOT reversible by design -- this is eradication, and re-creating
# malware persistence is never a desired undo. The evidence copy from step 2
# is the recovery path if the artifact turns out to be benign.
launchctl bootout "gui/$(id -u)/com.apple.windowserver.helper" 2>/dev/null
launchctl unload -w ~/Library/LaunchAgents/com.apple.windowserver.helper.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/com.apple.windowserver.helper.plist
rm -rf ~/.local/share/runtime
rm -f /var/tmp/.cache_???????? /tmp/.analytics_state
# --- Step 4. Verify -------------------------------------------------------
# Rollback: none required -- read-only.
launchctl list 2>/dev/null | grep -i 'windowserver.helper' && echo "FAIL: still loaded" || echo "PASS: agent gone"
ls ~/.local/share/runtime 2>/dev/null && echo "FAIL: runtime dir remains" || echo "PASS: runtime dir gone"
Removal is not remediation. A macOS host that reached this stage may already have fetched beacon_mac.bin through one of the five proxy hosts, and the final beacon was never available for analysis — so nobody knows what it did. Rebuild the host and rotate its credentials per §14; do not return a cleaned Mac to service on the strength of step 4 passing.
Playbook D · Network — deploy the campaign blocklist
Prerequisites: admin on the DNS/RPZ platform and the egress proxy or firewall; a change window if your DNS platform reloads zones disruptively. Reboot required: no. Rollback: step 5 — remove the zone/category and reload; effective in seconds.
# --- Step 1. Record current state (this is the rollback baseline) --------- # Rollback: none required -- read-only. cp -a /etc/bind/db.rpz "/etc/bind/db.rpz.bak.$(date +%Y%m%d)" 2>/dev/null || true # --- Step 2. RPZ entries. Wildcard the apex -- the macOS loader inserts a # session label under dl.wel1.ru that no report enumerated. # Rollback: delete these lines and reload (step 5). wel1.ru CNAME . *.wel1.ru CNAME . oob-worker.cf103-070.workers.dev CNAME . oob-worker.cf102-baf.workers.dev CNAME . oob-worker.cf99-9b3.workers.dev CNAME . package-proxy.cf5oobworker.workers.dev CNAME . package-proxy.cf6oobworker.workers.dev CNAME . package-proxy.cf7oobworker.workers.dev CNAME . package-proxy.cf8oobworker.workers.dev CNAME . package-proxy.cf11oobworker.workers.dev CNAME . # --- Step 3. Do NOT wildcard *.workers.dev --------------------------------- # It hosts a large volume of legitimate SaaS. Block the ten names above only, # and use Q5 to review new workers.dev hosts on their own merits. # --- Step 4. Reload and verify -------------------------------------------- rndc reload dig +short txt c.sdk.dl.wel1.ru # must return NXDOMAIN / empty dig +short oob-worker.cf99-9b3.workers.dev # must return NXDOMAIN / empty # Confirm the block is LOGGED, not just enforced -- a blocked query identifies # a compromised host, which is worth more than the block itself. # --- Step 5. ROLLBACK ------------------------------------------------------ cp -a "/etc/bind/db.rpz.bak.$(date +%Y%m%d)" /etc/bind/db.rpz rndc reload dig +short oob-worker.cf99-9b3.workers.dev # resolution restored # --- Note on the three tcsbank / cloudpayments hosts ----------------------- # Do NOT add nexus.tcsbank.ru, repo-linux.tcsbank.ru or # alertmanager.cloudpayments.ru to this list. The primary analysis states that # static evidence does not establish whether they are attacker-owned, # compromised, or decoys. Monitor them; blocking them risks breaking traffic to # legitimate financial services and proves nothing either way.
Containment Runbook
Standing assumption: both primary sources direct that a confirmed import be treated as a host compromise. The final Windows and macOS beacons were never available for analysis, so nobody can tell you what the second stage did. Do not scope this incident to what your telemetry happened to capture.
Phase 1 · Isolate (0 to 1 hour)
- Network-contain every host with a Q1, Q2, Q7, Q8 or Q11 hit via Falcon Host Containment. Contain, do not power off — the Windows stage 3 runs in memory and a shutdown destroys the only place it exists.
- Freeze the affected CI runners. Pause the runner pool rather than deleting instances; an ephemeral runner that is auto-recycled takes the evidence with it. If the pool has already recycled, the job logs are your only artifact — preserve them now (§7.6).
- Capture volatile state before anything else. Running process list with full command lines, network connections, the contents of
%TEMP%//var/tmp, the marker file timestamps (they date the first execution), and on Windows a memory image if your process supports it. - Suspend, do not yet rotate, the credentials reachable from the host. Rotating before eradication hands the new secret to a still-resident implant. Suspend npm publish rights, disable the GitHub token, and revoke active cloud sessions.
- Deploy the §10 blocklist fleet-wide immediately (Playbook D) even if only one host is confirmed. This is the cheapest action available and it converts every remaining infected host into a logged block event.
Phase 2 · Scope (1 to 8 hours)
- Run the full query set in the order given in §8 across the entire estate, full retention window, not just the suspected host group.
- Run the §7.1 dependency sweep everywhere — every repo, lockfile, SBOM, npm cache, internal mirror and container image layer. Gate G10. This is where the incident is either bounded or found to be much larger.
- Pull the authoritative package list from OpenSourceMalware and Sonatype
sonatype-2026-005660, then re-run the sweep against the full list rather than the four names in this pack. - Query the registry proxy for every request matching the campaign list, including 404s. A 404 tells you a developer or a build tried to resolve a slopsquat name, which is a training and policy finding even where no package existed.
- Enumerate the credential blast radius per affected host: npm tokens, GitHub App and PAT scopes, cloud role trust policies, signing keys, deployment secrets, SSH keys, and any
.envor secret file on disk. That list is the Phase 4 rotation scope. - Check published artifacts. If an affected runner held publish rights, review everything it published since the first marker-file timestamp. A compromised build agent is a supply-chain incident for your own consumers.
Phase 3 · Eradicate (8 to 48 hours)
- Rebuild, do not clean. Windows stage 3 is loaded reflectively and stage 2 patches ETW and AMSI. A host where those ran cannot be certified clean by scanning it — the instrumentation you would scan with is the instrumentation that was patched. Reimage from known-good.
- Where a rebuild is genuinely impossible, remove persistence explicitly and document the accepted risk: Windows Run key value and scheduled task (§7.4), the AppData self-copy,
%TEMP%\dotnet_diag_*andanalytics_state; macOS the LaunchAgent,~/.local/share/runtimeand/var/tmp/.cache_*(Playbook C); Linux/var/tmp/.cache_*and/tmp/.analytics_state. - Purge the dependency at every layer: remove it from
package.jsonand the lockfile, clear the npm cache, evict it from the internal mirror, rebuild any container image that included it, and rebuild any runner image with a warmnode_modules. - Rebuild affected CI runner images from source and confirm the new image resolves dependencies only through the malware-aware proxy.
- Verify eradication with the pack itself: re-run Q1, Q2, Q7, Q8, Q9, Q10 and Q11 scoped to the rebuilt hosts and confirm zero results across a full week.
Phase 4 · Recover (48 hours onward)
- Now rotate every credential identified in Phase 2 step 5 — after eradication, not before. npm tokens, GitHub tokens and App keys, cloud credentials, signing keys, deployment secrets, and any SSH key present on an affected host.
- Invalidate active sessions for the affected users across the registry, VCS and cloud consoles. A rotated secret does not end a session that is already open.
- Re-issue signing keys and re-sign artifacts if a host with signing material was affected. This is slow and unpleasant, and it is the step most often skipped.
- Return hosts to service only after a clean rebuild, a clean week of the query set, and confirmation that the immediate-tier hardening from §12 is deployed on that host.
- Close the loop: submit any new sample hash and any new staging host discovered via Q3 or Q5 to OpenSourceMalware and your registry vendor. Payload polymorphism and account churn mean the community list is only as good as what defenders feed back into it.
- Write up the dependency-introduction path. Which developer or which assistant suggested the package, whether review caught it, and what would have stopped it. That finding drives the strategic tier in §12 — every other control in this pack is downstream of that one decision.
Detection Coverage Map
Honest coverage, step by step. Four steps of the chain have no CQL coverage at all and are marked as such — a green row for every step would be a nicer picture and a worse pack.
| Chain step | Coverage | Queries | Notes |
|---|---|---|---|
| 1 · Slopsquat package enters the dependency graph | PARTIAL | Q13, §7.1 | Only 4 of roughly 800–850 names are public. Transitive dependencies never appear on a command line, so endpoint coverage alone cannot answer this — §7.1 is the real control and G10 is the gate. |
2 · require() detonates stage 1 | NONE | — | A module import inside a running Node process produces no distinct telemetry. Detected only by inference from steps 5 onward. Not fixable with better queries. |
| 3 · Kill-switch / rate-limit marker | PARTIAL | §7.2 | A plain non-executable file, so file-write events frequently miss it. Best found by sweeping the disk. Valuable because its timestamp dates the first execution. |
| 4 · OS + architecture fingerprint | NONE | — | In-process logic. No telemetry exists. Harmless as a gap — every following step is covered. |
| 5 · HTTPS payload fetch from Workers hosts | GOOD | Q2, Q5, §7.5 | Q2 covers the eight known hosts with near-zero FP. Q5 is the discovery path for hosts nine and onward but needs a per-tenant allowlist first. |
| 6 · DNS TXT chunked payload channel | STRONG | Q1, Q3, Q4 | The best-covered step. Q1 is atomic and dies on domain rotation; Q3 targets the protocol shape and survives it; Q4 catches the volume signature even if the labels change. Depends on gate G1 and G3 passing. |
| 7 · Payload written to a temp directory | STRONG | Q7, Q8, Q12 | Q7/Q8 are exact-name and near-zero FP. Q12 is the name-independent version that survives a rename. Between them this step is well covered on all three platforms. |
| 8 · Detached execution | GOOD | Q6, Q7, Q8 | Q6 scores rather than filters, which is what makes it usable on a developer fleet where Node shells out constantly. Score 3 is the campaign's exact signature. |
| 9a · Windows ETW / AMSI patching | PARTIAL | Q14 (proxy), IOA-3 | No discrete CQL-queryable event exists for an in-process AMSI or ETW patch. Q14 sees the surrounding process tree only. Real coverage is Falcon's in-product tamper detection — confirm it is enabled rather than assuming this row is green. |
| 9b · Windows Run key + scheduled task persistence | GOOD | Q9, Q10 | Both carry a real benign population from auto-updaters, which is why each is tiered into campaign-filename and generic-user-writable-path. Correlating Q9 and Q10 on the same aid is the strongest discriminator available. |
| 9c · Windows stage 3 reflective in-memory load | NONE | IOA-4 note only | The most consequential gap in the pack. The encrypted update_win.exe is decrypted and executed in memory, so there is no file write and no conventional process start to key on. Coverage depends entirely on Falcon Advanced Memory Scanning. This is why §14 treats a confirmed stage-1 execution as compromise regardless of stage-3 telemetry. |
| 10a · macOS anti-analysis checks | PARTIAL | — | File-existence probes for lldb, frida, VMware artifacts and a hw.memsize query are not reliably logged. Low value to chase — step 10b covers the same infection with far better fidelity. |
| 10b · macOS LaunchAgent persistence | STRONG | Q11, §7.3 | The Apple-masquerade anchor makes this durable well beyond this campaign's specific filename. |
| 10c · macOS stage-3 beacon fetch | GOOD | Q2, Q3 | Five proxy hosts are covered atomically by Q2; the DNS fallback by Q3. The port is unknown, so no port-based detection is possible or shipped. |
| 11 · Linux Sliver staging | PARTIAL | Q8, Q10, Q2 | The staging behavior is covered. The Sliver end state is not confirmed by the primary source, so no Sliver protocol detection is shipped — inventing one on secondary reporting would be a fabricated indicator. |
| 12 · Credential exposure | OUT OF SCOPE | §14 | No EDR coverage. Identity, registry and cloud audit logs own this, and §14 Phase 2 step 5 defines the enumeration. |
Summary
- 14 CQL queries — 6 high-confidence / low-FP, 6 medium with mandatory tuning, 2 discovery queries requiring a per-tenant allowlist before use.
- 4 Custom IOA recommendations, 2 of which are block-capable after a 48-hour monitor period.
- 24 atomic indicators in the Falcon import CSV — 14 blockable domains, 3 monitor-only leads, 7 hashes. All 24 trace to a saved source snapshot.
- 3 steps with no coverage (2, 4, 9c) and 5 partial (1, 3, 9a, 10a, 11). Steps 2 and 4 are unfixable by design. Step 9c is the one that should change your response posture, not your query set.
What would improve coverage most, in order
- Sensor coverage on ephemeral CI runners and container build stages. Every query in this pack is worthless on a host with no sensor, and that is precisely where this campaign lands with the best credentials available.
- Confirm Falcon Advanced Memory Scanning is enabled on developer and CI host groups. It is the only coverage for step 9c.
- Complete DNS telemetry from macOS and Linux sensors (gate G1). Half this pack's strongest detections are DNS-based.
- A malware-aware registry proxy. It moves detection left of everything here — Sonatype found this campaign that way.
Hunt Summary Ticket
HUNT: Flooding Dropper / WEL1DROPPER npm slopsquatting campaign
VERSION: v1.0 · 2026-08-08
PRIORITY: P1 -- Critical
TRACKING: sonatype-2026-005660 · CWE-506 · CVSS 8.7
DISCLOSED: 2026-08-05 (Sonatype) / 2026-08-06 (OpenSourceMalware) / 2026-08-07 (press)
SUMMARY
Roughly 800-850 AI-slopsquatted npm packages deliver WEL1DROPPER, a
cross-platform downloader staging OS-specific RAT/infostealer payloads for
Windows, macOS and Linux. The packages carry NO preinstall/postinstall hook --
the README instructs the developer to require() the package, and that import
detonates the chain. Install-time scanners and ignore-scripts controls do not
see it. Primary HTTPS delivery from three Cloudflare Workers hosts, with a DNS
TXT chunked-payload fallback under wel1[.]ru that survives an HTTPS blocklist.
SCOPE
Developer workstations AND CI runners / build agents, equally. Also: container
build stages, internal npm mirrors and caches, and any runner image that bakes
a lockfile or a warm node_modules.
HYPOTHESES: 11 (see section 3)
QUERIES: 14 CQL + 3 pivots (section 8)
IOAs: 4 recommended, 2 block-capable after 48h monitor (section 9)
ATOMIC IOCS: 24 in the Falcon import CSV -- 14 blockable domains,
3 monitor-only leads, 7 SHA-256 hashes (section 10)
GATES: 10 validation gates (section 11). G1, G3 and G10 are the load-bearing ones.
RUN ORDER
1. Q1, Q2 -- campaign infrastructure, full retention, near-zero FP
2. Q7, Q8, Q11 -- staged payload and persistence artifacts, exact names
3. Section 7.1 dependency sweep -- the only way to find the transitive case
4. Q3, Q4, Q6, Q12, Q14 -- behavioral, needs a baseline to read
5. Q5, Q13 -- discovery, build the allowlist before treating as alerts
ESCALATE IMMEDIATELY ON
Any Q1 or Q2 hit -> confirmed contact with campaign infra
Any Q7 or Q8 hit -> confirmed payload staged and executed
Q11 confidence=campaign-exact -> confirmed macOS persistence
Q9/Q10 match_kind=campaign-filename -> confirmed Windows persistence
Q13 match_tier=named-sample -> package resolved; check 7.2 for import
KNOWN GAPS (do not report these as clean)
- require() itself and the OS fingerprint produce no telemetry (steps 2, 4)
- Windows stage 3 is loaded reflectively in memory -- NO CQL coverage (step 9c);
depends entirely on Falcon Advanced Memory Scanning
- No confirmed C2 protocol, beacon interval or exfil destination exists in any
source, so this pack ships none. Absence of an exfil IOC is not absence of exfil.
- Sliver is reported by secondary sources and explicitly NOT confirmed by the
primary analysis; no Sliver protocol detection is shipped
- Payload polymorphism: the 7 hashes are single-sample facts, not coverage
STANDING ASSUMPTION
A confirmed import is a host compromise. Rebuild, do not clean -- stage 2
patches ETW and AMSI, so the instrumentation you would scan with is the
instrumentation that was tampered with. Rotate npm, GitHub, cloud, signing and
deployment credentials AFTER eradication, never before.
OWNER: ____________________ OPENED: 2026-08-08 DUE: ____________________
Changelog
bigops / bnpl / 35.x.y heuristics. Re-assess the three tcsbank / cloudpayments lead hosts by 2026-09-08. Update this pack if the final Windows or macOS beacon is recovered and analyzed, which would supply the C2 and exfiltration indicators this version deliberately omits, or if the Sliver attribution for the Linux final stage is confirmed by the primary analyst.
References
Sources fetched 2026-08-08 and saved verbatim to FloodingDropper-WEL1DROPPER-npm-Hunt-sources/. Snapshot numbers match the file names in that directory.
- [01 · PRIMARY] OpenSourceMalware — "Russian AI Slopsquatting Publishes 700+ Malicious NPM Packages", Paul McCarty, 2026-08-06. opensourcemalware.com/blog/russian-ai-slopsquatting-npm-campaign — full reverse-engineering writeup and the origin of every indicator in this pack.
- [03 · PRIMARY] Sonatype Research Team — "'Flooding Dropper' Campaign Hits npm With Nearly 850 Malicious Packages", 2026-08-05. sonatype.com/blog/flooding-dropper-hits-npm-with-850-malicious-packages — campaign scale, advisory
sonatype-2026-005660, and the Windows stage-2 analysis. - [02 · TIER 2] The Hacker News — "Nearly 800 Malicious npm Packages Deliver Cross-Platform RAT and Infostealer", Ravie Lakshmanan, 2026-08-07. thehackernews.com/2026/08/nearly-800-malicious-npm-packages.html — independent corroboration of hosts, payload domains and the WEL1DROPPER name.
- [05 · TIER 2] DevOps.com — "'Flooding Dropper' Is Hitting npm With a Tidal Wave of Malicious Packages", Jeff Burt, 2026-08-07. devops.com/flooding-dropper-is-hitting-npm-with-a-tidal-wave-of-malicious-packages — package naming, campaign scale and registry-moderation context.
- [06 · TIER 2] CyberPress — "Russian AI Slopsquatting Campaign Floods npm With Malicious Packages Targeting Developers". cyberpress.org/russian-ai-slopsquatting-campaign-floods-npm — growth trend and shared Moika tradecraft.
- [04 · NOT CITED] CyberTechWorld aggregator repost of the THN article. Snapshot retained for completeness; truncated syndication with no independent technical content and no indicators. Nothing in this pack derives from it.
Framework references
- MITRE ATT&CK Enterprise — techniques and mitigations cited throughout §6 and §12. attack.mitre.org
- CIS Software Supply Chain Security Guide — registry proxy, build-pipeline and dependency-review controls (§12).
- CIS Microsoft Windows Benchmark §18, and the Microsoft Security Baseline for Windows 11 — AppLocker/WDAC and ASR controls (§12, Playbook A).
- CIS Distribution Independent Linux Benchmark §1.1.2–1.1.5 —
noexec,nosuid,nodevon/tmpand/var/tmp(Playbook B). - CIS Apple macOS Benchmark §2 and §5 — LaunchAgent and user-directory controls (Playbook C).
- CIS Controls v8 §4, §5, §6, §13, §16 — DNS control, credential hygiene and application-software security.
- NIST SP 800-218 (SSDF) and NIST SP 800-81 — SBOM diffing and secure DNS deployment (§12).
Provenance note. Every atomic indicator shipped in §4 and §10 was mechanically traced back to the saved text of the sources above. Indicators that exist in reporting but could not be traced — a C2 protocol, a beacon interval, an exfiltration endpoint, the package-proxy port — are absent from this pack by design rather than by oversight, and are listed as intel gaps in §2 and in the JSON appendix.