srdusr
aboutsummaryrefslogtreecommitdiffstats
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
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.
-rw-r--r--PLAN.md28
-rw-r--r--README.md33
-rw-r--r--cmd/mitmux/main.go161
-rw-r--r--cmd/mitmux/payload_rules.go61
-rw-r--r--cmd/mitmux/payload_rules_test.go31
-rw-r--r--internal/ipc/ipc.go46
-rw-r--r--internal/ipc/server.go30
7 files changed, 362 insertions, 28 deletions
diff --git a/PLAN.md b/PLAN.md
index 2dac629..d874f69 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -124,10 +124,30 @@ the correct tab (send results carry the tab index they belong to), and
the status line/response pane it's shown in only updates live if that
tab is still the one on screen.
-Still open from "worth considering": Intruder payload processing
-(encoding/case rules) and grep-match/grep-extract on results, CA install
-UX per OS, multiple proxy listeners and upstream proxy chaining. None of
-these are started yet.
+Shipped since: 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), cycled with `c`/`e` - is applied
+client-side to each payload line before it ever crosses the IPC socket,
+case first then encode (encoding an already-case-folded value is safe;
+the reverse would corrupt e.g. Base64 padding), since it's a pure string
+transform with no proxy-side state involved and reuses the Decoder's own
+`urlEncodeAll`. Grep-match/grep-extract are optional Go regexps
+(`m`/`v` to edit, both gated to normal mode and both revert-on-esc /
+validate-on-enter the same way the history list's `/` search box
+works), evaluated server-side in `internal/ipc/server.go`'s "intrude"
+handler against each result's actual response bytes - chosen over a
+client-side implementation because the daemon already has `entry.
+ResponseRaw` in hand right where the result is built, and Burp's own
+grep options work the same way (matched against the real response, not
+a client-refetched copy). Grep-match flags a result (shown as a Match
+column); grep-extract captures the first submatch (or the whole match
+if the pattern has no capturing group) into an Extract column. Both are
+configured once before `ctrl+r` starts an attack and apply for that run
+only - matching Burp, which doesn't retroactively re-grep already-fired
+requests if you change the options mid-attack.
+
+Still open from "worth considering": CA install UX per OS, multiple
+proxy listeners and upstream proxy chaining. Neither is started yet.
Skipped deliberately (from the research, matches this tool's stated
scope): active/passive vulnerability scanning, plugin marketplace,
diff --git a/README.md b/README.md
index c91e455..67536c7 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,11 @@ list of what's deliberately not implemented (and why), see
- **Intruder** (Sniper only): mark positions in a request template
with `§markers§`, supply a payload list, fuzz one position at a time
against a shared payload set. Results land in the same history table
- as everything else, searchable the same way.
+ as everything else, searchable the same way. Payload processing
+ (optional case and encode rules, applied to every payload before it's
+ sent) and grep-match/grep-extract (flag or pull text out of each
+ result's response with a regexp) are both configurable before starting
+ an attack - see [Intruder](#intruder) below.
- **Match-and-replace**: header rewrite rules (add, remove, or modify)
for requests and/or responses, applied live as traffic passes
through. History still shows what was actually sent/received on each
@@ -218,6 +222,33 @@ closes the active one. All three only fire in normal mode, so they
don't interfere with typing (`[`/`]` show up in JSON bodies constantly,
and `ctrl+w` is the editor's own delete-word-backward while composing).
+### Intruder
+
+Beyond marking `§positions§` and supplying payloads, two more things are
+configurable before `ctrl+r` starts the attack - both normal-mode-only
+shortcuts, available from any pane:
+
+- `c` / `e` cycle **payload processing**: an optional case rule
+ (off/upper/lower) and an optional encode rule (off/URL/Base64/Hex/
+ HTML), shown in the status line above the results table. Applied to
+ every payload, case first then encode, right before it's substituted
+ into the request - case-folding an already-encoded value would
+ corrupt it (e.g. uppercasing Base64 padding), so case always runs on
+ the original text first.
+- `m` / `v` edit **grep-match** / **grep-extract**, each a Go regexp
+ evaluated against every result's actual response bytes (same `enter`
+ confirms / `esc` cancels pattern as the history list's `/` search - an
+ invalid regexp is rejected with an error rather than silently
+ accepted). Grep-match flags a result (a `Match` column) if the pattern
+ is found anywhere in the response; grep-extract captures the first
+ submatch - or the whole match, if the pattern has no capturing group -
+ into an `Extract` column. Both are optional and independent; leave
+ either blank to skip that check.
+
+Both settings apply for the attack you're about to start - changing
+them mid-run doesn't retroactively re-evaluate requests already sent,
+matching Burp's own behavior.
+
### Match-and-replace rules
Press `m` from the history view. Rules match request or response
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index 31491e9..1d6d3c5 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -8,6 +8,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "regexp"
"strings"
"time"
@@ -171,6 +172,24 @@ type model struct {
intruderCh <-chan ipc.IntrudeResultMsg
intruderClose func() error
+ // Payload processing: case/encode rules applied to each payload
+ // line client-side before it's sent (see payload_rules.go).
+ payloadCase payloadCaseRule
+ payloadEncode payloadEncodeRule
+
+ // Grep-match/grep-extract: optional regexps evaluated server-side
+ // against each result's response bytes (see proxy request "intrude"
+ // handling in internal/ipc/server.go). grepEditing mirrors the
+ // searching/searchInput pattern used by the history list's '/':
+ // 0 = not editing, 1 = editing the match pattern, 2 = editing the
+ // extract pattern; the *Src fields hold the last-confirmed pattern,
+ // the *Input fields are the live edit buffer.
+ grepEditing int
+ grepMatchSrc string
+ grepExtractSrc string
+ grepMatchInput textinput.Model
+ grepExtractInput textinput.Model
+
daemonStatus *ipc.StatusMsg
prevMode viewMode // for the ? help screen's "esc back" target
@@ -242,11 +261,13 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
iresultsCols := []table.Column{
{Title: "Pos", Width: 4},
- {Title: "Payload", Width: 24},
+ {Title: "Payload", Width: 20},
{Title: "Status", Width: 6},
- {Title: "Size", Width: 10},
+ {Title: "Size", Width: 8},
{Title: "Time", Width: 8},
- {Title: "Error", Width: 20},
+ {Title: "Match", Width: 5},
+ {Title: "Extract", Width: 18},
+ {Title: "Error", Width: 14},
}
iresults := table.New(table.WithColumns(iresultsCols), table.WithFocused(true))
iresults.SetStyles(st)
@@ -255,6 +276,11 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
din.ta.Placeholder = "text to encode/decode"
din.ta.ShowLineNumbers = false
+ gmIn := textinput.New()
+ gmIn.Placeholder = "regexp - flags a result if it matches the response"
+ geIn := textinput.New()
+ geIn.Placeholder = "regexp - extracts first capture group (or whole match) from the response"
+
return &model{
client: client,
subCh: subCh,
@@ -271,6 +297,8 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
intruderTemplate: itmpl,
intruderPayloads: ipayloads,
intruderResults: iresults,
+ grepMatchInput: gmIn,
+ grepExtractInput: geIn,
decoderInput: din,
}
}
@@ -477,6 +505,13 @@ func (m *model) enterIntruder(d *ipc.EntryDetail) {
m.intruderFocus = focusTemplate
m.intruderRunning = false
m.intruderCount = 0
+ m.payloadCase = payloadCaseNone
+ m.payloadEncode = payloadEncodeNone
+ m.grepEditing = 0
+ m.grepMatchSrc = ""
+ m.grepExtractSrc = ""
+ m.grepMatchInput.SetValue("")
+ m.grepExtractInput.SetValue("")
m.mode = viewIntruder
m.statusMsg = "wrap positions to fuzz in § (ctrl+g), fill payloads, ctrl+r to start"
}
@@ -499,12 +534,13 @@ func (m *model) startIntrude() tea.Cmd {
var payloads []string
for _, line := range strings.Split(m.intruderPayloads.Value(), "\n") {
if line != "" {
- payloads = append(payloads, line)
+ payloads = append(payloads, applyPayloadRules(line, m.payloadCase, m.payloadEncode))
}
}
path := m.socketPath
+ grepMatch, grepExtract := m.grepMatchSrc, m.grepExtractSrc
return func() tea.Msg {
- ch, closeFn, err := ipc.Intrude(path, scheme, host, template, payloads)
+ ch, closeFn, err := ipc.Intrude(path, scheme, host, template, payloads, grepMatch, grepExtract)
return intrudeStartedMsg{ch: ch, close: closeFn, err: err}
}
}
@@ -649,14 +685,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ruleMatch.Width = formWidth
m.ruleReplace.Width = formWidth
- itmplHeight := (h - 8) / 3
+ // h-9 rather than h-8: one extra line reserved for the payload
+ // rules / grep-match-extract status line in intruderView.
+ itmplHeight := (h - 9) / 3
ipayloadsHeight := itmplHeight
m.intruderTemplate.SetWidth(msg.Width)
m.intruderTemplate.SetHeight(itmplHeight)
m.intruderPayloads.SetWidth(msg.Width)
m.intruderPayloads.SetHeight(ipayloadsHeight)
m.intruderResults.SetWidth(msg.Width)
- m.intruderResults.SetHeight(h - 8 - itmplHeight - ipayloadsHeight)
+ m.intruderResults.SetHeight(h - 9 - itmplHeight - ipayloadsHeight)
+ m.grepMatchInput.Width = msg.Width - 2
+ m.grepExtractInput.Width = msg.Width - 2
return m, nil
case listLoadedMsg:
@@ -1080,6 +1120,51 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
case viewIntruder:
+ // Editing a grep pattern is a modal overlay on top of the
+ // normal template/payloads/results panes, same pattern as
+ // the history list's '/' search: enter commits (after
+ // validating the regexp compiles), esc discards the edit and
+ // reverts to the last-confirmed pattern.
+ if m.grepEditing != 0 {
+ input := &m.grepMatchInput
+ if m.grepEditing == 2 {
+ input = &m.grepExtractInput
+ }
+ switch msg.String() {
+ case "enter":
+ v := input.Value()
+ if v != "" {
+ if _, err := regexp.Compile(v); err != nil {
+ m.statusMsg = "invalid regexp: " + err.Error()
+ return m, nil
+ }
+ }
+ if m.grepEditing == 1 {
+ m.grepMatchSrc = v
+ } else {
+ m.grepExtractSrc = v
+ }
+ m.grepEditing = 0
+ input.Blur()
+ m.statusMsg = ""
+ return m, nil
+ case "esc":
+ if m.grepEditing == 1 {
+ input.SetValue(m.grepMatchSrc)
+ } else {
+ input.SetValue(m.grepExtractSrc)
+ }
+ m.grepEditing = 0
+ input.Blur()
+ return m, nil
+ case "ctrl+c":
+ return m, tea.Quit
+ }
+ var cmd tea.Cmd
+ *input, cmd = input.Update(msg)
+ return m, cmd
+ }
+
editing := (m.intruderFocus == focusTemplate && m.intruderTemplate.Mode() == viInsert) ||
(m.intruderFocus == focusPayloads && m.intruderPayloads.Mode() == viInsert)
switch msg.String() {
@@ -1116,6 +1201,30 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.intruderTemplate.InsertRune('§')
}
return m, nil
+ case "c":
+ if !editing {
+ m.payloadCase = (m.payloadCase + 1) % payloadCaseRule(len(payloadCaseNames))
+ return m, nil
+ }
+ case "e":
+ if !editing {
+ m.payloadEncode = (m.payloadEncode + 1) % payloadEncodeRule(len(payloadEncodeNames))
+ return m, nil
+ }
+ case "m":
+ if !editing {
+ m.grepEditing = 1
+ m.grepMatchInput.SetValue(m.grepMatchSrc)
+ m.grepMatchInput.CursorEnd()
+ return m, m.grepMatchInput.Focus()
+ }
+ case "v":
+ if !editing {
+ m.grepEditing = 2
+ m.grepExtractInput.SetValue(m.grepExtractSrc)
+ m.grepExtractInput.CursorEnd()
+ return m, m.grepExtractInput.Focus()
+ }
case "tab":
m.intruderFocus = (m.intruderFocus + 1) % 3
if m.intruderFocus == focusTemplate {
@@ -1344,6 +1453,8 @@ func (m *model) helpView() string {
"ctrl+g (Intruder template only) insert a § marker at cursor",
"]/[ (Repeater only) next/previous tab",
"ctrl+w (Repeater only) close current tab",
+ "c / e (Intruder only) cycle payload case / encode rule",
+ "m / v (Intruder only) edit grep-match / grep-extract regexp",
)
section("Rules",
"a add rule enter / e edit selected",
@@ -1588,6 +1699,30 @@ func (m *model) intruderView() string {
b.WriteString("\n")
b.WriteString(m.intruderPayloads.View())
b.WriteString("\n")
+
+ switch m.grepEditing {
+ case 1:
+ b.WriteString("grep-match (regexp): ")
+ b.WriteString(m.grepMatchInput.View())
+ b.WriteString("\n")
+ case 2:
+ b.WriteString("grep-extract (regexp): ")
+ b.WriteString(m.grepExtractInput.View())
+ b.WriteString("\n")
+ default:
+ grepMatch := m.grepMatchSrc
+ if grepMatch == "" {
+ grepMatch = "(none)"
+ }
+ grepExtract := m.grepExtractSrc
+ if grepExtract == "" {
+ grepExtract = "(none)"
+ }
+ b.WriteString(fmt.Sprintf("payload rules: case=%s encode=%s · grep-match: %s · grep-extract: %s",
+ payloadCaseNames[m.payloadCase], payloadEncodeNames[m.payloadEncode], grepMatch, grepExtract))
+ b.WriteString("\n")
+ }
+
b.WriteString(m.intruderResults.View())
b.WriteString("\n")
@@ -1602,7 +1737,11 @@ func (m *model) intruderView() string {
b.WriteString(statusStyle.Render(m.statusMsg))
b.WriteString("\n")
}
- b.WriteString(helpStyle.Render("i to edit (vi keys) · tab switch pane · ctrl+g insert § · ctrl+r start · enter (results) view · esc back/stop · ? help · ctrl+c quit"))
+ if m.grepEditing != 0 {
+ b.WriteString(helpStyle.Render("enter confirm · esc cancel · ctrl+c quit"))
+ } else {
+ b.WriteString(helpStyle.Render("i to edit (vi keys) · tab switch pane · ctrl+g insert § · c/e cycle case/encode · m/v edit grep-match/extract · ctrl+r start · enter (results) view · esc back/stop · ? help · ctrl+c quit"))
+ }
return b.String()
}
@@ -1613,12 +1752,18 @@ func intrudeRowsFor(rs []ipc.IntrudeResultMsg) []table.Row {
if r.StatusCode == 0 {
status = "ERR"
}
+ match := ""
+ if r.GrepMatch {
+ match = "✓"
+ }
rows[i] = table.Row{
fmt.Sprintf("%d", r.Position),
r.Payload,
status,
humanBytes(r.RespSize),
r.Duration.Round(time.Millisecond).String(),
+ match,
+ r.GrepExtract,
r.Error,
}
}
diff --git a/cmd/mitmux/payload_rules.go b/cmd/mitmux/payload_rules.go
new file mode 100644
index 0000000..6302f54
--- /dev/null
+++ b/cmd/mitmux/payload_rules.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "encoding/base64"
+ "encoding/hex"
+ "html"
+ "strings"
+)
+
+// payloadCaseRule is an optional case transform applied to each Intruder
+// payload before it's sent - Burp calls this class of feature "payload
+// processing". Applied client-side, before the payload list ever crosses
+// the IPC socket, since it's a pure string transform with no proxy-side
+// state involved.
+type payloadCaseRule int
+
+const (
+ payloadCaseNone payloadCaseRule = iota
+ payloadCaseUpper
+ payloadCaseLower
+)
+
+var payloadCaseNames = []string{"off", "upper", "lower"}
+
+// payloadEncodeRule is an optional encoding applied to each payload after
+// the case rule, right before it's substituted into the request template.
+type payloadEncodeRule int
+
+const (
+ payloadEncodeNone payloadEncodeRule = iota
+ payloadEncodeURL
+ payloadEncodeBase64
+ payloadEncodeHex
+ payloadEncodeHTML
+)
+
+var payloadEncodeNames = []string{"off", "URL", "Base64", "Hex", "HTML"}
+
+// applyPayloadRules runs the case rule, then the encode rule, over one raw
+// payload line. Order matters: case-folding an already-encoded payload
+// (e.g. uppercasing "aGVsbG8=") would corrupt it, so case always runs
+// first, against the original text.
+func applyPayloadRules(s string, c payloadCaseRule, e payloadEncodeRule) string {
+ switch c {
+ case payloadCaseUpper:
+ s = strings.ToUpper(s)
+ case payloadCaseLower:
+ s = strings.ToLower(s)
+ }
+ switch e {
+ case payloadEncodeURL:
+ s = urlEncodeAll(s)
+ case payloadEncodeBase64:
+ s = base64.StdEncoding.EncodeToString([]byte(s))
+ case payloadEncodeHex:
+ s = hex.EncodeToString([]byte(s))
+ case payloadEncodeHTML:
+ s = html.EscapeString(s)
+ }
+ return s
+}
diff --git a/cmd/mitmux/payload_rules_test.go b/cmd/mitmux/payload_rules_test.go
new file mode 100644
index 0000000..466f14f
--- /dev/null
+++ b/cmd/mitmux/payload_rules_test.go
@@ -0,0 +1,31 @@
+package main
+
+import "testing"
+
+func TestApplyPayloadRules(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ c payloadCaseRule
+ e payloadEncodeRule
+ want string
+ }{
+ {"no rules", "Hello World", payloadCaseNone, payloadEncodeNone, "Hello World"},
+ {"upper only", "Hello World", payloadCaseUpper, payloadEncodeNone, "HELLO WORLD"},
+ {"lower only", "Hello World", payloadCaseLower, payloadEncodeNone, "hello world"},
+ {"url encode only", "a b/c", payloadCaseNone, payloadEncodeURL, "a%20b%2Fc"},
+ {"base64 encode only", "hello", payloadCaseNone, payloadEncodeBase64, "aGVsbG8="},
+ {"hex encode only", "hi", payloadCaseNone, payloadEncodeHex, "6869"},
+ {"html encode only", "<x>", payloadCaseNone, payloadEncodeHTML, "&lt;x&gt;"},
+ {"upper then url encode", "a b", payloadCaseUpper, payloadEncodeURL, "A%20B"},
+ {"lower then base64 - case runs before encode", "HELLO", payloadCaseLower, payloadEncodeBase64, "aGVsbG8="},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := applyPayloadRules(tt.input, tt.c, tt.e)
+ if got != tt.want {
+ t.Errorf("applyPayloadRules(%q, %v, %v) = %q, want %q", tt.input, tt.c, tt.e, got, tt.want)
+ }
+ })
+ }
+}
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
})