介绍

分块传输编码( Chunked transfer encoding )是超文本传输协议( HTTP )中的一种数据传输机制,允许 HTTP由应用服务器发送给客户端应用(通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在 HTTP 协议 1.1 版本( HTTP/1.1 )中提供。通常,HTTP 应答消息中发送的数据是整个发送的,Content-Length 消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。通常数据块的大小是一致的,但也不总是这种情况。

一般情况 HTTP 请求包的Header 包含 Content-Length 来指明报文体的长度。有时候服务生成 HTTP 回应是无法确定消息大小的,比如大文件的下载,或者后台需要复杂的逻辑才能全部处理页面的请求,这时用需要实时生成消息长度,服务器一般使用 chunked 编码。在进行 Chunked 编码传输时,在回复消息的 HeadersTransfer-Encoding 域值为 chunked ,表示将用 chunked 编码传输内容。这在 http 协议中也是个常见的字段,用于 http 传送过程的分块技术,原因是 http 服务器响应的报文长度经常是不可预测的,使用 Content-length 的实体搜捕并不是总是管用。

示例

在头部加入 Transfer-Encoding: chunked 之后,就代表这个报文采用了分块编码。这时,post 请求报文中的数据部分需要改为用一系列分块来传输。每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的,也不包括分块数据结尾的,且最后需要用 0 独占一行表示结束。
例如下面这个数据包(注意最后的两个空行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /admin HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: text/plain
Transfer-Encoding: Chunked

7
abcdefg
a
0123456789
0


当开启了 Transfer-Encoding 时,Content-Length 的值就会不起作用,此时发的数据包的长度只会根据每个分块开头的长度值来发送数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

s = f'''POST /post HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: text/plain
Content-Length: 1
Transfer-Encoding: Chunked

7
abcdefg
a
0123456789
0


'''.encode()

io = remote('localhost', 5000)
io.send(s)
a = io.recvall()
print(a)
io.close()

利用

常规分块绕WAF

有的 http 层面的 waf 并没有考虑分块传输的情况,导致假如 waf 过滤了某个关键词,我们就可以通过分块传输来分开发送某个关键字,从而绕过 waf

利用 chunked-coding-converter 插件功能一键分块传输编码。(下面的图源自网上)

编码后:

注释分块绕WAF

我们可以在 RFC7230 中查看到有关分块传输的定义规范。通过阅读规范发现分块传输可以在长度标识处加上分号 ";" 作为注释,如:

1
2
3
4
5
9;kkkkk 
12345
4;ooo=222
67890
0\r\n\r\n

而很多可以识别 Transfer-Encoding 数据包的 WAF 并没有处理分块数据包中长度标识处的注释,导致在分块数据包中加入注释的话,WAF 就识别不出这个数据包了。

新版 chunked-coding-converter 插件已经可以自带给数据包中长度标识的后面加上随机注释了。不过要注意可能有的服务器也并没有识别分块数据包中长度标识处的注释。

编码后:

畸形的分块绕WAF

假如有的 waf 遇到了畸形的分块会不拦截,而有的服务器可以容忍畸形的分块,这种解析差异可能会导致绕过 waf ,例如 ModSecurity 这个 waf 作用在 Apache 服务器上。

我们发送下面畸形的分块数据包,这里 aa 数据长度为 2 ,但是前面的长度标识为 1

1
2
3
4
5
6
7
POST /sql.php?id=2%20union HTTP/1.1
......
Transfer-Encoding: chunked

1
aa
0\r\n\r\n

由于 ModSecurity 会放行这个畸形的数据包(没试过现在修复没有),而 Apache 虽然遇到这个畸形的数据包会报错,但是提交的数据依旧可以传到 php ,从而绕过 waf

延时分块传输绕WAF

延时分块传输的原理是利用 在上一块传输完成后,sleep一段时间,再发送下一块 ,从而让某些为了性能着想不会等待下一块的数据的 waf 直接放弃等待分块的数据。

需要注意我们块与块之间发送的间隔时间必须要小于后端中间件的 post timeoutTomcat 默认是 20sweblogic30s

延时分块传输的利用也可以用前面的工具来实现。

解析差异绕过

有的框架对 Transfer-Encoding 的值大小写不敏感,而有的框架敏感(必须要求全小写)。这就可能导致两个框架接收同一个请求获取到的数据是不一样的。假如某个请求的 Tranfer-Encoding 的值为 CHunked ,那么这个请求在大小写不敏感的框架中被解析就会按照分块传输的解析逻辑来解析(这种情况就会忽略 Content-Length 的值),而在大小写敏感的框架中就会按照 Content-Length 的长度来解析。

例如开头中原理介绍中给出的数据包例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /admin HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: text/plain
Content-Length: 5
Transfer-Encoding: CHunked

7
abcdefg
a
0123456789
0


大小写不敏感的框架的解析结果为:

1
abcdefg123456789

大小写敏感的框架的解析结果为:

1
2
7
abc

因为 Content-Length: 5 ,所以这里只能读取到五个字节,注意这里包括 7 后面的换行符。

那么假如存在这么一种情况,一个外网服务会将我们的请求转发给内网服务,而这两种服务存在上面介绍到的 Transfer-Encoding 解析差异。而外网服务要求请求数据中必须有 "admin" 字符串(举个例子而已)才会转发请求给内网服务,而内网服务会对请求数据就行判断,如果有 "admin" 就会进行某些危险操作,没有就进行常规操作。这种情况我们就可以利用 Transfer-Encoding 解析差异来绕过。

这里有个考察了这个知识点的赛题: 2024 d3ctf d3pythonhttp 附件地址flask 框架就是 Transfer-Encoding 大小写不敏感的框架,web.pyTransfer-Encoding 大小写敏感的框架。

参考文章

https://gv7.me/articles/2021/java-deserialized-data-bypasses-waf-through-sleep-chunked/
https://m.freebuf.com/articles/web/194351.html
https://blog.csdn.net/weixin_43571641/article/details/123749544