Java审计之XXE

写在前面

因为已经很久没有接触到XXE了,所以借此机会打算温习一遍XXE再来讲一下在Java中去审计XXE的一个思路和流程。

About XXE

基础知识

XXE(XML External Entity Injection) 全称为 XML 外部实体注入,与其他的注入漏洞类似,只不过XXE注入的是XML外部实体。

那么什么是XML?

XML是一种非常流行的标记语言,在1990年代后期首次标准化,并被无数的软件项目所采用。

XML用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。

XML被设计为传输和存储数据,其焦点是数据的内容,其把数据从HTML分离,是独立于软件和硬件的信息传输工具。它用于配置文件,文档格式(如OOXML,ODF,PDF,RSS,...),图像格式(SVG,EXIF标题)和网络协议(WebDAV,CalDAV,XMLRPC,SOAP,XMPP,SAML, XACML,...)。比如我们常见的web.xml,applicationContext.xml,Mybatis中用来写SQL的Mapper.xml等等

那什么是外部实体?

说到外部实体首先要说一下DTD(Document Type Definition) 即文档类型定义,用来为XML文档定义语义约束。可以嵌入在XML文档中(内部声明),也可以独立的放在一个文件中(外部引用)。在DTD中有一个实体 (ENTITY)概念,如果需要在XML文档种频繁的使用某一条数据,我们可以预先给这个数据起一个别名,也就是一个ENTITY,之后再在文档种调用它。

DTD实体的引用有内部声明实体和外部引用实体的区别。按类型分则分为:内置实体,字符实体,通用实体,参数实体。

而在XXE中最常见的就是通用实体和参数实体

内部实体

简单的内置实体格式如下:

<!DOCTYPE  文件名 [
<!ENTITY  实体名 "实体内容">
]>

内部实体可以通过如下格式声明:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE aaa[
<!ENTITY name "Zh1z3ven">
]>
<root>
<name>&name;</name>

上面提到的都是内部实体,而外部的DTD实体则需要通过URL来远程调用一个.dtd文件,也或者可以利用file协议引用一个本地的文件;

外部实体

下面以通用实体来举例子

通用实体:在DTD 中定义,在XML中以 &实体名; 引用

1.利用file协议

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE aaa [
<!ENTITY name SYSTEM "file:///c:/windows/win.ini" > 
]>
<name>&name;</name>

2.利用http协议

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE aaa [
<!ENTITY name SYSTEM "http://127.0.0.1/123.txt" >
]>
<name>&name;</name>

参数实体:使用 % 实体名 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
    <!ENTITY % name SYSTEM "http://127.0.0.1/test.dtd">
    %name;
]>
<d>&test;</d>

Test.dtd

<!ENTITY test SYSTEM "http://127.0.0.1/123.txt" >

当然不仅是file和http,也支持其他的协议,不过不同的语言支持的协议不一样,可以参考下图。

XXE相关更多的利用姿势也可以参考下面这篇文章

Java审计之XXE_html

注意:libxml2.9.1及以后,默认不解析外部实体;
在java中netdoc可以做到类似于file的功能

Java中的XXE

其实不仅是Java,其他语言依旧是一样的思路,XML解析一般在导入配置、数据传输接口等场景可能会用到,涉及到XML文件处理的场景可查看XML解析器是否禁用外部实体,从而判断是否存在XXE。审计时首先需要定位危险函数,在Java中有如下的几个函数支持XML解析

javax.xml.parsers.DocumentBuilder
javax.xml.parsers.DocumentBuilderFactory
javax.xml.parsers.SAXParser
javax.xml.parsers.SAXParserFactory
javax.xml.transform.TransformerFactory
javax.xml.validation.Validator
javax.xml.validation.SchemaFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.transform.sax.SAXSource
org.xml.sax.XMLReader
org.xml.sax.helpers.XMLReaderFactory
org.dom4j.io.SAXReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.bind.Unmarshaller
javax.xml.xpath.XpathExpression
javax.xml.stream.XMLStreamReader
org.apache.commons.digester3.Digester

之后就是判断是否参数可控且没有被禁用外部实体,例如如下代码会禁用外部实体

DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);

.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);

.setFeature("http://xml.org/sax/features/external-general-entities",false)

.setFeature("http://xml.org/sax/features/external-parameter-entities",false);

下面以https://github.com/JoyChou93/java-sec-code/项目为例去做一个简单分析

DocumentBuilder

这是JDK自带的类,以此产生的XXE是存在回显的

WebUtils

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import com.google.common.base.Preconditions;
import org.springframework.web.util.HtmlUtils;

public class WebUtils {

    // Get request body.
    public static String getRequestBody(HttpServletRequest request) throws IOException {
        InputStream in = request.getInputStream();
        return convertStreamToString(in);
    }

    // https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java
    public static String convertStreamToString(java.io.InputStream is) {
        java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
        return s.hasNext() ? s.next() : "";
    }

    public static String getCookieValueByName(HttpServletRequest request, String cookieName) {
        Cookie cookie = org.springframework.web.util.WebUtils.getCookie(request, cookieName);
        return cookie == null ? null : cookie.getValue();
    }

    public static String json2Jsonp(String callback, String jsonStr) {
        return HtmlUtils.htmlEscape(callback) + "(" + jsonStr + ")";
    }

    public static String getFileExtension(String fullName) {
        Preconditions.checkNotNull(fullName);
        String fileName = (new File(fullName)).getName();
        int dotIndex = fileName.lastIndexOf('.');
        return dotIndex == -1 ? "" : fileName.substring(dotIndex + 1);
    }

    public static String getNameWithoutExtension(String file) {
        Preconditions.checkNotNull(file);
        String fileName = (new File(file)).getName();
        int dotIndex = fileName.lastIndexOf('.');
        return dotIndex == -1 ? fileName : fileName.substring(0, dotIndex);
    }
}

漏洞代码

public String DocumentBuilderVuln01(HttpServletRequest request) {
        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
            StringBuilder buf = new StringBuilder();
            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(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));
                }
            }
            sr.close();
            return buf.toString();
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }

SAXReader

SAXReader是第三方的库,该类是无回显的

public String SAXReaderVuln(HttpServletRequest request) {
        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;
        }

SAXBuilder

public String SAXBuilderVuln(HttpServletRequest request) {
      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;

SAXParserFactory

该类也是JDK内置的类,但他不可回显内容,可借助dnslog平台

public String SAXParserVuln(HttpServletRequest request) {
        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;
        }
    }

XMLReaderFactory

 public String xmlReaderVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);
            XMLReader xmlReader = XMLReaderFactory.createXMLReader();
            xmlReader.parse(new InputSource(new StringReader(body)));  // parse xml
            return "xmlReader xxe vuln code";
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }

Digester

 public String DigesterVuln(HttpServletRequest request) {
        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;
        }
        return "Digester xxe vuln code";

XMLReader

public String XMLReaderVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser saxParser = spf.newSAXParser();
            XMLReader xmlReader = saxParser.getXMLReader();
            xmlReader.parse(new InputSource(new StringReader(body)));

        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }

        return "XMLReader xxe vuln code";
    }

小结

简单水一片文章,作为记录,后续遇到会没有的会继续补充。同时关于盲注外带XXE执行结果的利用姿势也可以参考先知的那篇文章

Reference

javascript:void(0)

https://xz.aliyun.com/t/3357#toc-21

https://github.com/JoyChou93/java-sec-code

所有内容仅限于维护网络安全学习参考