XXE注入

XXE(XML External Entity Injection)即XML外部实体注入,是一种和XML文档解析相关的Web安全漏洞。XXE漏洞可以和服务端请求伪造攻击SSRF联合使用,达到非法访问内网地址,甚至直接访问被攻击主机的文件系统内容的目的,可能存在较大危害。

XXE攻击原理

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风险。

XXE攻击示例

下面是用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注入攻击

防御XXE攻击其实也很简单,我们在XML解析库中配置禁止解析外部实体即可。下面例子代码直接将JAXP的DTD解析禁用:

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();

对于其它的XML解析库大多也有对应设置,具体参考相关文档。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap