srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/proxy/intrude_test.go
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-06-11 00:37:00 +0200
committersrdusr <[email protected]>2026-06-11 00:37:00 +0200
commit6114567258bcad0517a0d881168711aaacdba5d5 (patch)
tree6bb9f3af9cfe0c857140a51f7992e3853cb2a236 /internal/proxy/intrude_test.go
parenta2362fc08c31b23fb971279f8b160555123ad2f6 (diff)
downloadmitmux-6114567258bcad0517a0d881168711aaacdba5d5.tar.gz
mitmux-6114567258bcad0517a0d881168711aaacdba5d5.zip
Intruder: Battering ram, Pitchfork, and Cluster bomb attack modes
Generalizes Intrude beyond Sniper to all four of Burp's attack modes (proxy.AttackMode). Sniper and Battering ram only ever need one shared payload set; Pitchfork and Cluster bomb are inherently per-position, so they take one payload set per §marked§ position instead. Request-set generation (intrudeValues) is pure and side-effect free, so the total request count is validated against the existing 1000 cap before anything is dispatched - Cluster bomb's product is checked incrementally, one payload set at a time, so a pathological product bails out before ever trying to enumerate it. This also makes the combinatorics unit-testable without a live target. IntrudeResultMsg now reports Values (one substitution per marked position, in order) instead of a single Position/Payload pair, since three of the four modes touch multiple positions per request. TUI: `a` cycles the attack mode. Pitchfork/Cluster bomb reuse the existing single Payloads pane rather than a new multi-widget editor - sets are separated by a `---` delimiter line, in position order. Verified live against a real daemon: all four modes produce the expected substitution values and request counts, and pitchfork correctly rejects a payload-set count that doesn't match the template's marked positions.
Diffstat (limited to 'internal/proxy/intrude_test.go')
-rw-r--r--internal/proxy/intrude_test.go118
1 files changed, 104 insertions, 14 deletions
diff --git a/internal/proxy/intrude_test.go b/internal/proxy/intrude_test.go
index 6a0007f..59aa5a7 100644
--- a/internal/proxy/intrude_test.go
+++ b/internal/proxy/intrude_test.go
@@ -1,6 +1,7 @@
package proxy
import (
+ "fmt"
"reflect"
"testing"
)
@@ -66,36 +67,35 @@ func TestParseMarkers(t *testing.T) {
}
}
-func TestBuildRequest(t *testing.T) {
+func TestBuildRequestValues(t *testing.T) {
template := "GET /a/§1§/b/§2§/c/§3§ HTTP/1.1"
tests := []struct {
- active int
- payload string
- want string
+ values []string
+ want string
}{
- {0, "PAYLOAD", "GET /a/PAYLOAD/b/2/c/3 HTTP/1.1"},
- {1, "PAYLOAD", "GET /a/1/b/PAYLOAD/c/3 HTTP/1.1"},
- {2, "PAYLOAD", "GET /a/1/b/2/c/PAYLOAD HTTP/1.1"},
+ {[]string{"PAYLOAD", "2", "3"}, "GET /a/PAYLOAD/b/2/c/3 HTTP/1.1"},
+ {[]string{"1", "PAYLOAD", "3"}, "GET /a/1/b/PAYLOAD/c/3 HTTP/1.1"},
+ {[]string{"1", "2", "PAYLOAD"}, "GET /a/1/b/2/c/PAYLOAD HTTP/1.1"},
}
for _, tt := range tests {
- got, err := buildRequest([]byte(template), tt.active, tt.payload)
+ got, err := buildRequestValues([]byte(template), tt.values)
if err != nil {
- t.Fatalf("active=%d: unexpected error: %v", tt.active, err)
+ t.Fatalf("values=%v: unexpected error: %v", tt.values, err)
}
if string(got) != tt.want {
- t.Errorf("active=%d: got %q, want %q", tt.active, got, tt.want)
+ t.Errorf("values=%v: got %q, want %q", tt.values, got, tt.want)
}
}
}
-func TestBuildRequestPayloadContainingMarkerChar(t *testing.T) {
+func TestBuildRequestValuesPayloadContainingMarkerChar(t *testing.T) {
// A payload that itself contains the marker character must not be
- // reinterpreted as a marker on a later buildRequest call - each call
- // re-splits the ORIGINAL template, not the previously built request.
+ // reinterpreted as a marker - buildRequestValues splits the ORIGINAL
+ // template, never the already-substituted result.
template := "GET /§1§/§2§ HTTP/1.1"
- got, err := buildRequest([]byte(template), 0, "§injected§")
+ got, err := buildRequestValues([]byte(template), []string{"§injected§", "2"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -105,6 +105,96 @@ func TestBuildRequestPayloadContainingMarkerChar(t *testing.T) {
}
}
+func TestIntrudeValuesSniper(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(Sniper, positions, [][]string{{"1", "2"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ want := [][]string{
+ {"1", "b"}, {"2", "b"}, // position 0 fuzzed, position 1 at base
+ {"a", "1"}, {"a", "2"}, // position 1 fuzzed, position 0 at base
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesBatteringRam(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(BatteringRam, positions, [][]string{{"1", "2"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Same payload lands in every position at once, unlike Sniper.
+ want := [][]string{{"1", "1"}, {"2", "2"}}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesPitchfork(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(Pitchfork, positions, [][]string{{"1", "2", "3"}, {"x", "y"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Walks both sets in lockstep; stops at the shorter set's length (2),
+ // silently ignoring "3" from the longer one - Burp's own convention.
+ want := [][]string{{"1", "x"}, {"2", "y"}}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesPitchforkRequiresOneSetPerPosition(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ _, err := intrudeValues(Pitchfork, positions, [][]string{{"1", "2"}})
+ if err == nil {
+ t.Fatal("expected error for one payload set across two positions")
+ }
+}
+
+func TestIntrudeValuesClusterBomb(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}}
+ got, err := intrudeValues(ClusterBomb, positions, [][]string{{"1", "2"}, {"x", "y"}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Every combination - the rightmost (last) position cycles fastest.
+ want := [][]string{{"1", "x"}, {"1", "y"}, {"2", "x"}, {"2", "y"}}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestIntrudeValuesClusterBombOverCapRejected(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}, {Index: 1, Base: "b"}, {Index: 2, Base: "c"}}
+ big := make([]string, 20)
+ for i := range big {
+ big[i] = fmt.Sprintf("v%d", i)
+ }
+ // 20 * 20 * 20 = 8000, comfortably over the 1000 cap.
+ _, err := intrudeValues(ClusterBomb, positions, [][]string{big, big, big})
+ if err == nil {
+ t.Fatal("expected the request-count cap to reject this attack")
+ }
+}
+
+func TestIntrudeValuesOverCapRejected(t *testing.T) {
+ positions := []IntrudePosition{{Index: 0, Base: "a"}}
+ big := make([]string, maxIntrudeRequests+1)
+ for i := range big {
+ big[i] = fmt.Sprintf("v%d", i)
+ }
+ if _, err := intrudeValues(Sniper, positions, [][]string{big}); err == nil {
+ t.Error("Sniper: expected the request-count cap to reject this attack")
+ }
+ if _, err := intrudeValues(BatteringRam, positions, [][]string{big}); err == nil {
+ t.Error("BatteringRam: expected the request-count cap to reject this attack")
+ }
+}
+
func TestIntrudeRequestCount(t *testing.T) {
positions, _, err := ParseMarkers([]byte("GET /§a§/§b§ HTTP/1.1"))
if err != nil {