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 }