Webアプリケーションの脆弱性いろいろ
- スクリプト挿入攻撃(Script Insertion)
- クロスサイト スクリプティング(Cross Site Scripting / XXS)
- クロスサイト リクエスト フォージェリ(Cross Site Request Forgeries / CSRF)
- SQLインジェクション(SQL Injection)
- Time-based SQL Injection
- ブラインドSQLインジェクション
- JSON SQL Injection
- セッション・フィクセーション(session fixation)
- セッションアダプション
- Cookie Monster バグ
- セッションハイジャック
- HTTPヘッダインジェクション
- メールの第三者中継
- 変数汚染攻撃
- Nullバイト攻撃
- ディレクトリトラバーサル
- eval利用攻撃
- ファイルアップロード攻撃
- インクルード攻撃
- リモートファイル・インクルード攻撃
- ローカルファイル・インクルード攻撃
- パス・ディスクロージャ
- コマンド実行攻撃(Command Injection)
CSRF(クロスサイト・リクエスト・フォージェリ)やセッション・フィクセーションについて言えば,セッション管理に使うクッキー(Cookie)の動作を理解していないと対策が難しいようです。
『HTTP(Hypertext Transfer Protocol)』に『クッキー(Cookie)』が登場した経緯
HTTPは元来ハイパーテキストにおいて単にファイル転送を行うために開発された。
⇩
同じURLへのアクセスならその状況によらず同一の資源(HTML文書、画像、音声)を提供することが前提。(静的ページ)
⇩
World Wide Webが普及するにつれ、状況によって異なる内容のページを提供したいというニーズが生まれた。(動的ページ)
⇩
HTTPのみで満たすには、IPアドレスによって区別する、状態を表現したユニークなURLを生成するなどの方法がある。
ただし、容易に解決できない欠点を抱えていた。
- プライベートネットワークからのアクセスを区別できない
- 本来二度起きない状態が同じURLにアクセスすることで何度も発生する
- セキュリティの問題など
⇩
1994年にネットスケープコミュニケーションズ社によってクッキー( Cookie )が提案・実装された。
クッキーでは次のようにサーバとクライアント間の状態を管理する。
⇩
- WebサーバがWebブラウザにその状態を区別する識別子をHTTPヘッダに含める形で渡す。
- ブラウザは次にそのサーバと通信する際に、与えられた識別子をHTTPヘッダに含めて送信する。
- サーバはその識別子を元にコンテンツの内容をユーザに合わせてカスタマイズし、ブラウザに渡す。
必要があれば新たな識別子もHTTPヘッダに含める。
以降2、3の繰り返し。
この仕組みによって、ステートレスなプロトコルであるHTTP上でステートフルなサービスを実現する。
(※一度設定されたクッキーは、条件を満たす限り何度でも要求に組み込まれるという点である。HTMLページの要求だけでなく、画像を含むすべての要求が対象となる。)
⇩
1997年にクッキー( Cookie )はRFC 2109で初めて標準化され、2000年のRFC 2965、2011年のRFC 6265で更新された。
⇩
2015年現在ほとんどのWebサーバ、Webブラウザで利用可能である。
『クッキー』と『セッション管理』
携帯電話向けアプリケーションを除き,Webアプリケーションにおけるセッション管理ではクッキー( Cookie )を使うことが多いようです。
HTTPは基本、1度の要求(リクエスト)に対し、1度の返答(レスポンス)という1サイクルの通信で完結するものです。
そのため、あるユーザーから送られてきた前回のリクエストと,今回のリクエストを関連付けることはできません。
関連付けを可能にするのが『セッション管理』です。
Webアプリケーションの黎明期には,ユーザーはURLに識別情報を含める方法で『セッション管理』を実現していた。
問題点 ⇒
- 実装が複雑
- セキュリティ上の問題が発生しやすいなど
クッキーによるセッション管理が考案される。
⇩ 詳しくは下記サイトへ
Webからの脅威を攻略せよ-セッション管理編-第1回 まずは「クッキー」を理解すべし
スクリプト挿入攻撃(Script Insertion)
サイトのコンテンツにJavaScriptやVBScriptを挿入し、サイトを訪れた第三者に対して悪意あるコードを実行させる攻撃方法です。
掲示板などが狙われやすいようです。
この攻撃を受けた場合、
- ウィルスに感染させるような悪意あるサイトにリダイレクトされる
- COOKIEなどの情報を盗まれ、サイトの管理者権限を乗っ取られる
などの被害を受けることになります。
脆弱性のある例
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>スクリプト挿入攻撃</title>
</head>
<body>
<form name="Bulletin-board" action=" " method="post">
<input type="text" name="posting">
<input type="submit" value="投稿">
</form>
<p>
<?php
if(isset($_POST[ "posting" ])){
$input = $_POST[ "posting" ];
echo $input;
}
?>
</p>
</body>
</html>
もし、ユーザーが『"<script>alert('スクリプト挿入攻撃');</script>" 』のような入力をして投稿すると、JavaScriptのコードが実行されてしまいます。
対応方法
出力時にエスケープを行い、攻撃コードを無効化する必要があります。
以下のように書き換えることで無害化することができます。
⇩ htmlspecialchars( )関数については下記へ
・PHP: htmlspecialchars - Manual
⇩ スクリプト挿入攻撃については下記サイトへ
クロスサイト スクリプティング(Cross Site Scripting / XXS)
攻撃対象のサイトとは異なるサイトやメールなど間接的な手段を介して悪意あるコードを実行させる攻撃方法です。
『複数のサイトを横断させた』スクリプトを実行させることが『クロスサイト』と呼ばれる理由らしいです。
対応方法
スクリプト挿入攻撃の時と同じように、出力時に適切なエスケープを行うことで対応できるようです。
⇩ クロスサイトスクリプティングについては下記サイトへ
・クロスサイトスクリプティング対策の基本(前編):クロスサイトスクリプティング脆弱性とは? - @IT
クロスサイト リクエスト フォージェリ(Cross Site Request Forgeries / CSRF)
ユーザーが意図しない操作をユーザー自身の権限を用いて実行させる攻撃方法です。
ユーザーが意図しない投稿や削除、購入などの被害を引き起こします。
脆弱性のある例
/* 管理者かチェック */
function is_admin( ){
if(isset($_SESSION['is_admin']) === true && $_SESSION['is_admin'] === true){
$is_admin = true;
}else{
$is_admin = false;
}
return $is_admin;
}
session_start( );
/* POSTだった場合は、指定された処理を実行 */
if(strlower($_SERVER['REQUEST_METHOD'])==='post'){
// 管理者かチェック
if(isset($_POST['op']) === true
&& $_POST['op'] === 'delete'
&& is_admin( ) === true ){
// 管理者なら削除処理を行う
echo '記事の削除を行いました。';
}else if(isset($_POST['op']) === true && $_POST['op'] === 'login'){
// ログイン処理を実行
$_SESSION['is_admin'] === true;
echo '管理者としてログインしました。';
}else{
echo '権限がありません。';
}
}
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>クロスサイトリクエストフォージェリ</title>
</head>
<body>
<section>
<h2>管理者ログインフォーム</h2>
<form action=" " method="post">
<input type="hidden" name="op" value="login">
<input type="submit" value="管理者としてログイン">
</form>
</section>
<section>
<h2>記事削除フォーム</h2>
<form action="" method="post">
<input type="hidden" name="op" value="delete">
<input type="submit" value="記事を削除">
</form>
</section>
</body>
</html>
サイトに管理者としてログインした後で、下記のようなページにアクセスしてしまった場合、サイト管理者の権限を用いて意図しない記事の削除が実行されてしまいます。
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>攻撃ページ</title>
</head>
<!-- ページが読み込まれたら、nameがattack_formのformの中のsubmitを実行-->
<body onload="document.attack_form.submit( )">
<form name="attack_form" action="csrf_01.php" method="post">
<input type="hidden" name="op" value="delete">
<input type="submit" value="クリックしなくてもsubmitされます">
</form>
</body>
</html>
対応方法
『セッション毎にユニークで「第三者から推測が困難」なトークンによるチェック』
『投稿・編集・削除などの操作の際にはパスワードを認証させる』
などの対策が考えられるようです。
上記の で囲まれた部分を以下に書き換えます。
* ワンタイムトークンを生成する関数
*/
function get_token($key = ' ') {
$_SESSION['key'] = $key;
$token = sha1($key);
return $token;
}
/**
* ワンタイムトークンをチェックする関数
*/
function check_token($token = ' ') {
return ($token === sha1($_SESSION['key']));
}
/* セッション開始 */
session_start( );
// POSTだった場合は、指定された処理を実行
if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
// 管理者かチェック
if (isset($_POST['op']) === true
&& $_POST['op'] === 'delete'
&& is_admin( ) === true) {
// 管理者ならワンタイムトークンを確認
if (isset($_POST['token']) === false
|| check_token($_POST['token']) === false) {
// ワンタイムトークン不正のため削除は行わない
echo 'CSRF攻撃を受けた可能性があります';
} else {
// ワンタイムトークンが問題なければ削除処理を行う
echo '記事の削除を行いました';
}
} else if (isset($_POST['op']) === true
&& $_POST['op'] === 'login') {
// ログイン処理を実行
$_SESSION['is_admin'] = true;
echo '管理者としてログインしました';
} else {
// 管理者以外なのでエラー表示
echo '権限がありません';
}
}
// セッション内のワンタイムトークン用文字列を削除
if (isset($_SESSION['key']) === true) {
unset($_SESSION['key']);
}
// ワンタイムトークン取得
$token = get_token(session_id( ) . '_' . microtime( ));
⇩ クロスサイトリクエストフォージェリの対策で気をつけたいことは下記サイトへ
・CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例 - 徳丸浩の日記(2011-01-27)
⇩ クロスサイトリクエストフォージェリについては下記サイトへ
・CSRF(クロスサイトリクエストフォージェリ) | PHPの入門・リファレンス
SQLインジェクション(SQL Injection)
データベースに対する命令文(SQL)に対して意図しない命令を挿入することで、データベースに保存されているデータを改ざんしたり、盗み出したりする攻撃方法です。
脆弱性のある例
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
* 入力されたユーザ名とパスワードを元に、ユーザが存在するか確認する
*/
function check_user($name, $pass) {
$query = "SELECT * FROM users WHERE name = '$name' AND pass = '$pass' ";
$result = mysql_query($query);
// クエリ実行の戻り値がfalseでなければ、クエリ実行自体は正常
if ($result !== false) {
// 取得したレコード数が1以上ならログイン成功
$rows = mysql_num_rows($result);
if ($rows >= 1) {
return true;
}
}
return false;
}
// POSTの場合はログインチェック
if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
$conn = mysql_connect('localhost', 'root', ' ');
mysql_select_db('perfect');
$result = check_user($_POST['name'], $_POST['pass']);
if ($result === true) {
echo '<span style="color: #0000ff">ログインに成功しました</span>';
} else {
echo '<span style="color: #ff0000">ログインに失敗しました</span>';
}
mysql_close($conn);
exit( );
}
header('Content-type: text/html; charset=utf-8');
echo <<<EOF
<html>
<head>
<title>ログイン画面</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
</head>
<body>
<fieldset>
<legend>ログインフォーム</legend>
<form action=" " method="post">
ユーザ名<input type="text" name="name" value = " "/><br />
パスワード<input type="text" name="pass" value = " "/><br />
<input type="submit" value="ログインする" />
</form>
</fieldset>
</body>
</html>
EOF;
?>
⇩ header('Content-Type:text/html;charset=utf-8') ;の意味については下記サイトへ
・PHP でレスポンスヘッダーの Content-Type に文字コードを指定する方法 | Webセキュリティの小部屋
⇩ meta要素のhttp-equivコンテンツ属性については下記サイトへ
・HTML5入門 - meta要素 - http-equivコンテンツ属性の値と概要 - Webkaru
⇩ <fieldset>タグについては下記サイトへ
対応方法
『全てのクエリにバインドメカニズム(ストアドプロシージャ)を利用する』
『すべての変数を文字列として処理しエスケープする』
の2種類の対策があるようです。
今回は、プリペアドステートメントを利用します。
PHP Data Objects(PDO)のPDOStatement::bindParamを使ってます。
上記の で囲まれた部分を以下に書き換えます。
/**
* 入力されたユーザ名とパスワードを元に、ユーザが存在するか確認する
*/
function check_user($dbh, $name, $pass) {
$query = "SELECT * FROM users WHERE name = :name AND pass = :pass";
$sth = $dbh->prepare($query);
$sth->bindParam(':name', $name, PDO::PARAM_STR);
$sth->bindParam(':pass', $pass, PDO::PARAM_STR);
$result = $sth->execute( );
// クエリ実行の戻り値がfalseでなければ、クエリ実行自体は正常
if ($result !== false) {
// 取得したレコード数が1以上ならログイン成功
$rows = $sth->rowCount( );
if ($rows >= 1) {
return true;
}
}
return false;
}
// POSTの場合はログインチェック
if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
$dsn = 'mysql:dbname=perfect;host=localhost';
$dbh = new PDO($dsn, 'root', ' ');
$result = check_user($dbh, $_POST['name'], $_POST['pass']);
if ($result === true) {
echo '<span style="color: #0000ff">ログインに成功しました</span>';
} else {
echo '<span style="color: #ff0000">ログインに失敗しました</span>';
}
exit( );
}
⇩ SQLインジェクションについては下記サイトへ
⇩ Time-based SQL Injectionについては下記サイトへ
・Time-based SQL Injectionは意外に実用的だった | 徳丸浩の日記
⇩ ブラインドSQLインジェクションについては下記サイトへ
・OWASP AppSec USA 2013 レポート(前編):深刻な「ブラインドSQLインジェクション」の脅威 (1/2) - @IT
・第6回 意外に知られていないブラインドSQLインジェクション:なぜPHPアプリにセキュリティホールが多いのか?|gihyo.jp … 技術評論社
⇩ JSON SQL Injectionについては下記サイトへ
・不正なJSONデータによるSQL Injectionへの対策について (Json.pm+SQLクエリビルダー) — Mobage Developers Blog
・JSON SQL Injection、PHPならJSONなしでもできるよ | 徳丸浩の日記
セッション・フィクセーション(session fixation)
攻撃対象のユーザーに、任意のセッションIDを強制的に利用させる攻撃方法です。
fixの『固定する』という意味から、セッション固定攻撃とも呼ばれるようです。
脆弱性のある例
session_fixation_01.php
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
/**
* 認証用の関数です
* ダミーなので常にtrueを返しています
*/
function check_auth ( ) {
return true;
}
// セッションを有効にする
session_start( );
header('Content-type: text/html; charset=utf-8');
// ユーザIDがセッション変数に存在すれば、ログインユーザ専用ページを出力
if (isset($_SESSION['user_id'])) {
// ログインユーザ専用ページを表示する
echo ' ログインユーザ専用ページです<br /> ';
echo ' あなたのセッションIDは ' . session_id( ) . ' です '; // 脆弱性の確認用
} else if (
strtolower($_SERVER['REQUEST_METHOD']) === 'post'
&& isset($_POST['op']) === true
&& $_POST['op'] === 'login'
&& check_auth( ) === true) {
// ログイン処理の実行: 開始
// ...
$_SESSION['user_id'] = 1; // ダミーのユーザIDをセッションに格納
$script = basename($_SERVER['SCRIPT_FILENAME']);
echo <<<EOF
<html>
<head>
<title>ログイン成功</title>
<meta http-equiv="refresh" content="3; url=./{$script}" />
</head>
<body>
ログインに成功しました
</body>
</html>
EOF;
} else {
// ログインフォームを表示する
echo <<<EOF
<form action=" " method="post">
<input type="hidden" name="op" value="login" />
<input type="submit" name="login_button" value="ログイン" />
</form>
EOF;
}
対応方法
ログイン後にセッションIDの再発行を行うことでセッション固定攻撃を利用したセッションハイジャックに対応します。
上記の で囲まれた部分を以下に書き換えます。
PHP5.1.0以降のバージョン
$_SESSION['user_id'] = 1; // ダミーのユーザIDをセッションに格納
session_regenerate_id(true); // セッションIDを再発行
session_regenerate_id( )関数を呼び出し、セッションIDの再発行をすることで、ログイン前とログイン後でセッションIDが異なるため、ログイン後のセッションが乗っ取られる心配が無くなるようです。
PHP5.1.0より前のバージョンだと、session_regenerate_id( )関数を実行しても古いセッション情報がサーバから破棄されないので、以下のように書き換えます。
PHP5.1.0より前のバージョン
$_SESSION['user_id'] = 1; // ダミーのユーザIDをセッションに格納
$_SESSION = array( ); // セッション情報を初期化
session_regenerate_id(true); // セッションIDを再発行
session_decode($session_data); // セッション情報を初期化
⇩ セッション・フィクセーションについては下記サイトへ
・セッションフィクセイション脆弱性の影響を受けやすいサイトとは | 徳丸浩の日記
セッションハイジャック
何らかの方法で取得したセッションIDを利用して正規のユーザーのセッションを乗っ取る攻撃方法です。
この攻撃を受けると、攻撃者は正規のユーザーと同じ権限で操作が可能となるため、ユーザーの個人情報取得や改ざん、ユーザー登録してるクレジットカードで多額の買い物などの被害を受けます。
サイトの管理者がこの攻撃を受けると、サイトに登録してるユーザーの個人情報が盗まれたり、サイト自体にウィルスを仕掛けられるなど、被害は甚大です。
脆弱性の種類
- リファラによるセッションIDの漏洩
- クロスサイトスクリプティング(XSS)によるセッションIDの入手
- セッションフィクセーションによるセッションIDの強制
リファラによるセッションIDの漏洩
PHPではセッションIDを管理する方法には、
- クッキー(COOKIE)を用いる
- GET変数・POST変数を用いる
の2種類あります。
PHPのデフォルトの設定では、クッキー(COOKIE)による管理ですが、設定によってクッキー(COOKIE)が利用できない場合、GET変数・POST変数が利用されます。
セッションの管理方法に対する設定は、CドライブにXAMPPをインストールしてる場合
『C:¥xampp¥php¥php.ini』にあるphp.iniファイル に書いてあります。
( windows7のCドライブ直下にXAMPPをインストールしてる環境。
phpのバージョン PHP5.6.12 )
php.iniの1532行目あたり(ファイルをテラパッドで開いた場合)
設定名 | 内容 | デフォルト値 |
---|---|---|
session.use_cookies | セッションIDの管理にクッキーを使用するか | 1(有効) |
session.use_only_cookies | セッションIDの管理にクッキーのみを使用するか | 1(有効)※PHP5.3.0以前は0(無効) |
session.use_trans_sid | クッキーが利用できない場合にGET変数・POST変数によるセッション管理を行うか | 0(無効) |
セッションの管理方法に関する設定値が下記の場合、リファラによるセッションIDの漏洩が起きる可能性があります。
- session.use_only_cookies = 0
- session.use_trans_sid = 1
リファラ 【 referer 】 リファラー / リファーラ
リファラとは、あるWebページのリンクをクリックして別のページに移動したときの、リンク元のページのこと。Webサーバのアクセスログに記録される項目の一つ。
これを辿っていくと閲覧者がどこのサイトから訪問したのか、また、サイト内でどのような軌跡を辿ったのかなどを調べることができる。
検索エンジンからの訪問の場合には、URLのパラメータ部分を調べることによって、どんな言葉で検索した結果のページから来たのかを知ることができる。
Web広告の世界では、どのサイト/ページに掲載した広告に効果があったのかを調べることができる重要な項目である。
⇩ 携帯のリファラによるセッション漏洩については下記サイトへ
・再考・ケータイWebのセキュリティ(3):実は厄介、ケータイWebのセッション管理 (1/3) - @IT
クロスサイトスクリプティング(XSS)によるセッションIDの入手
ターゲットとするサイトに対して攻撃コードを含むリンクを張ったりすることで、被害者のセッションIDを盗み出します。
XSSを利用したリダイレクト先で元のサイトに再びリダイレクトさせれば、被害者はセッションIDを盗まれたことにも気付けない可能性があります。
イメージ図
( ※『パーフェクトPHP』引用 )
脆弱性のある例
session_hijack_01.php
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
session_start( );
// セッション変数内のカウントが未設定の場合は0を設定
if (isset($_SESSION['count']) !== true) {
$_SESSION['count'] = 0;
}
// 出力するメッセージをセット
if (isset($_GET['keyword']) === true && $_GET['keyword'] !== ' ') {
$_SESSION['count']++;
$message = ' あなたが入力したキーワードは[ '
. $_GET['keyword'] . ' ]です。<br /> ';
} else {
$message = 'キーワードは入力されていません。<br />';
}
// $message = $message . 'あなたがキーワードを入力したのは' .$_SESSION['count'] .'回目です。';は複合演算子『 .= 』で下記のように書けます
$message .= 'あなたがキーワードを入力したのは'
. $_SESSION['count'] .'回目です。';
echo <<<EOF
<html>
<head>
<title>キーワード表示画面</title>
</head>
<body>
{$message}<br />
<br />
<form action=" ">
<input type="text" name="keyword" value=" " />
<input type="submit" value="投稿" />
</form>
</body>
</html>
EOF;
⇩ 複合演算子については下記サイトへ
⇩ {$message}のように変数を波括弧で囲むことについては下記サイトへ
・PHPスクリプト講座:文字列型:string | そふぃのphp入門
対応方法
まずは、クロスサイトスクリプティング(XSS)に対する処理を行います。
上記の で囲まれた部分を以下に書き換えます。
/* ヒアドキュメント内では関数をそのまま記述できないので、クラスを定義してそのメソッドを使うか、可変変数を使う方法があるようです。今回はクラスを使っています。*/
class Foo {
function h($str){
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
}
$foo = new Foo( );
echo <<<EOF
<html>
<head>
<title>キーワード表示画面</title>
</head>
<body>
{$foo->h($message)}<br />
⇩ ヒアドキュメントに関しては下記サイトへ
・[PHP]ヒアドキュメント内で関数を使う | PHP Archive
セッションハイジャックに関しては、XSS対策を行った上で下記の対策をする必要があります。
- セッションIDをクッキーのみで扱う
- セッションハイジャックのチェック
- パスワードによる多重チェック
Webアプリケーションによっては使える対策が制限されるようなので、条件に応じて使い分ける必要があるようです。
・セッションIDをクッキーのみで扱う
『php.iniファイル』、『.htaccessファイル』、『ini_set( ) 関数』などでセッションに関する値を以下のように設定します。
- session.use_cookies = 1
- session.use_only_cookies = 1
- session.use_trans_sid = 0
・セッションハイジャックのチェック
通常、セッションの途中でブラウザの『Accept-Charset 』『Accept-Language 』『User-Agent 』が変更されることはほとんどないそうです。
『Accept-Charset 』『Accept-Language 』『User-Agent 』を元に生成した乱数をセッションに保存しておき、セッション開始の直後にセッション内の値と、アクセスしてきた情報を元に生成した乱数の整合性をチェックすることで、セッションハイジャックの危険性を少しでも軽減できるようです。
・パスワード入力による多重チェック
重要な処理を行う前には必ずユーザーにパスワードを入力させる方法です。
ネット銀行や通販サイトなど金銭を扱うサイトで見られるしくみです。
セッションフィクセーションによるセッションIDの強制
セッションフィクセーションの脆弱性がある場合、攻撃者は、ログインを必要とする対象サイトに攻撃者の指定したセッションIDを含むリンクを張り、被害者がそのリンクを踏んで対象サイトに移動した後、ログイン処理を行うのを待ってから、被害者のセッションIDを使って対象サイトにアクセスすることで攻撃が成立してしまうようです。
イメージ図
( ※『パーフェクトPHP』引用 )
対応方法
セッションフィクセーションの対策をした後で、
- セッションIDをクッキーのみで扱う
- セッションハイジャックのチェック
- パスワードによる多重チェック
の対応を行う必要があるようです。
HTTPヘッダインジェクション
ブラウザに対して不正なHTTPレスポンスヘッダを送りつけることで、任意のURLにリダイレクトさせたり不正なクッキー(COOKIE)をセットさせたりする攻撃方法です。
プログラムの脆弱性を利用して改行コードで分割した複数のヘッダを送りつけることから、『HTTPレスポンス分割攻撃』と呼ばれることもあるそうです。
PHP4.4.2およびPHP5.1.2でPHP自体が対応を行っているため、Webアプリケーション側で対応する必要はないようです。
脆弱性のある例
header_injection_01.php
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
$url = ' http://example.com/ ';
// ⇩ $url .= $_GET['page'];は、$url = $url . $_GET['page'];に同じ
$url .= $_GET['page'];
header('Location: ' .$url);
exit( );
echo 'ページ指定がありません';
}
対応方法
『PHPのバージョンを上げる』『プログラムで改行コードをチェックする』の2つの方法があるようです。
・PHPのバージョンを上げる
PHP4.4.2、およびPHP5.1.2ではHTTPヘッダインジェクションに対応してます。
それより、新しいバージョンでは、header( )関数に対して改行コードを含むヘッダをセットしようとすると、WarningとなりHTTPレスポンスヘッダは送られません。
なので、PHP5.2.0より新しいバージョンを利用するのが望ましいようです。
・プログラムで改行コードをチェックする
上記の で囲まれた部分を以下に書き換えます。
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
$url = ' http://example.com/ ';
if (isset($_GET['page']) === true && $_GET['page'] !== ' ') {
// 改行コードが含まれていた場合は処理をヘッダを送出しない
if (strpos($_GET['page'], "¥r") !== false
|| strpos($_GET['page'], "¥n") !== false) {
echo '不正なリクエストを検出しました';
} else {
// ⇩ $url .= $_GET['page'];は、$url = $url . $_GET['page'];に同じ
$url .= $_GET['page'];
header('Location: ' .$url);
exit( );
}
} else {
echo 'ページ指定がありません';
}
改行コードが見つかった場合に、ヘッダを送出しないようにしたことで、HTTPヘッダインジェクションの攻撃を無効化してます。
メールの第三者中継
メール送信プログラムなどの脆弱性を突くことで想定外の動作を起こし、不正なプログラムを送信させる攻撃方法です。
意図せずスパムメールの送信サーバとなってしまう可能性があります。
脆弱性の種類
・メールの宛先にリクエストからの値を利用してるケース
メールの宛先をhidden属性のinput要素に設定している場合、input要素の値を改ざんしてフォーム送信することで、スパムメール送信の踏み台とされることがあります。
脆弱性のある例
mail_falsification.php
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
// 文字コードを明示的に指定する
mb_language('ja');
mb_internal_encoding('utf-8');
$message = ' ';
// POSTの場合、送信処理を実行する
if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
$result = mb_send_mail(
$_POST['to'],
$_POST['subject'],
$_POST['body'],
'From: "フォームからの問い合わせ" <inquiry@example.com>'
);
if ($result === true) {
$message = 'メールを送信しました';
} else {
$message = 'メールの送信に失敗しました';
}
}
// メール送信フォームを表示
header('Content-type: text/html; charset=utf-8');
echo <<<EOF
<html>
<head>
<title>お問い合わせ</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
$message<br />
<fieldset>
<legend>お問い合わせフォーム</legend>
<form action=" " method="post">
件名<input type="text" name="subject" /><br />
内容<textarea name="body"></textarea><br />
<input type="submit" value="メール送信" />
<input type="hidden" name="to" value="admin@example.com" />
</form>
</fieldset>
</body>
</html>
EOF;
下記のようなプログラムを用意することで、簡単に任意の宛先にメールが送信させられてしまいます。
<head>
<title>mail_falsification.phpに投稿ページ</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>
<body>
<form action="mail_falsification.php" method="post">
宛先<input type="text" name="to" value="foo@example.com"><br/>
件名<input type="text" name="subject" value="spam mail" ><br/>
内容<textarea name="body"></textarea><br/>
<input type="submit" value="メール送信">
</form>
</body>
</html>
$url = ' http://example.commail_falsification.php ';
$header = array(
' Content-Type:application/x-www-form-urlencoded; charset=utf-8 ',
' User-Agent:PERFECT-SAMPLE ',
);
$options = array(
'http' => array(
'method' => 'post',
'content' => http_build_query(
array(
'to' => 'foo@example.com',
'subject' => 'spam mail',
'body' => 'プログラムによるスパム投稿です。',
)
),
'header' => implode(
"rn",
$header
),
)
);
$result = file_get_contents($url, false, stream_context_create($options));
echo $result;
・PHP: file_get_contents( )について
・PHP: stream_context_create( )について
⇩ ストリームとストリームコンテキストについては下記へ
どちらのプログラムも、宛先となるtoに任意の値を設定してmail_falsification.phpにPOSTすることで、任意の宛先に対するメールの第三者中継が実行されてしまうようです。
mb_send_mail( )関数やmail( )関数は、RFC2822に準拠したメールアドレスであれば、カンマで区切ることで宛先に複数メールアドレスを指定できる仕様となっています。
そのため、カンマで区切ったメールアドレスの不正挿入による一斉送信が行われないように注意する必要があります。
⇩ RFC2822(電子メールのメッセージ形式の仕様)に関しては下記サイトへ
・ASCII.jp:メールの宛名はどこにある?ヘッダを理解する|電子メールプロトコル再入門
対応方法
Webアプリケーションが一斉送信を行う仕様の場合は、テキストエリアで1行1アドレスずつ入力してもらい、1行ごとに入力されているメールアドレスのチェックを行うなどの工夫が必要なようです。
・メールヘッダインジェクションの脆弱性が存在するケース
メールのヘッダ部分に改行コードを含めた文字列を挿入して、送信するメールを改ざんする攻撃方法です。
任意のメールヘッダの追加や本文の改ざんが可能となり、スパムメール送信の踏み台にされてしまいます。
脆弱性のある例
mail_header_injection_01.php
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
// 文字コードを明示的に指定する
mb_language('ja');
mb_internal_encoding('utf-8');
$message = ' ';
if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
$result = mb_send_mail(
'admin@example.com',
$_POST['subject'],
$_POST['body'],
'From: "フォームからの問い合わせ" <inquiry@example.com>'
);
if ($result === true) {
$message = 'メールを送信しました';
} else {
$message = 'メールの送信に失敗しました';
}
}
header('Content-type: text/html; charset=utf-8');
echo <<<EOF
<html>
<head>
<title>お問い合わせ</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
$message<br />
<fieldset>
<legend>お問い合わせフォーム</legend>
<form action=" " method="post">
件名<input type="text" name="subject" /><br />
メール<input type="text" name="from" /><br />
内容<textarea name="body"></textarea><br />
<input type="submit" value="メール送信" />
<input type="hidden" name="to" value="admin@example.com" />
</form>
</fieldset>
</body>
</html>
EOF;
下記のようなページがあるとmail_header_injection_01.phpで指定した 宛先以外にもメールが送られてしまうようです。
メールヘッダインジェクションを仕掛けるHTML
<head>
<title>mail_header_injection_01.phpに投稿するページ</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<form action="mail_header_injection_01.php" method="post">
件名<input type="text" name="subject" value="spam mail">
メール<textarea name="from">foo@example.com
Bcc:bar@example.com,buz@example.com</textarea><br/>
内容<textarea name="body">メールヘッダインジェクションです。Bccで意図し ない相手にメールを送信しています。</textarea>
<input type="submit" value="メール送信">
</form>
</body>
</html>
原因は、mb_send_mail( )関数の第4引数に引用した$_POST['from']に改行コードが含まれていたからのようです。
対応方法
メールの宛先、メールヘッダにリクエストからの値を利用しないことが根本的な対応方法になるようです。
Webアプリケーションの仕様で外部からの値を利用せざるを得ない場合は、カンマで区切ったメールアドレスの不正挿入やメールヘッダインジェクションに対応しておく必要があります。
・メールアドレスの不正挿入への対応
メールアドレスが有効かどうかの判断には、
- メールアドレスの形式として妥当か
- メールアドレスのドメイン部が有効か
- メールアドレスのローカル部が有効か
などをチェックする必要があり、形式が妥当かどうかのチェックにしても、
など、入力されたメールアドレスが確実に有効か判定することは現実的に難しく、「メールアドレスの形式として正しいと思われる」レベルの判定に留めても問題ないらしいです。
PEAR::Mail_RFC822から抜き出したメールアドレスの正規表現による判定
⇩ メールチェックに関しては下記サイトへ
・PHP - 正規表現でメールアドレスをチェック - Qiita
・正規表現によるメールアドレスチェックの正解がわからないので、自己責任で。 | doli blog
・[PHP]実用的なメールアドレスの正規表現 | DevAchieve
⇩ PEARのRFC822については下記へ
・メールヘッダインジェクションへの対応
メールヘッダに利用する値に改行コードが含まれていないか確認します。
mail_header_injection_01.phpの で囲まれた部分を以下に書き換えます。
if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
$subject = $_POST['subject'];
$body = $_POST['body'];
$from = $_POST['from'];
// メールヘッダの利用部分に改行コードが含まれていないか確認
$is_newline = false;
if (preg_match('/[\r\n]/', $subject) != false
|| preg_match('/[\r\n]/', $from) != false) {
$is_newline = true;
}
if ($is_newline === false) {
$result = mb_send_mail(
'admin@example.com',
$subject,
$body,
'From: ' . $from
);
if ($result === true) {
$message = 'メールを送信しました';
} else {
$message = 'メールの送信に失敗しました';
}
} else {
$message = 'メールの送信を中止しました';
}
}
メールヘッダに利用する$subjectと$fromに改行が含まれていないか確認し、改行が含まれていた場合はメールを送信しないようにすることで、メールヘッダインジェクションを防ぐことができるようです。
変数汚染攻撃
外部リクエストの値を自動的に変数に展開させることで、グローバルスコープの変数やスーパーグローバル変数の値を改ざんする攻撃方法です。
変数汚染攻撃に対する脆弱性が存在する場合、本来は改ざん不可能な環境変数までも改ざんできてしまうため、セッション変数改ざんによる権限の不正取得やコマンド実行攻撃なども実行されてしまうようです。
変数汚染攻撃の脆弱性として、
- register_globalsが有効になっている
- foreachによるスーパーグローバル変数の配列展開を行っている
- extract( )関数によるスーパーグローバル変数の配列展開を行っている
- import_request_variables( )関数によるGET/POST/Cookie変数の配列展開を行っている
- parse_str( )関数によるクエリ文字列の変数展開を行っている
などがあるようです。
脆弱性のある例
variable_contamination_01.php
/**
* このプログラムは脆弱性のサンプルです。
* 公開サーバに設置しないで下さい
*/
// register_globalsの対応として、GET変数を展開する
foreach ($_GET as $key => $value) {
$$key = $value;
}
echo ' あなたのIPアドレスは" ' . $_SERVER['REMOTE_ADDR'] . ' "です ';
対応方法
register_globalsを無効(OFF)にした上で次の対応を行います。
(※register_globalsは、テラパッドで開いた場合php.iniファイルの722行目あたり。
PHPのバージョンがPHP5.6.12の場合。)
- スーパーグローバル変数に対する変数汚染チェック
- 処理で扱う変数以外は展開しないようにする
- foreachやextract()関数による変数展開をやめる
・スーパーグローバル変数に対する変数汚染チェック
リクエストのキーをチェックして変数汚染攻撃に対応
variable_contamination_02.php
// ブラックリストとしてスーパーグローバル変数の変数名を設定
$black_list = array('GLOBALS', '_SERVER', '_GET', '_POST', '_FILES',
'_COOKIE', '_REQUEST', '_SESSION', '_ENV');
// リクエストのキーがスーパーグローバル変数の変数名なら終了
foreach (array($_GET, $_POST, $_COOKIE) as $request) {
foreach ($black_list as $super_global) {
if (isset($request[$super_global]) === true) {
exit( );
}
}
}
// register_globalsの対応として、GET変数を展開する
foreach ($_GET as $key => $value) {
$$key = $value;
}
// GET変数を利用した処理:開始
// ...
// GET変数を利用した処理:終了
echo ' あなたのIPアドレスは" ' . $_SERVER['REMOTE_ADDR'] . ' "です ';
・処理で扱う変数以外は展開しないようにする
ホワイトリストによるチェックを行い変数汚染攻撃に対応する
variable_contamination_03.php
// ホワイトリストとして、利用するGET変数のキーを設定
$white_list = array('foo', 'bar');
// register_globalsの対応として、GET変数を展開する
foreach ($_GET as $key => $value) {
// GET変数のキーがホワイトリストに含まれていなければ終了
if (in_array($key, $white_list) === false) {
exit( );
}
$$key = $value;
}
// GET変数を利用した処理:開始
// ...
// GET変数を利用した処理:終了
echo ' あなたのIPアドレスは" ' . $_SERVER['REMOTE_ADDR'] . ' "です ';
・foreachやextract()関数による変数展開をやめる
変数を展開せずに、実際に処理する際に『$_GET['foo']』のように記述する方法です。
⇩ ブラックリスト方式とホワイトリスト方式については下記サイトへ
・セキュリティソフトの大半はブラックリスト型|セキュリティ対策|ディフェンスプラットフォーム|Defense Platform|ハミングヘッズ株式会社
⇩ 変数汚染攻撃については下記サイトへ
Nullバイト攻撃
文字列の終端を表す制御文字Nullバイト(「\0」「\x00」、URLエンコードでは「%00」)をリクエストの中に含めることで、セキュリティのチェックをくぐり抜けるリクエストを送りつける攻撃方法です。
PHPはファイルシステム関連の操作にC言語の関数を使用しているため、nullバイトを含む文字列の処理で誤動作を起こすことがあるそうです。
脆弱性のある例
null_byte_attack_01.php
file_exists( )関数が、nullバイトを正しく処理できず文字列の終端として処理してしまうバイナリセーフでない関数であることが問題のようです。
対応方法
処理の最初にnullバイトを削除することと、極力バイナリセーフな関数でチェックを行うことで、nullバイトが検出された時点で処理を終了させることが望ましいようです。
null_byte_attack_02.php
ディレクトリトラバーサル
ディレクトリーを遡ることによって、ドキュメントルートの外側に保存されているファイルにアクセスする攻撃方法です。
Nullバイト攻撃と組み合わせて利用されることが多いようです。
脆弱性のある例
directory_travasal_01.php
対応方法
リクエストからの値をファイル名などに利用しないことが一番確実のようです。
それができない場合は、
などを、要件にあわせて適用させる必要があります。
・ホワイトリスト方式
directory_travasal_02.php
・ブラックリスト方式
directory_travasal_03.php
・不正文字列除去方式
directory_travasal_04.php
・open_basedir方式
サーバー管理者であるかPHP5.3.0より新しいバージョンを利用している場合は、open_basedirを利用することで対応できる可能性があります。
PHP5.3.0より古いバージョンでは、php.iniファイル、httpd.confファイルでしか設定ができないためサーバー管理者でなければ設定できませんが、
PHP5.3.0より新しいバージョンでは、ini_set( )関数で設定が可能です。
eval利用攻撃
引数として渡した文字列をPHPコードとして評価するeval( )関数などの機能を利用して、任意のPHPコードを挿入し悪意あるプログラムを実行させる攻撃方法です。
脆弱性のある例
eval_attack_01.php
URLの後ろに「?keyword='.phpinfo( ).'」と付けてアクセスすると、eval( )関数の機能によりphpinfo( )関数が実行されてしまいます。
eval( )関数以外にも、
などのコールバック系関数でも悪意あるコードを実行させることができるため、リクエストからの値をそのまま関数に代入する場合は注意する必要があるようです。
対応方法
eval_attack_02.php
eval( )関数を実行する前に値のチェックを行い、値が適当でない場合は処理を行わないようにすることで、eval( )攻撃を無効化してます。
ファイルアップロード攻撃
悪意あるコードを含んだファイルをアップロードし、任意の処理を実行させる攻撃方法です。
大きく分けて、
- クロスサイトスクリプティング(XSS)などを仕込んだファイルに他人をアクセスさせる攻撃
- サーバに対する攻撃を仕込んだファイルに攻撃者自身がアクセスする攻撃
が存在するようです。
脆弱性のある例
upload_attack_01.php
アップロードされたファイルをドキュメントルートの内側にそのままのファイル名で保存しているため、攻撃者が以下のようなファイルをアップロードしてアクセスした場合、Webサーバの実行ユーザ権限で可能なサーバへの攻撃が成立してしまいます。
ルートディレクトリに存在するファイル一覧を取得する攻撃ファイル
対応方法
アップロードされたファイルは可能な限りドキュメントルートの外側に保存し、且つ、推測困難なファイル名で保存することが重要のようです。
upload_attack_02.php
レンタルサーバを使っていてドキュメントルートの外側に保存できない場合は、アップロードされたファイルを保存してるディレクトリに以下の内容を.htaccessファイルに書き足します。
.htaccess
Deny from All
画像ファイルをアップロードさせるWebアプリケーションなどで、悪意のあるPHPのコードなどを記述したファイルを画像ファイルとして保存し、そのファイルをアップロードする攻撃方法です。
このファイルを、includeやrequireで読み込んだ場合、PHPとして解釈され、攻撃コードが仕込まれていた場合、プログラム実行ユーザの権限で可能な攻撃が実行されてしまいます。
fake.image.gif
getimagesize( )関数でGIFに偽装したファイルをチェック
fake_image.php
getimagesize( )関数の戻り値をvar_dump( )関数で表示している箇所は、実際にはアップロード用のディレクトリにmove_upload_file( ) 関数でファイルを移動する処理を行うことが多いそうです。
GIFファイルに偽装したPHPファイルは、<img>タグでブラウザに表示される場合は壊れた画像ファイルとしてエラー表示されるだけですが、includeやrequireで読み込むとPHPのコードが実行されてしまうので、アップロードされたファイルをincludeやrequireしては危険です。
インクルード攻撃
任意のファイルを読み込ませてPHPコードとして処理を行わせることで想定外の動作を引き起こす攻撃方法です。
- include( )
- require( )
- include_once( )
- require_once( )
に対する引数にリクエストの値を利用している場合、インクルード攻撃に対する脆弱性が存在する可能性があります。
- リモートファイル・インクルード攻撃(外部のサーバに存在するファイルを読み込ませる)
- ローカルファイル・インクルード攻撃(ローカルサーバに存在するファイルを読み込ませる)
の2種類の攻撃方法が存在します。
リモートファイル・インクルード攻撃
include_attack_01.php
ローカルファイル・インクルード攻撃
include_attack_02.php
対応方法
リクエストからの値をインクルードするファイルの指定に利用しないことです。
Webアプリケーションの仕様上インクルードするファイルをリクエストで指定する必要がある場合は、
- リモートファイルのインクルード制限
- ホワイトリスト方式によるインクルードファイルの制限
などを行い対応していく必要があります。
リモートファイルのインクルード制限
php.iniファイル933行目あたりを下記のように変更します。
allow_url_include = Off
設定名 | 内容 | デフォルト値 |
---|---|---|
allow_url_fopen | ftpまたはhttpプロトコルを用いてfopenラッパーがリモートファイルにアクセスすることを許可するか | On(有効) |
allow_url_include | リモートファイルに対するインクルードを許可するか | Off(無効)※PHP5.2.0から設定可能 |
ホワイトリスト方式によるインクルードファイルの制限
インクルードを許可するファイルが決まっている場合、ホワイトリスト方式が有効です。
include_attack_03.php
basename( )関数を使って読み込みファイルを特定のディレクトリ内に制限
インクルードを許可するファイルが特定のディレクトリ内に多数存在している場合には、basename( )関数を使うのが有効です。
include_attack_04.php
basename( )関数はバイナリセーフではないため、basename( )関数だけだとディレクトリトラバーサルしか防げません。
Nullバイト攻撃への対応も忘れずに実装する必要があります。
パス・ディスクロージャ
故意にエラーを起こし、PHPのエラーレポートからスクリプトのフルパスを漏洩させる攻撃方法です。
php.iniの「error_reporting」「display_errors」の値が下記のように設定されていると、警告(E_NOTICEやE_WARNING)レベルを含めた全てのエラーが表示されます。
php.ini
display_errors = On
この設定が適用された環境で、リクエストの値を表示するプログラムを実行します。
path_disclosure_01.php
URLの後ろに「?string[ ]=1234」とつけてアクセスすると、エラーが発生しpath_disclosure_01.phpのフルパスとエラーが発生した行が表示されてしまいます。
文字列を処理するhtmlspecialchars( )関数に、配列を渡してるためエラーになります。
対応方法
エラーを画面に出力させずファイルにログとして記録する方法です。
PHP処理の最初の部分でエラーレポートの設定をする
path_disclosure_02.php
.htaccessでエラーレポートの設定をする
コマンド実行攻撃(Command Injection)
サーバー内の任意のコマンドを実行させる攻撃方法です。WebサーバのOSに用意されているコマンドを実行することから「OSコマンド実行攻撃」と呼ばれることが多いそうですが、OSが用意したコマンドに限らず外部コマンドを実行することが可能です。
などのプログラム実行関数や、バックティック演算子(実行演算子)を用いて実行するコマンドの中に外部からのリクエストが含まれる場合は、この脆弱性の存在をチェックする必要があります。
脆弱性のある例
command_injection_01.php
対応方法
プログラム実行関数の引数にリクエストからの値を利用しない。
仕様で利用せざるを得ない場合は、エスケープを行った上で、実行する外部コマンドに応じたホワイトリスト方式などのチェックを行うのが望ましいようです。
command_injection_02.php
詳しくは『パーフェクトPHP (PERFECT SERIES 3) 大型本 – 2010/11/12 小川 雄大 (著), 柄沢 聡太郎 (著), 橋口 誠 (著)』に載っています。
今回はこのへんで。