Author: Crimson7 Threat Intelligence
Date: August 5, 2026
Version: 1.0
Classification: TLP
Active threat. At publication,
awqhnjewqjkl[.]icuis 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).

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.

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.

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
| Source | Result |
|---|---|
| VirusTotal | 4/91 malicious, tagged "dga" |
| ThreatFox | botnet_cc, confidence 100 |
| Cisco Talos | High threat, "Questionable". NOT ON BLOCKLIST |
| AlienVault OTX | 0 PULSES |
| URLhaus | NOT LISTED |
| urlscan.io | 0 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.

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) | Action | Domains |
|---|---|---|
| 2026-05-25 07:41 | DEPLOY | Contract created |
| 2026-05-25 07:43 | replaceAll() | npm-cache.com, pypi-get.com, js-mirror.com |
| 2026-07-20 17:49 | replaceAll() | pypi-get.com only (reduced to 1) |
| 2026-07-22 16:56 | replaceAll() | npm-cache.com, pypi-get.com, js-mirror.com (restored) |
| 2026-08-04 19:43 | replaceAll() | 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
| Technique | ID | Implementation |
|---|---|---|
| Supply Chain Compromise | T1195.002 | npm preinstall hook |
| Command & Scripting Interpreter | T1059.007 | JavaScript via Bun runtime |
| Credentials from Password Stores | T1555 | AWS, Vault, K8s, .npmrc, wallets |
| Unsecured Credentials | T1552.004/.005 | SSH keys, cloud metadata (EC2 IMDS, ECS) |
| Exfiltration Over Web Service | T1567 | GitHub API + HTTPS POST |
| Dynamic Resolution | T1568.002 | Ethereum contract returns domains |
| Encrypted Channel | T1573.001 | AES-256-GCM + RSA-OAEP |
| Forge Web Credentials | T1606 | Sigstore attestation forgery |
| Dead Drop Resolver | T1102.001 | Ethereum contract as C2 store |
| Indicator Removal | T1070 | Temp cleanup, evidence deletion |
| Acquire Infrastructure | T1583.001 | 3 domains in 8 seconds (automated) |
Detection recommendations
- Network: Monitor for eth_call containing selector
0x53ed5143to contract0xE1f2395.... Blockawqhnjewqjkl[.]icu(IPs: 104.21.91.101, 172.67.215.154). Alert on HTTPS POST to/routeron recently-registered domains. - Endpoint: Detect PID lock file
tmp.dpkg_14527.lock. Monitor for_NODE_RUNTIME_INIT=1in process env vars. Detect systemd services or LaunchAgents namedgh-token-monitor. Alert on modifications to.claude/settings.jsonor.vscode/tasks.json. - Supply chain: Alert on npm packages with
preinstallhooks referencingsetup.mjs. Verify Sigstore attestations independently (this malware forges valid attestation). - Blockchain: Periodically query contract
0xE1f2395...with selector0x53ed5143to discover new C2 domains before blocklists update. Monitor the owner wallet for new transactions.
The persistent indicator. In the analyzed samples, the contract address
0xE1f2395ee43e45A1556EC6438a88c31B83493103is hardcoded and therefore more stable than the associated domains and IP addresses. Changing it would require distributing a modified payload.
Priority IOCs
| Indicator | Type | Context |
|---|---|---|
0xE1f2395ee43e45A1556EC6438a88c31B83493103 | Ethereum contract | Selectors: 0x53ed5143 (read), 0x8da5cb5b (owner) |
0x55f9780e1492344b7417fa723aedc4d0b97f31cd | Owner wallet | Only wallet authorized to rotate domains |
awqhnjewqjkl[.]icu | Active C2 | 104.21.91.101, 172.67.215.154, NameSilo, 2026-08-04 |
npm-cache[.]com, pypi-get[.]com, js-mirror[.]com | Previous C2 | Seeded 2026-05-25, restored 2026-07-22 |
54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668 | Stage 1 SHA-256 | setup.mjs |
9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc | Stage 2 SHA-256 | Math_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.