273 lines
11 KiB
Go
273 lines
11 KiB
Go
package cli
|
||
|
||
import (
|
||
"bufio"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/r3g1tpr0cs/burterm/internal/engine"
|
||
"github.com/r3g1tpr0cs/burterm/internal/payload"
|
||
)
|
||
|
||
// stringList — flag.Value, копящий повторяющиеся флаги в срез
|
||
// в порядке их указания в командной строке.
|
||
type stringList []string
|
||
|
||
func (s *stringList) String() string { return strings.Join(*s, ",") }
|
||
func (s *stringList) Set(v string) error { *s = append(*s, v); return nil }
|
||
|
||
// Flags — разобранные аргументы командной строки для одного запуска атаки.
|
||
type Flags struct {
|
||
RequestFile string
|
||
Mode string // "sniper" | "battering" | "clusterbomb"
|
||
Payloads stringList // спецификации вида "wordlist:<путь>" или "range:<from>:<to>:<step>", по одной на точку вставки
|
||
Base stringList // базовые значения для точек, не участвующих в переборе (режим sniper)
|
||
Concurrency int
|
||
RateLimit time.Duration
|
||
Timeout time.Duration
|
||
Insecure bool
|
||
Scheme string // "" (авто, дефолт https) | "http" | "https"
|
||
Project string // путь к файлу проекта (SQLite); пусто — не сохранять результаты
|
||
}
|
||
|
||
// newFlagSet регистрирует все флаги CLI-режима один раз — используется
|
||
// и Parse (реальный разбор), и Usage (печать справки для -h/--help),
|
||
// чтобы список флагов с их описаниями не дублировался в двух местах.
|
||
func newFlagSet() (*flag.FlagSet, *Flags) {
|
||
fs := flag.NewFlagSet("burterm", flag.ContinueOnError)
|
||
f := &Flags{}
|
||
|
||
fs.StringVar(&f.RequestFile, "request", "", "путь к файлу с сырым HTTP-запросом и §маркерами§ точек вставки (обязательно)")
|
||
fs.StringVar(&f.Mode, "mode", "sniper", "режим атаки: sniper | battering | pitchfork | clusterbomb")
|
||
fs.Var(&f.Payloads, "payload", "источник payload'ов; повторяй флаг в порядке точек вставки: wordlist:<путь> | range:<from>:<to>:<step> | preset:<имя> | brute:<алфавит>:<min>:<max> — к любому можно дописать |ext:.php,.html для перебора расширений")
|
||
fs.Var(&f.Base, "base", "базовое значение для точки вставки в режиме sniper, по одному флагу на точку")
|
||
fs.IntVar(&f.Concurrency, "concurrency", 10, "число параллельных воркеров")
|
||
fs.DurationVar(&f.RateLimit, "rate-limit", 0, "пауза между запросами на воркер, напр. 100ms")
|
||
fs.DurationVar(&f.Timeout, "timeout", 10*time.Second, "таймаут одного HTTP-запроса")
|
||
fs.BoolVar(&f.Insecure, "insecure", false, "не проверять TLS-сертификат сервера (self-signed стенды)")
|
||
fs.StringVar(&f.Scheme, "scheme", "", "принудительная схема: http | https (по умолчанию — как определит парсер запроса, обычно https)")
|
||
fs.StringVar(&f.Project, "project", "", "путь к файлу проекта (SQLite) — если указан, результаты атаки сохраняются туда же, что видит TUI на вкладке Project")
|
||
|
||
return fs, f
|
||
}
|
||
|
||
// Parse разбирает os.Args (без имени программы) в Flags и валидирует
|
||
// обязательные поля.
|
||
func Parse(args []string) (*Flags, error) {
|
||
fs, f := newFlagSet()
|
||
|
||
if err := fs.Parse(args); err != nil {
|
||
return nil, err
|
||
}
|
||
if f.RequestFile == "" {
|
||
return nil, fmt.Errorf("нужно указать -request <файл>")
|
||
}
|
||
if len(f.Payloads) == 0 {
|
||
return nil, fmt.Errorf("нужен хотя бы один -payload")
|
||
}
|
||
if f.Scheme != "" && f.Scheme != "http" && f.Scheme != "https" {
|
||
return nil, fmt.Errorf("-scheme должен быть http, https или пустым, получено %q", f.Scheme)
|
||
}
|
||
return f, nil
|
||
}
|
||
|
||
// Usage печатает список флагов headless CLI-режима в w — используется
|
||
// main.go для -h/--help, чтобы не дублировать регистрацию флагов.
|
||
func Usage(w io.Writer) {
|
||
fs, _ := newFlagSet()
|
||
fs.SetOutput(w)
|
||
fs.Usage()
|
||
}
|
||
|
||
// AttackMode транслирует строковое имя режима в engine.AttackMode.
|
||
func (f *Flags) AttackMode() (engine.AttackMode, error) {
|
||
switch f.Mode {
|
||
case "sniper":
|
||
return engine.ModeSniper, nil
|
||
case "battering":
|
||
return engine.ModeBattering, nil
|
||
case "pitchfork":
|
||
return engine.ModePitchfork, nil
|
||
case "clusterbomb":
|
||
return engine.ModeClusterBomb, nil
|
||
default:
|
||
return 0, fmt.Errorf("неизвестный режим %q (ожидался sniper, battering, pitchfork или clusterbomb)", f.Mode)
|
||
}
|
||
}
|
||
|
||
// BuildGenerators превращает спецификации --payload в конкретные
|
||
// payload.Generator, в том же порядке, в каком флаги были указаны
|
||
// (этот порядок соответствует порядку §маркеров§ в запросе).
|
||
func (f *Flags) BuildGenerators() ([]payload.Generator, error) {
|
||
gens := make([]payload.Generator, 0, len(f.Payloads))
|
||
for _, spec := range f.Payloads {
|
||
g, err := parsePayloadSpec(spec)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("payload %q: %w", spec, err)
|
||
}
|
||
gens = append(gens, g)
|
||
}
|
||
return gens, nil
|
||
}
|
||
|
||
// ParsePayloadSpec — экспортированная версия parsePayloadSpec. Используется
|
||
// не только CLI-флагами, но и TUI, чтобы оба интерфейса разбирали строки
|
||
// вида "wordlist:<путь>" / "range:<from>:<to>:<step>" одинаково.
|
||
func ParsePayloadSpec(spec string) (payload.Generator, error) {
|
||
return parsePayloadSpec(spec)
|
||
}
|
||
|
||
// parsePayloadSpec сначала снимает необязательный суффикс "|ext:..."
|
||
// (аналог -e у ffuf — список расширений, дописываемых к каждому слову),
|
||
// затем разбирает оставшуюся часть как обычную спецификацию через
|
||
// parseBaseSpec и, если расширения были заданы, оборачивает результат
|
||
// в payload.ExtensionGenerator.
|
||
func parsePayloadSpec(spec string) (payload.Generator, error) {
|
||
base, extensions, err := splitExtSuffix(spec)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
gen, err := parseBaseSpec(base)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(extensions) > 0 {
|
||
gen = payload.NewExtensionGenerator(gen, extensions)
|
||
}
|
||
return gen, nil
|
||
}
|
||
|
||
// splitExtSuffix отделяет "|ext:.php,.html,bak" от конца спецификации —
|
||
// суффикс может стоять у любого типа spec (wordlist/preset/range/brute).
|
||
// Ведущая точка у расширения необязательна во вводе ("bak" эквивалентно
|
||
// ".bak") — добавляется автоматически, чтобы не спотыкаться на забытой точке.
|
||
func splitExtSuffix(spec string) (base string, extensions []string, err error) {
|
||
idx := strings.LastIndex(spec, "|ext:")
|
||
if idx == -1 {
|
||
return spec, nil, nil
|
||
}
|
||
base = spec[:idx]
|
||
extPart := spec[idx+len("|ext:"):]
|
||
if extPart == "" {
|
||
return "", nil, fmt.Errorf("|ext: указан, но список расширений пуст")
|
||
}
|
||
for _, e := range strings.Split(extPart, ",") {
|
||
e = strings.TrimSpace(e)
|
||
if e == "" {
|
||
continue
|
||
}
|
||
if !strings.HasPrefix(e, ".") {
|
||
e = "." + e
|
||
}
|
||
extensions = append(extensions, e)
|
||
}
|
||
return base, extensions, nil
|
||
}
|
||
|
||
// parseBaseSpec разбирает одну спецификацию вида "тип:параметры" —
|
||
// без учёта возможного "|ext:" суффикса, который снимается выше.
|
||
func parseBaseSpec(spec string) (payload.Generator, error) {
|
||
parts := strings.SplitN(spec, ":", 2)
|
||
if len(parts) != 2 {
|
||
return nil, fmt.Errorf("ожидался формат тип:параметры, напр. wordlist:/путь/к/файлу")
|
||
}
|
||
kind, arg := parts[0], parts[1]
|
||
|
||
switch kind {
|
||
case "wordlist":
|
||
words, err := readLines(arg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(words) == 0 {
|
||
return nil, fmt.Errorf("файл словаря %q пуст", arg)
|
||
}
|
||
return payload.NewWordlistGenerator(words), nil
|
||
|
||
case "range":
|
||
nums := strings.Split(arg, ":")
|
||
if len(nums) != 3 {
|
||
return nil, fmt.Errorf("ожидался формат range:from:to:step")
|
||
}
|
||
from, err := strconv.Atoi(nums[0])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("некорректный from: %w", err)
|
||
}
|
||
to, err := strconv.Atoi(nums[1])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("некорректный to: %w", err)
|
||
}
|
||
step, err := strconv.Atoi(nums[2])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("некорректный step: %w", err)
|
||
}
|
||
if step <= 0 {
|
||
return nil, fmt.Errorf("step должен быть положительным, получено %d", step)
|
||
}
|
||
return payload.NewNumericRangeGenerator(from, to, step), nil
|
||
|
||
case "preset":
|
||
g, err := payload.NewFromPreset(arg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return g, nil
|
||
|
||
case "brute":
|
||
// Формат: brute:<алфавит>:<min>:<max> — прямой перебор без
|
||
// готовой заготовки, для случаев, когда ни одна из встроенных
|
||
// не подходит (свой алфавит, другая длина и т.п.).
|
||
parts := strings.SplitN(arg, ":", 3)
|
||
if len(parts) != 3 {
|
||
return nil, fmt.Errorf("ожидался формат brute:<алфавит>:<min>:<max>")
|
||
}
|
||
charset := parts[0]
|
||
if charset == "" {
|
||
return nil, fmt.Errorf("алфавит для brute не может быть пустым")
|
||
}
|
||
minLen, err := strconv.Atoi(parts[1])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("некорректный min: %w", err)
|
||
}
|
||
maxLen, err := strconv.Atoi(parts[2])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("некорректный max: %w", err)
|
||
}
|
||
if minLen < 1 || maxLen < minLen {
|
||
return nil, fmt.Errorf("ожидалось 1 <= min <= max, получено min=%d max=%d", minLen, maxLen)
|
||
}
|
||
return payload.NewBruteForceGenerator(charset, minLen, maxLen), nil
|
||
|
||
default:
|
||
return nil, fmt.Errorf("неизвестный тип payload'а %q (ожидался wordlist, range, preset или brute)", kind)
|
||
}
|
||
}
|
||
|
||
// readLines читает файл построчно, пропуская пустые строки
|
||
// и обрезая \r на случай CRLF-словарей.
|
||
func readLines(path string) ([]string, error) {
|
||
file, err := os.Open(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("не удалось открыть файл со словарём: %w", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
var lines []string
|
||
scanner := bufio.NewScanner(file)
|
||
for scanner.Scan() {
|
||
line := strings.TrimRight(scanner.Text(), "\r")
|
||
if line == "" {
|
||
continue
|
||
}
|
||
lines = append(lines, line)
|
||
}
|
||
if err := scanner.Err(); err != nil {
|
||
return nil, fmt.Errorf("ошибка чтения словаря: %w", err)
|
||
}
|
||
return lines, nil
|
||
}
|