srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/main.go313
1 files changed, 287 insertions, 26 deletions
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index 68d42ed..c53f975 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -58,7 +58,7 @@ func main() {
}
defer subClose()
- m := newModel(client, subCh)
+ m := newModel(client, subCh, path)
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "mitmux: %v\n", err)
@@ -73,6 +73,7 @@ const (
viewDetail
viewRepeater
viewRules
+ viewIntruder
)
type detailTab int
@@ -99,9 +100,18 @@ const (
fieldRegex
)
+type intruderFocus int
+
+const (
+ focusTemplate intruderFocus = iota
+ focusPayloads
+ focusResults
+)
+
type model struct {
- client *ipc.Client
- subCh <-chan store.Summary
+ client *ipc.Client
+ subCh <-chan store.Summary
+ socketPath string
mode viewMode
entries []store.Summary
@@ -137,13 +147,25 @@ type model struct {
ruleRegex bool
ruleField ruleField
+ intruderScheme string
+ intruderHost string
+ intruderTemplate textarea.Model
+ intruderPayloads textarea.Model
+ intruderResults table.Model
+ intruderRows []ipc.IntrudeResultMsg
+ intruderFocus intruderFocus
+ intruderRunning bool
+ intruderCount int
+ intruderCh <-chan ipc.IntrudeResultMsg
+ intruderClose func() error
+
statusMsg string
width int
height int
ready bool
}
-func newModel(client *ipc.Client, subCh <-chan store.Summary) *model {
+func newModel(client *ipc.Client, subCh <-chan store.Summary, socketPath string) *model {
columns := []table.Column{
{Title: "ID", Width: 5},
{Title: "Method", Width: 7},
@@ -188,18 +210,41 @@ func newModel(client *ipc.Client, subCh <-chan store.Summary) *model {
replaceIn := textinput.New()
replaceIn.Placeholder = "replacement"
+ itmpl := textarea.New()
+ itmpl.Placeholder = "raw request bytes - wrap positions to fuzz in § markers, e.g. /users/§123§"
+ itmpl.ShowLineNumbers = false
+
+ ipayloads := textarea.New()
+ ipayloads.Placeholder = "payloads, one per line"
+ ipayloads.ShowLineNumbers = false
+
+ iresultsCols := []table.Column{
+ {Title: "Pos", Width: 4},
+ {Title: "Payload", Width: 24},
+ {Title: "Status", Width: 6},
+ {Title: "Size", Width: 10},
+ {Title: "Time", Width: 8},
+ {Title: "Error", Width: 20},
+ }
+ iresults := table.New(table.WithColumns(iresultsCols), table.WithFocused(true))
+ iresults.SetStyles(st)
+
return &model{
- client: client,
- subCh: subCh,
- mode: viewList,
- table: t,
- reqArea: ta,
- searchInput: si,
- rulesTable: rt,
- ruleName: nameIn,
- ruleMatch: matchIn,
- ruleReplace: replaceIn,
- ruleScope: "request",
+ client: client,
+ subCh: subCh,
+ socketPath: socketPath,
+ mode: viewList,
+ table: t,
+ reqArea: ta,
+ searchInput: si,
+ rulesTable: rt,
+ ruleName: nameIn,
+ ruleMatch: matchIn,
+ ruleReplace: replaceIn,
+ ruleScope: "request",
+ intruderTemplate: itmpl,
+ intruderPayloads: ipayloads,
+ intruderResults: iresults,
}
}
@@ -213,10 +258,13 @@ type newEntryMsg struct {
ok bool
}
+// detailLoadedMsg carries a freshly loaded entry, plus where to route it:
+// "" for the plain detail view, "repeater" or "intruder" to seed and
+// jump straight to those views instead.
type detailLoadedMsg struct {
- detail *ipc.EntryDetail
- err error
- openRepeater bool
+ detail *ipc.EntryDetail
+ err error
+ dest string
}
type repeatSentMsg struct {
@@ -240,10 +288,10 @@ func (m *model) waitForEntry() tea.Msg {
return newEntryMsg{entry: e, ok: ok}
}
-func (m *model) loadDetail(id int64, openRepeater bool) tea.Cmd {
+func (m *model) loadDetail(id int64, dest string) tea.Cmd {
return func() tea.Msg {
d, err := m.client.Get(id)
- return detailLoadedMsg{detail: d, err: err, openRepeater: openRepeater}
+ return detailLoadedMsg{detail: d, err: err, dest: dest}
}
}
@@ -275,6 +323,57 @@ func (m *model) enterRepeater(d *ipc.EntryDetail) {
m.statusMsg = ""
}
+// enterIntruder seeds the Intruder view from an already-loaded entry.
+// The template starts with no § markers - the user adds them by hand
+// (or ctrl+p at the cursor) around whatever they want to fuzz.
+func (m *model) enterIntruder(d *ipc.EntryDetail) {
+ m.intruderScheme = d.Scheme
+ m.intruderHost = d.Host
+ m.intruderTemplate.SetValue(strings.ReplaceAll(string(d.RequestRaw), "\r\n", "\n"))
+ m.intruderTemplate.Focus()
+ m.intruderPayloads.Blur()
+ m.intruderRows = nil
+ m.intruderResults.SetRows(nil)
+ m.intruderFocus = focusTemplate
+ m.intruderRunning = false
+ m.intruderCount = 0
+ m.mode = viewIntruder
+ m.statusMsg = "wrap positions to fuzz in § (ctrl+p), fill payloads, ctrl+r to start"
+}
+
+type intrudeStartedMsg struct {
+ ch <-chan ipc.IntrudeResultMsg
+ close func() error
+ err error
+}
+
+type intrudeResultMsg struct {
+ result ipc.IntrudeResultMsg
+ ok bool
+}
+
+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, line)
+ }
+ }
+ path := m.socketPath
+ return func() tea.Msg {
+ ch, closeFn, err := ipc.Intrude(path, scheme, host, template, payloads)
+ return intrudeStartedMsg{ch: ch, close: closeFn, err: err}
+ }
+}
+
+func (m *model) waitForIntrudeResult() tea.Msg {
+ r, ok := <-m.intruderCh
+ return intrudeResultMsg{result: r, ok: ok}
+}
+
type rulesLoadedMsg struct {
rules []rules.Rule
err error
@@ -400,6 +499,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ruleName.Width = formWidth
m.ruleMatch.Width = formWidth
m.ruleReplace.Width = formWidth
+
+ itmplHeight := (msg.Height - 8) / 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(msg.Height - 8 - itmplHeight - ipayloadsHeight)
return m, nil
case listLoadedMsg:
@@ -433,9 +541,13 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.statusMsg = "get error: " + msg.err.Error()
return m, nil
}
- if msg.openRepeater {
+ switch msg.dest {
+ case "repeater":
m.enterRepeater(msg.detail)
return m, nil
+ case "intruder":
+ m.enterIntruder(msg.detail)
+ return m, nil
}
m.detail = msg.detail
m.activeTab = tabRequest
@@ -473,6 +585,30 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.statusMsg = "rule " + msg.action
return m, m.loadRules
+ case intrudeStartedMsg:
+ if msg.err != nil {
+ m.intruderRunning = false
+ m.statusMsg = "start error: " + msg.err.Error()
+ return m, nil
+ }
+ m.intruderCh = msg.ch
+ m.intruderClose = msg.close
+ m.intruderRunning = true
+ m.intruderCount = 0
+ m.statusMsg = "attack running..."
+ return m, m.waitForIntrudeResult
+
+ case intrudeResultMsg:
+ if !msg.ok {
+ m.intruderRunning = false
+ m.statusMsg = fmt.Sprintf("attack finished (%d requests)", m.intruderCount)
+ return m, nil
+ }
+ m.intruderCount++
+ m.intruderRows = append(m.intruderRows, msg.result)
+ m.intruderResults.SetRows(intrudeRowsFor(m.intruderRows))
+ return m, m.waitForIntrudeResult
+
case tea.KeyMsg:
switch m.mode {
case viewList:
@@ -502,12 +638,17 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.table.Cursor(); row >= 0 && row < len(m.entries) {
m.mode = viewDetail
m.statusMsg = ""
- return m, m.loadDetail(m.entries[row].ID, false)
+ return m, m.loadDetail(m.entries[row].ID, "")
}
case "r":
if row := m.table.Cursor(); row >= 0 && row < len(m.entries) {
m.statusMsg = ""
- return m, m.loadDetail(m.entries[row].ID, true)
+ return m, m.loadDetail(m.entries[row].ID, "repeater")
+ }
+ case "i":
+ if row := m.table.Cursor(); row >= 0 && row < len(m.entries) {
+ m.statusMsg = ""
+ return m, m.loadDetail(m.entries[row].ID, "intruder")
}
case "/":
m.searching = true
@@ -542,6 +683,11 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.enterRepeater(m.detail)
}
return m, nil
+ case "i":
+ if m.detail != nil {
+ m.enterIntruder(m.detail)
+ }
+ return m, nil
case "tab":
if m.activeTab == tabRequest {
m.activeTab = tabResponse
@@ -662,6 +808,65 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.rulesTable, cmd = m.rulesTable.Update(msg)
return m, cmd
+
+ case viewIntruder:
+ switch msg.String() {
+ case "esc":
+ if m.intruderRunning && m.intruderClose != nil {
+ m.intruderClose()
+ m.intruderRunning = false
+ }
+ m.mode = viewList
+ m.intruderTemplate.Blur()
+ return m, nil
+ case "ctrl+c":
+ return m, tea.Quit
+ case "ctrl+r":
+ if !m.intruderRunning {
+ m.statusMsg = "starting attack..."
+ return m, m.startIntrude()
+ }
+ return m, nil
+ case "ctrl+p":
+ if m.intruderFocus == focusTemplate {
+ m.intruderTemplate.InsertRune('§')
+ }
+ return m, nil
+ case "tab":
+ m.intruderFocus = (m.intruderFocus + 1) % 3
+ if m.intruderFocus == focusTemplate {
+ m.intruderTemplate.Focus()
+ m.intruderPayloads.Blur()
+ } else if m.intruderFocus == focusPayloads {
+ m.intruderTemplate.Blur()
+ m.intruderPayloads.Focus()
+ } else {
+ m.intruderTemplate.Blur()
+ m.intruderPayloads.Blur()
+ }
+ return m, nil
+ case "enter":
+ if m.intruderFocus == focusResults {
+ if row := m.intruderResults.Cursor(); row >= 0 && row < len(m.intruderRows) {
+ id := m.intruderRows[row].EntryID
+ if id != 0 {
+ m.mode = viewDetail
+ return m, m.loadDetail(id, "")
+ }
+ }
+ return m, nil
+ }
+ }
+ var cmd tea.Cmd
+ switch m.intruderFocus {
+ case focusTemplate:
+ m.intruderTemplate, cmd = m.intruderTemplate.Update(msg)
+ case focusPayloads:
+ m.intruderPayloads, cmd = m.intruderPayloads.Update(msg)
+ case focusResults:
+ m.intruderResults, cmd = m.intruderResults.Update(msg)
+ }
+ return m, cmd
}
}
return m, nil
@@ -681,6 +886,8 @@ func (m *model) View() string {
return m.ruleFormView()
}
return m.rulesView()
+ case viewIntruder:
+ return m.intruderView()
default:
return m.listView()
}
@@ -712,9 +919,9 @@ func (m *model) listView() string {
b.WriteString(statusStyle.Render(m.statusMsg))
b.WriteString("\n")
}
- help := "↑/↓ navigate · enter view · r repeater · / search · m rules · q quit"
+ help := "↑/↓ navigate · enter view · r repeater · i intruder · / search · m rules · q quit"
if m.query != "" {
- help = "↑/↓ navigate · enter view · r repeater · / search · m rules · esc clear filter · q quit"
+ help = "↑/↓ navigate · enter view · r repeater · i intruder · / search · m rules · esc clear filter · q quit"
}
b.WriteString(helpStyle.Render(help))
return b.String()
@@ -742,7 +949,7 @@ func (m *model) detailView() string {
b.WriteString("\n")
b.WriteString(m.viewport.View())
b.WriteString("\n")
- b.WriteString(helpStyle.Render("tab switch · ↑/↓ scroll · r repeater · esc back · q quit"))
+ b.WriteString(helpStyle.Render("tab switch · ↑/↓ scroll · r repeater · i intruder · esc back · q quit"))
return b.String()
}
@@ -846,6 +1053,60 @@ func rulesRowsFor(rs []rules.Rule) []table.Row {
return rows
}
+func (m *model) intruderView() string {
+ var b strings.Builder
+ title := fmt.Sprintf(" intruder - %s://%s ", m.intruderScheme, m.intruderHost)
+ if m.intruderRunning {
+ title = fmt.Sprintf(" intruder - %s://%s (running, %d sent) ", m.intruderScheme, m.intruderHost, m.intruderCount)
+ }
+ b.WriteString(titleStyle.Render(title))
+ b.WriteString("\n")
+
+ label := func(focus intruderFocus, text string) string {
+ if m.intruderFocus == focus {
+ return tabActive.Render(text)
+ }
+ return tabInactive.Render(text)
+ }
+ b.WriteString(label(focusTemplate, "Template (§mark§ positions)"))
+ b.WriteString(label(focusPayloads, "Payloads"))
+ b.WriteString(label(focusResults, fmt.Sprintf("Results (%d)", len(m.intruderRows))))
+ b.WriteString("\n")
+
+ b.WriteString(m.intruderTemplate.View())
+ b.WriteString("\n")
+ b.WriteString(m.intruderPayloads.View())
+ b.WriteString("\n")
+ b.WriteString(m.intruderResults.View())
+ b.WriteString("\n")
+
+ if m.statusMsg != "" {
+ b.WriteString(statusStyle.Render(m.statusMsg))
+ b.WriteString("\n")
+ }
+ b.WriteString(helpStyle.Render("tab switch pane · ctrl+p insert § · ctrl+r start · enter (results) view · esc back/stop · ctrl+c quit"))
+ return b.String()
+}
+
+func intrudeRowsFor(rs []ipc.IntrudeResultMsg) []table.Row {
+ rows := make([]table.Row, len(rs))
+ for i, r := range rs {
+ status := fmt.Sprintf("%d", r.StatusCode)
+ if r.StatusCode == 0 {
+ status = "ERR"
+ }
+ rows[i] = table.Row{
+ fmt.Sprintf("%d", r.Position),
+ r.Payload,
+ status,
+ humanBytes(r.RespSize),
+ r.Duration.Round(time.Millisecond).String(),
+ r.Error,
+ }
+ }
+ return rows
+}
+
func exactSuffix(exact bool) string {
if exact {
return ", exact"