DirtyBlanket: Fake Express npm Packages and a Self-Spreading Linux Worm

Nine malicious npm packages, a Wayback Machine and Codeberg staging chain, a CHAOS remote access tool disguised as a systemd font service, Tor C2, and spread over SSH keys, AUR packages and npm tokens.
Threat
DirtyBlanket (npm account dirtyblanket)
Severity
HIGH
Type
Software supply chain, Linux worm
Access
npm preinstall hook, SSH keys, AUR, npm tokens
Version
v0.1 (Draft) · 2026-10-09
Author
HuntPack
Confidence
High (packages, behavior), Medium (hashes, onion, paths)
01

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.

02

Source Review & Web Hunter Notes

TierSourceKey findingCarry forward
T1SafeDep, 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 tableyes. Snapshot 01-safedep-dirtyblanket.txt
T2OSV / OpenSSF malicious-packages records MAL-2026-17241 to 17248 and 17294Per-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 namesyes. Snapshot 04-osv-mal-records.txt
T2OpenSSF 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 valuespartial. Snapshot 03-ossf-advisory.txt
T2The 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 SafeDeppartial. 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-version dist.shasum values, 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.
03

Hunt Brief & Attack Chain

Hunt hypotheses (highest fidelity first)

#HypothesisMITREFalcon eventsExpected FPConf
H1A host ran the preinstall hook or a pushed copy of the worm: a process command line names the hellscripter account.T1195.002, T1105ProcessRollup2Analysts quoting the stringHIGH
H2A developer or CI host installed one of the nine package names.T1195.002, T1204.005ProcessRollup2Tools that pass a name without installingHIGH
H3A Linux host runs, or wrote to disk, a fake systemd font binary or the bundled Tor.T1036.005, T1543.002ProcessRollup2, ELFFileWrittenNone knownHIGH
H4systemctl or chattr touched the fake font units or binaries.T1543.002, T1222.002ProcessRollup2None knownHIGH
H5The known-bad CHAOS client hash executed.T1219ProcessRollup2None knownMEDIUM
H6A non-Tor process talks to the local Tor SOCKS port.T1090.003NetworkConnectIP4Tor-aware toolsMEDIUM
H7The worm is spreading: SSH fan-out, AUR list-repos, npm preinstall injection, npm publish bursts.T1021.004, T1195.002, T1552.001ProcessRollup2Automation and release pipelinesMEDIUM
H8Hosts that should not reach Codeberg or the Wayback Machine do (scoping only).T1102DnsRequestUbiquitous developer useLOW

Attack chain

StepBehaviorTelemetryHunt angle
1npm install runs the preinstall hook: curl fetches node.js through web.archive.org from the hellscripter repository and pipes it to node.Process, command lineQ1, Q2
2node.js checks for Linux, then runs curl of linux.sh from Codeberg piped into bash. Errors and output are discarded.Process, command lineQ1
3linux.sh backgrounds itself. As root it installs tor, openssh, git, npm and build tools.ProcessQ7
4Root: 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 auditQ3, Q4, Q5, Q6; unit-file write is a gap (auditd)
5The CHAOS client sends health, device and WebSocket traffic to the onion service through the local Tor SOCKS proxy.Loopback connect; onion traffic is not visibleQ8; payload traffic is a gap
6SSH 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 logsQ10, Q1 on the target
7AUR spread: ssh aur@... list-repos, clone, bump pkgrel, append a bash <(curl ...) line to the .install file, push.ProcessQ11, Q1
8npm spread: add curl ... | node to preinstall, npm version patch, npm publish once per .npmrc, then restore the local package.json.Process, registry historyQ12, Q13, native hunt N5
04

Consolidated IOC Table

TypeValueConfActionSource snapshotContext
npm accountdirtyblanketHIGHdetect01, 04Publisher of all nine packages, 2026-09-29 06:05 to 06:38 UTC
npm packagexeprews 5.2.1HIGHdetect01, 03, 04Copies Express
npm packageexpress-javascript 5.2.1HIGHdetect01, 03, 04Copies Express
npm packageexpress-nodejs 5.2.1HIGHdetect01, 03, 04Copies Express
npm packagereact-nodejs 19.3.0HIGHdetect01, 03, 04Copies React
npm packageexprdd 5.2.1HIGHdetect01, 03, 04Copies Express
npm packageexprrdd 5.2.1HIGHdetect01, 03, 04Copies Express
npm packageexptrdd 5.2.1HIGHdetect01, 03, 04Copies Express
npm packageexptred 5.2.1HIGHdetect01, 03, 04Copies Express
npm packageexptredd 5.2.1HIGHdetect01, 03, 04Copies Express. OSV also lists 5.2.3 and 5.2.4; match by name
Codeberg accounthellscripterHIGHdetect01, 04Owner of the install-scripts repository. Account name and repository path are campaign-specific; the host codeberg.org is not
Repository pathcodeberg.org/hellscripter/install-scripts (node.js, linux.sh, systemd-fontd on raw/branch/main)HIGHhunt01, 04Fetched directly and through web.archive.org. Hunt on the command line, do not block the hosts
SHA-25665f0a95b24e30305146346cbc2452cfabadab9e1ca35053e1134b67c38919577MEDIUMdetect01, 04linux.sh (227 lines)
SHA-2562c9dbc14809f1e1aebda114194368b002acf74c8760b88fc101f625d179793c2MEDIUMdetect01systemd-fontd, Go CHAOS client, 7.6 MB, Linux x86-64
Tor onions5n2uyo6gb6dhirsm5pihwohi6e7ayrwojx4xjow4cqabmbowpezenid.onion:80MEDIUMdetect01CHAOS 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.serviceMEDIUMdetect01All 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/MEDIUMdetect01Backdoor, two user units, Tor Expert Bundle directory
Unit description"Font Rendering Service", "Font Caching Service"MEDIUMdetect01, 04Cover descriptions for the Tor and CHAOS units
Go modulegithub.com/tiagorlampert/CHAOS/clientLOWhunt01Public open-source CHAOS client. Reused by unrelated actors: never a campaign identifier
Tor bundleTor Expert Bundle 15.0.23, dist.torproject.orgLOWhunt01Legitimate software; downloaded by the non-root path. Hunt only
Worm artifact/tmp/log on SSH-reached hostsLOWhunt01Output file of the remote run; generic name
05

Affected Surface & Telemetry Matrix

SurfaceTelemetryPriorityGap risk
Linux developer workstations, servers, CI runners (Falcon Linux sensor)ProcessRollup2, ELFFileWritten, NetworkConnectIP4, DnsRequestHIGHUnit-file writes and private-key file reads have no Falcon event used here. Use auditd. Onion traffic content is invisible.
macOS and Windows developer machinesProcessRollup2 for stage one onlyMEDIUMThe hook fetches node.js but the payload is skipped. A hit proves exposure of the package, not worm infection.
WSL on WindowsWindows sensor sees the wsl process; guest activity depends on sensor coverageMEDIUMThe worm also reads /mnt/c/Users/*/.ssh and .npmrc, exposing Windows-side keys and tokens from inside WSL.
npm registry and package maintainersRegistry publish history, npm view, git tagsHIGHThe worm restores the local package.json after publishing, so the infected version shows only in the registry.
Arch User Repository maintainersAUR git history of maintained packagesMEDIUMCommits use the maintainer's last commit name and email and the stock upgpkg message.
Remote hosts reached over SSHsshd auth logs; Falcon Linux sensor on the targetHIGHHosts without a sensor see only the run in /tmp/log. Hashed known_hosts entries stop the worm from reaching those hosts.
06

ATT&CK Mapping

TacticTechniqueObserved behaviorQuery or control
Initial AccessT1195.002 Compromise Software Supply ChainNine typosquat packages with a preinstall hook; worm republishes victims' npm packages and AUR packagesQ1, Q2, Q11, Q12, Q13; PB1, PB3, PB5
ExecutionT1204.005 User Execution: Malicious Librarynpm install runs the hookQ2
ExecutionT1059.004 Unix Shell; T1059.007 JavaScriptcurl piped to node and to bash; linux.shQ1, Q7
Command and ControlT1105 Ingress Tool Transfernode.js, linux.sh and the binary downloaded from CodebergQ1, Q6
Command and ControlT1102 Web ServiceWayback Machine and Codeberg used as stagingQ9 (scoping only)
PersistenceT1543.002 Systemd ServiceSystem and user units for the Tor and CHAOS processesQ4, Q5; PB4
Defense EvasionT1036.005 Match Legitimate Name or Locationsystemd-fontrenderd, systemd-fontcached, "Font Rendering Service"Q4, Q6
Defense EvasionT1222.002 File and Directory Permissions Modificationchattr +i on the binary and unit filesQ5; PB4
Command and ControlT1090.003 Multi-hop Proxy; T1573 Encrypted ChannelCHAOS client reaches the onion service through local TorQ4, Q8
Command and ControlT1219 Remote Access SoftwareCHAOS: shell, file transfer, screenshotsQ3, Q4
Credential AccessT1552.004 Private Keys; T1552.001 Credentials in FilesReads OpenSSH private keys, known_hosts and .npmrc tokensQ10, Q13; PB2, PB3
Lateral MovementT1021.004 SSHRuns the worm on each host a key unlocksQ10, Q1
CollectionT1113 Screen CaptureCHAOS screenshot command on X11Gap (no endpoint event used)
07

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
08

CrowdStrike LogScale CQL Hunt Queries

Pick your tenant's cloud first: every "Open in Falcon" button below uses this selection.
Q1 · Stage-one and stage-two fetches from the hellscripter Codeberg repository
CONF HIGHFP LOWCOST LOW

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])
Q2 · Install of one of the nine malicious packages
CONF HIGHFP LOWCOST LOW

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])
Q3 · Known-bad SHA-256 executing: CHAOS client and linux.sh
CONF MEDIUMFP LOWCOST LOW

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])
Q4 · Fake systemd font service processes and the bundled Tor
CONF HIGHFP LOWCOST LOW

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])
Q5 · Persistence commands: systemctl and chattr on the font units
CONF HIGHFP LOWCOST LOW

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])
Q6 · ELF write of the fake font binary
CONF HIGHFP LOWCOST LOW

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])
Q7 · Worm dependency bootstrap: the exact apt and pacman package set
CONF MEDIUMFP LOWCOST LOW

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])
Q8 · Local Tor SOCKS proxy used by a non-Tor process
CONF MEDIUMFP MEDIUMCOST MEDIUM

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)
Q9 · Hosts resolving codeberg.org or web.archive.org (commodity, hunt only)
CONF LOWFP HIGHCOST MEDIUM

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)
Q10 · SSH fan-out with BatchMode and a temporary known_hosts file
CONF MEDIUMFP MEDIUMCOST MEDIUM

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)
Q11 · AUR list-repos over SSH (spread into Arch User Repository packages)
CONF MEDIUMFP LOWCOST LOW

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])
Q12 · npm preinstall injection (behavioral, survives a changed URL)
CONF MEDIUMFP MEDIUMCOST LOW

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])
Q13 · npm publish fan-out from one host
CONF MEDIUMFP MEDIUMCOST MEDIUM

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)
09

CrowdStrike Custom IOA Recommendations

IOAPlatform / typePatternExclusionsDeployment
IOA-1 Fake systemd font serviceLinux, Process CreationImage 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 fetchLinux, macOS, Windows, Process CreationCommand 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 injectionLinux, macOS, Process CreationImage 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.

10

Machine-Readable IOC Appendix

Falcon IOC Management CSVbulk import, detect only
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
Malicious npm packagesregistry block by name
# 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 indicatorshost sweep
# 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
Commodity infrastructure (hunt only)never block
# 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 signaturestool-agnostic
# 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.

11

Detection Validation Gates

GateCheckPass criterion
Telemetry readyFalcon 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 baselineRun 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 testsIn 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.
PromotionA 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 gatesStructure, Falcon field, CQL syntax and IOC provenance linters run before this pack was staged.All exit 0.
12

Hardening: Tiered

Immediate (this week)
  • 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 for ignore-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 root with 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 .npmrc files. MITRE M1032, M1026 (T1552.001). Authority: npm documentation on granular access tokens. ⚠ best-practice, no formal benchmark. Verify: npm token list shows none that you do not expect. Playbook PB3.
Near term (1 to 4 weeks, pilot first)
  • 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 .install diffs before every build; for maintainers, hardware-backed SSH keys. MITRE M1033, M1018. Authority: Arch Linux wiki guidance on AUR packages. Playbook PB5.
Strategic (1 to 3 months)
  • Ephemeral, secretless CI runners that hold no ~/.ssh keys and no .npmrc tokens 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, Yarn npmMinimalAgeGate), 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.
13

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.

14

Containment Runbook

PhaseActionsOwnerEvidence
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 / IRQuery exports, lockfile hits
2 IsolateNetwork-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.IRContainment record, runner pause time
3 PreserveBefore 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 / forensicsHashed evidence archive
4 EradicatePreferred: 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 / sysadminBefore and after file listing
5 Rotate credentialsTreat 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 leadsRevocation log
6 Downstream spreadFor 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 ownersPer-host and per-package result table
7 Recover and verifyReinstall 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 / platformClean 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.

15

Detection Coverage Map

TechniqueBehaviorCQLIOACoverage
T1195.002 / T1204.005Typosquat package install and preinstall hookQ1, Q2IOA-2GOOD
T1105 / T1059.004node.js and linux.sh fetched and pipedQ1, Q6, Q7IOA-2GOOD
T1036.005 / T1543.002Fake systemd font processes and unitsQ3, Q4, Q5, Q6IOA-1GOOD
T1543.002Unit file written to diskNoneNoneGAP no Falcon event used. Use auditd (PB4) and N3
T1222.002chattr +i on binary and unitsQ5NoneGOOD
T1090.003 / T1219CHAOS over local Tor SOCKSQ3, Q8NonePARTIAL onion traffic content and DNS are not visible
T1021.004 / T1552.004SSH key use and fan-outQ10NonePARTIAL hashed known_hosts hosts are skipped by the worm; remote success needs sshd logs (N4)
T1195.002 (AUR)AUR list-repos and package poisoningQ11, Q1NonePARTIAL the git push itself is not an endpoint event here (N6)
T1195.002 / T1552.001 (npm)preinstall injection and publish burstQ12, Q13IOA-3PARTIAL the infected version is visible only in the registry (N5)
T1552.004 / T1552.001Reading private keys and .npmrc filesNoneNoneGAP no file-read event used
T1113CHAOS screenshot on X11NoneNoneGAP no endpoint event used
T1102Wayback and Codeberg stagingQ9NonePARTIAL 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.

16

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
17

Changelog

v0.12026-10-09Initial pack build from the SafeDep analysis, OSV malicious-package records (including an independent Amazon Inspector finding), the OpenSSF PR and The Hacker News bulletin. 13 CQL queries, 3 IOA candidates, 3 detect-only CSV rows, no prevent rows. Commodity items (CHAOS, Codeberg, Wayback Machine, Tor Expert Bundle) held at hunt.
18

References

TierSourceUsed forAccess date
T1SafeDep, DirtyBlanket: Fake Express Packages on npm Spread a Linux WormAll behavior, persistence, propagation, binary and hash indicators2026-10-09
T2OSV malicious-package records (MAL-2026-17241 to 17248, 17294)Second reporter for the package set and the hellscripter hook; later versions2026-10-09
T2OpenSSF malicious-packages PR #1579Corroboration of the nine names and indicator categories2026-10-09
T2The Hacker News, ThreatsDaySecondary coverage of the cluster2026-10-09
RefMITRE ATT&CK T1195.002Technique mapping2026-10-09
RefMITRE ATT&CK mitigationsM-number mapping2026-10-09
Refnpm CLI configuration (ignore-scripts)PB12026-10-09
RefCIS Benchmarks (Linux distributions)SSH and auditd hardening2026-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