diff options
Diffstat (limited to 'internal/proxy/intrude.go')
| -rw-r--r-- | internal/proxy/intrude.go | 233 |
1 files changed, 185 insertions, 48 deletions
diff --git a/internal/proxy/intrude.go b/internal/proxy/intrude.go index 6595c75..3538f8d 100644 --- a/internal/proxy/intrude.go +++ b/internal/proxy/intrude.go @@ -1,10 +1,8 @@ // 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. +// the syntax) and fuzz them across four attack modes - Sniper, Battering +// ram, Pitchfork, and Cluster bomb - matching Burp's own naming and +// semantics for how positions and payload sets combine. package proxy import ( @@ -17,11 +15,33 @@ import ( 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. +// maxIntrudeRequests caps the number of requests one attack can send - a +// safety limit against an accidental huge wordlist (or, for Cluster bomb, +// a payload-set product) turning into an unbounded flood, not a tuned +// production value. const maxIntrudeRequests = 1000 +// AttackMode selects how payload sets combine across marked positions, +// matching Burp's own four attack types. +type AttackMode string + +const ( + // Sniper fuzzes one position at a time through a single shared + // payload set; every other marked position holds its base value. + // Requests: positions × len(payloads). + Sniper AttackMode = "sniper" + // BatteringRam sends the same payload, from a single shared payload + // set, into every marked position at once. Requests: len(payloads). + BatteringRam AttackMode = "battering_ram" + // Pitchfork walks one payload set per position in lockstep - request + // i takes payload i from every set. Requests: the shortest set's + // length (Burp's own convention when sets are uneven). + Pitchfork AttackMode = "pitchfork" + // ClusterBomb tries every combination of one payload set per + // position. Requests: the product of every set's length. + ClusterBomb AttackMode = "cluster_bomb" +) + // IntrudePosition is one marked, resolved insertion point. type IntrudePosition struct { Index int // 0-based, in order of appearance @@ -52,11 +72,11 @@ func ParseMarkers(template []byte) (positions []IntrudePosition, stripped []byte 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) { +// buildRequestValues re-derives offsets from template's ORIGINAL marker +// layout (rather than operating on already-stripped bytes) and substitutes +// values[i] for the i-th marked position, in order. len(values) must equal +// the number of marked positions in template. +func buildRequestValues(template []byte, values []string) ([]byte, error) { parts := bytes.Split(template, []byte(marker)) if len(parts)%2 != 1 { return nil, fmt.Errorf("unterminated %s marker", marker) @@ -65,11 +85,10 @@ func buildRequest(template []byte, active int, payload string) ([]byte, error) { pos := 0 for i, part := range parts { if i%2 == 1 { - if pos == active { - buf.WriteString(payload) - } else { - buf.Write(part) + if pos >= len(values) { + return nil, fmt.Errorf("position %d has no value", pos) } + buf.WriteString(values[pos]) pos++ continue } @@ -78,48 +97,166 @@ func buildRequest(template []byte, active int, payload string) ([]byte, error) { 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 - } +// intrudeValues computes, for one full attack, every position-substitution +// set to send - one []string per request (indexed by position, in send +// order) - according to mode. Pure and side-effect free, so the request +// count can be validated against maxIntrudeRequests before anything is +// dispatched, and so it's testable without a live target. +// +// payloadSets[0] is the shared payload set for Sniper and BatteringRam, +// which only ever need one. Pitchfork and ClusterBomb are inherently +// per-position - theirs is the whole point of the two modes - so they +// require exactly len(positions) sets, one per marked position in order. +func intrudeValues(mode AttackMode, positions []IntrudePosition, payloadSets [][]string) ([][]string, error) { if len(positions) == 0 { - return fmt.Errorf("no %s-marked positions in the request template", marker) + return nil, fmt.Errorf("no %s-marked positions in the request template", marker) } - if len(payloads) == 0 { - return fmt.Errorf("no payloads") + if len(payloadSets) == 0 || len(payloadSets[0]) == 0 { + return nil, 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) + + bases := make([]string, len(positions)) + for i, p := range positions { + bases[i] = p.Base } - for _, pos := range positions { + var out [][]string + switch mode { + case "", Sniper: + payloads := payloadSets[0] + if total := len(positions) * len(payloads); total > maxIntrudeRequests { + return nil, fmt.Errorf("attack would send %d requests (%d positions × %d payloads), over the %d limit", + total, len(positions), len(payloads), maxIntrudeRequests) + } + for posIdx := range positions { + for _, payload := range payloads { + values := append([]string(nil), bases...) + values[posIdx] = payload + out = append(out, values) + } + } + + case BatteringRam: + payloads := payloadSets[0] + if len(payloads) > maxIntrudeRequests { + return nil, fmt.Errorf("attack would send %d requests, over the %d limit", len(payloads), maxIntrudeRequests) + } for _, payload := range payloads { - raw, err := buildRequest(template, pos.Index, payload) - if err != nil { - return err + values := make([]string, len(positions)) + for i := range values { + values[i] = payload + } + out = append(out, values) + } + + case Pitchfork: + if len(payloadSets) != len(positions) { + return nil, fmt.Errorf("pitchfork needs one payload set per position (%d positions, %d payload sets given)", + len(positions), len(payloadSets)) + } + n := len(payloadSets[0]) + for _, set := range payloadSets { + if len(set) == 0 { + return nil, fmt.Errorf("no payloads") } - raw = fixContentLength(raw) + if len(set) < n { + n = len(set) + } + } + if n > maxIntrudeRequests { + return nil, fmt.Errorf("attack would send %d requests, over the %d limit", n, maxIntrudeRequests) + } + for i := 0; i < n; i++ { + values := make([]string, len(positions)) + for p := range positions { + values[p] = payloadSets[p][i] + } + out = append(out, values) + } - // 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") + case ClusterBomb: + if len(payloadSets) != len(positions) { + return nil, fmt.Errorf("cluster bomb needs one payload set per position (%d positions, %d payload sets given)", + len(positions), len(payloadSets)) + } + // Checked incrementally, one set at a time, so a pathological + // product (e.g. three sets of 10000) bails out before ever + // trying to enumerate it, not after. + total := 1 + for _, set := range payloadSets { + if len(set) == 0 { + return nil, fmt.Errorf("no payloads") + } + total *= len(set) + if total > maxIntrudeRequests { + return nil, fmt.Errorf("attack would send at least %d requests, over the %d limit", total, maxIntrudeRequests) + } + } + idx := make([]int, len(positions)) + for { + values := make([]string, len(positions)) + for p := range positions { + values[p] = payloadSets[p][idx[p]] + } + out = append(out, values) - if !onResult(pos.Index, payload, e, sendErr) { - return nil + // Odometer increment, rightmost (last) position fastest - + // matches Burp's own cluster-bomb iteration order. + p := len(positions) - 1 + for p >= 0 { + idx[p]++ + if idx[p] < len(payloadSets[p]) { + break + } + idx[p] = 0 + p-- + } + if p < 0 { + break } } + + default: + return nil, fmt.Errorf("unknown attack mode %q", mode) + } + return out, nil +} + +// Intrude runs one attack of the given mode over a §marked§ request +// template. onResult is called synchronously after each request completes +// - with a 0-based iteration index, the values substituted into each +// marked position for that request (indexed by position, same order as +// ParseMarkers), 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, mode AttackMode, payloadSets [][]string, + onResult func(iteration int, values []string, entry *store.Entry, sendErr error) bool) error { + positions, _, err := ParseMarkers(template) + if err != nil { + return err + } + + requests, err := intrudeValues(mode, positions, payloadSets) + if err != nil { + return err + } + + for i, values := range requests { + raw, err := buildRequestValues(template, values) + 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(i, values, e, sendErr) { + return nil + } } return nil } |