おい、ちょっと手を止めてこっちを向いてくれ。
先日、とあるクライアントのWebアプリケーション診断で、見事なまでに美しく、そしてエグい脆弱性をブチ抜いた。表向きは「ただのPDF帳票出力機能」なんだ。ユーザーが入力したメタデータをXML形式でバックエンドに投げ、それをパーサが処理して帳票レイアウトを組み立てるという、よくある業務システムの一幕だ。
開発チームは「うちは社内ネットワーク内だし、外部からのアクセスはWAFで弾いてるから大丈夫です」と胸を張っていた。だが、レッドチームの視点からすれば、そんなものは防壁の内側から鍵を開けっ放しにしているようなものだ。
今回は、XML外部エンティティ(XXE)を起点にし、そこから内部ネットワークのポートスキャン、さらにはクラウドのメタデータサービス(IMDS)を叩き潰してインフラの根幹を奪い取る「XXE起因のSSRF(Server-Side Request Forgery)複合パターン」について、実戦の現場で得た知見を共有しよう。
後輩の君たちには、綺麗ごとの教科書ではなく、本物の攻撃者がどこを見て、どうシステムをハックするのか、そしてそれをどうねじ伏せるのかを叩き込んでおく。
—
1. なぜXXEからのSSRFは「エグい」のか?(攻撃のメカニズム)
通常のSSRFであれば、アプリケーションが持つHTTPクライアント機能(cURLやrequestsなど)を経由して外部や内部のリクエストを飛ばす。しかし、XXEを利用したSSRFは、XMLパーサそのものにネットワークリクエストを行わせる点に凶悪さがある。
攻撃者は、脆弱なXMLパーサに対して次のようなペイロードを送りつける。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<root>
<data>&xxe;</data>
</root>
このXMLを受け取った脆弱なパーサは、外部エンティティ(&xxe;)を解決しようとして、OSのネットワークスタックを直接叩き、指定されたURLへHTTPリクエストを送信する。
ここで現場のエンジニアが陥りがちな致命的な勘違いがある。
「うちのサーバーは外部インターネットに繋がっていないから、外部エンティティなんて解決できないはずだ」という思い込みだ。
違う。攻撃者が狙っているのは外部のインターネットじゃない。サーバー自身から見える「内部ネットワーク(Intranet)」や「クラウドの内部API(169.254.169.254など)」だ。
外部への通信がファイアウォールでブロックされていようとも、サーバー自身がクラウド基盤や社内LANと通信できる状態であれば、XXEはそのまま強力な踏み台(プロキシ)として機能してしまう。
—
2. 実際のインシデントシナリオ:内部ネットワークの蹂躙
ペネトレーションテストの現場では、この手法を使って次のようなステップで内部を崩壊させる。
1. ファイアウォールのバイパス: 外部からはアクセスできない社内向け管理画面(例: http://10.0.1.50:8080/admin)に対し、Webサーバーをプロキシとして使ってアクセスを試みる。
2. ポートスキャン: エンティティのURLをインクリメンタルに変更(http://127.0.0.1:80/, http://127.0.0.1:8080/, http://127.0.0.1:3306/ 等)し、レスポンスの応答速度やエラー内容の違いから、内部で稼働しているサービスを特定する。
3. クラウド環境の乗っ取り (IMDSv1): AWSなどの環境であれば、メタデータサービスから一時的なIAMロールのクレデンシャル(アクセスキーとシークレットキー)を窃取し、インフラ全体の管理者権限へと昇格する。
机上の空論だと思うか? 実際のインシデントでは、これが数分単位で自動化ツールによって実行されるんだ。
—
3. 【実装例】なぜ脆弱性が生まれるのか(脆弱なPHPコード)
まずは、何がダメなのかをコードで確認しておこう。以下のPHPスクリプトは、典型的な「地雷」を踏み抜いている実装だ。
<?php
// 【危険な実装例】外部エンティティの無効化を行っていないXMLパーサ
header('Content-Type: application/json; charset=utf-8');
// POSTリクエストで送信されたXMLデータを受け取る
$xmlData = file_get_contents('php://input');
if (empty($xmlData)) {
echo json_encode(["status" => "error", "message" => "XMLデータが空です"]);
exit;
}
// 脆弱性: libxmlのデフォルト設定のまま、外部エンティティの読み込みを許可している
$dom = new DOMDocument();
$loaded = $dom->loadXML($xmlData, LIBXML_NOENT | LIBXML_DTDLOAD);
if (!$loaded) {
echo json_encode(["status" => "error", "message" => "XMLのパースに失敗しました"]);
exit;
}
// パース結果から特定の要素を取得して処理する(つもり)
$dataNode = $dom->getElementsByTagName('data')->item(0);
$resultText = $dataNode ? $dataNode->nodeValue : "";
echo json_encode([
"status" => "success",
"processed_data" => $resultText
]);
?>
LIBXML_DTDLOAD や LIBXML_NOENT を指定している、あるいはお構いなしにデフォルトのままでDOMDocumentやSimpleXMLを使っている場合、外部からのDTDやエンティティの解決が有効になってしまう。これが全ての元凶だ。
—
4. 【完全防御】コピペで動くセキュアな実装サンプル(PHP)
では、これをどう修正すべきか。
結論から言えば、「外部エンティティ(DTDを含む)の読み込みをシステムレベルで完全に禁止する」これにつきる。
PHPの libxml ライブラリを使用する場合は、明示的に外部DTDのロードをオフにする関数を呼び出す必要がある。以下のコードを見てほしい。
<?php
/**
* セキュアなXMLパーサ実装サンプル (PHP)
* XXEおよびそれに伴うSSRFを完全にブロックする設計
*/
header('Content-Type: application/json; charset=utf-8');
// POSTデータの取得(サイズ制限も設けてDoSを防ぐ)
$xmlData = file_get_contents('php://input', false, null, 0, 1024 * 1024); // 最大1MB
if (empty($xmlData)) {
http_response_code(400);
echo json_encode(["status" => "error", "message" => "不正なリクエストです"]);
exit;
}
// 【重要】外部エンティティの読み込みを確実に無効化する
// libxml 2.9.0以降ではデフォルトで無効化されていますが、古い環境や明示的な安全性のために必須です
if (function_exists('libxml_disable_entity_loader')) {
libxml_disable_entity_loader(true);
}
// XML内部のエラーハンドリングを有効化し、例外を適切にキャッチする
libxml_use_internal_errors(true);
$dom = new DOMDocument();
// 【重要】LIBXML_DTDLOAD や LIBXML_NOENT は絶対に指定しないこと!
// 追加のフラグが必要な場合は、セキュリティ影響を完全に理解した上で最小限に留める
// 外部エンティティやDTDのフェッチを防ぐためのオプションを設定
$options = LIBXML_NONET; // ネットワークアクセスを禁止する最強のフラグ
$loaded = $dom->loadXML($xmlData, $options);
if (!$loaded) {
// ログにエラー詳細を記録しつつ、ユーザーには汎用的なエラーメッセージを返す
$errors = libxml_get_errors();
libxml_clear_errors();
// 攻撃者にヒントを与えないため、詳細なパースエラーは画面に出さない
http_response_code(400);
echo json_encode(["status" => "error", "message" => "XMLの解析に失敗しました"]);
exit;
}
// 安全に要素を取得して処理
$dataNode = $dom->getElementsByTagName('data')->item(0);
$resultText = $dataNode ? htmlspecialchars($dataNode->nodeValue, ENT_QUOTES, 'UTF-8') : "";
echo json_encode([
"status" => "success",
"processed_data" => $resultText
]);
?>
ポイントは LIBXML_NONET フラグの指定だ。これにより、パーサがネットワーク経由で外部リソースにアクセスしようとした瞬間にエラーが発生し、SSRFの連鎖を断ち切ることができる。
—
5. アプリケーション層だけじゃない!多層防御(Defense in Depth)の設定
プログラマがどれだけ気をつけていても、依存しているサードパーティ製ライブラリ(古いXMLパーサなど)の裏をかかれるリスクはゼロにはならない。だからこそ、インフラやネットワーク層での多層防御が不可欠だ。
A. クラウド環境におけるIMDSの保護 (AWSの例)
AWSを使っているなら、メタデータサービス(IMDSv1)を廃止し、IMDSv2を強制すること。IMDSv2ではセッショントークンが必要になるため、単純なHTTP GETリクエストしか発行できない古いタイプのXXE起因SSRFではクレデンシャルを抜けなくなる。
さらに、EC2インスタンスのプロファイルポリシーで、不要な権限(IAMロール)を一切付与しない「最小権限の原則」を徹底しろ。万が一踏み破られても、被害を最小限に抑え込める。
B. アウトバウンド・ファイアウォール(Egress Filtering)
WebサーバーやAPサーバーから、不要な外向き通信(特にプライベートIPレンジやクラウドメタデータIP 169.254.169.254 への通信)を厳格にブロックする。
Nginxやリバースプロキシ、OSのiptables/firewalldを使い、次のようなルーティングポリシーを適用しておこう。
# 例: サーバーからAWSメタデータIPへの直接アクセスを遮断するiptables設定
iptables -A OUTPUT -p tcp -d 169.254.169.254 -j DROP
—
6. まとめ:セキュリティチーフからのメッセージ
XXE起因のSSRFは、「ただのデータフォーマットのエラー」に見せかけて、インフラの中枢へと牙を剥く極めて厄介な脆弱性だ。
「XMLなんて枯れた技術だから大丈夫」
「社内システムだから外部からの攻撃は来ない」
そんな甘い認識を持っているメンバーがチームにいたら、今日のこの解説記事を叩きつけてやってくれ。
セキュアなコードを書くことは、単にバグを防ぐだけじゃない。自分たちが作り上げたシステムと、その先にある顧客の信頼を守るための唯一の盾なんだ。
実装時のバリデーション、パーサのフラグ設定、そしてインフラのネットワーク分離。すべてのレイアウトで隙を作らない「堅牢な設計」を、明日からの開発に必ず組み込んでほしい。期待しているぞ。
コメント