PhantomSync: npm-kryptopakker skjuler lommeboktyveri

PhantomSync: åtte kryptoutvikler-npm-pakker skjuler en forsinket, selvvedvarende dropper

TL; DR

A single npm publisher shipped eight small packages whose names read like everyday building blocks for blockchain and wallet development, base58-utils, abi-encode, eth-dev, arb-kit, layer2-sdk, solana-key-utils, eth-wallet-helpersog crypto-validate-lib. Each one does contain a working utility. Appended after that utility, though, is a self-invoking block of code that runs the moment the module is imported.

Roughly 37 seconds after import, that block decodes a payload that ships inside the package disguised as a test fixture, writes it to a hidden file under the user's home directory, registers itself to restart on every login across Windows, macOS, and Linux, and launches the decoded script as a detached process. Nothing about this fires during npm install; it waits for the code to be imported and run, which is a quieter moment than installation.

The payload is not opaque. It ships as plain base64, so decoding the "test fixture" recovers the second stage in full: a crypto-wallet and secrets stealer that waits for the machine to sit idle, harvests private keys and seed phrases, encrypts each find with a hardcoded RSA-4096 key, and exfiltrates it by pinning it to public IPFS storage, beaconing back every 12 hours.

Vi sporer klyngen som PhantomSync. All eight packages were live in the npm registry at the time of analysis, published under a single account.

ØkosystemNPM
Nettsidepakker base58-utils, abi-encode, eth-dev, arb-kit, layer2-sdk, solana-key-utils, eth-wallet-helpers, crypto-validate-lib
Platforms targetedWindows, macOS, Linux
KjerneoppførselDelayed, import-time dropper hidden as a test fixture; installs cross-platform persistence
nyttelastCrypto-wallet and secrets stealer, RSA-4096 encryption, exfiltration via public IPFS pinning

Angrepsanatomi

Each package looks unremarkable on a first read. base58-utils, for example, is a few kilobytes of zero-dependency Base58 / Bitcoin-WIF helper code — exactly what the name promises. The relevant code sits etter the module’s module.exports, where a reader skimming the top of the file is unlikely to look: a self-invoking function that schedules itself with a timer.

The chain of operations, reconstructed from the package source, runs like this:

1. Package is required() by the host project 2. A self-invoking function schedules a callback ~37,000 ms later (setTimeout) 3. On fire, the callback reads test/fixtures/keypairs.dat (a base64 blob) 4. It base64-decodes that blob into a Node.js script 5. It writes the script to ~/.cache-db/.node-sync/syncd.js (mode 0o700) 6. It installs login-persistence for that script (see below) 7. It spawns "node syncd.js" as a detached process

Two design choices stand out.

The payload travels as a test fixture. The second stage is not written as obvious code. It lives in test/fixtures/keypairs.dat, a base64 file whose name blends into a package that claims to handle key pairs. To a human skimming the tarball, it looks like sample data; to the appended code, it is a script to decode and run. The dropper itself contains no network address — those live in the second stage — but there is no additional obfuscation: the fixture is a single layer of base64, so decoding it (base64 -d) recovers the full syncd.js and its network behavior. The next section walks through what that recovered stage does.

Detonation is delayed and tied to import, not install. Because the trigger is krever () plus a ~37-second timer rather than an install hook, the behavior sidesteps checks that only watch the npm installasjon step, and the delay outlasts many short-lived sandbox and CI runs. By the time anything executes, the install that pulled the package in has long finished.

Once the decoded script is on disk at ~/.cache-db/.node-sync/syncd.js — a path chosen to read like a routine cache directory — the dropper makes it survive reboots on all three major platforms:

  • Linux: a cron entry that relaunches the script. The entry is installed by filtering the existing crontab through grep -v syncd, which has the side effect of leaving the new entry out of a plain listing that greps for the same name.
  • Windows: a scheduled task named WinNodeSync, set to re-run on a 12-minute interval.
  • MacOS: a launchd job labelled com.apple.syncd, present in several of the packages — a label that mimics a legitimate Apple system service.

The script is then started immediately as a detached process, so it continues running after the importing program exits.

What the second stage does

Because the fixture is a single base64 layer, the second stage decodes cleanly and can be read in full. All eight packages carry one of three variants of the same script, which identifies itself in a header comment as phantom syncd v3 — topo durmiente (“sleeping mole”). Its job is to steal cryptocurrency-wallet material and developer secrets and ship them off the machine. It runs in these steps:

  • 1. Wait until the machine is idle. Before doing anything, syncd.js checks how long the user has been inactive and only proceeds past a threshold (around 15 minutes) — xprintidle på Linux, ioreg HIDIdleTime on macOS, and a PowerShell idle-time query on Windows. Harvesting therefore tends to happen when nobody is at the keyboard.
  • 2. Fetch a remotely-controlled activation switch. The script pulls a small config from a dead-drop before acting. The primary source is a GitHub gist raw URL (gist.githubusercontent.com/juang55/…/cfg.txt); the script only activates if that config reads active=1. If the gist is unavailable, it falls back to three hardcoded IPFS content identifiers, fetched through the public gateways gateway.pinata.cloud, ipfs.ioog cloudflare-ipfs.com. This gives the operator an after-the-fact arm/disarm control and a takedown-resistant fallback. (The smallest variant, shipped in crypto-validate-lib, eth-wallet-helpersog solana-key-utils, has the gist layer removed and relies on the IPFS identifiers alone.)
  • 3. Harvest wallet material and secrets. The script walks the user’s home directory — ~/.config/solana, ~/.ethereum/nøkkelbutikk, ~/.foundry, ~/.hardhat, ~ / .sshog desktop/dokumenter/Nedlastinger (including Spanish-language folder names), plus ~/.konvolutt and shell rc files, and the equivalent AppData locations on Windows. It targets Ethereum private keys, Bitcoin WIF keys, BIP-39 seed phrases, Solana keypairs, Ethereum keystore JSON, SSH keys, and secret-bearing environment variables. The larger variant (in abi-encode, base58-utils, eth-dev) carries the full 2048-word BIP-39 dictionary and uses regular expressions to trekke ut individual keys and validated seed phrases from any text it reads; the two smaller variants instead match files by keyword (seed, mnemonic, lommebok, metamask, phantom, hovedbok, Trezor, …) and upload whole files.
  • 4. Encrypt and exfiltrate through a public pinning service. Each finding is bundled with a host fingerprint (username@hostname, platform, timestamp) and encrypted with a hardcoded RSA-4096 public key embedded in the script. The encrypted record is then uploaded by pinning it to IPFS via api.pinata.cloud/pinning/pinJSONToIPFS, authenticated with hardcoded Pinata API credentials. There is no bespoke C2 server to seize: the stolen data is parked in public decentralized storage, retrievable by the operator using the resulting content hashes. Uploads are spaced with a few seconds of random jitter, and a local log of timestamp | key-type | returned-hash is kept at ~/.cache-db/.node-sync/.sl.
  • 5. Persist and beacon on a 12-hour cycle. The second stage re-establishes its own persistence — a cron entry on Linux, a com.apple.syncd launchd job on macOS, and a scheduled task named WindowsNodeSync on Windows — set to re-run every 12 hours. Note this is a forskjellig Windows task name from the one the dropper installs (WinNodeSync); both are worth hunting for.

Tidslinje

The eight packages were published in a tight burst on 2026-07-13 and 2026-07-14. Several carry more than one version; the dropper is identical across versions, with only line offsets shifting as the size of the benign utility code above it changes.

Dato Event
2026-07-13 First packages in the cluster appear under one publisher (solana-key-utils, eth-wallet-helpers, crypto-validate-lib and early versions of the rest)
2026-07-13 → 07-14 Remaining names and follow-up versions published; the same appended dropper ships in each
2026-07-14 All eight flagged and analyzed; every package still installable from the registry

Across the burst, the publisher’s registry reputation moved from roughly neutral at the start of the cluster to strongly negative as detections accumulated — an observable side effect of the packages being flagged, not a designed feature.

Indikatorer for kompromiss

All indicators below were confirmed present at analysis time — those in the “Second-stage” tables by decoding test/fixtures/keypairs.dat and reading the recovered syncd.js.

Files and paths

Indikator Rolle
~/.cache-db/.node-sync/syncd.js Decoded second stage, written with mode 0o700
~/.cache-db/.node-sync/.sl Local exfiltration log (timestamp | key-type | IPFS hash)
test/fixtures/keypairs.dat Base64-encoded payload staged inside the tarball as a "test fixture"

Second-stage network infrastructure (recovered from syncd.js)

Indikator Rolle
gist.githubusercontent.com/juang55/b298754cb72942b1cdcf02ccd45cde2f/raw/cfg.txt Activation dead-drop; script runs only if config reads active=1
Qmcqz3w8j4qFQXDAXAxnrdc2oSX3nzBT4NqtpTqL8mr1ga IPFS config fallback (CID)
QmdTXoqVmTHY1i4ZWLdLkoQ9YChp5TXPh5cWXwnAYZt5iF IPFS config fallback (CID)
QmfJkLU5gdCpqbbqEjWYC2anXW9FmuEeSLLeLiHVJKYUjp IPFS config fallback (CID)
gateway.pinata.cloud, ipfs.io, cloudflare-ipfs.com IPFS gateways used to fetch the config fallback
api.pinata.cloud/pinning/pinJSONToIPFS Exfiltration endpoint, stolen data pinned to public IPFS
Pinata API key 13c766575b9270a9825d, hardcoded exfiltration credential

Second-stage collection targets (recovered from syncd.js)

Indikator Rolle
~/.config/solana, ~/.ethereum/keystore, ~/.foundry, ~/.hardhat, ~/.ssh, ~/.env, shell rc files Directories/files searched for keys and secrets
AppData\Roaming\Solana, AppData\Local\ethereum\keystore Windows equivalents searched
Artifact types harvested ETH private keys, Bitcoin WIF, BIP-39 seed phrases, Solana keypairs, Ethereum keystore JSON, SSH keys, secret env vars

Persistence artifacts

Plattform Indikator
Linux cron entry launching syncd.js; installed via crontab filtered through grep -v syncd
Windows planlagt oppgave WinNodeSync (dropper) and WindowsNodeSync (second stage)
macOS launchd label com.apple.syncd
Alle Second stage re-persists itself on a 12-hour cycle

Behavioral

  • Self-invoking function appended after module.exports, planlegger en setTimeout of ~37,000 ms on import.
  • barneprosess spawn(“node”, <syncd.js>) with the detached option set.
  • Idle-gated activation in the second stage (xprintidle / ioreg HIDIdleTime / PowerShell idle time; ~15-minute threshold).
  • Per-finding RSA-4096 encryption, then HTTPS POST to a public IPFS pinning API.

Packages and versions (npm, publisher solbuilder_io)

Pakke versjoner
base58-utils 1.0.0, 1.0.1, 1.0.3
abi-encode 1.0.0, 1.0.1, 1.0.2
eth-dev 1.0.0, 1.0.1, 1.0.2
arb-kit 1.0.0, 1.0.1
layer2-sdk 1.0.0, 1.0.1
solana-key-utils 1.0.0
eth-wallet-helpers 1.0.0
crypto-validate-lib 1.0.0

Publisher

  • solbuilder_io - angel_lopez89[@]proton[.]me, email unverified, no verified source-control account, no linked repository.

Attribusjon og observert atferd

The eight packages share one publisher account and one payload. Each is a trivial package — a few kilobytes of real utility code with the same dropper appended — published without a linked repository and under an unverified disposable-style email. That uniformity, the shared drop path, the shared persistence labels, and the shared keypairs.dat staging file are what tie the cluster together.

The packages are unusually candid about their own behavior. solana-key-utils carries inline comments that describe the appended block in plain terms — one labels it PHANTOM: invisible persistence, another (in Spanish) reads Ejecutar topo en background, “run the mole in the background.” These are the code’s own annotations of what it does; the campaign name in this post is drawn from that first label together with the fake node “sync daemon” (syncd) that the persistence machinery imitates.

We describe only what the code observably does. The naming of the packages — all crypto, wallet, and blockchain-tooling terms — indicates the developer audience most likely to pull them by name; it does not by itself establish who the publisher is. The second stage was fully recoverable by decoding the base64 fixture, and its behavior is described above: it collects wallet keys, seed phrases, and secrets and exfiltrates them, RSA-encrypted, to public IPFS storage via Pinata. A notable design choice is the absence of a private C2 server — configuration arrives from a GitHub gist and IPFS, and stolen data is parked in public decentralized storage keyed by content hash, both of which are harder to seize than a single attacker-controlled host. No prior public reporting matching this cluster was located at the time of writing.

Hvem som er eksponert. Anyone who added one of these packages to a Node project and then ran code that imports it. Because detonation is import-time rather than install-time, simply having the package present is not enough — but any normal use that loads the module reaches the payload. The lure names target developers building on Ethereum, Solana, Arbitrum, Layer-2, and general wallet/encoding tooling — the population most likely to have exactly the assets the second stage hunts for. Anyone who ran one of these modules on a machine holding wallet keys, seed phrases, keystores, SSH keys, or .og V secrets should treat those credentials as compromised and rotate them.

Two patterns worth internalizing. Først payload-as-fixture: shipping the second stage as base64 inside a plausibly-named data file (test/fixtures/keypairs.dat) keeps the visible source of the package looking clean and pushes the malicious content into a file that review tools and human skims often treat as inert data. Second, import-time delayed execution with cross-platform persistence: moving the trigger off the install hook, adding a timer, and then persisting across cron, scheduled tasks, and launchd is a deliberate step away from the noisier install-script techniques that automated registry scanning watches most closely.

Guidance for defenders and maintainers:

  • Treat appended code after module.exports as a review priority — dropper logic frequently hides below the “real” module surface.
  • Do not assume data files are inert. A base64 blob under test/fixtures/ that is read at runtime and decoded is executable-adjacent; flag runtime reads of fixture files that feed a Funksjon/eval/write-then-spawn kjede.
  • Hunt for the drop path ~/.cache-db/.node-sync/ and for the persistence units WinNodeSync (scheduled task) and com.apple.syncd (launchd) on developer machines that pulled these names.
  • Alert on Node processes that create cron entries, scheduled tasks, or launchd jobs — legitimate libraries rarely do this on import.
  • Prefer lockfiles and pinned versions, and review the diff of any new small “utility” dependency, especially zero-dependency packages published by unverified accounts with no linked repository.

For registry defenders, the takeaway is that install-hook monitoring is necessary but not sufficient: an import-time, timer-delayed dropper staged inside a data file will pass an install-time-only check, and the persistence step is often the loudest observable signal left to catch.

sca-tools-programvare-verktøy for komposisjonsanalyse
Prioriter, utbedre og sikre programvarerisikoene dine
Få din gratis konto.
Ingen kredittkort kreves.

Sikre programvareutviklingen og -leveringen din

med Xygeni-produktpakken