srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/ipc
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-03-27 21:32:00 +0200
committersrdusr <[email protected]>2026-03-27 21:32:00 +0200
commitb11e60dcccc836bf67b3720930600f7112f624dc (patch)
tree99c263a8d6c386af43df145445c12effcdd5b202 /internal/ipc
parent2bb1425dd0da46d8a3df0b888411f21340783d71 (diff)
downloadmitmux-b11e60dcccc836bf67b3720930600f7112f624dc.tar.gz
mitmux-b11e60dcccc836bf67b3720930600f7112f624dc.zip
Intruder payload processing and grep-match/grep-extract
Payload processing: an optional case rule (upper/lower) and an optional encode rule (URL/Base64/Hex/HTML) applied to every payload line before it's substituted into the request, cycled with 'c'/'e'. Case always runs before encode - folding an already-encoded value would corrupt it (e.g. uppercasing Base64 padding). Applied entirely client-side in startIntrude() (payload_rules.go): a pure string transform with no proxy-side state, so it needs no protocol changes and reuses the Decoder's own urlEncodeAll. Grep-match/grep-extract: two optional Go regexps, edited with 'm'/'v' using the same modal edit-buffer pattern as the history list's '/' search (enter validates-and-commits, esc reverts to the last-confirmed pattern, an unparseable regexp is rejected with an error rather than silently accepted). Evaluated server-side, in internal/ipc/server.go's "intrude" handler, against each result's actual entry.ResponseRaw - that's where the real response bytes already are, and it's how Burp's own grep options work (matched against the real response, not a client-refetched copy). Grep-match flags a result (new Match column); grep-extract captures the first submatch, or the whole match if the pattern has no capturing group (new Extract column). Both patterns are compiled once before the attack starts and apply for that run only, not retroactively if changed mid-attack. All four new keys (c/e/m/v) are gated to normal mode, checked in the view's outer key switch before ever reaching the template/payloads vi-textareas - otherwise they'd be either untypeable letters or steal keystrokes mid-edit. Same discipline as the Repeater tab keys. internal/ipc: Request gained GrepMatch/GrepExtract string fields (for "intrude"), IntrudeResultMsg gained GrepMatch bool/GrepExtract string, and the client Intrude() helper takes the two pattern strings as new trailing parameters. Verified live in tmux against a running daemon and real httpbin.org traffic: built a template with a §marked§ query param, payloads 1/2/3, grep-match `"id": "2"` and grep-extract `"id": "([0-9]+)"`, ran the attack and confirmed the Match column flagged only the payload=2 row and Extract correctly pulled 1/2/3 from each response respectively; cycled case/encode through all states; confirmed an invalid regexp (`[abc`) is rejected with a visible error and esc correctly reverts to the last-confirmed pattern instead of committing the invalid one. (Also confirmed, incidentally: a batch of vi normal-mode two-key commands like "gg"/"dd" sent as one multi-character tmux send-keys argument doesn't reliably reach the app as separate keystrokes - a tmux scripting artifact, not a bug in the vi-mode implementation, which works correctly when each key is sent as its own event, as any real keypress would be.) go build/vet/gofmt/test/mod tidy all clean.
Diffstat (limited to 'internal/ipc')
-rw-r--r--internal/ipc/ipc.go46
-rw-r--r--internal/ipc/server.go30
2 files changed, 61 insertions, 15 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go
index 7d81473..270b8e4 100644
--- a/internal/ipc/ipc.go
+++ b/internal/ipc/ipc.go
@@ -38,6 +38,16 @@ type Request struct {
// turn (Sniper-style - see proxy.Intrude).
Payloads []string `json:"payloads,omitempty"`
+ // For "intrude": optional Go regexps evaluated against each result's
+ // response bytes. GrepMatch flags whether it matched at all;
+ // GrepExtract additionally captures text (first submatch if the
+ // pattern has a capturing group, else the whole match) into the
+ // result. Either or both may be empty to skip that check. Compiled
+ // and validated once, server-side, before the attack starts - a bad
+ // pattern fails the same way a bad marker or empty payload set does.
+ GrepMatch string `json:"grep_match,omitempty"`
+ GrepExtract string `json:"grep_extract,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.
@@ -72,13 +82,15 @@ type StatusMsg struct {
// 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"`
+ 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"`
+ GrepMatch bool `json:"grep_match,omitempty"`
+ GrepExtract string `json:"grep_extract,omitempty"`
}
// EntryDetail is a full history entry, raw bytes included.
@@ -307,18 +319,22 @@ func Subscribe(path string) (<-chan store.Summary, func() error, error) {
// 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) {
+// 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) {
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 {
+ req := Request{Type: "intrude", Scheme: scheme, Host: host, Raw: template, Payloads: payloads, 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 28279a7..9602912 100644
--- a/internal/ipc/server.go
+++ b/internal/ipc/server.go
@@ -6,6 +6,7 @@ import (
"io"
"log"
"net"
+ "regexp"
"sync"
"mitmux/internal/rules"
@@ -155,6 +156,23 @@ func (s *Server) handleConn(conn net.Conn) {
enc.Encode(Response{Type: "error", Error: "intruder not available"})
continue
}
+ var grepMatchRe, grepExtractRe *regexp.Regexp
+ if req.GrepMatch != "" {
+ re, err := regexp.Compile(req.GrepMatch)
+ if err != nil {
+ enc.Encode(Response{Type: "error", Error: "grep-match: " + err.Error()})
+ continue
+ }
+ grepMatchRe = re
+ }
+ if req.GrepExtract != "" {
+ re, err := regexp.Compile(req.GrepExtract)
+ if err != nil {
+ enc.Encode(Response{Type: "error", Error: "grep-extract: " + err.Error()})
+ continue
+ }
+ 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}
@@ -169,6 +187,18 @@ func (s *Server) handleConn(conn net.Conn) {
if entry.Error != "" && r.Error == "" {
r.Error = entry.Error
}
+ if grepMatchRe != nil {
+ r.GrepMatch = grepMatchRe.Match(entry.ResponseRaw)
+ }
+ if grepExtractRe != nil {
+ if m := grepExtractRe.FindSubmatch(entry.ResponseRaw); m != nil {
+ if len(m) > 1 {
+ r.GrepExtract = string(m[1])
+ } else {
+ r.GrepExtract = string(m[0])
+ }
+ }
+ }
}
return enc.Encode(Response{Type: "intrude_result", IntrudeResult: &r}) == nil
})