最近少し調べていたのが、PHPの任意コード実行系の脆弱性です。中でも、preg_replace関数(Perl互換の正規表現による置換を行なうための関数)を不適切な方法で使った場合に発生する脆弱性について調べていました。
せっかくなので、日記にまとめてみます。
3種類の脆弱性
preg_replace関数を使ったPHPコード実行系の脆弱性には、大きく分けて3つの種類があります。
- 第一引数への挿入を許す
- e修飾子付き・第二引数への挿入を許す
- e修飾子付き・第三引数への挿入を許す
以下でそれぞれについて見ていきます。
タイプ1:第一引数への挿入
以下のコードに、任意のPHPコードが実行可能な脆弱性があります。
$m = preg_replace("/([^<]*)$kw([^>]*)/i", "\\1<font color=red>$kw</font>\\2", $m);
$kwと$mは外部から値を制御可能です。
このコードは、文字列$mの$kwに合致する部分を赤文字で表示するためのものです(いわゆるキーワードのハイライトを行なう)。
このコードの場合、攻撃者は$kwに「/e[NULL]」のような値を与えます。preg_replace関数の第一引数はNULLセーフでないため、パターン部分のNULL文字以降が無視され、上記はe修飾子付きの正規表現であると解釈されてしまいます。
あとは、$mの先頭に実行させたいPHPコードを埋め込むと、それが「\1」になって実行されます。なお、第二引数の先頭部分を攻撃者が自由にできないようなケースでは、仮に第一引数に「/e[NULL]」を挿入できても、任意コードの実行には至りません。
対策についても少し書きます。そもそもの話として、上記のような処理であればpreg_replace関数の代わりにstr_replace関数を使えば十分なのですが、その辺は本題では無いので置いておきます。
通常は、第一引数の$kwに含まれるパターン内における特殊記号(「.」「(」「[」「\」など)をpreg_quote関数でエスケープすればよいです。もし、正規表現での置換(例えばemacsのreplace-regexpのようなこと)をやりたいならば、NULL文字や「/」のエスケープが必要です。
発見された脆弱性の例:
Zeroboard Preg_replace Remote Command Execution Vulnerability
Invision Power Board Search.PHP Script Injection Vulnerability
タイプ2:e修飾子付き・第二引数への挿入
2つ目は、e修飾子付きのpreg_replace関数の第二引数に挿入可能なケースです。
$m = preg_replace('#^foo (.*)#iem', "'$u'.str_replace('<br>', '', '\\1')", $m);
$uと$mは外部から値を制御可能です。
上記コードでは、$mの「foo」で始まる行に対して、$uをくっつける処理、BRタグの除去処理をしています。
攻撃方法は非常にわかりやすいです。$uに「'.phpinfo().'」のような値を与えると、e修飾子によりeval実行されるPHPコードは以下のようになります。
''.phpinfo().''.str_replace('<br>'...(省略)
その結果、phpinfo関数が実行されます(当然ながら、phpinfo関数の代わりにsystem関数をねじ込むと、shell上でコマンド実行されます)。
対策としては、$uを第二引数に入れる際にエスケープする方法がまず思いつきます。しかし、エスケープ方法はそれ程単純ではありません(addslashesではダメです)。
それよりよいのは以下のようなコードへの書き換えです。
$m = preg_replace('#^foo (.*)#iem', "\$u.str_replace('<br>', '', '\\1')", $m);
この場合、eval実行されるPHPコードは以下になります。
$u.str_replace('<br>'...(省略)
元々の脆弱なコードでは、$uの値が展開された後にeval実行されますが、上のコードではeval実行により$uが展開されます。そのため、$u内のPHPコードが実行されることはありません。
しかし、最善なのはe修飾子を使用しないプログラムにすることです。無理に1つの文にせず、処理を複数のステップに分けてやれば、e修飾子を使用せずに同等の処理を実現できます。
発見された脆弱性の例:
MyBB DomeCode Remote PHP Script Code Injection Vulnerability
phpBB 'viewtopic.php' Remote Code Execution Vulnerability(Santy Wormに利用された脆弱性)
タイプ3:e修飾子付き・第三引数への挿入
3つ目は、ちょっとわかりにくい脆弱性です。
$s = preg_replace('/\[\[(.*?)\]\]/e', 'addlink("\\1")', $s);
wiki記法におけるリンクを処理するプログラムです。
外部から制御できるのは、第三引数の$sです。タイプ1・2とは異なり、第一引数と第二引数は固定です。
ぱっと見で、第二引数の「\\1」の部分に「"」が入るような$sを与えてやれば、eval実行されるコードの構造を壊すことができそうに見えますが、そうはいきません。preg_replace関数にe修飾子が付く場合、後方参照する変数に含まれる「"」などの文字は、PHPにより自動でaddslashesされるからです*1。
上記のプログラムでPHPコードを動かすためには、$sに以下のような値を与えます。
[[{${phpinfo()}}]]
この場合、eval実行されるPHPコードは以下になります。
addlink("{${phpinfo()}}")
これにより、phpinfo関数が実行されるのですが、ここは少々判りづらいところだと思います。
PHPやPerlでは、ダブルクォートで括った文字列リテラル内に書かれた「$myvar」のような変数が展開されるのはよく知られています。実は、書き方によっては変数の展開だけではなく、関数を起動することも可能です。
文字列内での関数の実行は、PHPではVer5以上のみで可能です。例えば、「"」で括った文字列リテラル内で「{${myfunc()}}」のように書くと、PHP5ではmyfunc関数が実行されます。
文字列をシングルクォートで括ったならば、変数の展開も関数の実行もされません。関数が実行されたり、変数が展開されたりするのは、ダブルクォートで括られた文字列内だけです。
先ほどの脆弱なプログラムに話を戻します。先ほどのプログラムは、本来は以下のように書かなければなりません。
$s = preg_replace('/\[\[(.*?)\]\]/e',
"addlink('\\1')", $s);
第二引数の全体のクォート文字はどちらでもよいですが、後方参照する変数はシングルクォート「'」で括ってやる必要があります。
もしくはpreg_replace関数のかわりに、preg_replace_callback関数を使うことでも、意図しないコード実行を避けられます(callbackの方は少々使いづらいところがありますが)。
発見された脆弱性の例:
DokuWiki Remote PHP Script Code Injection Vulnerability
Strawberry 'html.php' Remote Code Execution Vulnerability
まとめ
まとめの最初に書いておきますが、ごくごく普通の使い方(e修飾子無しで、パターン部分を固定にする)をしている限り、コード実行の脆弱性が生じることはありません。つまり、上記で紹介したような脆弱性は、かなり珍しい部類のものといえます。実際にCVEでこの手の脆弱性を検索しても、ヒットするのは10件もないと思います。
さらにいうと、仮に脆弱性が存在したとしても、ブラックボックス(ソースコードが無い状態)での発見はかなり難しいです*2。3つ取り上げたタイプで若干異なりますが、エラーメッセージが表示されない状態であれば、さらに難易度は上がります。
というわけで、無視されがちな脆弱性なのかもしれませんが、万が一脆弱性が存在してそれが発見・悪用された場合の被害は甚大です。PHPのコードを書いたり、他人のコードをチェックしたりする際には、要注意な箇所といえるでしょう。
既に説明したように、脆弱性が存在する可能性があるのは、パターンが外部から制御可能であったり、e修飾子を使用している箇所です。実際にCVEなどに登録されている脆弱性を見ると、HTMLを解析・操作するような比較的面倒なテキスト処理を行なう処理に問題が潜んでいたケースが見られました。
最後に、preg_replace関数そのものについて少し書きます。一言で言うと、使う上での落とし穴が多いです。関数の設計がいまいちだと思うところもあります。Perlの置換演算子と比べると*3、使いにくいなーと思ってしまいました。
*1:このエスケープする挙動は、コード実行の防止には役立ちますが、それとは別の気持ちの悪い問題をもたらします。これはPHPマニュアルのUser Contributed Notesに書かれていますので、興味がある方は見てください。
同時にこのエスケープされる仕様は、exploitコードの実行にも影響します。原則的にexploitコードは、「'」「"」「\」を含まないように作成する必要があります。
*2:逆に言うと、ホワイトボックスでは探しやすい脆弱性です。
*3:PHPでのpreg系関数は後付けした機能なので、Perlと比較すること自体が適切ではないかもしれませんが、どうしても比較してしまいます。