1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
// Intruder-equivalent: mark positions in a raw request template with §
// (Burp's own marker character, so anyone who's used Burp already knows
// the syntax), and Sniper-attack them - one position fuzzed at a time
// through a shared payload set, every other marked position holding its
// base value. Battering ram / pitchfork / cluster bomb are not
// implemented; Sniper covers the large majority of real Intruder usage
// and this whole feature is explicitly optional in the build order.
package proxy
import (
"bytes"
"context"
"fmt"
"mitmux/internal/store"
)
const marker = "§"
// maxIntrudeRequests caps positions × payloads for one attack - a safety
// limit against an accidental huge wordlist times several positions
// turning into an unbounded flood, not a tuned production value.
const maxIntrudeRequests = 1000
// IntrudePosition is one marked, resolved insertion point.
type IntrudePosition struct {
Index int // 0-based, in order of appearance
Base string // the text between its markers
}
// ParseMarkers finds every §base§ pair in template and returns the
// resolved positions plus template with the markers stripped out (the
// form actually used as the base request when no position is being
// fuzzed). An odd number of § markers is a user error - unterminated
// marker - reported rather than guessed at.
func ParseMarkers(template []byte) (positions []IntrudePosition, stripped []byte, err error) {
parts := bytes.Split(template, []byte(marker))
if len(parts)%2 != 1 {
return nil, nil, fmt.Errorf("unterminated %s marker - markers must come in pairs", marker)
}
if len(parts) == 1 {
return nil, template, nil
}
var buf bytes.Buffer
for i, part := range parts {
if i%2 == 1 {
positions = append(positions, IntrudePosition{Index: len(positions), Base: string(part)})
}
buf.Write(part)
}
return positions, buf.Bytes(), nil
}
// buildRequest re-inserts each position's base value into stripped
// (computed relative to the ORIGINAL template's marker layout, so this
// re-derives offsets rather than operating on the already-stripped
// bytes) except for `active`, which gets payload instead.
func buildRequest(template []byte, active int, payload string) ([]byte, error) {
parts := bytes.Split(template, []byte(marker))
if len(parts)%2 != 1 {
return nil, fmt.Errorf("unterminated %s marker", marker)
}
var buf bytes.Buffer
pos := 0
for i, part := range parts {
if i%2 == 1 {
if pos == active {
buf.WriteString(payload)
} else {
buf.Write(part)
}
pos++
continue
}
buf.Write(part)
}
return buf.Bytes(), nil
}
// Intrude runs a Sniper attack: template must contain at least one
// §marked§ position. For each position, in order, every payload is sent
// with that position replaced by the payload and all others at their
// base value; onResult is called synchronously after each request
// completes - with the position index, the payload used, the resulting
// entry (nil if sendErr is set), and any send error - so a caller can
// stream progress, and stops the attack early if it returns false.
func (s *Server) Intrude(ctx context.Context, scheme, host string, template []byte, payloads []string,
onResult func(position int, payload string, entry *store.Entry, sendErr error) bool) error {
positions, _, err := ParseMarkers(template)
if err != nil {
return err
}
if len(positions) == 0 {
return fmt.Errorf("no %s-marked positions in the request template", marker)
}
if len(payloads) == 0 {
return fmt.Errorf("no payloads")
}
if total := len(positions) * len(payloads); total > maxIntrudeRequests {
return fmt.Errorf("attack would send %d requests (%d positions × %d payloads), over the %d limit",
total, len(positions), len(payloads), maxIntrudeRequests)
}
for _, pos := range positions {
for _, payload := range payloads {
raw, err := buildRequest(template, pos.Index, payload)
if err != nil {
return err
}
raw = fixContentLength(raw)
// sendRaw is already self-bounding (dialForRepeat's own dial
// timeout, then conn.SetDeadline for the rest), so ctx here
// only needs to carry cancellation - e.g. the IPC connection
// driving this attack closing mid-run.
e, sendErr := s.sendRaw(ctx, scheme, host, raw, "intruder")
if !onResult(pos.Index, payload, e, sendErr) {
return nil
}
}
}
return nil
}
// fixContentLength recalculates an existing Content-Length header to
// match raw's actual body length after marker substitution. A fuzzed
// payload routinely differs in length from the base value it replaces;
// left as-is, a Content-Length carried over unchanged from the original
// captured request makes the target server wait for bytes that will
// never arrive, hanging that request for the full upstream timeout -
// confirmed: identical attacks against a URL-only marker (no body
// length change) completed in single-digit milliseconds per payload,
// the same attack with the marker inside a body parameter took 60s per
// payload. This is Intruder-specific, not something Repeater does to
// what's typed: a fuzzed value's length is a side effect of automated
// substitution, whereas a Repeater edit is deliberate and Repeater's own
// "no auto-fixed Content-Length" behavior is unchanged.
//
// Only touches a request with exactly one Content-Length header and a
// clean header/body boundary - zero found means nothing to fix, more
// than one is a request smuggling test's own deliberately ambiguous
// framing, and guessing which one to rewrite there would be worse than
// leaving both alone.
func fixContentLength(raw []byte) []byte {
sep := []byte("\r\n\r\n")
idx := bytes.Index(raw, sep)
if idx < 0 {
return raw
}
headerBlock, body := raw[:idx], raw[idx+len(sep):]
lines := bytes.Split(headerBlock, []byte("\r\n"))
foundIdx, count := -1, 0
for i, line := range lines {
if i == 0 {
continue // request line, not a header
}
colon := bytes.IndexByte(line, ':')
if colon < 0 {
continue
}
if bytes.EqualFold(bytes.TrimSpace(line[:colon]), []byte("Content-Length")) {
count++
foundIdx = i
}
}
if count != 1 {
return raw
}
lines[foundIdx] = []byte(fmt.Sprintf("Content-Length: %d", len(body)))
var out bytes.Buffer
out.Write(bytes.Join(lines, []byte("\r\n")))
out.Write(sep)
out.Write(body)
return out.Bytes()
}
|