srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/main.go161
-rw-r--r--cmd/mitmux/payload_rules.go61
-rw-r--r--cmd/mitmux/payload_rules_test.go31
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, "&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)
+ }
+ })
+ }
+}