本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
先日(2022年8月)、Gitコード管理ツールであるBitbucketのコマンド実行脆弱性(CVE-2022-36804)が修正されました。
開発ベンダからは以下のアドバイザリが公開されています。
その後、脆弱性の発見者であるMaxwell Garrett氏が、脆弱性の詳細や発見の経緯などを書いた記事を公開しています。
https://blog.assetnote.io/2022/09/14/rce-in-bitbucket-server/
脆弱性の内容については氏の記事でほぼ網羅されていますが、今回のブログでは、あらためて脆弱性の概要と、氏の記事では触れられていない点(他のアプリケーションにも同様の脆弱性が生じうるか等)について少し書きたいと思います。環境はLinuxを前提としています。
脆弱性の概要
Bitbucketは、Java言語で開発されたGitコード管理ツールです。Gitリポジトリの各種処理のために、内部でOSのgitコマンドを叩いています。例えば以下のURL(正常なURL)にアクセスすると、アプリケーション内部でgitのarchiveコマンドが実行されます。
http://host/rest/api/latest/projects/{projectKey}/repos/{repoSlug}/archive?prefix=x...
下はパラメータを操作したURLです。下線の%00(NULLバイト)以降が攻撃用に追加した文字列です。
http://host/rest/api/latest/projects/{projectKey}/repos/{repoSlug}/archive?prefix=x%00--exec=/bin/bash -c 'touch /tmp/haced%23'%00--remote=file:///%00x
Bitbucketは、受け取ったprefixパラメータの値を、gitコマンドへの引数とします。イメージ的には、以下のようなコマンド引数の配列を作って、コマンドを実行させていると思われます。
{"git", "archive", "--prefix", prefix, ...}
↑
x[00]--exec=/bin/bash -c 'touch /tmp/haced#'[00]--remote=file:///[00]x[00]...
最終的にシステムコールとしてコマンドを実行するのはNative側(JNI)です。JavaがNativeを呼ぶときには、以下のようにNULLバイトで結合したコマンド引数のバイト列をJava側で作り、これをNative側に渡す仕組みになっています。
git[00]archive[00]--prefix[00]x[00]--exec=/bin/bash -c 'touch /tmp/haced#'[00]--remote=file:///[00]x...
(上記はイメージです)
Native側は、本来の引数の区切りのNULLバイトと、攻撃者がねじ込んだNULLバイトの区別ができないため、--exec
もコマンドの引数として処理してしまいます。Git archive
の--exec
はコマンドを実行するためのオプションなので、bashを通してtouch /tmp/haced
が実行されます。
以上が脆弱性の概要です。なお、攻撃に使用できたのはarchive
だけではなかったようで、Rapid7の記事にはdiff
の--output=<file>
オプションを使ってJSPファイルを対象サーバ上に出力させ、任意のコマンドを実行させる方法も紹介されています。
攻撃の制約
上の攻撃の説明では、この手の脆弱性はNULLバイトさえ挿入できれば割と簡単に攻略できるように思われるかもしれませんが、いくつかの制約があるためそう簡単ではありません。
まず、攻撃者が挿入できるコマンド引数の数に制限があります。
下の例で言えば、JavaからNative側には、①NULLバイトで結合したバイト列(4つの引数が含まれる)とともに、②本来の正しい引数の総数(3個)も渡されます。
{"AAA", "BBB[00]XXX", "CCC"} ← 本来の引数の数は3個(②)
↓
AAA[00]BBB[00]XXX[00]CCC ← NULLバイトで結合した状態では4個ある(①)
Native側のプログラムは、②の正しい引数の数(3個)を前提に処理を行うため、起動するプログラムに渡されるのは、AAA, BBB, XXXの3つの引数だけになります。仮にNULLバイトを挿入できるのが最後の引数(CCC)であったならば、後ろに新たな引数を追加することはできないということになります。
また、起動されるコマンドに、都合の良いコマンドオプション(例えば、gitの--exec
のような)が存在するということも、意味のある攻撃を実行するための条件になります。
Bitbucketに関してはこれらの条件をクリアできたため攻略に至りました。しかし、一般論としては、この種のNULLバイトインジェクションを攻略するのはそれほど簡単ではないといえます。
実施された対策
Rapid7のパッチ解析によると、以下がコードがBitbucketに追加されたとのことです。
https://attackerkb.com/topics/iJIxJ6JUow/cve-2022-36804/rapid7-analysis
コード内で呼び出されているrequireNoNullChars()
といったメソッドの名前から、NULLバイト(NULL文字)のチェック処理が導入されたと推測されます。
environment
というメソッドの名前等を見ると、このコードは環境変数のチェック処理に見えますが、コマンド引数にも同様のチェックが追加されたと思われます。
プログラム言語/ライブラリ側の対策
アプリケーション開発者の立場で考えると、いちいち自分のアプリケーションでNULLバイトチェックをするのは手間です。もし、OSコマンドの実行処理を担うライブラリやプログラム言語の機能がチェックしてくれるのであれば、楽だし確実です。
というわけで、言語/ライブラリ側がNULLバイトチェックをしてくれるのか、調べてみました。
NuProcessライブラリ
Bitbucketでは、(Java言語に含まれているProcessBuilder
やRuntime
ではなく)NuProcessというライブラリを使用してOSコマンドを起動しているようです。Bitbucketの脆弱性が公開された時点においては、NuProcessにはNULLバイトチェックがありませんでした(そのために攻撃が可能だった)。
しかし、脆弱性が公開された後に、BitbucketのエンジニアからのPull requestによって以下のチェックが追加されました。
https://github.com/brettwooldridge/NuProcess/commit/d4005b63d310a4fdf5aec3d4b7db65a4ec424d13
NULLバイトチェックが追加されたのはNuProcessのv2.0.5です。NuProcessを使用している方は、それ以降のバージョンにアップデートすることをお勧めします。
JavaのProcessBuilder
OSコマンドを起動する時、多くのアプリケーションはJava標準のProcessBuilder
かRuntime
クラスを使用するでしょう。これらについても調べてみました。
まずはソースコードが調べやすいAndroidです。AOSPのProcessBuilder
のソースコードを見たところ、Android 7以前の同クラスにはNULLバイトチェックがなく、Android 8以降(2017年)にはチェックが入っていました。以下がそのコードです。
余談ですが、この処理(あるいはNuProcessの処理)のチェック対象はStringなので、NULLバイトではなくNULL文字のチェックをしていることになります。目的は最終的なバイト列にNULLバイトが含まれないようにすることなので、バイト列を作った段階でチェックした方が個人的にはしっくりきます(ただ実際的にはStringのチェックでも支障はないでしょう)。
次はOracle JDKについてみてみます。最近のバージョンのProcessBuilder
にはNULLバイトチェックが入っていることは把握していましたが、いつごろ追加されたか分からなかったので、バージョンを遡って調べてみました。
OracleからJDKをダウンロードし、ソースコード(src.zip)を解凍して中身を見る、という地味な作業を10回ほど繰り返した結果、NULLバイトのチェックが入ったのは2014年ごろのバージョンだということが分かりました。具体的には以下のバージョンです。
Java1.7 7u65(2014年)
Java1.8 8u11(2014年)
この変更には「S8036571: (process) Process process arguments carefully」というタイトルが付けられていますが、詳細は不明です。チェック処理部分のソースコードはAndroidのものと同じなので割愛します。
NULLバイトチェックが無い古いバージョン(Java SE 1.7.0_60-b19)で試してみたところ、実際にNULLバイトでコマンド引数を挿入できました。チェックがある新しいバージョンで同じ処理をすると、java.io.IOException: invalid null character in command
が発生します。
なお、JavaのRuntime
クラスは、内部的にProcessBuilder
を呼び出すようになっています。つまりProcessBuilder
にNULLバイトチェックがあるバージョンでは、Runtime#exec()
を実行してもNULLバイトチェックが実行されます。
他のプログラム言語での対策
NULLバイトによりコマンドオプションをねじ込む攻撃は、Java特有の内部処理に依存しているため、他の言語で同様の攻撃ができる見込みはそれほどありませんが、念のため調べてみました。
まずは筆者がよく使うPHPについてです。
PHPはv7.4以降、proc_open()
の引数を配列にすることで、シェルを介さずにOSコマンドを実行できるようになりました。proc_open()
のソースコードを見てみたところ、下のようなNULLバイトチェックが入っていることが分かりました。
https://github.com/php/php-src/blob/PHP-7.4.0/ext/standard/proc_open.c#L407
せっかくなので手持ちの検証環境に入っている、Python(3.8.10)、Ruby(2.7.0p0)、Node.js(16.17.1, 18.9.1)についても調べてみました。
Python、Rubyについては、NULLバイトチェックが実装されていました。NULLバイトを検出すると「NULLバイトが含まれている」旨のエラーになります。
一方で、Node.js(child_process.spawn()
)についてはNULLバイトチェックがありませんでした。結果として、引数のNULLバイト以降の文字列が無いものと扱われます。つまり、例えば["AAA\0XXX", "BBB"]
という引数を与えると、["AAA", "BBB"]
を与えた時と同じ結果となります。
Node.jsの挙動は「NULLチェックをしていない場合にはそうなるな」という、ある意味で自然な挙動であり、Javaの挙動よりは害が少ないかもしれません。しかしセキュリティ的にあまり望ましくはないので、issueとして報告しておきました。
まとめ
今回は、NULLバイトを利用したコマンド引数のインジェクション攻撃の概要と、プログラム言語等のレベルでの対策状況について取り上げました。
本文に書いたように、言語やライブラリの対策は徐々に進んでいて、NULLバイトで何らかの攻撃が可能なケースは減ってきていると思います(コマンド引数へのインジェクションに限った話ではなく、ファイル名に対するNULLバイトインジェクションについても同じことがいえると思います)。
ただし、コマンド引数で問題になるのはNULLバイトだけではありません。Garrett氏の記事でも触れられているように、NULLバイトが使用できないとしても、「-」から始まるオプションを挿入する攻撃がありえます。
http://host/rest/api/latest/projects/~USER/repos/repo1/browse?at=--help
(ここでは--helpオプションを挿入している)
この手の攻撃で、実際に意味のある攻撃ができるケースは限られると思いますが、オプションを解釈しうる位置にある引数については注意が必要になります。あまりよい例が思いつきませんが、grep PATTERN
のような引数のことです。この位置だとPATTERNが「-」から始まる時にオプションとして解釈されてしまいます(-e PATTERN
の位置であればオプションだと解釈されない)。
最後にOSコマンドインジェクション全般について言うと、これは間違いなく最大級に深刻な脆弱性です。WebアプリケーションからOSコマンドを起動することを検討しているならば、IPAの安全なウェブサイトの作り方(1.2 OSコマンドインジェクション)も参照いただければと思います。
おすすめ記事