package scanner import ( "bytes" "context" "crypto/tls" "fmt" "io" "net/http" "time" "github.com/r3g1tpr0cs/burterm/internal/engine" ) // maxBodyCapture — сколько байт тела ответа читаем для анализа сигнатур. // Больше, чем в Intruder (4КБ), потому что сигнатуры LFI/XXE/SQLi иногда // прячутся дальше в длинных страницах с логами/трассировками ошибок. const maxBodyCapture = 16 * 1024 // Config — параметры Scanner-клиента. type Config struct { Timeout time.Duration InsecureSkipVerify bool } // Scanner выполняет активные проверки над одной точкой вставки запроса. type Scanner struct { client *http.Client } // New создаёт Scanner с настроенным http.Client. func New(cfg Config) *Scanner { return &Scanner{ client: &http.Client{ Timeout: cfg.Timeout, Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify}}, }, } } // Run запускает выбранные проверки (по именам из checkNames, должны // совпадать с AllCheckNames) против одной точки вставки pointIndex. // base задаёт значения всех точек для baseline-запроса и для точек, // не участвующих в сканировании (та же идея, что и sniper-режим // в Intruder). Возвращает канал событий (прогресс + находки), // закрывающийся по завершении всех проверок или по отмене ctx. func (s *Scanner) Run(ctx context.Context, clean string, points []engine.InsertionPoint, pointIndex int, base []string, checkNames []string) (<-chan Event, error) { if pointIndex < 0 || pointIndex >= len(points) { return nil, fmt.Errorf("некорректный индекс точки вставки: %d (всего точек: %d)", pointIndex, len(points)) } if len(base) != len(points) { return nil, fmt.Errorf("base должен быть длины %d, получено %d", len(points), len(base)) } selected := make([]Check, 0, len(checkNames)) for _, c := range activeChecks() { if containsName(checkNames, c.Name()) { selected = append(selected, c) } } if len(selected) == 0 { return nil, fmt.Errorf("не выбрано ни одной проверки") } // +1 за baseline-запрос — он тоже реальный HTTP-запрос и тоже // должен учитываться в прогрессе, иначе N/Total разъедется на 1. total := 1 for _, check := range selected { total += len(check.Probes()) } out := make(chan Event) go func() { defer close(out) done := 0 emit := func(e Event) bool { select { case out <- e: return true case <-ctx.Done(): return false } } baseline := s.probe(ctx, clean, points, pointIndex, base, base[pointIndex]) done++ if !emit(Event{Kind: EventProgress, Done: done, Total: total}) { return } for _, check := range selected { for _, p := range check.Probes() { select { case <-ctx.Done(): return default: } resp := s.probe(ctx, clean, points, pointIndex, base, p) done++ if !emit(Event{Kind: EventProgress, Done: done, Total: total}) { return } if finding := check.Analyze(p, resp, baseline); finding != nil { if !emit(Event{Kind: EventFinding, Finding: *finding}) { return } } } } }() return out, nil } // probe собирает запрос с value в позиции pointIndex (остальные точки — // из base), отправляет и возвращает ProbeResult. Ошибки сборки/сети не // прерывают сканирование целиком — просто эта конкретная проверка не // сработает на этом payload'е (Err передаётся дальше, каждый Check сам // решает, игнорировать такой ProbeResult или нет — на практике все // текущие проверки его игнорируют). func (s *Scanner) probe(ctx context.Context, clean string, points []engine.InsertionPoint, pointIndex int, base []string, value string) ProbeResult { payloads := append([]string(nil), base...) payloads[pointIndex] = value start := time.Now() req, err := engine.BuildRequest(clean, points, payloads) if err != nil { return ProbeResult{Err: err} } req = req.WithContext(ctx) resp, err := s.client.Do(req) if err != nil { return ProbeResult{Err: err, Duration: time.Since(start)} } defer resp.Body.Close() body, _ := readBodyCapped(resp.Body, maxBodyCapture) return ProbeResult{ StatusCode: resp.StatusCode, Body: string(body), Headers: resp.Header, Duration: time.Since(start), } } // readBodyCapped — тот же принцип, что и одноимённая функция в engine // (см. её комментарий про keep-alive), продублирован здесь, а не // экспортирован из engine, чтобы scanner не тянул за собой лишнюю // связанность с внутренним устройством Intruder-движка ради одной // небольшой функции. func readBodyCapped(r io.Reader, cap int) ([]byte, error) { buf := make([]byte, 32*1024) var b bytes.Buffer for { n, rerr := r.Read(buf) if n > 0 && b.Len() < cap { remaining := cap - b.Len() if n < remaining { remaining = n } b.Write(buf[:remaining]) } if rerr == io.EOF { break } if rerr != nil { return b.Bytes(), rerr } } return b.Bytes(), nil } func containsName(names []string, name string) bool { for _, n := range names { if n == name { return true } } return false }