DirtyBlanket: Fake Express npm Packages and a Self-Spreading Linux Worm
Executive Summary
On 2026-09-29 the npm account dirtyblanket published nine packages in 33 minutes: eight impersonate Express and one impersonates React. All nine carry the same preinstall hook, which pipes a script fetched through the Internet Archive Wayback Machine from a Codeberg repository into node. On Linux that script fetches linux.sh from Codeberg and runs it with bash. linux.sh is a self-spreading worm.
The worm installs the open-source CHAOS remote access tool as a fake systemd font service, so the operator gets a shell, file access and screenshots over a Tor hidden service. It then uses every readable SSH private key to log in to hosts in known_hosts and rerun itself, edits Arch User Repository packages those keys can push to, and republishes npm projects it finds on disk using any .npmrc publish token. Each new host, AUR package or npm version restarts the chain.
Highest-value defensive angle. Stage one runs on every operating system, because only the second stage checks for Linux. A command line naming hellscripter (Q1) or an install of one of the nine package names (Q2) therefore finds exposed macOS and Windows developer machines as well as Linux. On Linux hosts the fake unit and binary names (Q4, Q5, Q6) are specific to this campaign and give low-noise detection after the fact.
Defender priority. Treat any Linux host or CI runner that installed one of the nine packages as fully compromised, as SafeDep advises. Rotate every SSH key, npm token and secret readable by the installing user, and check the npm and AUR packages you maintain for versions you did not publish.
Commodity versus campaign. CHAOS is a public open-source tool. Codeberg, the Wayback Machine and the Tor Expert Bundle are legitimate services and software. They appear here as hunt and scoping signals only. Nothing in this pack blocks codeberg.org or web.archive.org wholesale, and nothing that is commodity ships at prevent or critical severity.
Source Review & Web Hunter Notes
| Tier | Source | Key finding | Carry forward |
|---|---|---|---|
| T1 | SafeDep, DirtyBlanket: Fake Express Packages on npm Spread a Linux Worm (2026-09-29) | Full analysis: nine packages, preinstall hook, node.js and linux.sh behavior, root and non-root persistence, SSH/AUR/npm propagation, CHAOS binary analysis, onion C2, hashes and IOC table | yes. Snapshot 01-safedep-dirtyblanket.txt |
| T2 | OSV / OpenSSF malicious-packages records MAL-2026-17241 to 17248 and 17294 | Per-package malicious-code records. Includes an Amazon Inspector finding that independently reports the preinstall hook and the hellscripter URL, and lists later versions (for example 5.2.2) for several names | yes. Snapshot 04-osv-mal-records.txt |
| T2 | OpenSSF malicious-packages PR #1579 (merged 2026-10-02) | Adds SafeDep as finder and the worm details to the nine package reports. The page names the nine packages and the indicator categories but does not display the hash values | partial. Snapshot 03-ossf-advisory.txt |
| T2 | The Hacker News, ThreatsDay bulletin (2026-10-08) | Secondary report of the nine-package cluster, the dirtyblanket account, the 33-minute window and the worm behavior. Quotes SafeDep | partial. Snapshot 02-thn-threatsday.txt |
Decisions
- Independence. The worm analysis, the hashes, the onion address and the persistence paths come from one origin, SafeDep. The OpenSSF PR and The Hacker News repeat SafeDep. Only the package names and the preinstall hook with the hellscripter URL have a second, independent reporter (Amazon Inspector, through the OSV records). No indicator in this pack therefore ships at prevent or critical severity, and the Falcon import CSV contains detect rows only.
- Versions. SafeDep analysed version 5.2.1 (and react-nodejs 19.3.0). OSV lists later versions for several names, so hunts match package names, not versions.
- Snapshots. The text of all four sources was saved verbatim to
DirtyBlanket-npm-LinuxWorm-Hunt-sources/and every atomic indicator is traced against them by the provenance gate. The SafeDep page shows per-versiondist.shasumvalues, but its text hides the package-name column behind an email-protection placeholder, so the values are not mapped to packages here. Look them up at the source if you need them. - Naming. The pack uses only names the sources use: DirtyBlanket (the SafeDep title), the dirtyblanket npm account, the hellscripter Codeberg account, CHAOS,
systemd-fontd. - Untrusted content. No fetched page contained instructions addressed to an automated reader.
Hunt Brief & Attack Chain
Hunt hypotheses (highest fidelity first)
| # | Hypothesis | MITRE | Falcon events | Expected FP | Conf |
|---|---|---|---|---|---|
| H1 | A host ran the preinstall hook or a pushed copy of the worm: a process command line names the hellscripter account. | T1195.002, T1105 | ProcessRollup2 | Analysts quoting the string | HIGH |
| H2 | A developer or CI host installed one of the nine package names. | T1195.002, T1204.005 | ProcessRollup2 | Tools that pass a name without installing | HIGH |
| H3 | A Linux host runs, or wrote to disk, a fake systemd font binary or the bundled Tor. | T1036.005, T1543.002 | ProcessRollup2, ELFFileWritten | None known | HIGH |
| H4 | systemctl or chattr touched the fake font units or binaries. | T1543.002, T1222.002 | ProcessRollup2 | None known | HIGH |
| H5 | The known-bad CHAOS client hash executed. | T1219 | ProcessRollup2 | None known | MEDIUM |
| H6 | A non-Tor process talks to the local Tor SOCKS port. | T1090.003 | NetworkConnectIP4 | Tor-aware tools | MEDIUM |
| H7 | The worm is spreading: SSH fan-out, AUR list-repos, npm preinstall injection, npm publish bursts. | T1021.004, T1195.002, T1552.001 | ProcessRollup2 | Automation and release pipelines | MEDIUM |
| H8 | Hosts that should not reach Codeberg or the Wayback Machine do (scoping only). | T1102 | DnsRequest | Ubiquitous developer use | LOW |
Attack chain
| Step | Behavior | Telemetry | Hunt angle |
|---|---|---|---|
| 1 | npm install runs the preinstall hook: curl fetches node.js through web.archive.org from the hellscripter repository and pipes it to node. | Process, command line | Q1, Q2 |
| 2 | node.js checks for Linux, then runs curl of linux.sh from Codeberg piped into bash. Errors and output are discarded. | Process, command line | Q1 |
| 3 | linux.sh backgrounds itself. As root it installs tor, openssh, git, npm and build tools. | Process | Q7 |
| 4 | Root: writes /usr/lib/systemd/systemd-fontrenderd and a "Font Rendering Service" unit, enables it, sets chattr +i. Non-root: unpacks a Tor Expert Bundle and writes two user units, "Font Rendering Service" and "Font Caching Service". | Process, ELFFileWritten, file audit | Q3, Q4, Q5, Q6; unit-file write is a gap (auditd) |
| 5 | The CHAOS client sends health, device and WebSocket traffic to the onion service through the local Tor SOCKS proxy. | Loopback connect; onion traffic is not visible | Q8; payload traffic is a gap |
| 6 | SSH spread: reads every readable private key and every known_hosts, runs ssh -o BatchMode=yes to each host and runs the worm there. | Process, sshd auth logs | Q10, Q1 on the target |
| 7 | AUR spread: ssh aur@... list-repos, clone, bump pkgrel, append a bash <(curl ...) line to the .install file, push. | Process | Q11, Q1 |
| 8 | npm spread: add curl ... | node to preinstall, npm version patch, npm publish once per .npmrc, then restore the local package.json. | Process, registry history | Q12, Q13, native hunt N5 |
Consolidated IOC Table
| Type | Value | Conf | Action | Source snapshot | Context |
|---|---|---|---|---|---|
| npm account | dirtyblanket | HIGH | detect | 01, 04 | Publisher of all nine packages, 2026-09-29 06:05 to 06:38 UTC |
| npm package | xeprews 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express |
| npm package | express-javascript 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express |
| npm package | express-nodejs 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express |
| npm package | react-nodejs 19.3.0 | HIGH | detect | 01, 03, 04 | Copies React |
| npm package | exprdd 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express |
| npm package | exprrdd 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express |
| npm package | exptrdd 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express |
| npm package | exptred 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express |
| npm package | exptredd 5.2.1 | HIGH | detect | 01, 03, 04 | Copies Express. OSV also lists 5.2.3 and 5.2.4; match by name |
| Codeberg account | hellscripter | HIGH | detect | 01, 04 | Owner of the install-scripts repository. Account name and repository path are campaign-specific; the host codeberg.org is not |
| Repository path | codeberg.org/hellscripter/install-scripts (node.js, linux.sh, systemd-fontd on raw/branch/main) | HIGH | hunt | 01, 04 | Fetched directly and through web.archive.org. Hunt on the command line, do not block the hosts |
| SHA-256 | 65f0a95b24e30305146346cbc2452cfabadab9e1ca35053e1134b67c38919577 | MEDIUM | detect | 01, 04 | linux.sh (227 lines) |
| SHA-256 | 2c9dbc14809f1e1aebda114194368b002acf74c8760b88fc101f625d179793c2 | MEDIUM | detect | 01 | systemd-fontd, Go CHAOS client, 7.6 MB, Linux x86-64 |
| Tor onion | s5n2uyo6gb6dhirsm5pihwohi6e7ayrwojx4xjow4cqabmbowpezenid.onion:80 | MEDIUM | detect | 01 | CHAOS server. Paths /health, /device, /client |
| File path (root) | /usr/lib/systemd/systemd-fontrenderd, /etc/systemd/system/systemd-fontrenderd.service, /etc/systemd/system/multi-user.target.wants/systemd-fontrenderd.service | MEDIUM | detect | 01 | All three set immutable with chattr +i |
| File path (user) | ~/.config/systemd/systemd-fontcached, ~/.config/systemd/user/systemd-fontrenderd.service, ~/.config/systemd/user/systemd-fontcached.service, ~/.config/systemd/systemd-fontrenderd/ | MEDIUM | detect | 01 | Backdoor, two user units, Tor Expert Bundle directory |
| Unit description | "Font Rendering Service", "Font Caching Service" | MEDIUM | detect | 01, 04 | Cover descriptions for the Tor and CHAOS units |
| Go module | github.com/tiagorlampert/CHAOS/client | LOW | hunt | 01 | Public open-source CHAOS client. Reused by unrelated actors: never a campaign identifier |
| Tor bundle | Tor Expert Bundle 15.0.23, dist.torproject.org | LOW | hunt | 01 | Legitimate software; downloaded by the non-root path. Hunt only |
| Worm artifact | /tmp/log on SSH-reached hosts | LOW | hunt | 01 | Output file of the remote run; generic name |
Affected Surface & Telemetry Matrix
| Surface | Telemetry | Priority | Gap risk |
|---|---|---|---|
| Linux developer workstations, servers, CI runners (Falcon Linux sensor) | ProcessRollup2, ELFFileWritten, NetworkConnectIP4, DnsRequest | HIGH | Unit-file writes and private-key file reads have no Falcon event used here. Use auditd. Onion traffic content is invisible. |
| macOS and Windows developer machines | ProcessRollup2 for stage one only | MEDIUM | The hook fetches node.js but the payload is skipped. A hit proves exposure of the package, not worm infection. |
| WSL on Windows | Windows sensor sees the wsl process; guest activity depends on sensor coverage | MEDIUM | The worm also reads /mnt/c/Users/*/.ssh and .npmrc, exposing Windows-side keys and tokens from inside WSL. |
| npm registry and package maintainers | Registry publish history, npm view, git tags | HIGH | The worm restores the local package.json after publishing, so the infected version shows only in the registry. |
| Arch User Repository maintainers | AUR git history of maintained packages | MEDIUM | Commits use the maintainer's last commit name and email and the stock upgpkg message. |
| Remote hosts reached over SSH | sshd auth logs; Falcon Linux sensor on the target | HIGH | Hosts without a sensor see only the run in /tmp/log. Hashed known_hosts entries stop the worm from reaching those hosts. |
ATT&CK Mapping
| Tactic | Technique | Observed behavior | Query or control |
|---|---|---|---|
| Initial Access | T1195.002 Compromise Software Supply Chain | Nine typosquat packages with a preinstall hook; worm republishes victims' npm packages and AUR packages | Q1, Q2, Q11, Q12, Q13; PB1, PB3, PB5 |
| Execution | T1204.005 User Execution: Malicious Library | npm install runs the hook | Q2 |
| Execution | T1059.004 Unix Shell; T1059.007 JavaScript | curl piped to node and to bash; linux.sh | Q1, Q7 |
| Command and Control | T1105 Ingress Tool Transfer | node.js, linux.sh and the binary downloaded from Codeberg | Q1, Q6 |
| Command and Control | T1102 Web Service | Wayback Machine and Codeberg used as staging | Q9 (scoping only) |
| Persistence | T1543.002 Systemd Service | System and user units for the Tor and CHAOS processes | Q4, Q5; PB4 |
| Defense Evasion | T1036.005 Match Legitimate Name or Location | systemd-fontrenderd, systemd-fontcached, "Font Rendering Service" | Q4, Q6 |
| Defense Evasion | T1222.002 File and Directory Permissions Modification | chattr +i on the binary and unit files | Q5; PB4 |
| Command and Control | T1090.003 Multi-hop Proxy; T1573 Encrypted Channel | CHAOS client reaches the onion service through local Tor | Q4, Q8 |
| Command and Control | T1219 Remote Access Software | CHAOS: shell, file transfer, screenshots | Q3, Q4 |
| Credential Access | T1552.004 Private Keys; T1552.001 Credentials in Files | Reads OpenSSH private keys, known_hosts and .npmrc tokens | Q10, Q13; PB2, PB3 |
| Lateral Movement | T1021.004 SSH | Runs the worm on each host a key unlocks | Q10, Q1 |
| Collection | T1113 Screen Capture | CHAOS screenshot command on X11 | Gap (no endpoint event used) |
Native Audit-Log Hunts
Non-CQL checks for the parts of the chain Falcon does not see. Replace the angle-bracket placeholder before running.
N1 Lockfile and manifest sweep
grep -rEwl --include=package.json --include=package-lock.json --include=yarn.lock --include=pnpm-lock.yaml -e '(xeprews|express-javascript|express-nodejs|react-nodejs|exprdd|exprrdd|exptrdd|exptred|exptredd)' /srv /opt /home 2>/dev/null # Review each hit: -w treats a hyphen as a word break, so 'my-exprdd-lib' also matches.
N2 npm logs and cache
grep -rlE 'hellscripter|install-scripts' ~/.npm/_logs 2>/dev/null ls ~/.npm/_cacache 2>/dev/null | head # then search index entries for the nine package names
N3 Host sweep for the fake units and files
ls -la /usr/lib/systemd/systemd-font* /etc/systemd/system/systemd-font* 2>/dev/null lsattr /usr/lib/systemd/systemd-font* /etc/systemd/system/systemd-font* 2>/dev/null # an i flag is the worm's chattr +i find / -xdev \( -name 'systemd-fontrenderd*' -o -name 'systemd-fontcached*' -o -name 'systemd-fontd' \) 2>/dev/null for h in /home/* /root; do ls -la "$h/.config/systemd" "$h/.config/systemd/user" 2>/dev/null; done systemctl list-units --all --no-pager | grep -iE 'Font (Rendering|Caching) Service' sha256sum /usr/lib/systemd/systemd-fontrenderd /home/*/.config/systemd/systemd-fontcached 2>/dev/null # compare with the Falcon CSV hash
N4 sshd authentication logs
journalctl -u ssh -u sshd --since '7 days ago' | grep -E 'Accepted publickey' | awk '{print $9, $11}' | sort | uniq -c | sort -rn | head
# A single internal source key-logging into many hosts in a short window, especially as root, matches the SSH spread step.
N5 Registry history of packages you maintain
npm view <your-package> time --json # compare each published version time with your release log and git tags git tag --list --sort=-creatordate | head # a registry version with no matching tag or commit is suspect
N6 AUR packages you maintain
git log -p -n 5 -- PKGBUILD '*.install' # Look for a pkgrel bump with an 'upgpkg:' message you did not write, and for a curl line added to the .install file.
N7 Worm output on reached hosts
ls -la /tmp/log 2>/dev/null && head -50 /tmp/log
CrowdStrike LogScale CQL Hunt Queries
Looks for: Any process whose command line names the hellscripter Codeberg account, or a Wayback Machine URL wrapping a Codeberg URL. This covers the npm preinstall hook, the Node.js loader, the SSH-pushed copy of the worm on remote hosts, and the line the worm appends to AUR .install files. No platform filter: the hook runs on macOS and Windows too and only the payload is Linux-only, so a non-Linux hit still means the package was installed.
// HUNT: DirtyBlanket fetches from the hellscripter Codeberg repository // MITRE: T1195.002, T1105, T1059.004 // CONF: high FP: low COST: low // REQUIRES: Falcon process telemetry (ProcessRollup2) on developer hosts, CI runners and Linux servers // FALSE POSITIVES: an analyst or ticketing tool that merely quotes the account name in a command line. The Wayback-wrapped Codeberg branch is broader than this campaign, so review any hit that lacks the hellscripter string. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | CommandLine=/hellscripter|web\.archive\.org\/web\/https?:\/\/codeberg\.org/i | table([@timestamp, aid, ComputerName, UserName, FileName, ParentBaseFileName, CommandLine])
Looks for: npm, pnpm, yarn, bun or node command lines that install any of the nine package names. Matching is by name, not version, because the OSV records list later versions for several of the names (for example 5.2.2) beyond the 5.2.1 builds SafeDep analysed.
// HUNT: DirtyBlanket malicious package install // MITRE: T1195.002, T1204.005 // CONF: high FP: low COST: low // REQUIRES: Falcon process telemetry (ProcessRollup2) on developer hosts and CI runners // FALSE POSITIVES: a security tool or ticket search that passes a package name on its command line without installing it. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^(npm|npx|pnpm|yarn|bun|node)$/i | CommandLine=/\b(install|add|i)\b.*\b(xeprews|express-javascript|express-nodejs|react-nodejs|exprdd|exprrdd|exptrdd|exptred|exptredd)\b/i | table([@timestamp, aid, ComputerName, UserName, FileName, ParentBaseFileName, CommandLine])
Looks for: A process whose image hash is the SafeDep-reported hash of the systemd-fontd CHAOS client, or the hash of linux.sh. linux.sh runs as a bash script, so Falcon will normally record the interpreter hash rather than the script hash. Treat the second value as a record for file scanners and a hit here as unlikely. Both hashes come from one analysis (SafeDep) and are held at detect.
// HUNT: DirtyBlanket known-bad hashes executing // MITRE: T1219, T1105 // CONF: medium FP: low COST: low // REQUIRES: Falcon process telemetry (ProcessRollup2) with SHA256HashData // FALSE POSITIVES: none expected for the binary hash. The script hash will rarely appear as a process image. // LOOKBACK: 90d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | SHA256HashData=/^(2c9dbc14809f1e1aebda114194368b002acf74c8760b88fc101f625d179793c2|65f0a95b24e30305146346cbc2452cfabadab9e1ca35053e1134b67c38919577)$/i | table([@timestamp, aid, ComputerName, UserName, ImageFileName, SHA256HashData, ParentBaseFileName, CommandLine])
Looks for: Linux processes whose image name is systemd-fontrenderd, systemd-fontcached or systemd-fontd, or whose image is the Tor Expert Bundle extracted under systemd-fontrenderd in the user's ~/.config/systemd/. Names are matched on the image basename, so the install path does not matter.
// HUNT: DirtyBlanket fake systemd font service processes // MITRE: T1036.005, T1543.002, T1090.003 // CONF: high FP: low COST: low // REQUIRES: Falcon Linux sensor process telemetry (ProcessRollup2) // FALSE POSITIVES: none known. Check the distro file list (rpm -qf, dpkg -S, pacman -Qo) for any binary this finds before dismissing it. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | event_platform=Lin | ImageFileName=/systemd-font(renderd|cached|d)$|systemd-fontrenderd\/tor\/tor$/i | table([@timestamp, aid, ComputerName, UserName, ImageFileName, ParentBaseFileName, SHA256HashData, CommandLine])
Looks for: systemctl enabling or starting, and chattr setting the immutable flag on, any path or unit with the fake font-service names. The worm runs these as child processes of its script, so each one is an ordinary process event.
// HUNT: DirtyBlanket persistence via systemctl and chattr on fake font units // MITRE: T1543.002, T1222.002 // CONF: high FP: low COST: low // REQUIRES: Falcon Linux sensor process telemetry (ProcessRollup2) // FALSE POSITIVES: none known; the unit names are specific to this campaign. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | event_platform=Lin | FileName=/^(systemctl|chattr)$/i | CommandLine=/systemd-font(renderd|cached|d)/i | table([@timestamp, aid, ComputerName, UserName, FileName, ParentBaseFileName, CommandLine])
Looks for: Linux executable-written events whose target file name is one of the three fake font binary names. The worm downloads the CHAOS client with curl -o to /usr/lib/systemd/systemd-fontrenderd (root) or ~/.config/systemd/systemd-fontcached (non-root).
// HUNT: DirtyBlanket fake font binary written to disk // MITRE: T1105, T1036.005 // CONF: high FP: low COST: low // REQUIRES: Falcon Linux sensor ELFFileWritten telemetry // FALSE POSITIVES: none known. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=ELFFileWritten | TargetFileName=/systemd-font(renderd|cached|d)$/i | table([@timestamp, aid, ComputerName, ContextBaseFileName, TargetFileName, SHA256HashData])
Looks for: As root, linux.sh installs tor openssh git npm and build tools in one package-manager call. This query matches the two exact command lines SafeDep documents. It breaks if the operator edits the list, so treat a miss as uninformative.
// HUNT: DirtyBlanket worm dependency bootstrap // MITRE: T1059.004, T1105 // CONF: medium FP: low COST: low // REQUIRES: Falcon Linux sensor process telemetry (ProcessRollup2) // FALSE POSITIVES: a provisioning script that happens to install the same package set in the same order. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | event_platform=Lin | FileName=/^(apt-get|apt|pacman)$/i | CommandLine=/install -y tor openssh-client git npm build-essential|-S --needed --noconfirm tor openssh git npm base-devel/i | table([@timestamp, aid, ComputerName, UserName, FileName, ParentBaseFileName, CommandLine])
Looks for: Connections to 127.0.0.1:9050, the local Tor SOCKS port the worm exports as HTTP_PROXY, grouped by the initiating process. The CHAOS client reaches its onion C2 only this way, so the process name is the signal. Tor is legitimate software and the bundle is hunt-only, never a block. FP: Developers running Tor Browser tooling, torsocks, privacy-focused apps and some research hosts connect to 9050 legitimately. Exclude known process names and approved hosts.
// HUNT: DirtyBlanket Tor SOCKS use by a non-Tor process // MITRE: T1090.003, T1573 // CONF: medium FP: medium COST: medium // REQUIRES: Falcon Linux sensor NetworkConnectIP4 telemetry // FALSE POSITIVES: Tor-aware tools, privacy proxies and research hosts that use the local SOCKS port. // TUNING: extend the ContextBaseFileName exclusion with approved Tor clients, and exclude approved research hosts by ComputerName. // LOOKBACK: 14d (set with the console time picker) #event_simpleName=NetworkConnectIP4 | event_platform=Lin | RemoteAddressIP4=127.0.0.1 | RemotePort=9050 | ContextBaseFileName!=/^(tor|torsocks|torbrowser|firefox|firefox-esr)$/i | groupBy([aid, ComputerName, ContextBaseFileName], function=[count(as=connections), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | sort(connections, order=desc, limit=200)
Looks for: Which Linux hosts resolve Codeberg or the Wayback Machine, as a scoping pivot after Q1 or Q2. Both are legitimate services used by developers every day. This is context for a host you already suspect, not a detection, and it must never become a block on either domain. FP: Almost every developer workstation resolves both. Run it against a short list of suspect hosts, or restrict to servers and CI runners, which should not.
// HUNT: DirtyBlanket scoping pivot on Codeberg and Wayback Machine lookups // MITRE: T1102, T1105 // CONF: low FP: high COST: medium // REQUIRES: Falcon Linux sensor DnsRequest telemetry // FALSE POSITIVES: legitimate developer use of Codeberg and the Wayback Machine. // TUNING: run only against servers and CI runner groups, or add a ComputerName filter for the suspect host list from Q1 and Q2. Do not alert fleet-wide. // LOOKBACK: 14d (set with the console time picker) #event_simpleName=DnsRequest | event_platform=Lin | DomainName=/^(codeberg\.org|web\.archive\.org)$/i | groupBy([aid, ComputerName, DomainName], function=[count(as=lookups), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | sort(lookups, order=desc, limit=200)
Looks for: linux.sh runs ssh -o BatchMode=yes -o UserKnownHostsFile=<mktemp file> against every host in the collected known_hosts, once per key and per ssh_config path, so one infected host produces a burst of such processes. The query counts them per host. FP: Ansible control nodes, backup jobs and CI deploy steps also use BatchMode=yes. The temporary known_hosts file narrows this, but automation that does the same will still match.
// HUNT: DirtyBlanket SSH fan-out with BatchMode and a temp known_hosts file // MITRE: T1021.004, T1552.004 // CONF: medium FP: medium COST: medium // REQUIRES: Falcon Linux sensor process telemetry (ProcessRollup2) // FALSE POSITIVES: configuration-management and deploy automation that uses BatchMode with a temporary known_hosts file. // TUNING: exclude approved automation hosts by ComputerName, or raise the threshold above the largest legitimate fan-out in your fleet. // LOOKBACK: 7d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | event_platform=Lin | FileName=/^ssh$/i | CommandLine=/BatchMode=yes/i | CommandLine=/UserKnownHostsFile=\/tmp\//i | groupBy([aid, ComputerName], function=[count(as=ssh_attempts), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | test(ssh_attempts >= 10) | sort(ssh_attempts, order=desc, limit=200)
Looks for: With every private key, the worm runs list-repos over SSH against the AUR to find packages the key owner can push to, then clones, edits and pushes them. Ordinary AUR maintainers run list-repos rarely, and almost never from a CI runner or a server.
// HUNT: DirtyBlanket AUR list-repos over SSH // MITRE: T1195.002, T1021.004 // CONF: medium FP: low COST: low // REQUIRES: Falcon Linux sensor process telemetry (ProcessRollup2) // FALSE POSITIVES: an AUR maintainer listing their own packages by hand. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | event_platform=Lin | FileName=/^ssh$/i | CommandLine=/aur@/i | CommandLine=/list-repos/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, CommandLine])
Looks for: The worm rewrites each package.json it finds with npm pkg set scripts.preinstall=... before running npm version patch and npm publish. This matches the injection step itself, so it still fires if the operator moves the payload to a different host. FP: Developers occasionally set a preinstall script with npm pkg set. Review the value being set: a curl piped to node or a shell is the signal.
// HUNT: DirtyBlanket npm preinstall injection // MITRE: T1195.002, T1059.007 // CONF: medium FP: medium COST: low // REQUIRES: Falcon process telemetry (ProcessRollup2) on developer hosts, CI runners and servers // FALSE POSITIVES: developers or release tooling that set a legitimate preinstall script with npm pkg set. // TUNING: exclude approved release-automation hosts, and rank hits whose CommandLine contains curl, wget or a pipe to a shell or node. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^(npm|node)$/i | CommandLine=/pkg\s+set\s+scripts\.preinstall/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, CommandLine])
Looks for: The worm launches one npm publish per .npmrc it finds, for every project directory it finds. Several publish commands from one host within the lookback, outside your release pipeline, indicate token misuse. FP: Monorepo releases and release pipelines publish many packages by design. Developer laptops should publish rarely.
// HUNT: DirtyBlanket npm publish fan-out // MITRE: T1195.002, T1552.001 // CONF: medium FP: medium COST: medium // REQUIRES: Falcon process telemetry (ProcessRollup2) on developer hosts, CI runners and servers // FALSE POSITIVES: monorepo release jobs and publish pipelines. // TUNING: exclude the named release-pipeline hosts and service accounts by ComputerName, and review developer laptops first. // LOOKBACK: 7d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^(npm|node)$/i | CommandLine=/\bpublish\b/i | groupBy([aid, ComputerName], function=[count(as=publishes), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | test(publishes >= 3) | sort(publishes, order=desc, limit=200)
CrowdStrike Custom IOA Recommendations
| IOA | Platform / type | Pattern | Exclusions | Deployment |
|---|---|---|---|---|
| IOA-1 Fake systemd font service | Linux, Process Creation | Image File Name .*systemd-font(renderd|cached|d)$ | None needed. Names are specific to this campaign. | Detect. Keep at detect until a second independent source names the binary and unit names. |
| IOA-2 hellscripter fetch | Linux, macOS, Windows, Process Creation | Command Line .*hellscripter.* | None needed. | Detect first. The account name has two independent reporters (SafeDep, Amazon Inspector via OSV), so it may move to prevent after a 14-day soak shows no benign hits. |
| IOA-3 npm preinstall injection | Linux, macOS, Process Creation | Image File Name .*/(npm|node)$ and Command Line .*pkg\s+set\s+scripts\.preinstall.* | Approved release-automation hosts. | Detect only. Matches the behavior, not an indicator. |
No IOA is proposed for CHAOS, Codeberg, the Wayback Machine or the Tor Expert Bundle. Those are commodity software and services and would block legitimate use.
Machine-Readable IOC Appendix
type,value,action,severity,expiration,description,tags sha256,2c9dbc14809f1e1aebda114194368b002acf74c8760b88fc101f625d179793c2,detect,high,2027-04-09,DirtyBlanket systemd-fontd CHAOS client Linux x86-64 (single source: SafeDep),campaign:DirtyBlanket sha256,65f0a95b24e30305146346cbc2452cfabadab9e1ca35053e1134b67c38919577,detect,medium,2027-04-09,DirtyBlanket linux.sh worm script (SafeDep and OSV record from the same analysis),campaign:DirtyBlanket domain,s5n2uyo6gb6dhirsm5pihwohi6e7ayrwojx4xjow4cqabmbowpezenid.onion,detect,medium,2027-04-09,DirtyBlanket CHAOS server onion service port 80 (single source: SafeDep). Falcon may reject .onion; keep as a record,campaign:DirtyBlanket
# DirtyBlanket malicious npm packages (publisher account: dirtyblanket) # Block by NAME in your registry proxy or artifact manager. OSV lists later versions for several names. xeprews@5.2.1 express-javascript@5.2.1 express-nodejs@5.2.1 react-nodejs@19.3.0 exprdd@5.2.1 exprrdd@5.2.1 exptrdd@5.2.1 exptred@5.2.1 exptredd@5.2.1
# Campaign file and unit indicators (host sweep, EDR file search) /usr/lib/systemd/systemd-fontrenderd (root, chattr +i) /etc/systemd/system/systemd-fontrenderd.service (root, chattr +i) /etc/systemd/system/multi-user.target.wants/systemd-fontrenderd.service (root, chattr +i) ~/.config/systemd/systemd-fontcached ~/.config/systemd/user/systemd-fontrenderd.service ~/.config/systemd/user/systemd-fontcached.service ~/.config/systemd/systemd-fontrenderd/ (Tor Expert Bundle) Unit descriptions: Font Rendering Service, Font Caching Service Binary name in the repository: systemd-fontd
# HUNT ONLY. Legitimate services and public software. Do NOT block these hosts wholesale. codeberg.org/hellscripter/install-scripts (hunt the account and path in command lines, not the host) web.archive.org/web/https://codeberg.org/hellscripter/install-scripts/raw/branch/main/node.js web.archive.org/web/https://codeberg.org/hellscripter/install-scripts/raw/branch/main/linux.sh web.archive.org/web/https://codeberg.org/hellscripter/install-scripts/raw/branch/main/systemd-fontd dist.torproject.org Tor Expert Bundle 15.0.23 (non-root path) socks5://127.0.0.1:9050 (HTTP_PROXY set in the service files) github.com/tiagorlampert/CHAOS/client (public CHAOS client module path)
# Behavioral signatures 1. npm package.json preinstall value: curl <url> | node 2. curl or wget of a script piped straight into node or bash from a Codeberg raw URL or a Wayback-wrapped Codeberg URL 3. Process named systemd-font* that is not owned by a distro package 4. chattr +i on files under /usr/lib/systemd or /etc/systemd/system 5. Non-Tor process connecting to 127.0.0.1:9050 6. Burst of ssh -o BatchMode=yes -o UserKnownHostsFile=<tmp file> from one host 7. ssh to the AUR running list-repos from a non-maintainer host 8. npm pkg set scripts.preinstall, npm version patch and npm publish in sequence on one host
The CSV has no prevent rows and no critical severity. The three values have a single analytical origin (SafeDep), so they ship detect. Package names are not Falcon IOC types; enforce them in your registry proxy.
Detection Validation Gates
| Gate | Check | Pass criterion |
|---|---|---|
| Telemetry ready | Falcon Linux sensor reports ProcessRollup2, ELFFileWritten, NetworkConnectIP4 and DnsRequest for developer hosts, servers and CI runners. | Each event type returns rows for a known Linux host in the last 24 hours. |
| Benign baseline | Run Q8, Q9, Q10, Q12, Q13 over 14 days before alerting. | Record legitimate Tor-aware tools, automation hosts and release pipelines, then add them to the TUNING exclusions. |
| Positive tests | In an isolated lab host, create a harmless file named systemd-fontcached and run a process by that name; run npm pkg set scripts.preinstall=test in a scratch project. | Q4/Q6 and Q12 fire. Do not download or run any campaign artifact. |
| Promotion | A query becomes an alert after two clean baseline weeks. IOA-2 may move to prevent only after the soak and review. | Document the owner and the rollback in the ticket. |
| Build gates | Structure, Falcon field, CQL syntax and IOC provenance linters run before this pack was staged. | All exit 0. |
Hardening: Tiered
- Disable npm lifecycle scripts by default on developer machines and in CI (
ignore-scripts=true), then allow-list the few packages that need a build step. Stops the preinstall hook that starts this chain. MITRE M1038 Execution Prevention, M1033 Limit Software Installation (T1195.002, T1204.005). Authority: npm CLI documentation forignore-scripts. ⚠ best-practice, no formal benchmark. Verify:npm config get ignore-scripts. Playbook PB1. - Block the nine package names in your registry proxy or artifact manager and fail builds whose lockfiles name them. M1033. ⚠ best-practice, no formal benchmark. Verify: a test install through the proxy is refused; native hunt N1 returns nothing.
- Protect SSH private keys with passphrases and hash known_hosts so a stolen key is not directly usable and a copied known_hosts does not list targets. MITRE M1027, M1026 (T1552.004, T1021.004). Authority: OpenSSH ssh_config(5)
HashKnownHosts; hashed entries cannot be used by this worm per SafeDep. ⚠ best-practice, no formal benchmark for HashKnownHosts. Verify:ssh -G example.test | grep -i hashknownhosts. Playbook PB2. - Disable direct root SSH login (
PermitRootLogin no), since the worm tries-l rootwith every key. MITRE M1018, M1042. Authority: CIS Linux distribution benchmarks, SSH Server Configuration section (control numbers differ per distro and version). Verify:sshd -T | grep -i permitrootlogin. Playbook PB2. - Revoke classic npm tokens and remove plaintext tokens from developer
.npmrcfiles. MITRE M1032, M1026 (T1552.001). Authority: npm documentation on granular access tokens. ⚠ best-practice, no formal benchmark. Verify:npm token listshows none that you do not expect. Playbook PB3.
- Publish from CI only, using trusted publishing with provenance, so no long-lived publish token sits on a workstation for the worm to find. MITRE M1032, M1026, M1048. Authority: npm trusted publishing documentation. Playbook PB3.
- auditd rules for systemd unit directories and chattr to cover the unit-file write that Falcon queries in this pack do not. MITRE M1047 Audit. Authority: CIS Linux benchmarks, Logging and Auditing section; DISA Linux STIG audit rules. Verify:
auditctl -l | grep sysd. Playbook PB4. - Scoped egress control for build hosts and servers: allow-list the package registries, alert on Tor relay and Tor bundle downloads, and alert (do not block) on Wayback Machine
web/fetches from CI runners. Do not block codeberg.org or web.archive.org organisation-wide. MITRE M1037 Filter Network Traffic. ⚠ best-practice, no formal benchmark. Verify: Q8 and Q9 results from a pilot server group. - AUR hygiene: on Arch hosts, review PKGBUILD and
.installdiffs before every build; for maintainers, hardware-backed SSH keys. MITRE M1033, M1018. Authority: Arch Linux wiki guidance on AUR packages. Playbook PB5.
- Ephemeral, secretless CI runners that hold no
~/.sshkeys and no.npmrctokens during dependency install. MITRE M1048 Application Isolation and Sandboxing, M1026. ⚠ best-practice, no formal benchmark. - Dependency cooldown policy: where your package manager supports a minimum release age (pnpm
minimumReleaseAge, YarnnpmMinimalAgeGate), set it so newly published versions are not installed immediately. This does not stop a typosquat that you type by name, so it complements the controls above. Verify the setting exists in your version. M1051 Update Software, M1033. - Retire shared deploy keys in favour of short-lived SSH certificates, so a stolen key expires. MITRE M1026, M1032. ⚠ best-practice, no formal benchmark.
Deployable Playbooks
Each step lists Prerequisites, Reboot required and Rollback. Test on a pilot host first. Placeholders in angle brackets are replaced by you.
PB1 npm lifecycle script lockdown (T1195.002, M1038, M1033)
PB1.1 Disable install scripts for the user and for CI
Prerequisites: npm 7 or newer; shell access to the developer account or the CI image.
Reboot required: No
npm config set ignore-scripts true --location=user # CI: install with scripts off, then rebuild only what needs it npm ci --ignore-scripts npm rebuild <package-that-needs-a-build> # Verify npm config get ignore-scripts
Rollback: npm config delete ignore-scripts --location=user. Packages with native build steps (for example those using node-gyp) will fail to build until you rebuild them explicitly.
PB1.2 Sweep lockfiles for the nine names and block them at the registry
Prerequisites: Read access to repositories and build caches; admin access to the registry proxy.
Reboot required: No
grep -rEwl --include=package.json --include=package-lock.json --include=yarn.lock --include=pnpm-lock.yaml -e '(xeprews|express-javascript|express-nodejs|react-nodejs|exprdd|exprrdd|exptrdd|exptred|exptredd)' /srv /opt /home 2>/dev/null # Registry proxy: add the nine package names to the block or exclude list in your product's own configuration.
Rollback: Remove the block-list entries. The sweep is read-only, so Rollback: none required.
PB2 SSH hardening (T1552.004, T1021.004, M1027, M1018)
PB2.1 Hash known_hosts and require it for new entries
Prerequisites: OpenSSH client; root for the system-wide file.
Reboot required: No
echo 'HashKnownHosts yes' | sudo tee /etc/ssh/ssh_config.d/90-hashknownhosts.conf # Convert an existing file (keeps known_hosts.old with the clear-text names) ssh-keygen -H -f ~/.ssh/known_hosts # Verify ssh -G example.test | grep -i hashknownhosts
Rollback: Delete /etc/ssh/ssh_config.d/90-hashknownhosts.conf and restore ~/.ssh/known_hosts.old. Delete the .old file securely afterwards, because it lists every host in clear text.
PB2.2 Find and protect private keys that have no passphrase
Prerequisites: Shell access as the key owner; an agent or secrets process for any key used by automation.
Reboot required: No
for k in ~/.ssh/id_*; do case "$k" in *.pub) continue;; esac ssh-keygen -y -P '' -f "$k" >/dev/null 2>&1 && echo "NO PASSPHRASE: $k" done # Add a passphrase to a flagged key ssh-keygen -p -f <path-to-key>
Rollback: ssh-keygen -p -N '' -f <path-to-key> removes the passphrase again. Do this only where an unattended service depends on the key, and prefer a short-lived certificate instead.
PB2.3 Disable direct root login over SSH
Prerequisites: A tested non-root account with sudo and working key login; console access in case of lockout.
Reboot required: No
echo 'PermitRootLogin no' | sudo tee /etc/ssh/sshd_config.d/90-noroot.conf sudo sshd -t && sudo systemctl reload sshd # the unit is named ssh on Debian and Ubuntu # Verify sudo sshd -T | grep -i permitrootlogin
Rollback: Delete /etc/ssh/sshd_config.d/90-noroot.conf and reload sshd.
PB3 npm credential hygiene (T1552.001, M1032, M1026)
PB3.1 Inventory and revoke publish tokens
Prerequisites: npm account access on the machine being cleaned.
Reboot required: No
grep -n '_authToken' ~/.npmrc /root/.npmrc 2>/dev/null | sed 's/=.*/=<redacted>/' npm token list npm token revoke <token-id>
Rollback: A revoked token cannot be restored. Issue a replacement scoped to a single package with the shortest lifetime that works, and store it in the CI secret store rather than on disk.
PB3.2 Publish from CI with provenance instead of a stored token
Prerequisites: The package is configured for trusted publishing in the npm package settings; a CI platform that issues OIDC tokens.
Reboot required: No
# GitHub Actions workflow excerpt permissions: contents: read id-token: write steps: - run: npm publish --provenance --access public
Rollback: Restore the previous publish step and its token secret. Keep the token revoked until the trusted-publishing setup is verified.
PB4 auditd coverage for systemd persistence (T1543.002, T1222.002, M1047)
PB4.1 Watch unit directories and chattr
Prerequisites: auditd installed and running; confirm the path of chattr with command -v chattr.
Reboot required: Only if the audit configuration is locked with -e 2
sudo tee /etc/audit/rules.d/90-systemd-persistence.rules >/dev/null <<'EOF' -w /etc/systemd/system -p wa -k sysd_unit_write -w /usr/lib/systemd -p wa -k sysd_lib_write -a always,exit -F arch=b64 -S execve -F exe=/usr/bin/chattr -k chattr_exec EOF sudo augenrules --load # Verify sudo auditctl -l | grep -E 'sysd_|chattr_' sudo ausearch -k sysd_unit_write -ts today
Rollback: sudo rm /etc/audit/rules.d/90-systemd-persistence.rules && sudo augenrules --load. Expect extra audit volume during package upgrades.
PB5 AUR hygiene (T1195.002, M1033, M1018)
PB5.1 Check local AUR build caches and your maintained packages
Prerequisites: Arch Linux host; read access to the AUR helper cache directories (paths vary by helper).
Reboot required: No
grep -rIlE 'hellscripter|install-scripts' ~/.cache/yay ~/.cache/paru 2>/dev/null # Maintainers: inspect each package you can push to git log -p -n 5 -- PKGBUILD '*.install'
Rollback: Read-only, so Rollback: none required. If you find a poisoned commit, revert it with a new commit and notify the AUR team.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| 1 Triage (read-only) | Run Q1, Q2, Q4 and Q5 for 30 days. Confirm the install with lockfiles, npm logs (N1, N2) and process lineage. List every host that the affected user's keys could reach. | SOC / IR | Query exports, lockfile hits |
| 2 Isolate | Network-contain the host in Falcon and pause the CI runner or job queue. Contain the whole host: onion traffic cannot be blocked by domain. Isolate before any cleanup. | IR | Containment record, runner pause time |
| 3 Preserve | Before removal, copy the service binary, unit files, ~/.config/systemd/, /tmp/log, shell history and ~/.npm/_logs. Record lsattr output and hashes. A forensic image is preferred. | IR / forensics | Hashed evidence archive |
| 4 Eradicate | Preferred: rebuild the host from a known-good image. If you must clean in place: stop and disable the units, clear the immutable flag with chattr -i on the three root paths, delete the files, systemctl daemon-reload, and for non-root remove ~/.config/systemd/ entries and systemctl --user units. Check whether tor.service existed before the incident before removing it. | IR / sysadmin | Before and after file listing |
| 5 Rotate credentials | Treat every SSH private key, .npmrc token, AUR key and secret readable by the installing user as stolen. If the worm ran as root, that is every user on the host. Revoke and reissue, and review recent use. | IAM / dev leads | Revocation log |
| 6 Downstream spread | For each host in the user's known_hosts that accepted a key: run Q1, Q4 and N3. For npm packages you maintain: compare registry versions with git tags (N5), deprecate or unpublish unexpected versions and report to npm. For AUR packages: review history (N6) and report to the AUR team. | IR / package owners | Per-host and per-package result table |
| 7 Recover and verify | Reinstall without scripts (PB1), apply PB2 to PB4, rerun the full query set for 30 days and require zero hits before returning the host to service. | SOC / platform | Clean query exports, change tickets |
Rollback and reversibility. Eradication is destructive by design. Preserve evidence first (phase 3). If a cleanup step removes something legitimate, restore it from the evidence archive or the rebuild image. Prerequisites for cleanup: root access, the host isolated, evidence copied. Reboot required: not for cleanup; a rebuild replaces the host.
Detection Coverage Map
| Technique | Behavior | CQL | IOA | Coverage |
|---|---|---|---|---|
| T1195.002 / T1204.005 | Typosquat package install and preinstall hook | Q1, Q2 | IOA-2 | GOOD |
| T1105 / T1059.004 | node.js and linux.sh fetched and piped | Q1, Q6, Q7 | IOA-2 | GOOD |
| T1036.005 / T1543.002 | Fake systemd font processes and units | Q3, Q4, Q5, Q6 | IOA-1 | GOOD |
| T1543.002 | Unit file written to disk | None | None | GAP no Falcon event used. Use auditd (PB4) and N3 |
| T1222.002 | chattr +i on binary and units | Q5 | None | GOOD |
| T1090.003 / T1219 | CHAOS over local Tor SOCKS | Q3, Q8 | None | PARTIAL onion traffic content and DNS are not visible |
| T1021.004 / T1552.004 | SSH key use and fan-out | Q10 | None | PARTIAL hashed known_hosts hosts are skipped by the worm; remote success needs sshd logs (N4) |
| T1195.002 (AUR) | AUR list-repos and package poisoning | Q11, Q1 | None | PARTIAL the git push itself is not an endpoint event here (N6) |
| T1195.002 / T1552.001 (npm) | preinstall injection and publish burst | Q12, Q13 | IOA-3 | PARTIAL the infected version is visible only in the registry (N5) |
| T1552.004 / T1552.001 | Reading private keys and .npmrc files | None | None | GAP no file-read event used |
| T1113 | CHAOS screenshot on X11 | None | None | GAP no endpoint event used |
| T1102 | Wayback and Codeberg staging | Q9 | None | PARTIAL scoping only; both are legitimate services |
Validation gates are in Section 11. Gaps above are real: they are not covered by any query in this pack and need the native checks in Section 7 or host auditing.
Hunt Summary Ticket
TITLE: DirtyBlanket: nine fake Express and React npm packages and a self-spreading Linux worm
SEVERITY: HIGH, developer and CI supply chain, Linux worm with SSH, AUR and npm propagation
SCOPE: Developer machines, CI runners and Linux servers that installed xeprews,
express-javascript, express-nodejs, react-nodejs, exprdd, exprrdd, exptrdd,
exptred or exptredd, and every host their SSH keys can reach
HYPOTHESIS: A host that ran a DirtyBlanket package shows a command line naming hellscripter,
a process or file named systemd-fontrenderd, systemd-fontcached or systemd-fontd,
chattr +i or systemctl on those names, or a non-Tor process on 127.0.0.1:9050.
QUERIES: Q1-Q13 (Section 8); IOA-1 to IOA-3 (Section 9)
DO FIRST: 1. Search lockfiles and registry proxy logs for the nine package names (N1).
2. Run Q1, Q2, Q4, Q5 and Q6 fleet-wide for 30 days.
3. On any hit: contain the host, preserve evidence, rotate SSH keys and npm tokens.
4. Disable npm install scripts (PB1) and block the nine names at the registry.
FINDINGS: REPLACE_WITH_FINDINGS_SUMMARY_AFTER_HUNT_EXECUTION
GAPS: Unit-file writes, private-key and .npmrc reads, onion traffic, X11 screenshots (Section 15).
ACTIONS: Deploy IOA-1 to IOA-3 in detect mode; enforce PB1 to PB4; audit npm and AUR packages you maintain.
OWNER: REPLACE_WITH_HUNT_OWNER
VERSION: v0.1 (Draft) 2026-10-09
Changelog
References
| Tier | Source | Used for | Access date |
|---|---|---|---|
| T1 | SafeDep, DirtyBlanket: Fake Express Packages on npm Spread a Linux Worm | All behavior, persistence, propagation, binary and hash indicators | 2026-10-09 |
| T2 | OSV malicious-package records (MAL-2026-17241 to 17248, 17294) | Second reporter for the package set and the hellscripter hook; later versions | 2026-10-09 |
| T2 | OpenSSF malicious-packages PR #1579 | Corroboration of the nine names and indicator categories | 2026-10-09 |
| T2 | The Hacker News, ThreatsDay | Secondary coverage of the cluster | 2026-10-09 |
| Ref | MITRE ATT&CK T1195.002 | Technique mapping | 2026-10-09 |
| Ref | MITRE ATT&CK mitigations | M-number mapping | 2026-10-09 |
| Ref | npm CLI configuration (ignore-scripts) | PB1 | 2026-10-09 |
| Ref | CIS Benchmarks (Linux distributions) | SSH and auditd hardening | 2026-10-09 |
Four fetched sources were saved to DirtyBlanket-npm-LinuxWorm-Hunt-sources/ for the provenance gate (01 to 04).
Generated 2026-10-09 by HuntPack · defensive content only