155 lines
6.7 KiB
Go
155 lines
6.7 KiB
Go
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
|
||
}
|