你的扫描器可以绕过防火墙么(三)

背景

How to Break Through Cloud-Based WAF 提到 “post请求体过大时会绕过某些waf”。

刚好今天看到一个用此方法绕过的案例:Java反序列化数据绕WAF之加大量脏数据

这个绕过姿势记得在2017年就有人分享过,因为我对”因为性能原因所以导致waf放行”这个解释有点疑问,所以研究了一下。

本文主要研究:

  • 为什么请求体过大时会绕过waf的防护?
  • 什么场景下可以使用这种绕过姿势攻击到后端服务?
  • 怎么测试?

以下研究的waf都是基于openresty实现的。

分析过程

  • openresty怎么读请求体?

    获取请求 body 的代码:

    1
    2
    3
    ngx.req.read_body()
    local data = ngx.req.get_body_data()
    -- local file_name = ngx.req.get_body_file()

    根据 openresty文档 得知

    如果请求体存放到了临时文件,就应该调用 ngx.req.get_body_file 接口来获取临时文件名。

    如果请求体存放到内存中,就应该调用 ngx.req.get_body_data 接口来获取请求体。

    什么时候请求体会存放到临时文件呢?

    有两个配置选项决定nginx是否会把请求体放到临时文件:

    client_body_buffer_size:如果请求体大小超过此值,就会把整个请求体或者请求体的一部分写到一个临时文件。

    client_body_in_file_only:此选项直接决定nginx是否将请求体放到文件中。

  • 为什么请求体过大时会绕过waf的防护?

    请求体过大,超过 client_body_buffer_size 配置的值时,请求体会写到临时文件。

    而很多waf不处理临时文件。相当于拿到的请求体会为空。比如曾经很火的 ngx_lua_waf 就没有临时文件相关操作。

    那为啥waf作者没有处理临时文件呢?

    可能是忘了,也可能觉得损耗性能。这里的性能我理解主要是与 io、内存有关。

    在openresty中读文件可以有两个api:

    ngx_io.open,此api是非阻塞的

    io.open,此api是阻塞的

    如果使用io.open读”临时文件”,整个nginx worker就会卡在这个读文件步骤中,肯定会导致整个集群能服务的最大并发数变低。

    如果使用lua-io-nginx-module模块的ngx_io.open非阻塞api来代替io.open阻塞api来读文件,虽然不会阻塞整个nginx worker,但”读文件io操作”还是要和磁盘打交道,需要中断和多次内存拷贝,还是有性能损耗。

    并且,如果服务是将文件一次性读取到内存中时,那”不怀好意”的人多搞几个这样”post请求体过大”的请求,就有可能导致机器内存耗尽。

  • 什么场景下可以使用这种绕过姿势?

    即使waf放行了,也需要后端支持这种超大的请求体。而不少服务对请求体大小是有限制的。

    比如nginx的client_max_body_size:如果请求体大小超过此设置值,就会返回 413 (Request Entity Too Large) 错误。 此配置为0表示不限制请求体大小。

    再比如php的post-max-size:如果请求体大小超过此设置值,$_POST$_FILES会为空。

    所以满足以下条件时,应该可以利用成功:

    请求体大小超过waf client_body_buffer_size配置限制,且waf没有处理临时文件

    请求体大小未超过后端服务的大小限制

怎么测试

客户端的测试代码如下,需要看情况调整big_str的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests


host = "a.baidu.com"
url = "http://127.0.0.1:9998/a.php"
payload = "../../../"
big_str = "A"*1024*1024*200 # 200M

headers = {"Host": host, "Content-Type": "application/x-www-form-urlencoded"}
data = "a=%s&b=%s" % (big_str, payload)

res = requests.post(url, data=data, headers=headers)
print(res.content.decode("utf8"))
print(res.status_code)

总结

本文并没有研究waf厂商是怎么解决这个问题的,比如pdf提到的stream模式是怎么实现的、stream模式是否可以绕过。

绕过waf防护的原因是请求体过大时会被存放到”临时文件”中,而不处理”临时文件”是出于io、内存等性能考虑。

这种绕过姿势还需要”请求体大小未超过后端服务的大小限制”才能利用成功。

另外 “post请求过大为什么会绕过waf”还有两个可能的原因: