目录
前言:
(一)XML 的常见接口
1.XMLReader
2.SAXBuilder
3.SAXReader
4.SAXParserFactory
5.Digester
6.DocumentBuilderFactory
(二)XXE 漏洞审计
(三)XXE 漏洞修复
(四)小结
前言:
XXE 为 XML 外部实体注入。当应用程序在解析 XML 输入时,在没有禁止外部实体的加载而导致加载了外部文件及代码时,就会造成 XXE 漏洞。 XXE 漏洞可以通过 file 协议或是 FTP 协议来读取文件源码,当然也可以通过 XXE 漏洞来对内网进行探测或者功击,如图 2-96 所示。漏洞危害有:任意文件读取、内网探测、攻击内网站点、命令执行、DOS 攻击等。
(一)XML 的常见接口
想要了解 XXE 漏洞,我们首先需要知道常见的能够解析 XML 的方法,在 Java 中我们一般用以下几种常见的接口来解析 XML 语言。
1.XMLReader
XMLReader 接口是一种通过回调读取 XML 文档的接口,其存在于公共区域中。XMLReader 接口是 XML 解析器实现 SAX2 驱动程序所必需的接口,其允许应用程序设置和查询解析器中的功能和属性、注册文档处理的事件处理程序,以及开始文档解析。当XMLReader 使用默认的解析方法并且未对 XML 进行过滤时,会出现 XXE 漏洞。
try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.parse(new InputSource(new StringReader(body)));
} catch (Exception e) {
return EXCEPT;
}
2.SAXBuilder
SAXBuilder 是一个 JDOM 解析器,其能够将路径中的 XML 文件解析为 Document 对 象。SAXBuilder 使用第三方 SAX 解析器来处理解析任务,并使用 SAXHandler 的实例侦听 SAX 事件。当 SAXBuilder 使用默认的解析方法并且未对 XML 进行过滤时,会出现XXE 漏洞
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXBuilder builder = new SAXBuilder();
// org.jdom2.Document document
builder.build(new InputSource(new StringReader(body))); // cause xxe
return "SAXBuilder xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
3.SAXReader
DOM4J 是 dom4j.org 出品的一个开源 XML 解析包,使用起来非常简单,只要了解基本的 XML-DOM 模型,就能使用。 DOM4J 读 / 写 XML 文档主要依赖于 org.dom4j.io包,它有 DOMReader 和 SAXReader 两种方式。因为使用了同一个接口,所以这两种方式的调用方法是完全一致的。同样的,在使用默认解析方法并且未对 XML 进行过滤时,其也会出现 XXE 漏洞。
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXReader reader = new SAXReader();
// org.dom4j.Document document
reader.read(new InputSource(new StringReader(body))); // cause xxe
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
4.SAXParserFactory
SAXParserFactory 使应用程序能够配置和获取基于 SAX 的解析器以解析 XML 文档。其受保护的构造方法,可以强制使用 newInstance() 。跟上面介绍的一样,在使用默认解析方法且未对 XML 进行过滤时,其也会出现 XXE 漏洞。
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
return "SAXParser xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
5.Digester
Digester 类用来将 XML 映射成 Java 类,以简化 XML 的处理。它是 Apache Commons库中的一个 jar 包: common-digester 包。一样的在默认配置下会出现 XXE 漏洞。其触发的 XXE 漏洞是没有回显的,我们一般需通过 Blind XXE 的方法来利用:
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
Digester digester = new Digester();
digester.parse(new StringReader(body)); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
6.DocumentBuilderFactory
javax.xml.parsers 包中的 DocumentBuilderFactory 用于创建 DOM 模式的解析器对象,DocumentBuilderFactory 是一个抽象工厂类,它不能直接实例化,但该类提供了一个newInstance() 方法,这个方法会根据本地平台默认安装的解析器,自动创建一个工厂的对象并返回。
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
Document document = db.parse(is); // parse xml
// 遍历 xml 节点 name 和 value
StringBuffer buf = new StringBuffer();
NodeList rootNodeList = document.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList child = rootNode.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node = child.item(j);
buf.append(node.getNodeName() + ":" + node.getTextContent() + "\n");
}
}
sr.close();
return buf.toString();
}catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
(二)XXE 漏洞审计
XXE 漏洞的审计方法和其他漏洞类似,只是搜索的关键字有所不同。这次我们采用了 GitHub 开源的 XXE 靶场: XXE-Lab 来进行审计。通过搜索 XML 的常见关键字,可以快速地对关键代码进行定位,如图 2-1 所示。
根据搜索结果可知,使用了“DocumentBuilderFactory.newInstance() ”,因此可通过双击跟进对应代码段查看 XML 是否可控。
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,
IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db;
String result="";
try {
db = dbf.newDocumentBuilder();
Document doc = db.parse(request.getInputStream());
String username = getValueByTagName(doc,"username");
String password = getValueByTagName(doc,"password");
if(username.equals(USERNAME) && password.equals(PASSWORD)){
result = String.format("<result><code>%d</code><msg>%s</msg> </result>",1,username);
}else{
result = String.format("<result><code>%d</code><msg>%s</msg></result>",0,username);
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
result = String.format("<result><code>%d</code><msg>%s</msg></result>",3,e.getMessage());
} catch (SAXException e) {
e.printStackTrace();
result = String.format("<result><code>%d</code><msg>%s</msg></result>",3,e.getMessage());
}
response.setContentType("text/xml;charset=UTF-8");
response.getWriter().append(result);
}
通过通读源码可以发现:程序首先通过“ DocumentBuilderFactory.newInstance() ”获取一个“ DocumentBuilderFactory ”的实例,随后通过“ DocumentBuilder::parse ”来解析通过“ request.getInputStream() ”传入的 body 内容,并返回一个 Document 实例,漏洞关键代码如图 2-2 所示。
继续跟进程序可以发现,其通过 getValueByTagName 函数获取了 username 节点的内容,并在对比数据后将其输出到页面中。也就是说,这里可以通过控制 username 的值来达到回显 XXE 的目的,如图 2-3 所示。
审计到这里,我们已经清楚了漏洞的整个触发流程及原理。接下来,可以构造一个用于 XXE
漏洞审计的常见的
Payload
。常见的
XXE Payload
如下所示:
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY file SYSTEM "file:///etc/passwd"> ]>
<xxe>&file;</xxe>
该源码中会将 username 节点的内容进行打印,因此我们只需将上面的 XXE 节点换为username 即可被成功解析并输出, XXE 执行效果如图 2-4 所示。最终的 Payload 如下
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY file SYSTEM "file:///etc/passwd"> ]>
<user>
<username>&file;</username>
<password>admin</password>
</user>
(三)XXE 漏洞修复
对于 XXE
漏洞的防御比较简单,只需在使用
XML
解析器时设置其属性,禁用
DTD或者禁止使用外部实体,一般常见的修复方案如下所示:
//实例化解析类之后通常会支持三个配置
obj.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
obj.setFeature("http://xml.org/sax/features/external-general-entities", false);
obj.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
当然,在使用不同的解析库时修复方案也会略有不同。值得注意的是,
“
DocumentBuilder builder = dbf.newDocumentBuilder();
”这行代码需要写在
dbf.setFeature()
之后安全措施才能够生效,否则将无法防范 XXE
漏洞。以前面的实例代码为例,我们可以通过如下方法进行修复:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,
IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db;
String result="";
try {
String FEATURE = null;
FEATURE = "http://javax.xml.XMLConstants/feature/secure-processing";
dbf.setFeature(FEATURE, true);
FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
dbf.setFeature(FEATURE, true);
FEATURE = "http://xml.org/sax/features/external-parameter-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://xml.org/sax/features/external-general-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
dbf.setFeature(FEATURE, false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
db = dbf.newDocumentBuilder();
Document doc = db.parse(request.getInputStream());
String username = getValueByTagName(doc,"username");
String password = getValueByTagName(doc,"password");
if(username.equals(USERNAME) && password.equals(PASSWORD)){
result = String.format("<result><code>%d</code><msg>%s</msg></result>",1,username);
}else{
result = String.format("<result><code>%d</code><msg>%s</msg></result>",0,username);
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
result = String.format("<result><code>%d</code><msg>%s</msg> </result>",3,e.getMessage());
} catch (SAXException e) {
e.printStackTrace();
result = String.format("<result><code>%d</code><msg>%s</msg></result>",3,e.getMessage());
}
response.setContentType("text/xml;charset=UTF-8");
response.getWriter().append(result);
}
(四)小结
为了防御 XXE 漏洞,较为有效的处理方式是开发者在不影响系统业务的前提下做好安全配置。如果系统业务需要引用外部实体,应在后端做好参数校验。此外,有许多第三方组件曾被爆出 XXE 漏洞(例如, Spring-data-XMLBean 、 JavaMelody 组件 XXE 、 Apache OFBiz 、微信支付 SDK-XXE ),建议读者朋友关注这类安全问题,避免因为使用第三方组件而引入 XXE 漏洞。