diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/mitmux/main.go | 313 |
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" |