0x00 前言

WAF(Web Application Firewall)是很常见的 Web 安全基础设施,许多云厂商、大厂、乙方安全公司均有相应的产品。然而,不得不承认,WAF 只能有限提升安全防护能力,不能拦截一些稍微复杂的攻击。正常业务不应当过度依赖 WAF,况且 WAF 还存在误拦截正常业务流量的可能。

目前已知的一些绕过 WAF 的手段包括但不限于:

  • Chunked encoding 绕过
  • IBM037 等罕见编码绕过

多嘴一句:最早提出 IBM037 编码绕过 WAF 的应该是 Soroush Dalili 在 SteelCon 2017 上的议题,然而国内众多相关文章,基本没有标记出处,很奇怪。

笔者最近在分析 Go 语言的 HTTP 协议解析实现的时候,发现了一种能够利用 multipart boundary 绕过 WAF 的方法,在 Python 的一些 Web 框架上也适用,因而将其分享出来。

0x01 绕过

multipart/form-data 是一种非常常见的 HTML 表单编码方式,绝大部分的 Web 服务器、框架实现,均支持此编码。其编码后的请求大致如下所示,表单数据通过boundary分割。

POST /test HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=“boundary”

—boundary
Content-Disposition: form-data; name=“field1”

value1
—boundary
Content-Disposition: form-data; name=“field2”; filename=“example.txt”

value2
—boundary—

那么只要满足上述协议要求,服务端就可以正常获取到字段内容了,如下图所示。

那么如果构造多个 boundary 会有什么效果?很遗憾,一些服务端实现(比如 Go 语言)不允许多个 boundary,数据传递失败:

多个 Content-Type 倒是可行,然而多数服务端,包括 WAF 的实现,基本上只认第一个出现的 Content-Type 。

只能另辟蹊径。

回到协议解析本身

在 Go 语言的实现中,multipart/form-data 中对 boundary 的解析是通过 mime.ParseMediaType 实现的:

然而 mime.ParseMediaType 对 MediaType 参数的解析有个有趣的细节,正常情况下,参数不允许重复,如下图 190行 所示,这也是上文请求失败的原因。

然而在 203行 处,却允许参数的覆盖,只要目标参数满足 RFC 2231 的格式。RFC 2231中描述了一种名为 Parameter Value Continuations 的规范,其核心部分如下图所示,大意是一个参数URL,可以等价拆成两个分别名为URL*0URL*1的参数。

那么,boundary 能否通过同样的方式覆盖呢?实际测试一下,发现可行。Go 会将boundary*0="real-";boundary*1="boundary" 当作最终的 boundary 。

绕过某云 WAF

上节中提到的怪异但符合Parameter Value Continuations 规范的数据包,应该是一个绕过 WAF 的神器。笔者随即写了一个存在 SQL 注入漏洞的服务,挂在某国内领头羊云厂商的 WAF 后面进行测试,证实了这个猜想。

这个测试服务的 id 字段存在注入,正常情况下,因为没有任何攻击特征,WAF 不会拦截:

进行注入,WAF 会正常拦截:

然而当我们请出 RFC 2231 大爷,整个注入攻击变得畅通无阻。

本质上是利用了 WAF 和 服务端 的协议解析差异来绕过防护的,应当可以绕过一大票 WAF 产品。这里没有一一测试各家产品,不是为了避免拿来党,主要还是因为懒。

0x02 举一反三

上述绕过方式是基于 RFC 2231 的,因此其它支持 RFC 2231 的服务端实现也应当可以绕过。笔者对比较流行的 Python 框架 —— Flask 进行了测试,毫无意外地利用成功了。然而 Flask 的服务端实现和 Go 有细微的差异,最终解析出来的 boundary 参数,会拼接原始的 boundary 参数,如下图所示。

其它语言、框架应有类似的特性。

0x03 总结

WAF 绕过的本质是利用了 WAF 和 服务端 的协议解析差异。类似的差异应该还有许多。最后重复一下一开始提到的观点:WAF 并不可靠,不要过度依赖 WAF。