依存関係の混乱

Reading time: 15 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

基本情報

依存関係の混乱(別名:置換攻撃)は、パッケージマネージャーが意図しない、信頼性の低いレジストリ/ソース(通常は公開レジストリ)から依存関係名を解決する際に発生します。これは通常、攻撃者が制御するパッケージのインストールにつながります。

一般的な根本原因:

  • タイポスカッティング/スペルミス:reqestsをインポートする代わりにrequests(公開レジストリから解決)。
  • 存在しない/放棄された内部パッケージ:もはや内部に存在しないcompany-loggingをインポートすると、リゾルバーは公開レジストリを探し、攻撃者のパッケージを見つけます。
  • 複数のレジストリ間でのバージョンの優先:内部のcompany-requestsをインポートする際、リゾルバーは公開レジストリもクエリでき、攻撃者によって公開された「最良の」/新しいバージョンを優先します。

重要なアイデア:リゾルバーが同じパッケージ名の複数のレジストリを確認でき、「最良」の候補をグローバルに選択できる場合、解決を制約しない限り、脆弱です。

悪用

warning

すべてのケースにおいて、攻撃者は公開レジストリからビルドが解決する依存関係と同じ名前の悪意のあるパッケージを公開するだけで済みます。インストール時のフック(例:npmスクリプト)やインポート時のコードパスは、しばしばコード実行を提供します。

スペルミスと存在しないもの

プロジェクトがプライベートレジストリに存在しないライブラリを参照しており、ツールが公開レジストリにフォールバックする場合、攻撃者はその名前の悪意のあるパッケージを公開レジストリに仕込むことができます。あなたのランナー/CI/開発マシンはそれを取得して実行します。

未指定のバージョン / インデックス間の「最良バージョン」選択

開発者はしばしばバージョンを固定せず、広範囲を許可します。リゾルバーが内部と公開のインデックスの両方で構成されている場合、ソースに関係なく最新のバージョンを選択する可能性があります。内部名のrequests-companyの場合、内部インデックスに1.0.1があるが、攻撃者が公開レジストリに1.0.2を公開し、リゾルバーが両方を考慮する場合、公開パッケージが勝つ可能性があります。

AWSの修正

この脆弱性はAWS CodeArtifactで発見されました(詳細はこのブログ投稿を参照)。AWSは依存関係/フィードを内部と外部としてマークする制御を追加し、クライアントが上流の公開レジストリから「内部」名を取得しないようにしました。

脆弱なライブラリの発見

依存関係の混乱に関する元の投稿で、著者は内部パッケージ名を推測するために何千もの公開されたマニフェスト(例:package.jsonrequirements.txt、ロックファイル)を探し、その後、公開レジストリに高バージョンのパッケージを公開しました。

実践的な攻撃者プレイブック(認可されたテストのためのレッドチーム向け)

  • 名前を列挙:
  • マニフェスト/ロックファイルと内部ネームスペースのためにリポジトリとCI設定をgrepします。
  • 組織特有のプレフィックス(例:@company/*company-*、内部groupIds、NuGet IDパターン、Goのプライベートモジュールパスなど)を探します。
  • 公開レジストリでの可用性を確認:
  • 名前が公開で未登録の場合は登録し、存在する場合は内部の遷移名をターゲットにしてサブ依存関係のハイジャックを試みます。
  • 優先順位をつけて公開:
  • 「勝つ」セマンティックバージョン(例:非常に高いバージョン)を選択するか、リゾルバーのルールに一致させます。
  • 適用可能な場合は最小限のインストール時実行を含めます(例:npmのpreinstall/install/postinstallスクリプト)。Pythonの場合、ホイールは通常インストール時に任意のコードを実行しないため、インポート時の実行パスを優先します。
  • コントロールの外部流出:
  • CIから制御されたエンドポイントへのアウトバウンドが許可されていることを確認します。そうでない場合は、DNSクエリやエラーメッセージをサイドチャネルとして使用してコード実行を証明します。

caution

常に書面による承認を取得し、エンゲージメントのためにユニークなパッケージ名/バージョンを使用し、テストが終了したら直ちに公開を取り消すか、クリーンアップを調整します。

防御者プレイブック(実際に混乱を防ぐもの)

エコシステム全体で機能する高レベルの戦略:

  • ユニークな内部ネームスペースを使用し、それを単一のレジストリにバインドします。
  • 解決時に信頼レベルを混合しないようにします。承認された公開パッケージをプロキシする単一の内部レジストリを優先し、パッケージマネージャーに内部と公開のエンドポイントの両方を提供しないでください。
  • サポートされているマネージャーの場合、パッケージを特定のソースにマッピングします(レジストリ間でのグローバルな「最良バージョン」はなし)。
  • 固定とロック:
  • 解決されたレジストリのURLを記録するロックファイル(npm/yarn/pnpm)を使用するか、ハッシュ/証明書のピン留めを使用します(pip --require-hashes、Gradle依存関係の検証)。
  • レジストリ/ネットワーク層で内部名の公開フォールバックをブロックします。
  • 将来のスカッティングを防ぐために、可能な場合は公開レジストリで内部名を予約します。

エコシステムノートとセキュアな設定スニペット

以下は、依存関係の混乱を減少または排除するための実用的で最小限の設定です。これらをCIおよび開発者環境で強制することを優先してください。

JavaScript/TypeScript(npm、Yarn、pnpm)

  • すべての内部コードにスコープ付きパッケージを使用し、スコープをプライベートレジストリに固定します。
  • CIでのインストールを不変に保ちます(npmロックファイル、yarn install --immutable)。

.npmrc(プロジェクトレベル)

# Bind internal scope to private registry; do not allow public fallback for @company/*
@company:registry=https://registry.corp.example/npm/
# Always authenticate to the private registry
//registry.corp.example/npm/:_authToken=${NPM_TOKEN}
strict-ssl=true

package.json (内部パッケージ用)

{
"name": "@company/api-client",
"version": "1.2.3",
"private": false,
"publishConfig": {
"registry": "https://registry.corp.example/npm/",
"access": "restricted"
}
}

Yarn Berry (.yarnrc.yml)

npmScopes:
company:
npmRegistryServer: "https://registry.corp.example/npm/"
npmAlwaysAuth: true
# CI should fail if lockfile would change
enableImmutableInstalls: true

運用のヒント:

  • @company スコープ内でのみ内部パッケージを公開してください。
  • サードパーティのパッケージについては、クライアントから直接ではなく、プライベートプロキシ/ミラーを介して公開レジストリを許可してください。
  • 公開するパブリックパッケージのトレーサビリティを高めるために、npmパッケージの出所を有効にすることを検討してください(混乱を防ぐものではありません)。

Python (pip / Poetry)

コアルール: 信頼レベルを混合するために --extra-index-url を使用しないでください。いずれかを選択してください:

  • 承認されたPyPIパッケージをプロキシおよびキャッシュする単一の内部インデックスを公開するか、
  • 明示的なインデックス選択とハッシュピンニングを使用してください。

pip.conf

[global]
index-url = https://pypi.corp.example/simple
# Disallow source distributions when possible
only-binary = :all:
# Lock with hashes generated via pip-tools
require-hashes = true

pip-toolsを使用してハッシュ化された要件を生成する:

# From pyproject.toml or requirements.in
pip-compile --generate-hashes -o requirements.txt
pip install --require-hashes -r requirements.txt

もし公開のPyPIにアクセスする必要がある場合は、内部プロキシを介して行い、明示的な許可リストを維持してください。CIでは--extra-index-urlを避けてください。

.NET (NuGet)

パッケージIDパターンを明示的なソースに結びつけ、予期しないフィードからの解決を防ぐために、パッケージソースマッピングを使用してください。

nuget.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="corp" value="https://nuget.corp.example/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="corp">
<package pattern="Company.*" />
<package pattern="Internal.Utilities" />
</packageSource>
</packageSourceMapping>
</configuration>

Java (Maven/Gradle)

Maven settings.xml (すべてを内部にミラー; Enforcerを介してPOM内のアドホックリポジトリを禁止):

<settings>
<mirrors>
<mirror>
<id>internal-mirror</id>
<mirrorOf>*</mirrorOf>
<url>https://maven.corp.example/repository/group</url>
</mirror>
</mirrors>
</settings>

POMに宣言されたリポジトリを禁止し、あなたのミラーの使用を強制するためにEnforcerを追加します:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>enforce-no-repositories</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireNoRepositories />
</rules>
</configuration>
</execution>
</executions>
</plugin>

Gradle: 依存関係を集中管理し、ロックする。

  • settings.gradle(.kts) のみでリポジトリを強制する:
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven { url = uri("https://maven.corp.example/repository/group") }
}
}
  • 依存関係の検証(チェックサム/署名)を有効にし、gradle/verification-metadata.xmlをコミットします。

Go Modules

プライベートモジュールを設定して、パブリックプロキシとチェックサムDBが使用されないようにします。

# Use corporate proxy first, then public proxy as fallback
export GOPROXY=https://goproxy.corp.example,https://proxy.golang.org
# Mark private paths to skip proxy and checksum db
export GOPRIVATE=*.corp.example.com,github.com/your-org/*
export GONOSUMDB=*.corp.example.com,github.com/your-org/*

Rust (Cargo)

承認された内部ミラーまたはベンダーディレクトリでビルドするために crates.io を置き換えます。任意の公開フォールバックを許可しないでください。

.cargo/config.toml

[source.crates-io]
replace-with = "corp-mirror"

[source.corp-mirror]
registry = "https://crates-mirror.corp.example/index"

公開する際は、--registryを明示的に指定し、認証情報はターゲットレジストリに限定してください。

Ruby (Bundler)

ソースブロックを使用し、マルチソースGemfileを無効にして、gemが意図したリポジトリからのみ取得されるようにします。

Gemfile

source "https://gems.corp.example"

source "https://rubygems.org" do
gem "rails"
gem "pg"
end

source "https://gems.corp.example" do
gem "company-logging"
end

設定レベルで強制する:

bundle config set disable_multisource true

CI/CDとレジストリ制御の助け

  • プライベートレジストリを単一の入口として使用:
  • 開発者/CIが到達できる唯一のエンドポイントとしてArtifactory/Nexus/CodeArtifact/GitHub Packages/Azure Artifactsを使用します。
  • 内部の名前空間が上流の公開ソースから解決されないように、ブロック/許可ルールを実装します。
  • CIにおけるロックファイルは不変です:
  • npm: package-lock.jsonをコミットし、npm ciを使用します。
  • Yarn: yarn.lockをコミットし、yarn install --immutableを使用します。
  • Python: ハッシュ化されたrequirements.txtをコミットし、--require-hashesを強制します。
  • Gradle: verification-metadata.xmlをコミットし、未知のアーティファクトで失敗します。
  • アウトバウンドエグレス制御: 承認されたプロキシを介さずにCIから公開レジストリへの直接アクセスをブロックします。
  • 名前予約: サポートされている公開レジストリに内部の名前/名前空間を事前登録します。
  • パッケージの出所/証明: 公開パッケージを公開する際には、改ざんを下流でより検出しやすくするために出所/証明を有効にします。

参考文献

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする