趣味の検査ツール作成

ここのところ日記も書かずにいましたが、実は自前のWebアプリケーションの検査ツールを作るのに没頭していました。

作ったツールの特徴をいくつか挙げると、

  • 手動検査の補助ツールの位置付け
  • ツール自体がWebアプリ(PHPで8KL以下の分量)
  • 主にインジェクション系の脆弱性を検出する
  • それ自体にはプロキシ機能はない
  • レポート作成用の機能はない

のような感じです。

ツール自体は今のところ公開する予定はありませんが、一部のシグネチャについては(私が所属する会社固有のノウハウと思われる部分を除いて)日記に書いていこうと思います。

自作検査ツール - OSコマンドインジェクション編

OSコマンドインジェクションを許す欠陥を持つアプリケーションは非常に少ないです。最近のアプリケーションは特に、メールを送信するために直接sendmailコマンドを叩くようなことをしなくなってきており、欠陥を持つアプリは殆ど見られなくなっています。

また、OSコマンドインジェクションは、仮に脆弱性が存在したとしてもなかなかブラックボックスでの発見が難しい脆弱性です。脆弱性の発見が困難となる要因としては、コマンドの実行結果がレスポンスに含まれるとは限らない(例えばsendmailを実行してメールを送る場合)、OSコマンド内への挿入箇所が「'」や「"」で括られている場合がある――などが挙げられます。

脆弱性が存在する可能性が低く、また発見しづらい脆弱性であれば、無視してしまってもいいようなものですが、悪用された際の危険度が最大級の脆弱性であるために無視する訳にもいきません。何とも扱いづらい脆弱性です。

今回作成したツールでは、最小限の手数で、かつ極力多くの脆弱性を検出できるように、シグネチャのパターンを検討しました。

UNIX系OS用のシグネチャ

OSコマンドインジェクションのサンプルとして典型と言えるのが、UNIX環境におけるPerlCGIプログラムではないかと思います。

PerlCGIで、OSコマンドインジェクションが可能なコードとしては、以下のようなものが挙げられます。

□open(file)
A1: open(P, "foo/$p");
A2: open(P, "$p.txt");

□open(pipe)
B1: open(P, "|/tmp/mycmd -foo $p -bar xxx");
B2: open(P, "|/tmp/mycmd -foo '$p' -bar 'xxx'");
B3: open(P, "|/tmp/mycmd -foo \"$p\" -bar \"xxx\"");

□system関数
C1: system("/tmp/mycmd -foo $p -bar xxx");
C2: system("/tmp/mycmd -foo '$p' -bar 'xxx'");
C3: system("/tmp/mycmd -foo \"$p\" -bar \"xxx\"");

□backtick演算子
D1: $a = `/tmp/mycmd -foo $p -bar xxx`;
D2: $a = `/tmp/mycmd -foo '$p' -bar 'xxx'`;
D3: $a = `/tmp/mycmd -foo "$p" -bar "xxx"`;

※「$p」は攻撃者が制御可能な変数です

コマンド部分(上記で「mycmd」の部分)を制御可能な場合などもありえますが、実際に多いのは上記のようなパターンではないかと思います。

通常であれば、上記のパターンに合せた何種類かの方法で検査を行う必要があり、実際商用のツールの中にはそのようにしているものもあるようです。

しかし、滅多に検出されない脆弱性のために何種類ものパターンを試すのは、(やるのはツールだとしても)時間の無駄ですので、自作の検査ツールには以下の1種類の検査文字列のみ載せました。

|【元の値】\'`/bin/sleep 10`;echo;set #";echo;set #|

かなりトリッキーですが、上記のA1〜D3までの全11パターンに対応できるようにしています。ただし、値がエスケープや何らかのフィルタが施された上でコマンドに挿入されると、動作しない可能性があります。

脆弱性の検出方法は、環境変数を出力/設定するコマンドであるsetを実行させて、レスポンスにその実行結果が含まれると判断した場合に脆弱性有りと判定します。コマンドの実行結果がレスポンスに出力されないケースもあるため、保険としてsleepコマンドを入れてTime Delayによる検出も行います。

ただしTime Delayによる検出は誤検知が多いという問題があります。それを緩和するために、実際の検査ツールではアプリケーションの応答速度によってsleepさせる秒数を調整したり、Time Delayによる反応があった場合は「sleep 0」を与えた際のレスポンスタイムも測定するなどの方法をとっています。

検査文字列の構成

先頭の「|」はA2、末尾の「|」はA1のパターンで必要です。

【元の値】はあってもなくてもよいのですが、アプリケーションによる入力値チェックをパスできる可能性を多少でも高めるために、念のために入れています。例えば、メールアドレスをsendmailコマンドに渡しているアプリケーションで、値に「@」が含まれるか程度のチェックを行っている場合、【元の値】を入れておくとチェックをパスできるかもしれません。

次の「\'」ですが、B2,C2,D2のように「'」で括られた箇所に値が出力される場合、ここで「'」を終端させます。頭に「\」を付けて「\'」としているため、もともと「'」や「"」で括られていない場合は「'」での括りは開始されません。

続いてバッククォートで/bin/sleepを起動しています。バッククォートは、B3,C3,D3のように「"」で括られた部分でも動作します。sleepは、/usr/bin/sleepに実体が置かれているOSもありますが、大抵は/bin/sleepにシンボリックリンクがあると思います。

その次に「echo;set」が2つ続きます。後の「echo;set」は「"」で括られた部分に値が出力される場合(B3,C3,D3)にのみ機能します。

setの前にechoを付けているのは、改行を出力させるためです。改行がないと、HTTPヘッダの出力よりも前に検査文字列内のコマンドが実行される場合に500エラーとなり、setコマンドの実行結果がレスポンスされないことがあります。

最後に「#」(行コメント)は、それ以降の部分を無視させるために入れています。

なお、この検査文字列を手動で試す場合は、「#」や「;」などに適切なエンコードを施すのを忘れないようにしてください。

コマンド実行結果が得られるケース

前述のように、この検査ツールのシグネチャは、コマンド(setコマンド)の実行結果がレスポンスから得られる場合はそれで判定を行い、得られない場合は補助的にTime Delayによる判定を行うように作っています。

どのような場合にコマンドの実行結果が得られるのか?ということですが、私の実験では、上記のA2、B1〜B3、C1〜C3のケースでコマンドの実行結果がレスポンスに出力されました。

逆に、A1、D1〜D3については、コマンドの実行結果がレスポンスに出力されるかはアプリ次第です。具体的には、A1ではファイルをopenしその中身を読みこんで「print」する処理がされている場合には、コマンド実行結果がレスポンスされます。D1〜D3では、Webアプリケーション側でbacktick演算子の実行結果である「$a」を「print」している場合には、実行結果がレスポンスされます。

上記の11種類には含めていませんが、openの第二引数の末尾に「|」を入れているケースも同様です。

環境による違い

OSコマンドインジェクションについては、OS、Webサーバ等の環境によって結果が異なる可能性があります。

私が検証を行ったメインの環境は、OSはLinux(Fedora5)、WebサーバはApache(v2.2)、Perlはv5.8(ノーマルのCGIとして動作)です。サブの環境として、上記以外のいくつかの環境でも若干試していますので、以下にその結果を少しだけ書きます。

プログラム言語に関しては、LinuxApache上で、C言語CGI(system関数)もしくはPHP(system関数、exec関数、backtick演算子)でのOSコマンドインジェクションを試したところ、同様にコマンド実行が可能でした。なお、JavaのRuntime#exec()は、shellを経由せずにコマンドを起動するようなので、試していません(明示的に「sh -c」するとshellが利用できる)。

OSに関しては、OSの違いによって生じるshell構文等の違いによって、うまく動作しないケースが出てくるかもしれないと予想していました。しかし、Linux(Fedora5)/Solaris(V10)/FreeBSD(V7)上のApachePerlCGIおよびPHPで試した限りではそのようなことはありませんでした。

いずれのケースでも/bin/shLinuxでは/bin/bashと同じ)上でコマンドが起動されました(PerlPHPとも、/etc/passwdでのshell設定は無視されました)。なお、同じ/bin/shでもOSによって挙動は異なりますが、些細な違いであるため検査の際には問題にならないと思います。

残念ながら商用UNIX OS、商用Webサーバについては環境が無いため確認していません。

Windows系OS用のシグネチャ

実際のところ、Windows上でshellを叩くことは(UNIXと比べても)少ないように思いますが、シグネチャが無いのもなんですのでWindows用のパターンも作成しました。

Windows系の動作検証はWindowsXPIISをメインに実施しています。Windows2000/2003+IISでも一部確認を行いました。いずれの環境でも、IISの実行ユーザを、デフォルトの「IUSR_コンピュータ名」から然るべき権限を持つユーザに変更しないと、コマンド実行に失敗します。

Windows系OS用のシグネチャは以下の3つです。

1: 【元の値】"|set&"|set&
2: &set|  (UNIXと兼用)
3: |set&  (UNIXと兼用)

1はC言語/Active Perlのsystem関数や、ASPのWscript.Shellオブジェクトによって実行されるコマンドにインジェクション可能なケースを想定したものです。念のため「"」で括られた内側に値が出力される可能性も考慮しています。

2,3はついては、Perlのopen関数の第二引数の末尾もしくは先頭に値が挿入されるケースを想定したものです(UNIXのA1、A2に対応します)。

A1: open(P, "foo/$p");
A2: open(P, "$p.txt");

2,3は、Windows環境だけではなくUNIXでも動作するため、Windows/UNIX兼用のシグネチャという位置付けです。

UNIX環境については、open関数のパターンも含めてUNIX用のシグネチャ(sleep入りのシグネチャ)でカバーされますが、UNIX用の検査文字列は長くて記号類も多く含まれるために、入力値チェックではじかれる可能性もあります。そのため、多少ともこの2,3でカバーしたい…という思惑もあります。

コマンドに関しては、UNIXと同じく「set」にしています。Windows環境では、通常のEXEのコマンドにすると、WINDOWSとWINNT配下の両方を試さなければならなかったりして面倒な訳ですが(コマンドをフルパスで書かないという手もありますが)、shellの組み込みコマンドであるsetであればその辺りの悩みがないのでsetにしました。

それに合せる形で、UNIXの方もsetコマンドを実行させるようにしています。

検査の危険性

当然ですが、OSコマンドインジェクションを利用して、「/bin/rm -rf *」のようなコマンドを実行させると検査対象のサーバにダメージを与える可能性があります。

そうでなくても、OSコマンドインジェクションの検査は、サーバ側で実行されるコマンドに何らかの形で干渉するものです。例えば、「|」などを使ってコマンドを途中で切って、別のコマンドを挿入します。そのため、検査文字列を与えることにより、プログラマが意図したのとは違うコマンドが実行され、結果として検査対象のサーバにダメージを与える可能性も無いとはいえません。

アプリケーションの機能やネットワーク環境などの状況によっては、よりダメージを与える可能性が低い方法で検査できる場合もありますが、検出の確実性が劣るのと、検査の手数が若干増えるという問題があります。

参考になる情報

OSコマンドインジェクションの検査手法については、ネット上の情報や海外の書籍などを見ても、「|/bin/ls|」のようなパターンしか載っていないものが多いです。

私が見つけた中では、有用な情報が載っているのは以下の書籍くらいでした。

The Web Application Hacker's Handbook: Discovering and Exploiting Security Flaws

The Web Application Hacker's Handbook: Discovering and Exploiting Security Flaws

この本には、UNIXWindows兼用の、pingコマンドでTime Delayを起こさせる検査文字列など、なかなか興味深い手法が書かれています。

自作検査ツール - ディレクトリトラバーサル編

ディレクトリトラバーサルは、比較的発見が容易な脆弱性です。

検査文字列を検討する際にポイントとなるのは、どのファイルを見に行くか、NULL文字を入れるか、くらいではないかと思います。

UNIX系OS用のシグネチャ

UNIX用のシグネチャは1パターンしか用意しませんでした。

/../../..(省略)/etc/hosts[0x00]【元の値】

ファイルは/etc/hostsとしました。

/etc/hostsは、LinuxSolarisFreeBSDAIXHP-UXなど主要なUNIX OSに存在します(商用UNIXについては手元に環境が無いので、ネット上の情報からそう判断してます)。

検査文字列の先頭を「../」にするか、あるいは「/../」にするかも一つのポイントです。【元の値】が「/foo/bar.txt」のような値であるならば、検査文字列の先頭が「../」だとうまくいかない可能性があります。そのため、検査文字列の先頭は常に「/../」から始めています。

判定はシンプルに、レスポンス内にhostsファイルと思われる内容が含まれている場合、脆弱性ありとします。

Windows系OSのシグネチャ

Windows用のシグネチャも1パターンのみ用意しました。ただし、WindowsUNIXよりも少々複雑で考慮すべき点が多いです。

/../../..(省略)/$windir/win.ini[0x00]【元の値】

$windirのところは、通常は「windows」とします。しかし、今でもWindowsNTや2000の環境は残っているため、レスポンスヘッダからIISのバージョンが判り、かつそれが5.1未満である場合は「winnt」にします。

ファイルはboot.iniにするか迷いました。ネット上の情報や海外の書籍などを見てもboot.iniと書いているものが多いのですが(win.iniも少しはありました)、boot.iniファイルはPower Users以上の権限を持つユーザしか参照できません。

(Windows 2000 ServerやXP環境でのcacls実行結果)
C:\>cacls boot.ini
C:\boot.ini BUILTIN\Power Users:R
            BUILTIN\Administrators:F
            NT AUTHORITY\SYSTEM:F

したがって、デフォルトのIIS設定でASPなどのアプリケーションを実行している場合には、boot.iniは参照できないことになります。というわけで今回はwin.iniにしました。

また、NULL文字を入れるべきかも迷いました。

Windowsでの検証では、ASP(.NET)でFSO(File System Object)を使ってファイルを読む場合や、Java/Perlなどでファイルを読む場合には、NULLを入れると以降の文字列が無視されました。

一方、ASP.NET上で、FileOpen関数やSystem.IO.StreamReaderクラスを利用してファイルを読み込むと、パスにNULL文字が入っているだけでExceptionが発生します。

NULL入りとNULL無しの2パターンのシグネチャを作ろうかとも思いましたが、余りパターンを増やしたくないので、今のところNULL入りのシグネチャしか作っていません。ASP環境では、どちらを送っても同じ結果になるだろうという理由もあります。

BlindシグネチャUNIX/Windows兼用)

このシグネチャは、ディレクトリトラバーサルのBlind版とも言えるようなものです。

(リクエスト1)
./././【元の値】

(リクエスト2)
././0/【元の値】

判定は少々複雑で、まず最初に上記のリクエスト1を送り、元々のレスポンスと類似しているかを調べます。元々のレスポンスと、リクエスト1のレスポンスが類似している場合、リクエスト2を送ってリクエスト1のレスポンスと類似しているかを調べます。

その結果、リクエスト1とリクエスト2のレスポンスが類似して「いない」場合には、「要注意」と判定します(作成したツールの判定結果は、「脆弱性あり」「要注意」「なし」の3段階としています)。

これで何をやっているかというと、検査対象のパラメータがファイルパスとして使用されているかを探っています。

何故このようなシグネチャを作ったかというと、上記のUNIX用あるいはWindows用のシグネチャでは検出できないケースがまま存在するからです。

  1. サンドボックスが働いている(PHPのopen_basedir設定など)
  2. Windowsでシステムドライブ以外のファイルを読んでいる
  3. 入力値チェックやフィルタが存在する(「../」やNULL文字など)

このシグネチャでPOSITIVEな反応が出た場合は、手動であれこれ試していくことになります。注意すべきはシグネチャでPOSITIVEな結果が出たとしても、パラメータがファイルパスの一部を構成し、なおかつ入力値チェックがそれほどきつくないことが推測できるだけであるということです。要は、利用可能な脆弱性があるとは限らないということです。

特に上記の1,2の場合には、検査者がアプリケーション内のパス構成に関する知識を持っていない場合の打ち手は限られており、下手に粘ると検査時間だけを無駄に消費してしまうことになります。

そういう意味で、このシグネチャで「要注意」であった場合の次の打ち手をシグネチャ化した方がよいのでしょうけども、まだそこまでは作っていません。

検証を行なった環境

Windows系はWindowsXPIISをメインに、UNIX系はLinux(Fedora5)+Apacheをメインに実施しました。

検査の危険性

ディレクトリトラバーサルの検査で危険なのは、ファイルの変更や削除を行なう「更新系」のアプリケーションです。

パーミッションなどにもよりますが、最悪の場合、検査文字列を送ることによって、消してはならないファイルを消去したり壊したりすることもあります。

したがって、最悪壊れてもシステムが起動困難になったりしにくいファイルを選ぶのがよいと思います(そういう意味でも、boot.iniは避けた方がよいかもしれません)。

(おまけ)アプリ環境とディレクトリの遡り

ディレクトリ遡りの可能性は、パラメータの値がファイルパスのどの部分に挿入されるかによって変わってきます。

(パターンA)
$file = "/path/$p";

(パターンB)
$file = "/path/$p.txt";

(パターンC)
$file = "/path/foo_$p.txt";

# ファイルを読み込んで出力する
(省略)

※「$p」は攻撃者が制御可能な変数です

上記のA,B,Cでは、一般的にA,Bにて遡りが可能です(正確には、Bで遡りが可能かは状況によります)。Cについては、$pに「/../../(省略)」のような値を与えても、その前の「foo_」が邪魔になって遡りができません。

ただし、Windows環境(JavaASP.NET)や、UNIX環境でもPHPを使っている場合は、Cのパターンでも遡りができました。

プログラム言語環境によって、実際にパスを辿りながらパスの正規化(「../」などの解釈)を実施したり、あるいは正規化をしてからパスを辿るかなどの挙動の違いがあることが判ります。

なお、最後に紹介したBlind的なシグネチャは、Cで遡りが可能なWindowsPHPの環境であっても、A,Bのパターンにのみ有効です。