Root Cause
CryptoJS.lib.WordArray.random() in versions prior to 4.0.0 uses Math.random() — a non-cryptographic PRNG — to produce entropy for wallet seed and private key generation. The effective key-space is reduced to approximately 2^39–2^47 bits, small enough to enumerate exhaustively on commodity hardware within hours.
// CryptoJS < 4.0.0 — INSECURE (simplified)
CryptoJS.lib.WordArray.random = function (nBytes) {
var words = [];
for (var i = 0; i < nBytes; i += 4) {
// Math.random() is a PRNG, NOT a CSPRNG; seed state is tiny
words.push((Math.random() * 0x100000000) | 0);
}
return CryptoJS.lib.WordArray.create(words, nBytes);
};
// CryptoJS >= 4.0.0 — SECURE
CryptoJS.lib.WordArray.random = function (nBytes) {
var typedArray = new Uint8Array(nBytes);
(self.crypto || self.msCrypto).getRandomValues(typedArray);
return CryptoJS.lib.WordArray.create(Array.from(typedArray), nBytes);
};
Because JavaScript engines seed Math.random() from an internal state of 64 bits or fewer (and do not re-seed between calls), an attacker who enumerates likely seed values can precompute the full set of private keys generated by affected apps. CryptoJS has been effectively unmaintained since 2023, and downstream wallet packages continued to depend on old, unpatched versions.
Attack Steps
| Step | Actor | Action |
|---|---|---|
| 1 | Researcher/Attacker | Identify wallet apps that link CryptoJS < 4.0.0 for key generation via npm dependency audit |
| 2 | Attacker | Precompute (seed → private key → EVM/BTC address) lookup table covering 2^39–2^47 seeds using commodity hardware |
| 3 | Attacker | Scan on-chain state for funded addresses matching the precomputed address set |
| 4 | Attacker – Wave 1 | 2026-05-27: sweep 431 wallets, drain ~$3.14M |
| 5 | Attacker – Wave 2 | 2026-05-30 through 2026-07-13: drain 522 additional addresses for ~$2.55M |
| 6 | Coinspect | August 2026: publish "Ill Bloom" investigation; NanChat issues public advisory 2026-08-06 |
Impact
- Total losses (lower bound): ~$5,690,922 across ~2,100 wallets
- Wave 1: ~$3.14M from 431 addresses (2026-05-27)
- Wave 2: ~$2.55M from 522 addresses (2026-05-30 – 2026-07-13)
- Chains: Ethereum and other EVM-compatible networks; Bitcoin
- Confirmed affected apps: RWallet (RRWallet), Bexo Wallet, NanChat, Bitcoin Libre, Milo Wallet
- Disclosure status: As of 2026-08-06, NanChat is the only affected app known to have published a public advisory. CryptoJS itself has been unmaintained since 2023.
Lessons for Auditors
- Audit dependency entropy sources. Any keygen or seed-generation code must be reviewed for use of
Math.random(), legacy CryptoJS, or other non-CSPRNG sources. Onlycrypto.getRandomValues()(browser/Node Web Crypto API) orcrypto.randomBytes()(Node.js) are acceptable. - Pin and audit transitive cryptographic dependencies. Packages that declare
cryptojsas a peer or transitive dependency without a version floor silently inherit this vulnerability. Include dependency scanning in CI. - Supply-chain hygiene around unmaintained libraries. Treat unmaintained cryptographic libraries as vulnerabilities. Establish a policy of replacing (not just upgrading) abandoned cryptographic dependencies.
- Monitor disclosure lag. Exploitation began in May 2026; the first public advisory appeared in August — a 2+ month gap. On-chain anomaly detection (unexpected balance sweeps across many young wallets) could surface active exploitation much earlier.