diff options
| author | srdusr <[email protected]> | 2026-02-03 00:38:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-02-03 00:38:00 +0200 |
| commit | ce6ce32469da720105258cb66e0274b2b009cd1d (patch) | |
| tree | bf1766efcdb323755e5cf3128a74b137eeb34b88 /internal/ipc | |
| parent | c2443f27ef5a844f045c038c7689d217d1dbf0c4 (diff) | |
| download | mitmux-ce6ce32469da720105258cb66e0274b2b009cd1d.tar.gz mitmux-ce6ce32469da720105258cb66e0274b2b009cd1d.zip | |
Intruder-equivalent: Sniper attacks with § markers
Implements build-order step 7, the last (optional) item. Scoped to
Sniper only - one payload set, one §-marked position fuzzed at a time,
others held at their base value - since that covers most real Intruder
usage; battering ram / pitchfork / cluster bomb aren't implemented.
Sequential sending, capped at 1000 generated requests as a fixed safety
limit.
internal/proxy: repeat.go's Repeat() is refactored into a shared
sendRaw(..., source) primitive so Intrude can reuse the exact same
raw-byte send/record path with source="intruder" instead of
duplicating it. intrude.go adds ParseMarkers/buildRequest (marker
parsing and payload substitution, covered by intrude_test.go - this is
fiddly byte-splicing logic, worth locking down with real tests rather
than trusting it by inspection) and Intrude(), which walks positions ×
payloads calling sendRaw and streaming each result through a callback.
internal/ipc gains a dedicated streaming "intrude" connection (same
shape as Subscribe, but blocking sends rather than drop-on-slow-
consumer - each result is the attack's actual data, not a
notification). cmd/mitmux gains an Intruder view: editable request
template (ctrl+p inserts a § marker at the cursor - typing § directly
also works, ctrl+p just doesn't require a keyboard layout that can
produce it), editable payload list, and a live results table wired to
the existing detail view (selecting a row and hitting enter opens the
full request/response for that specific attack request).
Verified live against real external traffic: a Sniper attack against
httpbin.org/status/§200§ with payloads 200/404/500 produced exactly the
three corresponding real status codes back (not a canned/local result),
confirmed the three requests landed in history tagged source="intruder"
with the § markers correctly stripped from what was actually sent, and
confirmed opening a result row's full detail from the results table.
This closes out the full build order from PLAN.md (steps 1-7).
Diffstat (limited to 'internal/ipc')
| -rw-r--r-- | internal/ipc/ipc.go | 86 | ||||
| -rw-r--r-- | internal/ipc/server.go | 44 |
2 files changed, 125 insertions, 5 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 19c9c23..03a4bc7 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -10,6 +10,7 @@ import ( "fmt" "net" "sync" + "time" "mitmux/internal/rules" "mitmux/internal/store" @@ -17,7 +18,7 @@ import ( // Request is sent by a client to the daemon. type Request struct { - Type string `json:"type"` // "list", "get", "subscribe", "repeat", "rules_list", "rules_save", "rules_delete", or "rules_toggle" + Type string `json:"type"` // "list", "get", "subscribe", "repeat", "intrude", "rules_list", "rules_save", "rules_delete", or "rules_toggle" Limit int `json:"limit,omitempty"` BeforeID int64 `json:"before_id,omitempty"` ID int64 `json:"id,omitempty"` @@ -28,10 +29,15 @@ type Request struct { Query string `json:"query,omitempty"` // For "repeat": send Raw to scheme://host exactly as given. + // For "intrude": Raw is the §marked§ template - see proxy.Intrude. Scheme string `json:"scheme,omitempty"` 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 "rules_save": add (Rule.ID == 0) or update (Rule.ID != 0) a // match-and-replace rule. For "rules_delete"/"rules_toggle": RuleID // (and RuleEnabled for toggle) identify the target. @@ -42,12 +48,27 @@ type Request struct { // Response is sent by the daemon to a client. type Response struct { - Type string `json:"type"` // "list", "get", "new", "repeat", "rules", or "error" + Type string `json:"type"` // "list", "get", "new", "repeat", "rules", "intrude_result", "intrude_done", or "error" Entries []store.Summary `json:"entries,omitempty"` // for "list" Detail *EntryDetail `json:"detail,omitempty"` // for "get" and "repeat" New *store.Summary `json:"new,omitempty"` // for "new" (subscribe push) Rules []rules.Rule `json:"rules,omitempty"` // for "rules" - Error string `json:"error,omitempty"` + + // For "intrude_result": one completed attack request. + IntrudeResult *IntrudeResultMsg `json:"intrude_result,omitempty"` + + Error string `json:"error,omitempty"` +} + +// IntrudeResultMsg is one completed Intruder attack request. +type IntrudeResultMsg struct { + Position int `json:"position"` + Payload string `json:"payload"` + EntryID int64 `json:"entry_id"` + StatusCode int `json:"status_code"` + RespSize int `json:"resp_size"` + Duration time.Duration `json:"duration"` + Error string `json:"error,omitempty"` } // EntryDetail is a full history entry, raw bytes included. @@ -237,3 +258,62 @@ 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. 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) 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) (<-chan IntrudeResultMsg, func() error, error) { + conn, err := net.Dial("unix", path) + if err != nil { + return nil, nil, fmt.Errorf("dial %s: %w", path, err) + } + if err := json.NewEncoder(conn).Encode(Request{Type: "intrude", Scheme: scheme, Host: host, Raw: template, Payloads: payloads}); err != nil { + conn.Close() + return nil, nil, err + } + + dec := json.NewDecoder(conn) + var first Response + if err := dec.Decode(&first); err != nil { + conn.Close() + return nil, nil, err + } + if first.Type == "error" { + conn.Close() + return nil, nil, errors.New(first.Error) + } + + ch := make(chan IntrudeResultMsg) + go func() { + defer close(ch) + deliver := func(resp Response) bool { + switch resp.Type { + case "intrude_result": + if resp.IntrudeResult != nil { + ch <- *resp.IntrudeResult + } + return true + default: // "intrude_done", or anything else - stop + return false + } + } + if !deliver(first) { + return + } + for { + var resp Response + if err := dec.Decode(&resp); err != nil { + return + } + if !deliver(resp) { + return + } + } + }() + return ch, conn.Close, nil +} diff --git a/internal/ipc/server.go b/internal/ipc/server.go index 1fe8d8f..50dff7f 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -18,6 +18,13 @@ 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 - +// 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 +} + // Hub fans out newly captured history entries to subscribed clients. type Hub struct { mu sync.Mutex @@ -62,12 +69,17 @@ type Server struct { db *store.Store hub *Hub repeater Repeater + intruder Intruder } // NewServer creates a control-protocol Server backed by db, broadcasting -// through hub and sending Repeater requests through rep. +// through hub and sending Repeater/Intruder requests through rep. func NewServer(db *store.Store, hub *Hub, rep Repeater) *Server { - return &Server{db: db, hub: hub, repeater: rep} + s := &Server{db: db, hub: hub, repeater: rep} + if in, ok := rep.(Intruder); ok { + s.intruder = in + } + return s } // Serve accepts connections on ln until it returns an error (e.g. the @@ -128,6 +140,34 @@ func (s *Server) handleConn(conn net.Conn) { } enc.Encode(Response{Type: "repeat", Detail: detailFromEntry(e)}) + case "intrude": + if s.intruder == nil { + enc.Encode(Response{Type: "error", Error: "intruder not available"}) + continue + } + 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} + if sendErr != nil { + r.Error = sendErr.Error() + } + if entry != nil { + r.EntryID = entry.ID + r.StatusCode = entry.StatusCode + r.RespSize = len(entry.ResponseRaw) + r.Duration = entry.Duration + if entry.Error != "" && r.Error == "" { + r.Error = entry.Error + } + } + return enc.Encode(Response{Type: "intrude_result", IntrudeResult: &r}) == nil + }) + if err != nil { + enc.Encode(Response{Type: "error", Error: err.Error()}) + continue + } + enc.Encode(Response{Type: "intrude_done"}) + case "rules_list": rs, err := s.db.ListRules() if err != nil { |