srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/proxy
diff options
context:
space:
mode:
Diffstat (limited to 'internal/proxy')
-rw-r--r--internal/proxy/intrude.go55
-rw-r--r--internal/proxy/intrude_test.go47
2 files changed, 102 insertions, 0 deletions
diff --git a/internal/proxy/intrude.go b/internal/proxy/intrude.go
index 933a309..6595c75 100644
--- a/internal/proxy/intrude.go
+++ b/internal/proxy/intrude.go
@@ -108,6 +108,7 @@ func (s *Server) Intrude(ctx context.Context, scheme, host string, template []by
if err != nil {
return err
}
+ raw = fixContentLength(raw)
// sendRaw is already self-bounding (dialForRepeat's own dial
// timeout, then conn.SetDeadline for the rest), so ctx here
@@ -122,3 +123,57 @@ func (s *Server) Intrude(ctx context.Context, scheme, host string, template []by
}
return nil
}
+
+// fixContentLength recalculates an existing Content-Length header to
+// match raw's actual body length after marker substitution. A fuzzed
+// payload routinely differs in length from the base value it replaces;
+// left as-is, a Content-Length carried over unchanged from the original
+// captured request makes the target server wait for bytes that will
+// never arrive, hanging that request for the full upstream timeout -
+// confirmed: identical attacks against a URL-only marker (no body
+// length change) completed in single-digit milliseconds per payload,
+// the same attack with the marker inside a body parameter took 60s per
+// payload. This is Intruder-specific, not something Repeater does to
+// what's typed: a fuzzed value's length is a side effect of automated
+// substitution, whereas a Repeater edit is deliberate and Repeater's own
+// "no auto-fixed Content-Length" behavior is unchanged.
+//
+// Only touches a request with exactly one Content-Length header and a
+// clean header/body boundary - zero found means nothing to fix, more
+// than one is a request smuggling test's own deliberately ambiguous
+// framing, and guessing which one to rewrite there would be worse than
+// leaving both alone.
+func fixContentLength(raw []byte) []byte {
+ sep := []byte("\r\n\r\n")
+ idx := bytes.Index(raw, sep)
+ if idx < 0 {
+ return raw
+ }
+ headerBlock, body := raw[:idx], raw[idx+len(sep):]
+
+ lines := bytes.Split(headerBlock, []byte("\r\n"))
+ foundIdx, count := -1, 0
+ for i, line := range lines {
+ if i == 0 {
+ continue // request line, not a header
+ }
+ colon := bytes.IndexByte(line, ':')
+ if colon < 0 {
+ continue
+ }
+ if bytes.EqualFold(bytes.TrimSpace(line[:colon]), []byte("Content-Length")) {
+ count++
+ foundIdx = i
+ }
+ }
+ if count != 1 {
+ return raw
+ }
+
+ lines[foundIdx] = []byte(fmt.Sprintf("Content-Length: %d", len(body)))
+ var out bytes.Buffer
+ out.Write(bytes.Join(lines, []byte("\r\n")))
+ out.Write(sep)
+ out.Write(body)
+ return out.Bytes()
+}
diff --git a/internal/proxy/intrude_test.go b/internal/proxy/intrude_test.go
index 5df80e6..6a0007f 100644
--- a/internal/proxy/intrude_test.go
+++ b/internal/proxy/intrude_test.go
@@ -114,3 +114,50 @@ func TestIntrudeRequestCount(t *testing.T) {
t.Fatalf("expected 2 positions, got %d", len(positions))
}
}
+
+func TestFixContentLength(t *testing.T) {
+ tests := []struct {
+ name string
+ raw string
+ want string
+ }{
+ {
+ name: "recalculates a stale length",
+ raw: "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 3\r\n\r\nfuzzedvalue",
+ want: "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 11\r\n\r\nfuzzedvalue",
+ },
+ {
+ name: "case-insensitive header name",
+ raw: "POST / HTTP/1.1\r\nHost: x\r\ncontent-length: 1\r\n\r\nabc",
+ want: "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 3\r\n\r\nabc",
+ },
+ {
+ name: "no content-length header - unchanged",
+ raw: "GET /§1§ HTTP/1.1\r\nHost: x\r\n\r\n",
+ want: "GET /§1§ HTTP/1.1\r\nHost: x\r\n\r\n",
+ },
+ {
+ name: "no body boundary - unchanged",
+ raw: "GET / HTTP/1.1\r\nHost: x",
+ want: "GET / HTTP/1.1\r\nHost: x",
+ },
+ {
+ name: "two content-length headers - left alone, ambiguous smuggling case",
+ raw: "POST / HTTP/1.1\r\nContent-Length: 3\r\nContent-Length: 999\r\n\r\nabc",
+ want: "POST / HTTP/1.1\r\nContent-Length: 3\r\nContent-Length: 999\r\n\r\nabc",
+ },
+ {
+ name: "empty body recalculates to zero",
+ raw: "POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n",
+ want: "POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := fixContentLength([]byte(tt.raw))
+ if string(got) != tt.want {
+ t.Errorf("fixContentLength(%q) = %q, want %q", tt.raw, got, tt.want)
+ }
+ })
+ }
+}