VCSツール(gitとmercurial)使用時の引数インジェクションを再検証する
Alessio Della Libera
2022年8月23日
0 分で読めます今回の調査の主な目的の一つは、コマンドインジェクションを防ぐ安全なAPIを使用していても、任意のコマンドを実行できてしまう仕組みを調べることでした。対象とするのは、gitやhg(mercurial)などのバージョン管理システム(VCS)ツールです。これらのツールには、状況によって任意のコマンドを実行できるオプションがあります。
今回の調査対象は、安全なAPIを使ってこれらのコマンドを呼び出すWebアプリケーションとライブラリプロジェクト(プログラミング言語は問いません)です。CLIプロジェクトや、設定ファイルなどの保存されたデータを使ってこれらのコマンドを呼び出すプロジェクトは対象としていません。
このブログ記事では、安全なAPIを使用している場合でも、引数オプションのインジェクションによって任意のコマンドを実行できてしまう一般的なケースを紹介します。また、オープンソースのメンテナーに向けて、修正例と推奨事項も提示します。
引数インジェクションの基礎
次のような状況を考えてみましょう。開発者は、リポジトリを操作するために、プロジェクトディレクトリからgitまたはhgコマンドを実行したいと考えています。しかし、ユーザーが指定した値をこれらのコマンドのパラメーターとして使用し、リポジトリURLやブランチ名などをユーザーが指定できるようにしています。開発者はコマンドインジェクションの脆弱性を認識しているため、安全なAPIを使ってシェルコマンドを実行することにしました。たとえば、プロジェクトがNode.jsの場合、child_processモジュールのspawn APIを次のように呼び出すと、コマンドインジェクションを防げます。
しかし、必ずしもそうとは限りません。後ほど説明するように、APIが呼び出すコマンドによっても状況は異なります。引数インジェクションについて詳しく知りたい場合は、Staaldraadの記事が参考になります。
なぜgitが重要なのか?
gitの一部のサブコマンドには、実行するプログラムやコマンドをユーザーが指定できるオプションがあります。その一例が--upload-packオプションで、次のgitサブコマンドで使用できます。
ここでは、--upload-packオプションを実装しているgit fetch、ls-remote、pullサブコマンドを例に見てみましょう。
コマンドによって、必須引数の後に任意の引数が続きます。たとえばgit fetchは、リポジトリなどの引数を少なくとも1つ必要とし、その後にrefspecと一連のオプションを指定します。
ターミナルで次のコマンドを実行すると、シェルコマンドtouch HELLOが実行されます。
「git」を使った脆弱なコードとは?
この脆弱性を説明するため、JavaScriptとPythonを使います。例を簡潔にするため、urlとbranchの値はユーザーが指定し、適切にサニタイズされていないものとします。
次の例では、従来型のコマンドインジェクションを防ぐ安全なAPIを使っていても、任意のコードを実行できてしまうことを示します。
JavaScript(NodeJS)
最後の行は、通常のコマンドインジェクションペイロード(コマンドを連結するために「; & |」などの特殊文字を使うもの)が機能しないことを示しています。
Python
上記のケースでコマンドを実行できるのは、対応するgitサブコマンドに必要な引数が指定されている場合に限られる点に注意が必要です。たとえば、git ls-remoteサブコマンドに引数を1つだけ指定し、他の引数がない場合、先ほどのケースは機能しません。
JavaScript(NodeJS)
指定された唯一の引数がリポジトリとして解釈されるためです(git ls-remoteの場合)。コマンドに別の有効な引数を渡せば、先ほどと同じ状況になり、引き続きコードを実行できます。
修正方法の提案
先ほどのケースに対する解決策の一つは、ユーザーが指定した値の前に--を追加することです。公式ドキュメントには、「任意のユーザー入力を処理するスクリプトを書く場合、適切な位置に区切り文字の--を置いて引数を明確にするのがよい」と説明されています。
たとえば、次のコードではコマンドが実行されなくなります。
hg(mercurial)に注意が必要なのはなぜ?
前のセクションで見たように、任意のコマンドを実行するには、必須引数などの条件を満たす必要があります。しかし、hgではこの条件が当てはまらないため、悪用がはるかに容易です。
mercurialでは、さまざまなオプションに加えて、–configオプションを使い、clone、init、logなど、すべてのコマンドにaliasesとhooksを指定できます。
mercurialのドキュメントによると、エイリアスを使うと「他のコマンド(またはエイリアス)をもとに、必要に応じて引数を含めた独自のコマンドを定義できます。エイリアスの先頭に感嘆符(「!」)を付けると、シェルエイリアスになります。シェルエイリアスはシェルで実行されるため、たとえば次のように任意のコマンドを実行できます。」
こうすると、"hg echo foo"を実行して、ターミナルに"foo"を表示できます。
一方、フックとは、「コミットの開始時や終了時など、さまざまなアクションによって自動的に実行されるコマンドまたはPython関数」です。条件が満たされたときに実行されるコマンドで、他のmercurialコマンドの実行前(pre-<command>)または実行後(post-<command>)に実行できます。
「hg init」を使った脆弱なコードとは?
サニタイズされていないユーザー入力を使ってhg initコマンドを呼び出した場合に、任意のコマンドを実行できる例を見てみましょう。
エイリアスの例
touch HELLOコマンドが実行されます。initコマンドのエイリアスが定義されており、hg initコマンドの代わりに実行されるためです。
コードから同じコマンドを実行すると、次のようになります(前と同様、sourceはユーザーが指定し、適切にサニタイズされていないものとします)。
JavaScript(NodeJS)
Python
フックの例
touch HELLOコマンドは、initコマンドの実行前(つまりpre-<command>オプション)に実行されます。
コードから同じコマンドを実行すると、次のようになります。
JavaScript(NodeJS)
Python
gitの場合とは異なり、mercurialコマンドで任意のコマンドを実行するには、引数が1つ制御できれば十分です。たとえそれが唯一のパラメーターであっても実行できます。
修正方法の提案
ここでも、ユーザーが指定した引数の前に--を追加することでこの問題に対処できます。
たとえば、次の悪用方法は機能しなくなり、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コマンドを呼び出す、該当コードは次のとおりです。
概念実証
任意のコマンドを実行するには、ユーザーは翻訳プロジェクトで新しい翻訳コンポーネントを作成し、リポジトリのブランチに--config=alias.pull=!id>/app/cache/static/output_rce1.txtを指定します。この場合、idコマンドが実行され、その出力はhttp://localhost:8888/static/output_rce1.txtからアクセスできるページにリダイレクトされます。
1. アプリケーションにログインします。
2. 新しい翻訳プロジェクトを追加し、保存をクリックします。

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

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

修正方法
ご覧のとおり、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コマンドを呼び出す、該当コードは次のとおりです。
概念実証
次のPoCは、任意のコマンドを実行できたことを示しています。
修正方法
この問題は、メンテナーによってバージョン1.11.0で修正されました(GitHubのリリース)。前回の問題と同様に、この修正では、ユーザー制御値の前に--を追加しました。
要点
…開発者向け
コマンドインジェクションを防ぎ、ユーザー制御値を受け付ける安全なAPIを使ってコマンドを呼び出す際は、ユーザーが指定した値によってコマンドオプションが挿入・操作され、コマンドの動作が変わらないようにすることが重要です。このような混乱を避けるため、--を使って引数とユーザー制御値を区切ることができます。
…メンテナー向け
関数やAPIが、ユーザー制御値を受け取るコマンドの呼び出しをラップしている場合、その動作を文書化しておくとよいでしょう。安全なAPIを使ってコマンドインジェクションを防ぐなど、一部のサニタイズには対応している一方で、入力が危険なオプションとして使われ、予期しない動作につながるケースには対応していないと知ったユーザーの混乱を避けられます。
…セキュリティ研究者向け
今回はgitとhgのコマンドに注目しましたが、同様の動作をするコマンドはほかにも数多くあります。コマンドインジェクションを防ぐ安全なAPIを使っていても、セキュリティ上の問題が発生する可能性がある点にも注意が必要です。問題が起きるかどうかは、実行するコマンドによって異なります。私たちのプログラムが支援するオープンソースプロジェクトで同様の問題(またはその他のセキュリティ脆弱性)を見つけた場合は、Snykの脆弱性開示フォームから報告してください。



