WAF(Web Application Firewall)是很常见的 Web 安全基础设施,许多云厂商、大厂、乙方安全公司均有相应的产品。然而,不得不承认,WAF 只能有限提升安全防护能力,不能拦截一些稍微复杂的攻击。正常业务不应当过度依赖 WAF,况且 WAF 还存在误拦截正常业务流量的可能。
目前已知的一些绕过 WAF 的手段包括但不限于:
Chunked encoding 绕过
IBM037 等罕见编码绕过
多嘴一句:最早提出 IBM037 编码绕过 WAF 的应该是Soroush Dalili 在 SteelCon 2017 上的议题 [1],然而国内众多相关文章,基本没有标记出处,很奇怪。
笔者最近在分析 Go 语言的 HTTP 协议解析实现的时候,发现了一种能够利用 multipart boundary 绕过 WAF 的方法,在 Python 的一些 Web 框架上也适用,因而将其分享出来。
multipart/form-data 是一种非常常见的 HTML 表单编码方式,绝大部分的 Web 服务器、框架实现,均支持此编码。其编码后的请求大致如下所示,表单数据通过boundary分割。
POST /test HTTP/1.1
那么只要满足上述协议要求,服务端就可以正常获取到字段内容了,如下图所示。
那么如果构造多个 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 的规范 [2],其核心部分如下图所示,大意是一个参数URL,可以等价拆成两个分别名为URL*0、URL*1的参数。
那么,boundary 能否通过同样的方式覆盖呢?实际测试一下,发现可行。Go 会将boundary*0="real-";boundary*1="boundary" 当作最终的 boundary 。
上节中提到的怪异但符合Parameter Value Continuations 规范[2] 的数据包,应该是一个绕过 WAF 的神器。笔者随即写了一个存在 SQL 注入漏洞的服务,挂在某国内领头羊云厂商的 WAF 后面进行测试,证实了这个猜想。
这个测试服务的 id 字段存在注入,正常情况下,因为没有任何攻击特征,WAF 不会拦截:
进行注入,WAF 会正常拦截:
然而当我们请出 RFC 2231 [2]大爷,整个注入攻击变得畅通无阻。
本质上是利用了 WAF 和 服务端 的协议解析差异来绕过防护的,应当可以绕过一大票 WAF 产品。这里没有一一测试各家产品,不是为了避免拿来党,主要还是因为懒。
上述绕过方式是基于 RFC 2231 的,因此其它支持 RFC 2231 [2]的服务端实现也应当可以绕过。笔者对比较流行的 Python 框架 —— Flask 进行了测试,毫无意外地利用成功了。然而 Flask 的服务端实现和 Go 有细微的差异,最终解析出来的 boundary 参数,会拼接原始的 boundary 参数,如下图所示。
其它语言、框架应有类似的特性。
WAF 绕过的本质是利用了 WAF 和 服务端 的协议解析差异。类似的差异应该还有许多。最后重复一下一开始提到的观点:WAF 并不可靠,不要过度依赖 WAF。
引用:
[1] https://www.slideshare.net/SoroushDalili/a-forgotten-http-invisibility-cloak