BLOG

Earliest Known PolinRider Malware Found in 2025 npm Packages

A hijacked maintainer account shipped the North Korean payload 8 months before the first documented victims.

By c0a15726-c5b1-4b0d-85e6-fe15553df9e2, cb482791-4ef1-4762-96ad-b0ca4bdd538e ·

Earliest Known PolinRider Malware Found in 2025 npm Packages

itsa-utils hadn’t published a new version since August 2018. Then, at 20:15 UTC on Sunday, May 4, 2025, an attacker published version 1.4.2 on npm. Over the next 90 minutes, versions of eight more npm packages from the same maintainer followed. Every version we could recover contains a malicious payload with a RAT and an infostealer.

This story isn’t new. Account takeovers have become increasingly common. But when we analyzed these packages, we were in for a big surprise. The malware was obfuscated JavaScript appended to the end of existing JavaScript files and hidden by hundreds of whitespace characters. That pattern looked a lot like the PolinRider campaign, which had no documented victims before January 2026. Like an insect trapped in amber, we wanted to study it and see what we could learn.

What we found is a building block. The payload, the blockchain delivery chain, and the habit of appending code to a config file are all PolinRider components. The self-spreading behavior that defines the campaign in 2026 isn’t in the 2025 evidence.

TL;DR

  • What we found: A PolinRider attack from May 2025, eight months before the earliest documented victims. The 2025 sample carries PolinRider’s payload but not its documented spread behavior, which appears in the same maintainer’s GitHub repositories in four windows between January and July 2026. The blockchain records behind it date attacker activity back to February 2025.

  • How it hit: Nine npm versions from one maintainer, published in 90 minutes on May 4, 2025. We confirmed the payload in four of them.

  • The proof: The same payload pattern, marker, XOR key, command server and server paths that OpenSourceMalware has documented for PolinRider.

  • The payload: JADESNOW, an INVISIBLEFERRET bootstrapper, a Socket.IO RAT, and a Python credential stealer, delivered through BNB Smart Chain and Ethereum transactions.

  • Impact: Remote command execution, plus theft from browsers, wallets, Git, GitHub, password managers, and cloud storage, on Windows, Linux, and macOS.

The proof that this is PolinRider

Eight of nine traits tie this sample to PolinRider as OpenSourceMalware has documented it. The ninth, spread by force-push, appears only in 2026. That makes the 2025 sample a building block of PolinRider, much like TasksJacker. The shared XOR key and the shared command server are the strongest links.

OpenSourceMalware folded TasksJacker into PolinRider because it shared the propagation script, blockchain addresses, and decryption keys. This sample shares a decryption key, the command server, and the server paths. It does not show the propagation script.

Trait

PolinRider as documented

This sample, May 2025

Payload appended after whitespace

Obfuscated JavaScript appended to existing files, pushed off screen by runs of whitespace

The same, in index.js and webpack.config.js, after 1,012 or 253 whitespace characters

Marker string

global['_V'] and global['!']

global['_V']='5-itsa'

XOR key

ThZG+0jfXE6VAGOJ

The same key decrypts the JADESNOW stage

Blockchain dead drop

TRON first, with Aptos and BNB Smart Chain as fallbacks

BNB Smart Chain, then Ethereum calldata

Command server

154.91.0.103

The bootstrapper’s server

Server paths

/verify-human/{channel}, /u/f, /d/python.zip, /d/python.7z, /d/7zr.exe

All five appear in the chain

Payload family

BeaverTail loader and InvisibleFerret backdoor

INVISIBLEFERRET bootstrapper, Socket.IO RAT, Python stealer

Exfiltration

Some payloads send data through a Telegram bot

The stealer sends data through a Telegram bot

Spread by force-push

A script folds the change into an existing commit and force-pushes it as the developer, with backdated commits

Not seen in the 2025 packages. The same maintainer’s repositories show it from January 2026 (variants B, B2, and C).

What’s unique in this sample:

  • A different wallet. The BNB Smart Chain wallet 0x9BC1355344B54DEDf3E44296916eD15653844509.

  • Three different keys. $v$5;kmc$ldm*5SA, hk7#cH%v806Q/GH!, and 9KyASt+7D0mjPHFY.

  • A marker format. The values 5-itsa, 5-55, and 5-1-55 share a shape with build markers such as 9-10094. We think they’re campaign IDs.

  • An older chain design. There’s no TRON or Aptos.

Decoding the chain

As an example, we’ll walk through how the malware works in itsa-utils@1.4.2.

The package has no install hooks. The trigger is the entry point: the first 11 lines of index.js export the utility functions, and line 12 holds a heavily obfuscated program that runs when an application imports the package. Apart from that line, the file is identical to the clean 1.4.3.

The chain uses EtherHiding, which stores executable content in the calldata of blockchain transactions. The loader reads the transactions through public explorer APIs, but the encrypted stages stay on-chain. Google Threat Intelligence documented the same BNB Smart Chain contract, owner wallet, and transaction lineage in UNC5342’s use of JADESNOW to deliver INVISIBLEFERRET.
Stage 1 loader

What it shows: the malware depends on blockchain data it never has to host.

The package sets two globals, then enters layers of generated functions and shuffled strings:

global['_V'] = '5-itsa';
global['r'] = require;

_V is a campaign marker passed to every later stage. It ends up in command server paths and victim messages.

After deobfuscation, the loader asks Binplorer for the latest mint operation on the attacker wallet, takes operations[0].transactionHash, and queries /getTxInfo/<hash>. It then processes the transaction’s input value in this order: remove 0x, hex decode to UTF-8, split on .., Base64 decode the second part, XOR with $v$5;kmc$ldm*5SA, and run the result with node -e, with global['_V'] prepended. On non-Windows systems the child process is detached with its streams ignored. Errors are discarded.

Binplorer no longer accepts type=mint on getAddressHistory, so the loader’s exact request fails today. The transaction and payload remain. We recovered the calldata through public RPC queries.

Stage 2 JADESNOW

What it shows: the February 7, 2025 date. This stage sits in block 46456153 on BNB Smart Chain, timestamped 2025-02-07 17:37:33 UTC.

The transaction input holds a Base64 string between two .. delimiters. Decoding it and XORing with the key recovers JADESNOW, a second obfuscated loader. It switches to Ethereum and reads one transaction through three explorers for redundancy: Ethplorer, BlockCypher, and Blockchair. It hex-decodes the calldata, XORs it with ThZG+0jfXE6VAGOJ, and passes the result to eval.

Stage 3 bootstrapper

What it shows: the link to the documented campaign. The server and paths here match OpenSourceMalware’s lists.

The Ethereum transaction is in block 21795760, timestamped 2025-02-07 16:15:11 UTC, sent from the attacker wallet to 0x000000000000000000000000000000000000dEaD. The decrypted bootstrapper sets up its server at 154.91.0.103:27017. Before it continues, it queries ip-api.com. If the country code is RU, it reports Blocked! and exits. Otherwise it installs missing Node dependencies and tries to make Python 3.12.7 available, downloading support files from /d/python.zip, /d/python.7z, /d/7zr.exe, and /d/pyenv.zip. It reports status to POST /verify-human/5-itsa. Then it fetches two more payloads from Ethereum.

Stage 4A Node.js RAT

What it shows: interactive access, not just theft.

The RAT connects to a Socket.IO server at 85.239.62.36:3306, a port usually used by MySQL. It identifies the victim by hostname, username, operating system, and a random process UUID, then waits for operator commands. It supports shell commands, changing directory, uploading a file or a whole directory, stopping an upload, resetting its directory, and terminating itself. Uploads go to 85.239.60.213:27017/u/f. Commands run with shell: true, and on Windows the attacker’s portable Python is added to PATH.

Stage 4B Python stealer

What it shows: the theft is broad and covers developer credentials.

The transaction produces a small Python wrapper. Reversing its Base64 value and decompressing the zlib stream gives a 65,500-byte credential stealer. It works on Windows, Linux, and macOS, installs missing libraries, sets a single-instance lock, and collects host identifiers.

Browsers. It targets Chrome, Chromium, Opera, Opera GX, Brave, Edge, Arc, Vivaldi, and Firefox. It recovers encryption material through Windows DPAPI, the macOS Keychain, Linux Secret Service, or KDE Wallet, then exports passwords, cookies, cards, profiles, extension storage, and Firefox NSS credentials. It also enumerates Windows Credential Manager and Linux Secret Service entries.

Wallets and developer data. It targets more than 80 browser extensions, including MetaMask, Phantom, Coinbase, Binance, Trust Wallet, TronLink, OKX, Ronin, Keplr, Rabby, Exodus, and SafePal. It also collects Git credentials, GitHub CLI tokens, Solana key files, 1Password and Dashlane data, Exodus and Atomic wallets, Electrum, Bitcoin and Dogecoin wallets, macOS login keychains, and WinAuth and Proxifier profiles. Cloud storage discovery covers Dropbox, Google Drive, OneDrive, iCloud, Box, MEGA, and pCloud. On Linux it also checks mounted Windows volumes under /mnt.

Exfiltration. Loot is staged in %USERPROFILE%\.npm on Windows or /tmp/.npm elsewhere, packed into AES-encrypted ZIPs with the password ,./,./,./, and sent to 85.239.62.36:27017/u/f. Telegram is the fallback for archives under 50 MB. After a successful upload, the stealer deletes the archives and staging directories.

The Telegram bot and persistence

The stealer contains a Telegram bot token and a hardcoded chat ID. The token was still valid when we checked in October 2026.

Field

Value

Bot ID

7870147428

Username

@file_1018_bot

Name

File Bot

Can join groups

Yes

Can read all group messages

No

Webhook

None

Pending updates

0

Destination chat ID

-4697384025

The bot is send-only. The malware calls sendDocument and has no getUpdates, getFile, or command handling. Telegram’s API doesn’t expose the bot’s past messages, so we retrieved no victim archives. An active token proves only that the credential worked when we tested it. We’ve redacted it here, and the bot and chat should be reported to Telegram.

None of the stages creates a run key, scheduled task, launch agent, or service, so nothing survives a reboot. The malware lasts through detached Node.js processes, hidden Windows process creation, ignored streams, Socket.IO reconnects every five seconds, a portable Python install, runtime installation of dependencies, silent error handling, and a Russia kill switch.

The attack in nine packages

The maintainer has 59 packages on npm. On May 4, 2025, nine of them got a new version within 90 minutes. itsa-utils was first.

Published (UTC)

Package and version

Where the payload sits

Status now

20:15

itsa-utils@1.4.2

index.js, line 12

Removed

20:19

itsa-jsext@2.0.3

index.js, line 11

Live

20:26

itsa-react-docviewer@16.1.2

webpack.config.js, line 23

Live

21:22

itsa-cli@16.5.3

Not read

Removed

21:25

itsa-react-button@16.0.11

webpack.config.js, line 26

Live

21:28

itsa-react-maskedinput@16.1.2

Not read

Removed

21:30

itsa-react-checkbox@16.0.5

Not read

Removed

21:44

itsa-react-focuscontainer@16.0.10

Not read

Removed

21:45

itsa-react-input@16.1.2

Not read

Removed

We recovered four of these tarballs: the three live versions, and itsa-utils 1.4.2, which we pulled from a mirror and matched to the registry’s original checksums. The payload line is byte-identical in all four. The other five were removed before we could read them. They sit in the same burst, so we count them as infected.

Three versions went out in the first 11 minutes. After a 56-minute pause, six more followed in 23 minutes. The next morning, from 10:07 to 11:18 UTC, the maintainer published seven clean replacements, including itsa-utils 1.4.3. Those moved the latest version forward but left the infected ones in place, so anyone who pins a version can still install them. Six of the nine have since vanished from the registry, two in 2025 and four in October 2026, judging by last-modified times. We don’t know who removed them.

Bulk publishing isn’t unusual for this maintainer, who once published 36 packages in a day. The toolchain is what stands out. The four burst versions with a registry record were published with npm 10.9.2 and Node 22.14.0. Every other recorded publish from May 2025 to April 2026 used npm 5.5.1 and Node 8.9.1. The npm client reports those versions, so they aren’t proof. They do point to someone with the maintainer’s publishing rights, working from an environment the maintainer doesn’t normally use.

The return in 2026

The same maintainer’s GitHub repositories carried newer variants of the payload in four windows: January 22, March 23, May 29, and July 9, 2026. Four repositories were affected: itsa-react-table, itsa-react-select, itsa-react-docviewer, and itsa-react-fileuploadarea. We decoded only variant A, the chain above. We haven’t decoded B, B2, or C, so we don’t know whether they use the same route.

Variant

Marker

Where it sits

When

Payload line SHA-256 (first 16 hex)

A

global['_V']='5-itsa'

npm: index.js and webpack.config.js

May 4, 2025

e929abd9511ed060

B

global.i='5-55'

GitHub: webpack.config.js

Jan 22 and Mar 23, 2026

279e6ba3b447dd75

B2

global.i='5-55'

GitHub: webpack.config.js

May 29, 2026

83146a69cfba0db6

C

global.o='5-1-55'

GitHub: webpack.config.js

Jul 9, 2026

e7f65f261602af44

Every variant is a long line appended to an existing file after padding. Three things changed.

  • The marker. Variant A sets _V to 5-itsa, which contains the maintainer’s package prefix. The later variants use i and o with values that name nothing. We think these are build markers, and the later builds dropped the name.

  • The channel and file. In 2025 the payload entered at the npm publish step. In 2026 it entered through GitHub force-pushes, and every infected commit changed only webpack.config.js.

  • The payload and the commit metadata. All four payload hashes differ, even where the marker is the same. Six 2026 commits have committer dates 31 to 66 days after the author date, offsets of +09:00 or -08:00 against the maintainer’s +01:00 or +02:00, and a shortened committer name. The four July commits look clean in git log but carry committer dates set 34 to 152 days before the push. When you hunt, trust GitHub’s push times over commit dates.

No plaintext URLs, IPv4 addresses, or 0x addresses appear in any of the four variants. Those sit inside the obfuscation.

The 2026 payloads never reached npm, which doesn’t surprise us since the version of PolinRider we know well isn’t actively operated by a human. Typically, the maintainer has to ship a new version of a package in order for the hidden malicious code to sneak into a npm package.

Timeline

All times are UTC. The 2026 rows come from GitHub’s push records, which are more reliable than commit dates. The on-chain records predate the first npm publish by 86 days.

Date

What happened

2025-02-07

Ethereum stage (16:15) and BNB Smart Chain stage (17:37) written on-chain

2025-05-04, 20:15 to 21:45

Nine package versions published, starting with itsa-utils@1.4.2

2025-05-05, 10:07 to 11:18

Seven clean replacement versions published, including itsa-utils@1.4.3

2026-01-22

itsa-react-table infected (variant B), restored February 7

2026-03-23

itsa-react-table and itsa-react-select infected 22 seconds apart (variant B), restored the same day

2026-05-29

itsa-react-docviewer, itsa-react-fileuploadarea, and itsa-react-select infected (variant B2), restored June 5

2026-07-09

Four repositories infected within 43 seconds (variant C), restored July 11

2026-10-16

Two package versions disappear from the registry

2026-10-28

Two more, including itsa-utils@1.4.2, disappear

How the payload got in, and what we think

2025 entered at the publish step. The itsa-utils repository has five commits, the last from July 7, 2016. No commit matches 1.4.2 or 1.4.3, and a history search for the _V marker across 49 of the owner’s repositories found nothing. The published package doesn’t match the repository either. Git holds a 9-line index.js, and the published 1.4.2 has 12. The itsa-react-button 16.0.11 webpack.config.js matches git byte for byte, apart from the added line. Someone ran npm publish on a directory that held the payload, and the payload never touched git.

2026 entered through force-pushes. We found 20 force-pushes across the 49 repositories we scanned. All came from the owner’s own account, and all 20 rewrote history. Ten were infections. Each time, the tip commit was amended to add the payload to webpack.config.js, keeping its message and author date, and then force-pushed over master. The other ten restored the clean original, within 16 days of each infection and from the same account.

Based on our experience with the PolinRider campaign, we think this is probably what happened:

  • 2025 was an account takeover while 2026 was an automated infection. The nine 2025 versions were published with the maintainer’s npm access, using PolinRider’s payload before the campaign’s spread behavior appears in our evidence. In 2026, PolinRider’s documented behavior appears in the maintainer’s GitHub repositories, and no npm version carries those payloads.

  • The infection likely persisted on the maintainer’s machine from 2025, because the 2026 pushes ran from it. That matches PolinRider’s documented propagation script, which rolls back the clock, folds the payload into the tip commit, and force-pushes with the developer’s own credentials. It explains the amended commits, the odd timezone offsets, the back-dated July commits, and four repositories infected within seconds.

  • The 2026 infections didn’t reach npm because no release went out while they were live. The PolinRider campaign doesn’t choose packages to infect. It happens accidentally when an infected source code repo ships a new package version to npm. Each window lasted 19 hours to 16 days, and the maintainer’s releases landed between them. Four windows roughly two months apart fit an infection that returns after each cleanup.

Indicators of compromise

Malicious packages

npm/itsa-utils@1.4.2 (removed)
npm/itsa-jsext@2.0.3
npm/itsa-react-docviewer@16.1.2
npm/itsa-react-button@16.0.11
npm/itsa-cli@16.5.3
npm/itsa-react-maskedinput@16.1.2
npm/itsa-react-checkbox@16.0.5
npm/itsa-react-focuscontainer@16.0.10
npm/itsa-react-input@16.1.2

Blockchain addresses and transactions

0x9bc1355344b54dedf3e44296916ed15653844509
0x8eac3198dd72f3e07108c4c7cff43108ad48a71c

0x5c77567fcf00c317b8156df8e00838105f16fdd4fbbc6cd83d624225397d8856
0xc2da361c40279a4f2f84448791377652f2bf41f06d18f19941a96c720228cd0f
0x86d1a21fd151e344ccc0778fd018c281db9d40b6ccd4bdd3588cb40fade1a33a
0xf9d432745ea15dbc00ff319417af3763f72fcf8a4debedbfceeef4246847ce41

Network infrastructure

154.91.0.103:27017
85.239.62.36:3306
85.239.62.36:27017
85.239.60.213:27017

http://154.91.0.103:27017/verify-human/5-itsa
http://154.91.0.103:27017/d/python.zip
http://154.91.0.103:27017/d/python.7z
http://154.91.0.103:27017/d/7zr.exe
http://154.91.0.103:27017/d/pyenv.zip
http://85.239.60.213:27017/u/f
http://85.239.62.36:27017/u/f

Telegram

Bot ID:       7870147428
Bot username: @file_1018_bot
Chat ID:      -4697384025
Token:        7870147428:AAGbYG_[REDACTED]

XOR keys and campaign markers

global['_V']='5-itsa'
global['r']=require
global.i='5-55'
global.o='5-1-55'
$v$5;kmc$ldm*5SA
ThZG+0jfXE6VAGOJ
hk7#cH%v806Q/GH!
9KyASt+7D0mjPHFY

File hashes (SHA-256)

c9dab995cf61a066db149c8d5d4ce171fb9d20d246994a1fa4f27a5885088bbb  itsa-utils-1.4.2.tgz
8ef2c6fc0347853469bd19203c4aa37f17192cc6faec21c8c4be0b1cdbf00104  itsa-utils-1.4.2 package/index.js
9a55dc9cc568d1b0864c76fcd7acbacba63d05459c6044b5fc0a8c35955df078  itsa-jsext-2.0.3.tgz
8eede6e92a37119937351ca956bdba123b486aa07c090d21adc1361075167bf7  itsa-jsext-2.0.3 index.js
e86a5bacd23e63f19f2cf74d6d5b13d8e8759d39e2310aa4e8e8868a63dfbd91  itsa-react-button-16.0.11.tgz
489df9fa032455af30066af90404285cab7191823ef6770c691b9ef8bb84c16f  itsa-react-button-16.0.11 webpack.config.js
cf658925d5ec72e696432b20b8d3e384fe3eca871f087833b58a12a751ab18d1  itsa-react-docviewer-16.1.2.tgz
8c934e6fd9f689d60233bbec4370896778f6b286e9d00df7e94afd6dc9f44cc4  itsa-react-docviewer-16.1.2 webpack.config.js
e929abd9511ed0603c8efc44f1a0670699dbca33760585de3f8fdbd14681b1b6  variant A payload line (identical in all four)
4b766d33afb2552178939fff99bc2a01773d2aa69a000e27df4e8fdb185470e6  JADESNOW
f30ddf3727e3be98962d6c9f24a93a43c42c32e28b930c80c703b38697a97248  bootstrapper
4d28e46ae8181a665cdc6814ced72348720281b35275c95fd44fabadf7993aa8  Node.js RAT
efbe59e9bf34b9dae8a7d809dafd77751db5b45431417436675485d1d01bcf19  stealer, wrapped
c7c221ae9920b9b2e8f248b1aba4d801d91e5da934d5af77afa63f9bfc22fe54  stealer, unpacked

Infected GitHub commits (2026, all webpack.config.js)

itsa-react-table          77689157262a14633fb510648c757696200b67b3  variant B
itsa-react-table          c0e09f7880c24ce68b86433eb509497692a2fa7b  variant B
itsa-react-select         7af052f59725e56ba0adcb12cb46cb7160bffc68  variant B
itsa-react-docviewer      3b2c847807836a032752f17841073f7a0ac7a122  variant B2
itsa-react-fileuploadarea 061cefdf1d1bde1c71b84a34c4c7bdc6d6657dea  variant B2
itsa-react-select         2336c5eae6063bc94a5bb73fa9afa793ef409026  variant B2
itsa-react-docviewer      e7ce8bbbd9e63ac811800511c621e769d38656ef  variant C
itsa-react-fileuploadarea a64d14ce5c65310a69de56f58826ccce50d4b194  variant C
itsa-react-select         0e62fc4619fe7c4be9a9e4054eb91892732badf6  variant C
itsa-react-table          3cb381ef60ee16f0fe21a252e78ef1086e03a0d9  variant C

Local artifacts

%LOCALAPPDATA%\Temp\tmp7A863DD6.tmp
/tmp/tmp7A863DD6.tmp
%USERPROFILE%\.npm
/tmp/.npm
~/.node_modules
~/.pyenv
python.zip
python.7z
pyenv.zip
7zr.exe

How to hunt for this activity

Hunt for imports of the affected versions and for Node.js command lines that contain both -e and global['_V']. Network telemetry can reveal the chain even when the source is heavily obfuscated.

  • Lockfiles and node_modules containing itsa-jsext@2.0.3, itsa-react-button@16.0.11, or itsa-react-docviewer@16.1.2

  • Very long lines preceded by hundreds of spaces in index.js or webpack.config.js, and the strings global['_V'], global.i=, and global.o=

  • GitHub force-pushes that change only one config file, keep the message and author date, and carry a committer date in a different timezone offset

  • Several repositories force-pushed within one minute

  • Node.js querying Binplorer, Ethplorer, BlockCypher, or Blockchair immediately before spawning node -e

  • Node.js connections to port 3306 that speak HTTP or Socket.IO instead of MySQL

  • Developer workstations contacting public blockchain explorers and then the C2 addresses above

  • Creation of tmp7A863DD6.tmp, /tmp/.npm, ~/.node_modules, or ~/.pyenv by unexpected Node.js processes

  • Runtime installation of socket.io-client, pyzipper, pycryptodomex, portalocker, or secretstorage by an imported dependency

  • Recursive collection of browser profile and wallet extension directories

  • Telegram sendDocument traffic from developer systems

The operators can replace on-chain configuration and keep the same loader structure, so behavioral detections will outlast transaction hashes.