burterm/internal/engine/request.go
2026-09-14 10:55:07 +03:00

155 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package engine
import (
"bufio"
"fmt"
"net/http"
"regexp"
"strings"
)
// InsertionPoint описывает одну точку вставки payload'а,
// найденную в шаблоне запроса между маркерами §...§.
type InsertionPoint struct {
Start int // индекс начала в исходной строке (после удаления маркеров)
End int
Name string // человекочитаемое имя для UI, напр. "param:id"
}
// RequestTemplate — сырой HTTP-запрос с маркерами позиций вставки.
type RequestTemplate struct {
Raw string // текст запроса с §маркерами§
Method string
URL string
}
// ParseMarkers вычленяет §...§ из шаблона и возвращает
// чистую строку + список точек вставки. Start/End в InsertionPoint —
// это байтовые смещения в очищенной строке (то, чего ожидает срезовая
// арифметика в BuildRequest), но сканирование ведётся по рунам через
// range — иначе многобайтовый § (U+00A7 кодируется в UTF-8 двумя байтами,
// 0xC2 0xA7) детектится только по второму байту, а первый молча утекает
// в вывод как мусор. По той же причине пишем через b.WriteRune, а не
// b.WriteByte — это же касается и любого другого не-ASCII текста в
// запросе (например, кириллицы в значениях полей).
func ParseMarkers(raw string) (clean string, points []InsertionPoint, err error) {
var b strings.Builder
inMarker := false
start := -1
for _, r := range raw {
if r == '§' {
if !inMarker {
inMarker = true
start = b.Len()
} else {
inMarker = false
points = append(points, InsertionPoint{
Start: start,
End: b.Len(),
Name: fmt.Sprintf("pos%d", len(points)+1),
})
}
continue
}
b.WriteRune(r)
}
if inMarker {
return "", nil, fmt.Errorf("незакрытый маркер § в позиции %d", start)
}
return b.String(), points, nil
}
// InsertBeforeMarker вставляет insertion непосредственно перед index-м по
// счёту §маркером§ в сыром (ещё не прогнанном через ParseMarkers) тексте
// запроса — используется рекурсией в Intruder: нашли директорию по слову
// из словаря, нужно дофаззить уже внутри неё, то есть перед точкой
// вставки должен появиться "найденное_слово/". Возвращает новый сырой
// текст, всё ещё содержащий §маркеры§ — готовый для повторного
// ParseMarkers+BuildRequest, ровно как исходный. Использует тот же
// принцип счёта по рунам, что и DetectParamName (см. её комментарий
// про многобайтовый § и связанный баг, который был здесь исправлен).
func InsertBeforeMarker(raw string, index int, insertion string) (string, error) {
count := -1
for i, r := range raw {
if r != '§' {
continue
}
count++
if count != index*2 {
continue
}
return raw[:i] + insertion + raw[i:], nil
}
return "", fmt.Errorf("маркер с индексом %d не найден", index)
}
// DetectParamName — эвристика для "умной" генерации payload'ов: пытается
// определить имя параметра, стоящего непосредственно перед index-м по
// счёту §маркером§ в сыром (ещё не прогнанном через ParseMarkers) тексте
// запроса. index отсчитывается от 0 в порядке появления маркеров.
// Понимает и query/form-параметры (name=§...§), и JSON-поля
// ("name": "§...§" или "name": §...§). Возвращает пустую строку, если
// определить не удалось — это подсказка, а не гарантия.
func DetectParamName(raw string, index int) string {
count := -1
for i, r := range raw {
if r != '§' {
continue
}
count++
if count != index*2 {
continue
}
if m := paramNamePattern.FindStringSubmatch(raw[:i]); m != nil {
return m[1]
}
return ""
}
return ""
}
// paramNamePattern ищет идентификатор, за которым сразу следует "=" или
// ":" (с возможным пробелом/открывающей кавычкой) непосредственно перед
// концом проверяемого префикса — то есть прямо перед местом, где стоял §.
var paramNamePattern = regexp.MustCompile(`([A-Za-z0-9_\-\.]+)\s*[:=]\s*"?$`)
// BuildRequest подставляет payloads в точки вставки (в обратном порядке,
// чтобы не сбивать индексы) и парсит результат в *http.Request.
func BuildRequest(clean string, points []InsertionPoint, payloads []string) (*http.Request, error) {
if len(payloads) != len(points) {
return nil, fmt.Errorf("ожидалось %d payload'ов, получено %d", len(points), len(payloads))
}
result := clean
for i := len(points) - 1; i >= 0; i-- {
p := points[i]
result = result[:p.Start] + payloads[i] + result[p.End:]
}
return parseRawHTTP(result)
}
// parseRawHTTP разбирает сырой HTTP-запрос (request line + headers + body)
// через стандартный http.ReadRequest. Тело запроса становится ReadCloser,
// пригодным для http.Client.Do.
func parseRawHTTP(raw string) (*http.Request, error) {
reader := bufio.NewReader(strings.NewReader(raw))
req, err := http.ReadRequest(reader)
if err != nil {
return nil, fmt.Errorf("не удалось распарсить сырой HTTP-запрос: %w", err)
}
// http.ReadRequest возвращает запрос с относительным URL и без схемы/хоста —
// достраиваем абсолютный URL из заголовка Host, чтобы http.Client мог
// выполнить запрос напрямую.
if req.URL.Scheme == "" {
req.URL.Scheme = "https"
}
if req.URL.Host == "" {
req.URL.Host = req.Host
}
req.RequestURI = "" // обязательное требование http.Client.Do
return req, nil
}