LibXMLのエンティティ変換とXXEと…

(Last Updated On: 2022年9月18日)

今のlibxmlは意図しないエンティティ変換により意図しない情報漏洩などを防ぐ為にエンティティ変換をしない仕様になっています。libxml関数にLIBXML_NOENT(エンティティ変換を行わせる為のフラグ)を渡して処理しないとエンティティ変換が行われません。しかし、例外があります。

XXE(XML eXternal Entity)攻撃

XXE攻撃はエンティティ( < などです)を変換する機能を使った攻撃です。

エンティティ変換をするとローカルファイルの読み取りも可能になります。

<?php
$xml = '<!DOCTYPE root [<!ENTITY c PUBLIC "bar" "/etc/passwd">]>
<test>&c;</test>';
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT);
echo $dom->saveXML();

これを実行すると

<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY c PUBLIC "bar" "/etc/passwd">
]>
<test>root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
(省略)

のように/etc/passwdの中身が読み取れます。

LibXMLの動作

loadXML()にLIXML_NOENTフラグを渡すとエンティティ変換あり

<?php
$xml = '<!DOCTYPE test [<!ENTITY c "TEST">]>
<test>&c;</test>';
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT);
echo $dom->saveXML();

出力

<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY c "TEST">
]>
<test>TEST</test>

&cが変換されてTESTになっています。

デフォルトはエンティティ変換無し

<?php
$xml = '<!DOCTYPE test [<!ENTITY c "TEST">]>
<test>&c;</test>';
$dom = new DOMDocument();
$dom->loadXML($xml);
echo $dom->saveXML();

出力

<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY c "TEST">
]>
<test>&c;</test>

&c; はエンティティ変換されず &c; のままです。

しかし、loadXML()ではなくtextContentを使うとエンティティ変換します。

<?php
$xml = '<!DOCTYPE test [<!ENTITY c "TEST">]>
<test>&c;</test>';
$dom = new DOMDocument();
$dom->loadXML($xml);
echo $dom->textContent;

出力

TEST

&c; ではなくTEST を出力し、エンティティ変換しています。

textContentプロパティの実装は以下のように xmlNodeGetContent()を呼ぶだけのラッパーになっています。

/* {{{ textContent	string
readonly=no
URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#Node3-textContent
Since: DOM Level 3
*/

int dom_node_text_content_read(dom_object *obj, zval *retval)
{
	xmlNode *nodep = dom_object_get_node(obj);
	char *str = NULL;

	if (nodep == NULL) {
		php_dom_throw_error(INVALID_STATE_ERR, 1);
		return FAILURE;
	}

	str = (char *) xmlNodeGetContent(nodep);

	if (str != NULL) {
		ZVAL_STRING(retval, str);
		xmlFree(str);
	} else {
		ZVAL_EMPTY_STRING(retval);
	}

	return SUCCESS;
}

つまり、xmlNodeGetContent()を呼ぶとエンティティ変換が行われます。利用されている箇所を探すと

[yohgaki@dev PHP-master]$ grep -n xmlNodeGetContent ext/dom/*.[ch]
ext/dom/attr.c:124:	if ((content = xmlNodeGetContent((xmlNodePtr) attrp)) != NULL) {
ext/dom/characterdata.c:48:	if ((content = xmlNodeGetContent(nodep)) != NULL) {
ext/dom/characterdata.c:97:	content = xmlNodeGetContent(nodep);
ext/dom/characterdata.c:131:	cur = xmlNodeGetContent(node);
ext/dom/characterdata.c:203:	cur = xmlNodeGetContent(node);
ext/dom/characterdata.c:250:	cur = xmlNodeGetContent(node);
ext/dom/characterdata.c:307:	cur = xmlNodeGetContent(node);
ext/dom/entity.c:102:		content = (char *) xmlNodeGetContent((xmlNodePtr) nodep);
ext/dom/node.c:142:			str = (char *) xmlNodeGetContent(nodep);
ext/dom/node.c:145:			str = (char *) xmlNodeGetContent(nodep->children);
ext/dom/node.c:745:	str = (char *) xmlNodeGetContent(nodep);
ext/dom/php_dom.c:1298:	xmlChar	*strContent = xmlNodeGetContent(nodep);
ext/dom/php_dom.c:1319:						strContent = xmlNodeGetContent(nextp);
ext/dom/processinginstruction.c:106:	if ((content = xmlNodeGetContent(nodep)) != NULL) {
ext/dom/text.c:135:	cur = xmlNodeGetContent(node);

となります。(gitのmaster)メソッドがプロパティによってはLIBXML_NOENTフラグが無くてもエンティティ変換してしまう場合があるということです。

出力しなくてもDoSが可能

LIBXML_PARSEHUGE (int)Sets XML_PARSE_HUGE flag, which relaxes any hardcoded limit from the parser. This affects limits like maximum depth of a document or the entity recursion, as well as limits of the size of text nodes.

Note: Only available in Libxml >= 2.7.0 (as of PHP >= 5.3.2 and PHP >= 5.2.12)

LIBXML_PARSEHUGEオプションを設定すると、ユーザーへの出力なしだと情報漏洩はしませんがDoSが可能になります。

エンティティ変換をすると小さい文字列を大きな文字列に変換可能になります。これを利用するとメモリを大量に消費させる増幅攻撃が可能になります。

例えば、次のようなXMLファイルを読み込ませて処理させます。

<!DOCTYPE test [<!ENTITY c "<?php echo str_repeat('c', 2**16);?>">]>
<test><?php echo str_repeat('&c;', 2**16); ?></test>;

上のXMLは193KB程のファイルになりますが、&c; が 2万倍以上に増幅されるので大量のメモリを消費します。textContentプロパティは単純なlibxmlのラッパー関数になっているので処理される際にPHPのメモリ制限機能は働かず、使いたいだけのメモリを消費してしまいます。

LIBXML_PARSEHUGEオプションを設定しないと10MBまでに制限されます。(制限値はlibxmlのバージョンによって変わる可能性あり)

ネットワークの利用

XMLはネットワーク経由で定義などを読み取れるようになっています。

<root   xmlns="http://www.example.org"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.example.org http://www.example.org/xsds/example.xsd">
<!DOCTYPE root SYSTEM "http://www.example.org/dtds/example.dtd">

等です。この機能を利用するとSSRFが可能になります。以下のような何かを削除するURL

http://target.example.com/not_really_secure_delete?action=delete&id=1234

に内部のWebサーバーからアクセスされると困る環境は普通にあります。

libxmlにはネットワークアクセスを禁止するオプションフラグがあります。このオプションはデフォルトでは無効です。

LIBXML_NONET (int) Disable network access when loading documents

ネットワークアクセスが不必要な場合、XMLをロードする際に指定しておくべきです。

まとめ

  • エンティティ変換(LIBXML_NOENTオプションで全て変換)を有効にしていなくても、エンティティ変換が起きる場合がある
  • ローカルファイルの読み取りやSSRFだけでなく、増幅攻撃もできる(LIBXML_PARSEHUGEオプションで制限なしで処理)
  • ネットワークリソースにアクセスした場合、SSRFが可能になる(LIBXML_NONETオプションで無効化)
  • “<!DOCTYPE “が無くて良いなら、無いことをバリデーションして不正XMLは拒否する方が良い

投稿者: yohgaki