自作検査ツール - 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を起こさせる検査文字列など、なかなか興味深い手法が書かれています。