内容纲要

XXE ( XML外部实体注入 )

XXE是基于XML的 , 如果对XML不熟悉 , 可以看一下这篇文章

XML基本操作


XML注入

XXE可以看做是普通XML注入的一种升级版 , 普通的XML注入利用面非常狭窄 .

举个例子

    <?xml version="1.0" encoding="UTF-8"?>
    <USER role="guest">用户输入内容</USER>

如果用户输入的内容没有被检测过虑而是直接被放入XML文档中 , 那么会出现XML注入漏洞

     # 输入内容如下
     User1</USER><USER role="admin">User2

这样XML文档就变成了如下所示 , 攻击者添加了一个管理员( admin )角色

    <?xml version="1.0" encoding="UTF-8"?>
    <USER role="guest">User1</USER>
    <USER role="admin">User2</USER>

但是一般来说 , 程序员是不可能放任用户输入任意数据的 , 所以现在普通的XML注入已经很难存在了 .


XXE ( XML 外部实体注入 )

那么到底什么是XXE( XML External Entity Injection )呢 ? 我也不知道为啥叫XXE而不叫XEE , 看文档知道XXE第二个 " X " , 取得是 " External " 中的 " X "

XML外部实体注入是通过注入XML实体 , "system" 等关键字导致XML解析器可以从本地文件或者远程URI中读取数据 , 所以攻击者可以通过XML实体传递自己构造的恶意值 , 让处理程序解析它

来看下外部实体默认支持的协议

png

PHP还拥有几个扩展协议 , 如下所示

png

配合上面这些协议 , 在引用外部实体时 , 通过构造恶意内容 , 可以实现 读取任意文件 , 执行系统命令 , 探测内网端口 , 攻击内网网站 等多种攻击方式

  • 我们已经知道可以通过 <!Entity 实体名称 SYSTEM "外部实体路径"> 来引入外部的DTD文件 , 那么如果可以将这个外部实体路径修改为某个敏感文件的内容 , 那么服务器在解析这个XML文件的时候就会把敏感文件的内容赋值给指定的实体名称了 . 只要我们再将该实体的内容读取出来 , 那么就可以读取敏感文件的内容了

  • 如果这个外部实体路径是指向某个端口呢 ? 我们都知道当访问一个开放的端口会响应banner信息或者报错信息 , 那么就可以通过回显来判断目标端口是否开放 .
    png

  • 一般来说 , 服务器解析XML有两种方式 ,

    1. 一次性将整个XML加载到内存中进行解析

    2. 一部分一部分的 , 流式地加载 , 解析XML文档

    如果一次性调用巨量的定义 , 那么服务器内存就会被消耗完 , 达到拒绝服务的目的

  • SSRF( Server-Side Request Forgery )在很多时候可以和 XXE 组合攻击 , 通过SSRF可以轻易达到攻击内网应用的目的

本章的重点是XXE读取文件 , 下面举几个例子来看一看XXE究竟是如何读取文件的!


XXE任意文件读取

  1. 假设本地服务器上有如下PHP代码 , 这些代码是用来解析XML文档的

  2. 然后在浏览器中提交一个POST请求 , 请求的内容为构造的XML文档

  3. 网上的demo都是通过 Hackbar 直接构造POST请求 , 但是我这里好像不可以了 , 通过 Burpsuite 抓包后你会发现整个POST请求已经面目全非 . 如果你也出现了我这种情况 , 可以参考下面的内容

    png

    Hackbar通过 " = " 将POST请求分开了 , 前面内容为参数名 , 后面内容为参数值 , 形成了一个键值对 . 所以需要重新修改POST请求

    png

  4. 发送响应包 , 可以看到响应包中包含了敏感文件/etc/passwd的内容 , 该攻击可以发展为任意文件读取漏洞

    png

  • 另一个注意点 : 非法字符

    首先你要知道 : 在XML元素中 , " < " 和 " & " 是非法的

    1. " < " 会被解析器解释为新元素的开始

    2. " & " 会被解释器解释为字符实体的开始

    所以 , 当我们访问带有非法字符的文件时 , XML解析器会报错

    png

    第一行报错提示 : 在实体中的标记声明中检测到错误 , 这是因为 /etc/fstab 中存在下面这行

    png

    因为这行存在很多非法字符 , 所以我们需要使用 CDATA 来包含读取文件的脚本 . 在CDATA中的所有内容是不会被XML解析器解析的 . 其中的所有字符都会被当做元素字符数据的常量部分而不是XML标记

    png

    虽然依旧报错 , 但报错内容已经变了 : 实体中未注册的错误消息 , 后面又提示 start 解析失败 . 看来是在XML中拼接失败了 .

    如果不能在XML中进行拼接 , 那么就必须在DTD中拼接完毕后 , 直接在XML中调用 . 那么什么实体既能够在DTD中定义 , 又能够在DTD中调用? 很明显答案就是 参数实体

    1. 在刚才的Payload下面引入一个外部DTD文件

    <!ENTITY % dtd SYSTEM "http://127.0.0.1/evil.dtd"> %dtd;

    1. http://127.0.0.1/evil.dtd 中完成拼接操作
    # evil.dtd
    <?xml version="1.0" encoding="UTF-8"?> 
    <!ENTITY all "%start;%happy;%end;">
    1. 最后在XML文档中调用已经拼接完毕的字符串

    <test>&all;</test>

    此时 , 可以成功读取 /etc/fstab 中的内容

    png


Bind OOB XXE ( 无回显任意文件读取 )

第一个任意文件读取的例子在实际生产环境中是不会出现的 . 因为XML的本质是用来传输和存储数据 , 除去一些特别极端的情况 , 很多情况下XML文件的内容是不会被实例化后输出的 . 所以我们不能依赖页面的回显

面对这种情况 , 你肯定会想到OOB( Out Of Band , 外带数据 ) , 即将数据发送到远程服务器上 , 然后再读取内容

此时的漏洞代码

  1. 向这个页面发送一个POST请求 , 请求内容如下所示

    png

  2. 在DTD中导入了一个名为 "remote" 的外部实体 , 这个实体包含了如下内容

    png

    通过PHP伪协议(php://filter)读取了/etc/passwd中的内容 , 为了避免特殊字符的干扰 , 将文件内容通过 base64 进行编码 , 最后将数据通过GET请求转发到 192.168.56.102:9999 端口上

  3. 最后通过 nc 或者 socat 监听端口 , 得到加密后的数据

    png

  4. 解密后即可得到 /etc/passwd 文件中的值~

其实这里我想谈一谈在实验中踩到的坑

  1. evil.dtd 中% send 中的 %

    " % " + " 实体名称 " 是定义参数实体的方式 , 但是因为解析问题 , 如果此处改为 % send , 则会存在如下报错 , 提示除了引用实体外 , " % "是被禁止使用的 .

    png

    因为这里" % send"是直接出现在实体值中 , 而非用于定义实体名称 . 所以需要进行替换.

  2. PrivateTmp的使用

    其实这个问题和本实验没有什么关系 , 开始时我在/tmp下创建了一个文件做试验品 , 后来实验成功后发现还是读取不了/tmp下的这个文件 . 经过一些学习发现是这个PrivateTmp参数的问题

    这个参数位于/lib/systemd/system/apache2.service , 你可以通过systemctl status apache2.conf来查看具体路径

    png

    只要使用systemd进程作为启动进程的Linux系统 , 其子进程都有这个PrivateTmp参数 , 用于设置是否使用私有的tmp目录

    什么意思 ? 你的tmp目录下可能会存在如下一些目录

    png

    这些比较长的目录都是设置了PrivateTmp=True的进程所拥有的私有tmp目录 . 举个例子 , /tmp/systemd-private-93940188ee144693b291a3250a454861-apache2.service-tQBKI8/tmp指向的就是apache2的/tmp目录 , 你可以把它看做是一种重定向

    所以 , 你想要在/tmp下写一个临时文件供Apache2读取 , 就需要在/tmp/systemd-private-93940188ee144693b291a3250a454861-apache2.service-tQBKI8/tmp目录下写入

  3. zlib.deflate

    最开始我是没有加zlib.deflate这个参数的 , 结果读取内容时一直有如下报错

    png

    这个 Detected an entity reference loop 的报错困扰了我很久 , 因为自己对XML也不太熟悉 , 最后发现可能是文件长度的问题.

    关于Blind XXE(好帖收藏) 有这么一句话 : libxml解析器(php,python,ruby)默认限制外部实体长度为2K , 并且不处理空格换行符. , 所以这里/etc/passwd文件的内容很可能超过了2K , 查看了一下果然是这样

    png

    因此 , 就要想办法绕过这个长度限制或者压缩文件大小 . 很明显压缩文件大小是比较简便的方法

    因为是通过PHP伪协议读取的文件 , 自然想到PHP伪协议中的压缩过滤器 zlib.* , 这类压缩过滤器在PHP5.1后就可以使用了 , 所以现在几乎不存在不兼容的情况

    zlib.deflate( 压缩 ) 和 zlib.inflate ( 解压 ) 实现了数据的压缩与解压 , zlib.deflate 的压缩等级默认为 " 6 " , 应该足够用了

    所以 , 在通过监听端口得到数据后 , 我们需要对数据进行解压和解密

    png

    真正用于解密的代码其实就一行 , 因为之前是先压缩再编码的 , 所以这里需要先解码再解压

    png

    这样就可以成功的拿到数据 !


基础的内容差不多就到这里了 , 其实XXE的利用方式还有很多很多( 配合文件上传 , SSRF , ... ) , 但因为篇幅原因这一章就写到这 . 后面我还会陆续补充剩余的内容 ~

本章的内容主要是XXE配合文件读取 , 剩余的利用方式我会在下一章进行研究整理~

最后修改日期:2019年12月19日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。