diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/mitmux/main.go | 161 | ||||
| -rw-r--r-- | cmd/mitmux/payload_rules.go | 61 | ||||
| -rw-r--r-- | cmd/mitmux/payload_rules_test.go | 31 |
3 files changed, 245 insertions, 8 deletions
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, "<x>"}, + {"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) + } + }) + } +} |