srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-06-11 00:37:00 +0200
committersrdusr <[email protected]>2026-06-11 00:37:00 +0200
commit6114567258bcad0517a0d881168711aaacdba5d5 (patch)
tree6bb9f3af9cfe0c857140a51f7992e3853cb2a236 /cmd
parenta2362fc08c31b23fb971279f8b160555123ad2f6 (diff)
downloadmitmux-6114567258bcad0517a0d881168711aaacdba5d5.tar.gz
mitmux-6114567258bcad0517a0d881168711aaacdba5d5.zip
Intruder: Battering ram, Pitchfork, and Cluster bomb attack modes
Generalizes Intrude beyond Sniper to all four of Burp's attack modes (proxy.AttackMode). Sniper and Battering ram only ever need one shared payload set; Pitchfork and Cluster bomb are inherently per-position, so they take one payload set per §marked§ position instead. Request-set generation (intrudeValues) is pure and side-effect free, so the total request count is validated against the existing 1000 cap before anything is dispatched - Cluster bomb's product is checked incrementally, one payload set at a time, so a pathological product bails out before ever trying to enumerate it. This also makes the combinatorics unit-testable without a live target. IntrudeResultMsg now reports Values (one substitution per marked position, in order) instead of a single Position/Payload pair, since three of the four modes touch multiple positions per request. TUI: `a` cycles the attack mode. Pitchfork/Cluster bomb reuse the existing single Payloads pane rather than a new multi-widget editor - sets are separated by a `---` delimiter line, in position order. Verified live against a real daemon: all four modes produce the expected substitution values and request counts, and pitchfork correctly rejects a payload-set count that doesn't match the template's marked positions.
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/main.go106
1 files changed, 90 insertions, 16 deletions
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index d11d18b..304d3fc 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -20,6 +20,7 @@ import (
"mitmux/internal/ca"
"mitmux/internal/ipc"
+ "mitmux/internal/proxy"
"mitmux/internal/rules"
"mitmux/internal/scope"
"mitmux/internal/store"
@@ -199,6 +200,7 @@ type model struct {
intruderResults table.Model
intruderRows []ipc.IntrudeResultMsg
intruderFocus intruderFocus
+ intruderMode proxy.AttackMode
intruderRunning bool
intruderCount int
intruderCh <-chan ipc.IntrudeResultMsg
@@ -331,12 +333,12 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string)
itmpl.ta.ShowLineNumbers = false
ipayloads := newViTextarea()
- ipayloads.ta.Placeholder = "payloads, one per line"
+ ipayloads.ta.Placeholder = "payloads, one per line (pitchfork/cluster bomb: separate one set per position with a line of ---)"
ipayloads.ta.ShowLineNumbers = false
iresultsCols := []table.Column{
- {Title: "Pos", Width: 4},
- {Title: "Payload", Width: 20},
+ {Title: "#", Width: 4},
+ {Title: "Payload(s)", Width: 24},
{Title: "Status", Width: 6},
{Title: "Size", Width: 8},
{Title: "Time", Width: 8},
@@ -708,6 +710,7 @@ func (m *model) enterIntruder(d *ipc.EntryDetail) {
m.intruderRows = nil
setTableRows(&m.intruderResults, nil)
m.intruderFocus = focusTemplate
+ m.intruderMode = proxy.Sniper
m.intruderRunning = false
m.intruderCount = 0
m.payloadCase = payloadCaseNone
@@ -718,7 +721,7 @@ func (m *model) enterIntruder(d *ipc.EntryDetail) {
m.grepMatchInput.SetValue("")
m.grepExtractInput.SetValue("")
m.mode = viewIntruder
- m.statusMsg = "wrap positions to fuzz in § (ctrl+g), fill payloads, ctrl+r to start"
+ m.statusMsg = "wrap positions to fuzz in § (ctrl+g), fill payloads, a to change attack mode, ctrl+r to start"
}
type intrudeStartedMsg struct {
@@ -732,20 +735,58 @@ type intrudeResultMsg struct {
ok bool
}
+// parsePayloadSets turns the Payloads textarea into one or more payload
+// sets. Sniper and BatteringRam only ever need one shared set, so every
+// non-empty line is a payload. Pitchfork and ClusterBomb are inherently
+// per-position, so the same textarea instead holds several sets separated
+// by a line containing exactly "---", in position order - proxy.Intrude
+// validates the count matches the template's marked positions.
+func parsePayloadSets(text string, mode proxy.AttackMode, caseRule payloadCaseRule, encodeRule payloadEncodeRule) [][]string {
+ lines := strings.Split(text, "\n")
+ if mode != proxy.Pitchfork && mode != proxy.ClusterBomb {
+ var set []string
+ for _, line := range lines {
+ if line != "" {
+ set = append(set, applyPayloadRules(line, caseRule, encodeRule))
+ }
+ }
+ if set == nil {
+ return nil
+ }
+ return [][]string{set}
+ }
+
+ var sets [][]string
+ var cur []string
+ flush := func() {
+ if cur != nil {
+ sets = append(sets, cur)
+ cur = nil
+ }
+ }
+ for _, line := range lines {
+ if strings.TrimSpace(line) == "---" {
+ flush()
+ continue
+ }
+ if line != "" {
+ cur = append(cur, applyPayloadRules(line, caseRule, encodeRule))
+ }
+ }
+ flush()
+ return sets
+}
+
func (m *model) startIntrude() tea.Cmd {
scheme, host := m.intruderScheme, m.intruderHost
// Same CRLF restoration as Repeater, same trade-off - see sendRepeat.
template := []byte(strings.ReplaceAll(m.intruderTemplate.Value(), "\n", "\r\n"))
- var payloads []string
- for _, line := range strings.Split(m.intruderPayloads.Value(), "\n") {
- if line != "" {
- payloads = append(payloads, applyPayloadRules(line, m.payloadCase, m.payloadEncode))
- }
- }
+ mode := m.intruderMode
+ payloadSets := parsePayloadSets(m.intruderPayloads.Value(), mode, 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, grepMatch, grepExtract)
+ ch, closeFn, err := ipc.Intrude(path, scheme, host, template, mode, payloadSets, grepMatch, grepExtract)
return intrudeStartedMsg{ch: ch, close: closeFn, err: err}
}
}
@@ -1752,6 +1793,11 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.intruderTemplate.InsertRune('§')
}
return m, nil
+ case "a":
+ if !editing && !m.intruderRunning {
+ m.intruderMode = nextAttackMode(m.intruderMode)
+ return m, nil
+ }
case "c":
if !editing {
m.payloadCase = (m.payloadCase + 1) % payloadCaseRule(len(payloadCaseNames))
@@ -2339,6 +2385,34 @@ func scopeRowsFor(rs []scope.Rule) []table.Row {
return rows
}
+// nextAttackMode cycles Sniper -> BatteringRam -> Pitchfork -> ClusterBomb
+// -> Sniper.
+func nextAttackMode(mode proxy.AttackMode) proxy.AttackMode {
+ switch mode {
+ case proxy.Sniper:
+ return proxy.BatteringRam
+ case proxy.BatteringRam:
+ return proxy.Pitchfork
+ case proxy.Pitchfork:
+ return proxy.ClusterBomb
+ default:
+ return proxy.Sniper
+ }
+}
+
+func attackModeLabel(mode proxy.AttackMode) string {
+ switch mode {
+ case proxy.BatteringRam:
+ return "battering ram"
+ case proxy.Pitchfork:
+ return "pitchfork"
+ case proxy.ClusterBomb:
+ return "cluster bomb"
+ default:
+ return "sniper"
+ }
+}
+
func (m *model) intruderView() string {
var b strings.Builder
title := fmt.Sprintf(" intruder - %s://%s ", sanitizeLine(m.intruderScheme), sanitizeLine(m.intruderHost))
@@ -2382,8 +2456,8 @@ func (m *model) intruderView() string {
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(fmt.Sprintf("attack: %s · payload rules: case=%s encode=%s · grep-match: %s · grep-extract: %s",
+ attackModeLabel(m.intruderMode), payloadCaseNames[m.payloadCase], payloadEncodeNames[m.payloadEncode], grepMatch, grepExtract))
b.WriteString("\n")
}
@@ -2404,7 +2478,7 @@ func (m *model) intruderView() string {
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"))
+ b.WriteString(helpStyle.Render("i to edit (vi keys) · tab switch pane · ctrl+g insert § · a cycle attack mode · 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()
}
@@ -2421,8 +2495,8 @@ func intrudeRowsFor(rs []ipc.IntrudeResultMsg) []table.Row {
match = "✓"
}
rows[i] = table.Row{
- fmt.Sprintf("%d", r.Position),
- sanitizeLine(r.Payload),
+ fmt.Sprintf("%d", r.Iteration),
+ sanitizeLine(strings.Join(r.Values, " | ")),
status,
humanBytes(r.RespSize),
r.Duration.Round(time.Millisecond).String(),