Get our latest research in your inbox

New threat intelligence, detection engineering, and red team write-ups, delivered when we publish.

Shai-Hulud's Ethereum C2: Inside a Blockchain-Backed npm Supply-Chain Attack

August 5, 2026Crimson7 Threat Intelligence
threat intelligencesupply chainnpmEtherHidingEthereumTeamPCPShai-HuludChainDropcredential theftthreat hunting

Author: Crimson7 Threat Intelligence Date: August 5, 2026 Version: 1.0 Classification: TLP Threat Type: Software Supply-Chain Compromise / Blockchain C2 Severity: High (active, C2 live at publication)


Active threat. At publication, awqhnjewqjkl[.]icu is live and absent from the major public threat intelligence feeds we checked. Products relying on those feeds may not block it.


1. Key findings

On August 4, 2026, the npm package keyv (~1.1M weekly downloads) was found shipping a trojanized version (6.0.0) as part of the ChainDrop campaign, attributed to the Russian-speaking threat actor TeamPCP (UNC6780). The malware harvests credentials from 140+ file paths across Linux, Windows, and macOS, targeting cloud providers, cryptocurrency wallets, AI coding tools, and secret management systems.

The sample contains no hardcoded C2 domains. Instead, it retrieves the current exfiltration endpoint from an Ethereum smart contract at runtime (a technique called EtherHiding), allowing the operator to rotate domains without modifying the package.

We reproduced the malware's eth_call, recovered the active C2 domain (awqhnjewqjkl[.]icu) and its rotation history (7 transactions over 72 days), and decrypted 294 obfuscated strings with an offline decryptor. The contract owner's wallet was funded through the FixedFloat exchange.


2. From npm install to compromise

The attack begins when a developer runs npm install keyv@6.0.0, or when npm update pulls in version 6.0.0 as part of a routine dependency upgrade. The trojanized package.json includes a preinstall hook that executes node setup.mjs automatically, with no prompt and no warning. The package contains two unexpected files alongside the legitimate dist/ directory: setup.mjs (Stage 1, 29KB) and Math_Symbol.js (Stage 2, 727KB).

Figure 1: Trojanized package.json. The files array bundles setup.mjs and Math_Symbol.js alongside legitimate dist/ and LICENSE. The preinstall hook (line 83) runs node setup.mjs automatically on npm install.

Beyond keyv, the same payload appeared in flat-cache@6.1.24, file-entry-cache@11.1.6, cacheable-request@13.0.20, and cache-manager@7.2.10. Together, the affected packages receive more than two million downloads per week. The packages appear to have been published using compromised npm publisher tokens.

Two-stage architecture

Stage 1 (setup.mjs) downloads Bun v1.3.13 from official GitHub releases and executes Stage 2 via bun Math_Symbol.js. Downloading Bun from its official GitHub repository reduces suspicion around the executable, though the malicious behavior remains visible in the surrounding script.

Before continuing, Stage 2 performs three checks. First: a Russian locale check. If the system locale is Russian, the malware calls process.exit(0) silently. The check is hidden behind a JavaScript comma operator trick and a basE91 decoder layer, making static identification difficult. Second: CI/CD detection. Despite its name, checkOS() checks for CI/CD environments, not the OS type. In CI, the malware runs in the foreground (CI environments hold secrets). On a developer workstation, it spawns a detached background process and the parent exits, so npm install returns normally. Third: a PID lock file at ${TMPDIR}/tmp.dpkg_14527.lock, using a filename resembling a Debian package-manager lock.

Figure 2: The init() function. Three gates: Russian locale kill switch (comma operator trick), CI/CD detection with process backgrounding, and PID lock file with SIGINT trapping.


3. How the Ethereum C2 works

Grep across all 11,696 deobfuscated lines for known C2 domains (npm-cache, pypi-get, js-mirror) returns zero matches. The binary contains 35 hardcoded Ethereum RPC endpoints (decoded from an encrypted array), seven with embedded API keys. The malware cycles through these RPCs to reach the Ethereum network.

Figure 3: Main execution flow after init(). contractAddress (base64-encoded), port 443, harvestCredentials() call, GitHubExfil and HTTPSFallback channel setup, and ExfilDispatcher orchestration.

The contract address 0xE1f2395ee43e45A1556EC6438a88c31B83493103 and function selector 0x53ed5143 are extracted from the deobfuscated source (lines 11632-11650). The contract type is StringListStore with a replaceAll(string[]) write function.

Replicating the eth_call

An eth_call is a local, read-only query against blockchain state. It costs no gas and requires no wallet:

// JSON-RPC request (read-only, no gas, no wallet)
POST https://ethereum-rpc.publicnode.com
{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [{
    "to": "0xE1f2395ee43e45A1556EC6438a88c31B83493103",
    "data": "0x53ed5143"
  }, "latest"],
  "id": 1
}

The response contains ABI-encoded data. Decoding the nested offsets yields a string[] with one entry: awqhnjewqjkl.icu. At the time of analysis, the contract returned this domain; it does not appear directly in the binary.

Verifying the extracted domain

SourceResult
VirusTotal4/91 malicious, tagged "dga"
ThreatFoxbotnet_cc, confidence 100
Cisco TalosHigh threat, "Questionable". NOT ON BLOCKLIST
AlienVault OTX0 PULSES
URLhausNOT LISTED
urlscan.io0 SCANS

The contract also exposes owner() (selector 0x8da5cb5b, standard OpenZeppelin Ownable pattern). Querying it returns the owner wallet: 0x55f9780e1492344b7417fa723aedc4d0b97f31cd. This is the only wallet authorized to call replaceAll() and rotate domains.


4. What happens after infection

Credential harvesting

Decrypting 294 strings exposed roughly 70 Linux paths, 50 macOS paths, and 20 Windows paths targeted by the sample. Targets include AWS credentials, GCP service accounts, Azure tokens, SSH keys, cryptocurrency wallets (Bitcoin, Ethereum/Brownie), HashiCorp Vault tokens, Kubernetes service accounts, and AI coding tool configs (.claude/credentials.json, Cursor, Windsurf). The malware also reads 40+ environment variables, covering CI platform tokens (GitHub, GitLab, Jenkins, CircleCI, Travis, Bitbucket), cloud credentials, and Kubernetes/Vault secrets. In CI runners, it uses sudo python3 with /proc/*/mem to dump process memory for secrets.

Three exfiltration channels

Channel 1: HTTPS POST. Credentials encrypted with AES-256-GCM, AES key wrapped with the attacker's RSA-4096 public key via RSA-OAEP. POSTed to https://<C2>:443/router. The C2 response can contain a code field passed directly to eval(), giving the attacker arbitrary RCE on infected machines.

Channel 2: GitHub commits. Creates public repos under the victim's account using stolen tokens (Dune-themed names: prescient, tleilaxu, siridar, sandworm). Data committed via GraphQL mutations. Based on observed repo naming patterns and token scope, this produced an estimated 25,000-27,000 repositories.

Channel 3: FIRESCALE fallback. If primary C2 and all 35 Ethereum RPCs fail, searches GitHub commits for marker strings ("thebeautifulmarchoftime", "thebeautifulsnadsoftime") to resolve a backup C2 domain.

Worm propagation and persistence

The malware writes malicious configuration to .claude/settings.json and .vscode/tasks.json in repositories for IDE-level persistence. It creates a GitHub Actions workflow (.github/workflows/codeql_analysis.yml) on a branch named to look like a Dependabot update, exfiltrating all repo secrets via ${{ toJSON(secrets) }}. Git commits are authored as claude@users.noreply.github.com.

A dead-man's switch installs persistent token revocation watchers via systemd user services (Linux) and LaunchAgents (macOS). These poll the GitHub API every 60 seconds and trigger a handler when tokens are revoked. This allows the malware to react when defenders revoke a token.

Breaking the obfuscation: 294 strings + basE91

The sample protects its embedded configuration using two encoding and encryption layers. Layer 1: PBKDF2-HMAC-SHA256 (200,000 iterations, salt svksjrhjkcejg) derives a master key, then a Fisher-Yates shuffle with counter-mode SHA-256 PRNG generates a per-byte polyalphabetic substitution cipher. Our offline decryptor recovered 294 strings (1,482 lines of output), including RPC URLs, target file paths, CI variables, Dune-themed repo names, GitHub/npm API endpoints, the OpenSearch-JS dependency pin, and debug messages. Layer 2: ~25 additional strings use basE91 encoding via a makeDecoder() function (lines 540-570) with a custom 91-character alphabet across 12+ call sites. These remain undecrypted because decoding them requires runtime execution, which this investigation avoided.

Sigstore attestation forgery

The code uses Sigstore's OIDC flow to produce a cryptographically valid attestation containing false provenance: it exchanges a GitHub Actions OIDC token for an npm publish token, obtains a Fulcio certificate, records the attestation in the Rekor transparency log, and constructs a fake SLSA v1 provenance document. If this executes successfully, npm audit signatures on an infected package would show valid attestation.

Figure 4: Sigstore attestation forgery constants. Fulcio certificate signing, Rekor transparency log, SLSA provenance type, in-toto statement format, and GitHub Actions build type URI.


5. Wallet tracing, domain rotation, and intelligence gaps

Following the money

The contract owner wallet (0x55f9780e...) was funded with 0.01806 ETH from 0x4E5B2e1dc63F6b91cb6Cd759936495434C7e972F, a FixedFloat exchange hot wallet flagged as Phish/Hack. A secondary operational wallet (0x44f55B3f...) received 0.00436 ETH from the owner. At the observed transaction cost (~0.00002 ETH per replaceAll() call), the remaining 0.01358 ETH balance would fund roughly 680 additional rotations.

Domain rotation timeline

Blockscout shows the following contract transactions:

Date (UTC)ActionDomains
2026-05-25 07:41DEPLOYContract created
2026-05-25 07:43replaceAll()npm-cache.com, pypi-get.com, js-mirror.com
2026-07-20 17:49replaceAll()pypi-get.com only (reduced to 1)
2026-07-22 16:56replaceAll()npm-cache.com, pypi-get.com, js-mirror.com (restored)
2026-08-04 19:43replaceAll()awqhnjewqjkl.icu (emergency rotation)

84 seconds between contract deploy and the first domain seed. On July 20, the owner temporarily reduced the list to one domain, restoring the original three two days later. About four hours after public disclosure on August 4, the owner replaced the list with a single .icu domain registered through NameSilo. The original 3 domains were registered within 8 seconds with near-sequential registry IDs, consistent with automated registration. One of them, js-mirror[.]com, is a re-registered expired domain. Reusing an expired domain may have provided residual reputation, though the registration data does not establish why it was selected.

Threat intel coverage gaps

Only npm-cache[.]com has an OTX pulse. The other 3 C2 domains have zero OTX pulses, zero urlscan.io scans, and are not in URLhaus. At the time checked, three of the four domains were absent from all three platforms. ThreatFox has two separate submission sets from different reporters with a 2.5-month gap.


6. Detection and incident response

MITRE ATT&CK mapping

TechniqueIDImplementation
Supply Chain CompromiseT1195.002npm preinstall hook
Command & Scripting InterpreterT1059.007JavaScript via Bun runtime
Credentials from Password StoresT1555AWS, Vault, K8s, .npmrc, wallets
Unsecured CredentialsT1552.004/.005SSH keys, cloud metadata (EC2 IMDS, ECS)
Exfiltration Over Web ServiceT1567GitHub API + HTTPS POST
Dynamic ResolutionT1568.002Ethereum contract returns domains
Encrypted ChannelT1573.001AES-256-GCM + RSA-OAEP
Forge Web CredentialsT1606Sigstore attestation forgery
Dead Drop ResolverT1102.001Ethereum contract as C2 store
Indicator RemovalT1070Temp cleanup, evidence deletion
Acquire InfrastructureT1583.0013 domains in 8 seconds (automated)

Detection recommendations

  • Network: Monitor for eth_call containing selector 0x53ed5143 to contract 0xE1f2395.... Block awqhnjewqjkl[.]icu (IPs: 104.21.91.101, 172.67.215.154). Alert on HTTPS POST to /router on recently-registered domains.
  • Endpoint: Detect PID lock file tmp.dpkg_14527.lock. Monitor for _NODE_RUNTIME_INIT=1 in process env vars. Detect systemd services or LaunchAgents named gh-token-monitor. Alert on modifications to .claude/settings.json or .vscode/tasks.json.
  • Supply chain: Alert on npm packages with preinstall hooks referencing setup.mjs. Verify Sigstore attestations independently (this malware forges valid attestation).
  • Blockchain: Periodically query contract 0xE1f2395... with selector 0x53ed5143 to discover new C2 domains before blocklists update. Monitor the owner wallet for new transactions.

The persistent indicator. In the analyzed samples, the contract address 0xE1f2395ee43e45A1556EC6438a88c31B83493103 is hardcoded and therefore more stable than the associated domains and IP addresses. Changing it would require distributing a modified payload.

Priority IOCs

IndicatorTypeContext
0xE1f2395ee43e45A1556EC6438a88c31B83493103Ethereum contractSelectors: 0x53ed5143 (read), 0x8da5cb5b (owner)
0x55f9780e1492344b7417fa723aedc4d0b97f31cdOwner walletOnly wallet authorized to rotate domains
awqhnjewqjkl[.]icuActive C2104.21.91.101, 172.67.215.154, NameSilo, 2026-08-04
npm-cache[.]com, pypi-get[.]com, js-mirror[.]comPrevious C2Seeded 2026-05-25, restored 2026-07-22
54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668Stage 1 SHA-256setup.mjs
9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bccStage 2 SHA-256Math_Symbol.js

Attribution

TeamPCP (UNC6780 per Mandiant). Russian-speaking. Campaign: ChainDrop. Malware family: Shai-Hulud (Malpedia: js.shai_hulud). Hosting: IP Vendetta Inc. (AS209101, Seychelles), Virtual Systems LLC (vsys.host, Netherlands). CVE-2026-45321 (CVSS 9.6). FBI Flash Alert issued 2026-07-02.


7. Conclusion

Shai-Hulud resolves its C2 through an Ethereum contract and spreads through developer tooling and GitHub Actions. If an affected package executed, query the contract for the current domain, hunt for gh-token-monitor services and modified IDE configuration files, and rotate credentials accessible to the affected host or CI runner.


This research was conducted by Crimson7 as part of an independent threat hunting investigation. All analysis was performed on artifacts obtained from public sources. Domains are defanged per standard practice. The Ethereum contract queries documented here are read-only operations requiring no private keys or privileged access.