A Bite of HTTP/2.0:HTTP2 的底层探索与新攻击边界

背景:

在积累了一些场景之后,Web Fuzzer HTTP2 的测试需求逐渐应该被提上日程;很多时候,在测试的时候遇到 H2 的网站,用户还是不得不去切换成 Burp 以继续测试,被迫忍受交互和扩展性都有蛮大问题的 Repeater / Intruder。但是实际上,HTTP2 的支持经过一些简单的探索之后,也并不是特别复杂,完全可以以一些 “无感” 的底层基础设置支持。

HTTP2 为协议测试设下的障碍

HTTP/2 与 HTTP/1.1 差别巨大,最直观的感受是,HTTP2 默认强制执行 TLS,H2C(HTTP 2 Cleartext) 变成了“不受推荐的协议”;事实确实如此。这给了我们研究 “HTTP2”非常大的不便:我们默认居然无法通过 wireshark 来分析 H2 的数据包?

当然,配合一些其他手段,我们可以看到 H2 的具体数据包,这不是我们这篇文章的重点,所以就不介绍这部分内容了,之后有机会另外写内容为大家讲如何使用 H2C

RFC7541 HPACK 的引入

hpack 主要是针对 http header 的压缩算法,经过这种算法压缩之后,header 的体积将会明确减小,同时也很直观的 “我们没有办法直接观察到协议描述的 Header” 了。

我们以一个简单的例子来讲,HPACK 压缩以前后,数据长啥样子:

// 最简单的格式
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

// 原本的数据头 HEXDUMP 的格式
([]uint8) (len=129 cap=243) {
 00000000  55 73 65 72 2d 41 67 65  6e 74 3a 20 4d 6f 7a 69  |User-Agent: Mozi|
 00000010  6c 6c 61 2f 35 2e 30 20  28 57 69 6e 64 6f 77 73  |lla/5.0 (Windows|
 00000020  20 4e 54 20 31 30 2e 30  3b 20 57 69 6e 36 34 3b  | NT 10.0; Win64;|
 00000030  20 78 36 34 29 20 41 70  70 6c 65 57 65 62 4b 69  | x64) AppleWebKi|
 00000040  74 2f 35 33 37 2e 33 36  20 28 4b 48 54 4d 4c 2c  |t/537.36 (KHTML,|
 00000050  20 6c 69 6b 65 20 47 65  63 6b 6f 29 20 43 68 72  | like Gecko) Chr|
 00000060  6f 6d 65 2f 38 33 2e 30  2e 34 31 30 33 2e 31 31  |ome/83.0.4103.11|
 00000070  36 20 53 61 66 61 72 69  2f 35 33 37 2e 33 36 0d  |6 Safari/537.36.|
 00000080  0a                                                |.|
}

// 原本的数据头 HPACK 的格式
([]uint8) (len=99 cap=99) {
 00000000  40 88 e0 82 d8 b4 33 16  a4 ff d8 d0 7f 66 a2 81  |@.....3......f..|
 00000010  b0 da e0 53 fa e4 6a a4  3f 84 29 a7 7a 81 02 e0  |...S..j.?.).z...|
 00000020  fb 53 91 aa 71 af b5 3c  b8 d7 f6 a4 35 d7 41 79  |.S..q..<....5.Ay|
 00000030  16 3c c6 4b 0d b2 ea ec  b8 a7 f5 9b 1e fd 19 fe  |.<.K............|
 00000040  94 a0 dd 4a a6 22 93 a9  ff b5 2f 4f 61 e9 2b 0f  |...J."..../Oa.+.|
 00000050  32 b8 17 68 20 65 70 85  c5 37 0e 51 d8 66 1b 65  |2..h ep..7.Q.f.e|
 00000060  d5 d9 73                                          |..s|
}

H2 Frame:多种帧

除了最基础的 TLS 要求变化之外,H2 基于 “帧” 的通信,也给研究和一些内容造成了一系列的困难。

这个帧同样,我们以大家最能理解的方式来描述变化:

  1. SETTING 帧可设置协商 H2 连接中的各种参数
  2. UpdateWindows 帧:可以设置通信帧的窗口
  3. Header 帧,通过 hpack 压缩的 header 数据,不光可以传输部分数据,也可以标志流的结束。
  4. Data 帧,类似于 chunk 数据也可以分为多个帧,可以 END_STREAM 标志流结束
  5. 控制帧:PING / SETTING_ACK / GOAWAY / CONTINUATION 等等各种帧可以极大增强 H2 的表现力

这里大家有个印象即可,我们其实本文并不会去着重探索各种数据帧的内容。

H2 全双工通信

我们知晓数据帧的概念之后,将会很快意识到,数据包传输的过程中,我们可以同时一边传数据,一边传控制帧。没错,这个就是全双工的最简单的表现形式。

实际在 Golang 标准库实现过程中,这个实现更加明确(伪代码表示):

go readLoop();

...
...

fn readLoop() {
     for {
         frame, err = framer.ReadFrame();
         if err != nil { break }
         
         switch frame.type {
         case http2.MetaFrameHeader:
             ...
         case http2.DataFrame:
             ...
         case http2.SettingFrame:
             ...
         case http2.UpdateWindow:
             ...
         case http2......
         }
     }
}

如何构建 HTTP2 连接?

一般来说,构建 HTTP2 的连接,我们需要先构建 TLS,为了大家能快速理解这个过程,我把 Yaklang 中实现 HTTP2 连接的步骤总结成一个很容易理解的时序图:

当我们大致有概念之后,基本可以理解,HTTP2 在建立完通信之后,才会进行大家熟悉的 HTTP 通信。

当然,因为主要讨论一次通信的过程,我有意在上述内容中做了一些屏蔽,比如 STREAM_ID,以及 SETTING 中常见的具体配置内容。

那么我们实际前面的握手的过程,我们可以把它看作一个固定的 “仪式” 的时候,可能将会更关注于 “HTTP/1.1” 的数据包如何转成 “HTTP/2.0”,所以我们将会讲解一下这个数据包的转化过程

HTTP/1.1 数据包如何转成 HTTP/2.0?

这个过程其实一点都不高深

HTTP1.1 Header 转 HTTP2 HeaderFrame

注:以下内容来源是 Golang HTTP2 标准库

GET /path HTTP/1.1
Connection: close
User-Agent: Demo-User-Agent
Host: Example Domain

上述数据包是一个非常简单的案例,我们看一眼就知道

  1. Method 为 GET
  2. RequestURI 为 /path
  3. 有三个 Header

然后经过思考之后,HTTP2 的协议好像并没有规定 Method RequestURI 这类字段有类似 HTTP1.1 的位置,那么就一定有别的机制额外存储这些重要的内容

HTTP1 关键字段转 HTTP2 表

类型HTTP/1.1 字段HTTP/2.0 字段备注
HTTP 请求Method:method
ReqestURI:pathCONNECT 方法下不存在
Scheme:schemeCONNECT 方法下不存在
Host:authority
HTTP 响应StatusCode:status

我们发现,HTTP1 中的字段通过上述表的内容转 HTTP2 的时候,会给关键字段加上 ":"。

这非常关键:在 Golang 标准库的 hpack 实现中,我们通过 IsPseudo 来区分这种情况,我们可以理解为 Pseudo 为这些关键字头部内容的描述,这是兼容 HTTP/1.1 的关键。

不应该再出现的 Header

当然,众所周知,HTTP/1.1 中有很多 HEADER 其实并不适合 H2 的情况,比如说 Connection,Proxy-Connection 等,这些头其实在 Golang http2 标准库中默认将会被移除,那具体会移除哪些内容呢?

我们进行以下总结:

  1. Connection
  2. Proxy-Connection
  3. Transfer-Encoding
  4. Upgrade
  5. Keep-Alive

Cookie 的兼容与建议

在标准库中,Cookie 处理遵循性能优化

Per 8.1.2.5 To allow for better compression efficiency, the
Cookie header field MAY be split into separate header fields,
each with one or more cookie-pairs.

实际上根据描述的内容,这只是一个建议内容而已,如果为了提高性能,可以这么做,实际测试过程中,也并没有因为不这么做受到惩罚,我们为了在 Cookie 中保留更多更复杂和多样的内容,可以选择 Cookie 和原内容完全一致。

HTTP1.1 转 HTTP2.0 数据帧

这个内容其实相比 header 之外,其实没有什么其他的处理障碍,可以直接使用 http2.Framer.WriteData(streamId, endStream, bodyBytes) 即可实现。

攻击边界思考

在我们理解了 HTTP2 的基础问题之后,我们将会需要思考一下,HTTP2 的引入,将可能带来哪些潜在可测试的面?

HTTP/2.0 降级 HTTP/1.1

众所周知,HTTP2 如果失败,将会自动降级为 HTTP1,一般来说这个过程在大部分的客户端,尤其是浏览器中,他是无感的;服务端为了兼容,也比较少会出现 “只允许 HTTP2” 的情况发生。这个过程,我们不必过多描述;但是一个容易让人忽略的点是 “HTTP/1.1 如何升级为 HTTP2”

HTTP/1.1 升级为 HTTP/2.0

这个升级的过程,其实大多数人并不知道,我们可以简单描述一下这个升级的方法,其实非常类似 websocket,具体方法是说在原来的 1.1 数据包加三个额外的头来升级 Connection

Connection: Upgrade, HTTP2-Settings
Upgrade: h2
HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA

通过这种方法,可以绕过 Golang 标准库不支持 H2C 的问题。

那么,这种方式也很容易成本,发现一个 Web 服务器到底支持不支持 HTTP2 的依据。

HTTP/2.0 数据帧的分片

众所周之,在 HTTP/1.1 中,chunk 可以直接分片 body,但是在 HTTP/2.0 的协议下,framer.WriteData 可以设置 END_STREAM,于是只要不设置 END_STREAM 就可以进行任意 “分片”。

我们以一段简单的代码来解释这个过程:

func TestFramer(t *testing.T) {
   var buf bytes.Buffer
   framer := http2.NewFramer(&buf, &buf)
   for _, block := range funk.Chunk([]byte(`hello world
hello world
hello worldhello worldhello worldhello worldasdfasdfh
`), 10).([][]byte) {
      framer.WriteData(1, false, block)
   }
   framer.WriteData(1, true, nil)
   spew.Dump(buf.Bytes())
}

我们执行上述代码之后,执行结果为:

([]uint8) (len=159 cap=311) {
 00000000  00 00 0a 00 00 00 00 00  01 68 65 6c 6c 6f 20 77  |.........hello w|
 00000010  6f 72 6c 00 00 0a 00 00  00 00 00 01 64 0a 68 65  |orl.........d.he|
 00000020  6c 6c 6f 20 77 6f 00 00  0a 00 00 00 00 00 01 72  |llo wo.........r|
 00000030  6c 64 0a 68 65 6c 6c 6f  20 00 00 0a 00 00 00 00  |ld.hello .......|
 00000040  00 01 77 6f 72 6c 64 68  65 6c 6c 6f 00 00 0a 00  |..worldhello....|
 00000050  00 00 00 00 01 20 77 6f  72 6c 64 68 65 6c 6c 00  |..... worldhell.|
 00000060  00 0a 00 00 00 00 00 01  6f 20 77 6f 72 6c 64 68  |........o worldh|
 00000070  65 6c 00 00 0a 00 00 00  00 00 01 6c 6f 20 77 6f  |el.........lo wo|
 00000080  72 6c 64 61 73 00 00 08  00 00 00 00 00 01 64 66  |rldas.........df|
 00000090  61 73 64 66 68 0a 00 00  00 00 01 00 00 00 01     |asdfh..........|
}

肉眼可见的,我们使用 HTTP2.0 的 Framer 把传输的数据进行了分片,实现了类似 chunk 的效果。

注意

实际使用中,大多数 HTTP2 客户端都会主动移除 Transfer-Encoding,使用 HTTP2 的 DataFrame 的 END_STREAM 标识位来进行分片。但是,这是一个君子协定,实现不实现其实由具体服务器和中间件来实现。

HTTP/2.0 头部帧的分片写入

当然,受上述案例的启发,HTTP2 的头部帧自然也可以分片写入,更细节地来说,HTTP2 头部帧写入的时候,我们可以通过控制各种各样的参数来控制内容:

http2.HeadersFrameParam{
   // 这个帧数据具体哪个数据流?
   StreamID:      1,
   
   // 具体传输的数据
   BlockFragment: hpackHeaderBytes,
   // 判断这个数据流是否结束,一般来说,确定没有 Body 要传输,可以设置为 True
   EndStream:     false,  
   // 确定 Headers 传输结束了
   EndHeaders:    false,
   
   // 控制 Padding 和优先级的,暂不讨论
   PadLength:     0,
   Priority:      http2.PriorityParam{},
}

为此我们就不多介绍了,但是值得注意的是:EndHeaders 这个字段可以传输多次完整 hpack 编码的 Header,这是非常有意思的设定。

HTTP2 Frame-Stream 构造“时空交错”

一般来说,Framer 为传输介质,介质中传输的一个请求或者响应,我们称之为一个 Stream;

因此在 http2 标准库的一般客户端实现中,每次新建一个 Stream 为 n+2;以为 Request 为 N,Response 将为 N+1,那么下一个 Request 就是 N+2;

那么思路打开!我们可以通过 STREAM ID 来单独控制一个数据帧到底是为哪个 Stream 服务的,也就是说,我可以同时传输:

  1. Stream2-Header[1]
  2. Stream1-Data[1]
  3. Stream2-Data[1]
  4. Stream1-Data[end]

这样,Stream2 作为干扰数据,可以穿插在 Stream1 的 Data 中,在卸载 HTTPS 后,可以快速致盲无法还原 HTTP2 数据流的网络设备,还可以干扰正常 IDS/IPS 的各种 “静态” 规则。

总结

上述描述的内容,绝大部分都来源于 Golang http2 的标准库,在我们深入理解上述描述的过程以及关键点之后,我们可以自己实现 HTTP2 的客户端。

实际实现过程中,Yaklang 引擎的 HTTP 部分移除了标准库中各种 “限制”,直接使用 http2 的 Framer 和 hpack 来构建 HTTP2 的客户端,可以实现控制通信中的任何一个步骤来实现 HTTP2 的通信。

Web Fuzzer 的 HTTP2.0 支持

通过我们上述的描述,我们实现了 HTTP/1.1 风格数据包到 HTTP2.0 数据帧的转换,因此

在数据包的 Proto 部分是 HTTP/2.0,且 HTTPS 打开的时候,Web Fuzzer 将会自动切换为 HTTP2 的形式。

Happy Game!

Yak官方资源

Yak 语言官方教程:
https://yaklang.com/docs/intro/
Yakit 视频教程:
https://space.bilibili.com/437503777
Github下载地址:
https://github.com/yaklang/yakit
Yakit官网下载地址:
https://yaklang.com/
Yakit安装文档:
https://yaklang.com/products/download_and_install
Yakit使用文档:
https://yaklang.com/products/intro/
常见问题速查:
https://yaklang.com/products/FAQ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值