From 6114567258bcad0517a0d881168711aaacdba5d5 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:37:00 +0200 Subject: Intruder: Battering ram, Pitchfork, and Cluster bomb attack modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/ipc/ipc.go | 48 +++++++++++++++++++++++++++++------------------- internal/ipc/server.go | 14 ++++++++------ 2 files changed, 37 insertions(+), 25 deletions(-) (limited to 'internal/ipc') diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 89a8343..d581313 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "mitmux/internal/proxy" "mitmux/internal/rules" "mitmux/internal/scope" "mitmux/internal/store" @@ -35,9 +36,13 @@ type Request struct { Host string `json:"host,omitempty"` Raw []byte `json:"raw,omitempty"` - // For "intrude": the payload set, applied to each marked position in - // turn (Sniper-style - see proxy.Intrude). - Payloads []string `json:"payloads,omitempty"` + // For "intrude": the attack mode (proxy.Sniper and so on - empty + // defaults to Sniper) and its payload sets. Sniper and BatteringRam + // only ever use PayloadSets[0] (one shared set); Pitchfork and + // ClusterBomb require exactly one set per §marked§ position, in + // order - see proxy.Intrude. + Mode proxy.AttackMode `json:"mode,omitempty"` + PayloadSets [][]string `json:"payload_sets,omitempty"` // For "intrude": optional Go regexps evaluated against each result's // response bytes. GrepMatch flags whether it matched at all; @@ -118,10 +123,14 @@ type StatusMsg struct { HistoryCount int64 `json:"history_count"` } -// IntrudeResultMsg is one completed Intruder attack request. +// IntrudeResultMsg is one completed Intruder attack request. Values holds +// what was substituted into each §marked§ position for this request, in +// position order - for Sniper, every entry but the one fuzzed position +// equals that position's base value; for the other three modes every +// entry is an actual payload. type IntrudeResultMsg struct { - Position int `json:"position"` - Payload string `json:"payload"` + Iteration int `json:"iteration"` + Values []string `json:"values"` EntryID int64 `json:"entry_id"` StatusCode int `json:"status_code"` RespSize int `json:"resp_size"` @@ -468,23 +477,24 @@ func Subscribe(path string) (<-chan store.Summary, func() error, error) { return ch, conn.Close, nil } -// Intrude starts a Sniper attack (see proxy.Intrude): template must -// contain at least one §marked§ position, fuzzed in turn through -// payloads. grepMatch/grepExtract are optional Go regexps evaluated -// server-side against each result's response bytes (empty string -// disables either check) - see IntrudeResultMsg. Unlike Subscribe's live -// feed, no result is ever dropped for a slow consumer - each one is the -// attack's actual data, not a notification with the real thing -// recoverable elsewhere. A setup error (bad markers, empty payload set, -// too many requests, an unparseable grep regexp) is returned directly -// rather than through the channel. The returned channel closes when the -// attack finishes or the connection is closed early. -func Intrude(path, scheme, host string, template []byte, payloads []string, grepMatch, grepExtract string) (<-chan IntrudeResultMsg, func() error, error) { +// Intrude starts an attack (see proxy.Intrude and proxy.AttackMode): +// template must contain at least one §marked§ position. mode selects how +// payloadSets combine across positions; "" defaults to Sniper. +// grepMatch/grepExtract are optional Go regexps evaluated server-side +// against each result's response bytes (empty string disables either +// check) - see IntrudeResultMsg. Unlike Subscribe's live feed, no result +// is ever dropped for a slow consumer - each one is the attack's actual +// data, not a notification with the real thing recoverable elsewhere. A +// setup error (bad markers, empty payload set, too many requests, an +// unparseable grep regexp) is returned directly rather than through the +// channel. The returned channel closes when the attack finishes or the +// connection is closed early. +func Intrude(path, scheme, host string, template []byte, mode proxy.AttackMode, payloadSets [][]string, grepMatch, grepExtract string) (<-chan IntrudeResultMsg, func() error, error) { conn, err := net.Dial("unix", path) if err != nil { return nil, nil, fmt.Errorf("dial %s: %w", path, err) } - req := Request{Type: "intrude", Scheme: scheme, Host: host, Raw: template, Payloads: payloads, GrepMatch: grepMatch, GrepExtract: grepExtract} + req := Request{Type: "intrude", Scheme: scheme, Host: host, Raw: template, Mode: mode, PayloadSets: payloadSets, GrepMatch: grepMatch, GrepExtract: grepExtract} if err := json.NewEncoder(conn).Encode(req); err != nil { conn.Close() return nil, nil, err diff --git a/internal/ipc/server.go b/internal/ipc/server.go index c83254e..c890a54 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -9,6 +9,7 @@ import ( "regexp" "sync" + "mitmux/internal/proxy" "mitmux/internal/rules" "mitmux/internal/scope" "mitmux/internal/store" @@ -20,11 +21,12 @@ type Repeater interface { Repeat(ctx context.Context, scheme, host string, raw []byte) (*store.Entry, error) } -// Intruder runs a Sniper attack over a §marked§ request template - +// Intruder runs an attack (Sniper, Battering ram, Pitchfork, or Cluster +// bomb - see proxy.AttackMode) over a §marked§ request template - // implemented by *proxy.Server. type Intruder interface { - Intrude(ctx context.Context, scheme, host string, template []byte, payloads []string, - onResult func(position int, payload string, entry *store.Entry, sendErr error) bool) error + Intrude(ctx context.Context, scheme, host string, template []byte, mode proxy.AttackMode, payloadSets [][]string, + onResult func(iteration int, values []string, entry *store.Entry, sendErr error) bool) error } // Hub fans out newly captured history entries to subscribed clients. @@ -174,9 +176,9 @@ func (s *Server) handleConn(conn net.Conn) { } grepExtractRe = re } - err := s.intruder.Intrude(context.Background(), req.Scheme, req.Host, req.Raw, req.Payloads, - func(position int, payload string, entry *store.Entry, sendErr error) bool { - r := IntrudeResultMsg{Position: position, Payload: payload} + err := s.intruder.Intrude(context.Background(), req.Scheme, req.Host, req.Raw, req.Mode, req.PayloadSets, + func(iteration int, values []string, entry *store.Entry, sendErr error) bool { + r := IntrudeResultMsg{Iteration: iteration, Values: values} if sendErr != nil { r.Error = sendErr.Error() } -- cgit v1.2.3