1
1
mirror of https://github.com/OJ/gobuster.git synced 2025-09-23 18:47:21 +02:00
gobuster/libgobuster/libgobuster.go
2025-06-30 12:44:20 +02:00

354 lines
8.8 KiB
Go

package libgobuster
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
)
// PATTERN is the pattern for wordlist replacements in pattern file
const PATTERN = "{GOBUSTER}"
// SetupFunc is the "setup" function prototype for implementations
type SetupFunc func(*Gobuster) error
// ProcessFunc is the "process" function prototype for implementations
type ProcessFunc func(*Gobuster, string) ([]Result, error)
// ResultToStringFunc is the "to string" function prototype for implementations
type ResultToStringFunc func(*Gobuster, *Result) (*string, error)
// Gobuster is the main object when creating a new run
type Gobuster struct {
Opts *Options
Logger *Logger
plugin GobusterPlugin
Progress *Progress
}
type Guess struct {
word string
discoverOnSuccess bool
}
type Wordlist struct {
scanner *bufio.Scanner
guessesPerLine int
isStream bool
}
// NewGobuster returns a new Gobuster object
func NewGobuster(opts *Options, plugin GobusterPlugin, logger *Logger) (*Gobuster, error) {
var g Gobuster
g.Opts = opts
g.plugin = plugin
g.Logger = logger
g.Progress = NewProgress()
return &g, nil
}
func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess, successChan chan<- *Guess, wg *sync.WaitGroup) {
defer wg.Done()
for {
// Prioritize stopping when the context is done
select {
case <-ctx.Done():
return
default:
}
select {
case <-ctx.Done():
return
case guess := <-guessChan:
// Mode-specific processing
res, err := g.plugin.ProcessWord(ctx, guess.word, g.Progress)
if err != nil {
// do not exit and continue
g.Progress.ErrorChan <- fmt.Errorf("error on word %s: %w", guess.word, err)
}
if res != nil {
g.Progress.ResultChan <- res
select {
case <-ctx.Done():
g.Progress.incrementRequests()
return
case successChan <- guess:
}
}
g.Progress.incrementRequests()
select {
case <-ctx.Done():
case <-time.After(g.Opts.Delay):
}
}
}
}
func feed(ctx context.Context, guessChan chan<- *Guess, words []string, discoverOnSuccess bool) {
for _, w := range words {
guess := &Guess{word: w, discoverOnSuccess: discoverOnSuccess}
// Prioritize stopping when the context is done
select {
case <-ctx.Done():
return
default:
}
select {
// need to check here too otherwise guessChan will block
case <-ctx.Done():
return
case guessChan <- guess:
}
}
}
func (g *Gobuster) feeder(ctx context.Context, guessChan chan<- *Guess, words []string, discoverOnSuccess bool, wg *sync.WaitGroup) {
defer wg.Done()
feed(ctx, guessChan, words, discoverOnSuccess)
}
func (g *Gobuster) feedWordlist(ctx context.Context, guessChan chan<- *Guess, wordlist *Wordlist, wg *sync.WaitGroup) {
defer wg.Done()
for wordlist.scanner.Scan() {
// Prioritize stopping when the context is done
select {
case <-ctx.Done():
return
default:
}
word := strings.TrimSpace(wordlist.scanner.Text())
switch {
case wordlist.isStream && len(word) != 0:
// Increment to keep track of expected work
g.Progress.IncrementTotalRequests(wordlist.guessesPerLine)
case wordlist.isStream && len(word) == 0:
// Skip empty lines without incrementing
continue
case len(word) == 0:
// Skip empty lines removing expected work
g.Progress.IncrementTotalRequests(-1 * wordlist.guessesPerLine)
continue
}
if len(g.Opts.Patterns) > 0 {
for _, w := range g.processPatterns(word) {
guess := &Guess{word: w, discoverOnSuccess: true}
select {
case <-ctx.Done():
return
case guessChan <- guess:
}
feed(ctx, guessChan, g.plugin.AdditionalWords(w), true)
}
} else {
guess := &Guess{word: word, discoverOnSuccess: true}
select {
case <-ctx.Done():
return
case guessChan <- guess:
}
feed(ctx, guessChan, g.plugin.AdditionalWords(word), true)
}
}
}
func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, error) {
// calculate expected requests
var guessesPerLine int
if len(g.Opts.Patterns) > 0 {
nPats := len(g.Opts.Patterns)
guessesPerLine = nPats + nPats*g.plugin.AdditionalWordsLen()
} else {
guessesPerLine = 1 + g.plugin.AdditionalWordsLen()
}
if g.Opts.Wordlist == "-" {
// Read directly from stdin
return &Wordlist{scanner: bufio.NewScanner(os.Stdin), guessesPerLine: guessesPerLine, isStream: true}, nil
}
lines, err := lineCounter(wordlist)
if err != nil {
return nil, fmt.Errorf("failed to get number of lines: %w", err)
}
if lines-g.Opts.WordlistOffset <= 0 {
return nil, errors.New("offset is greater than the number of lines in the wordlist")
}
g.Progress.IncrementTotalRequests(lines * guessesPerLine)
// add offset if needed (offset defaults to 0)
g.Progress.incrementRequestsIssues(g.Opts.WordlistOffset * guessesPerLine)
// rewind wordlist after lineCounter
_, err = wordlist.Seek(0, 0)
if err != nil {
return nil, fmt.Errorf("failed to rewind wordlist: %w", err)
}
wordlistScanner := bufio.NewScanner(wordlist)
// skip lines
for range g.Opts.WordlistOffset {
if !wordlistScanner.Scan() {
if err := wordlistScanner.Err(); err != nil {
return nil, fmt.Errorf("failed to skip lines in wordlist: %w", err)
}
return nil, errors.New("failed to skip lines in wordlist")
}
}
return &Wordlist{scanner: wordlistScanner, guessesPerLine: guessesPerLine, isStream: false}, nil
}
// Run the busting of the website with the given
// set of settings from the command line.
func (g *Gobuster) Run(ctx context.Context) error {
defer close(g.Progress.ResultChan)
defer close(g.Progress.ErrorChan)
defer close(g.Progress.MessageChan)
if err := g.plugin.PreRun(ctx, g.Progress); err != nil {
return err
}
workerCtx, workerCancel := context.WithCancel(ctx)
defer workerCancel()
feederCtx, feederCancel := context.WithCancel(ctx)
defer feederCancel()
var workerGroup, feederGroup sync.WaitGroup
workerGroup.Add(g.Opts.Threads)
guessChan := make(chan *Guess, g.Opts.Threads*3)
successChan := make(chan *Guess)
var f io.ReadSeekCloser
if g.Opts.Wordlist != "-" { // stdin case is handled inside getWordlist
var err error
f, err = os.Open(g.Opts.Wordlist)
if err != nil {
return fmt.Errorf("failed to open wordlist: %w", err)
}
defer f.Close()
}
wordlist, err := g.getWordlist(f)
if err != nil {
return err
}
// Create goroutines for each of the number of threads
// specified.
for range g.Opts.Threads {
go g.worker(workerCtx, guessChan, successChan, &workerGroup)
}
feederGroup.Add(1)
go g.feedWordlist(feederCtx, guessChan, wordlist, &feederGroup)
ListenForMore:
for {
// Prioritize stopping when the context is done
select {
case <-ctx.Done():
break ListenForMore
default:
}
select {
case <-ctx.Done():
break ListenForMore
case successGuess := <-successChan:
// Add more guesses based on the results of previous attempts
if successGuess.discoverOnSuccess {
discoverWords := g.plugin.AdditionalSuccessWords(successGuess.word)
if len(discoverWords) > 0 {
g.Progress.IncrementTotalRequests(len(discoverWords))
feederGroup.Add(1)
go g.feeder(feederCtx, guessChan, discoverWords, false, &feederGroup)
}
patternDiscoverWords := g.processDiscoverPatterns(successGuess.word)
if len(patternDiscoverWords) > 0 {
g.Progress.IncrementTotalRequests(len(patternDiscoverWords))
feederGroup.Add(1)
go g.feeder(feederCtx, guessChan, patternDiscoverWords, false, &feederGroup)
}
}
case <-time.After(200 * time.Millisecond):
// With requests issued only after the results are synchronously
// reported, this is well ordered without the timeout, however it would
// exert a lot of lock pressure during the run to keep doing this in a
// hot loop
if g.Progress.RequestsExpected() == g.Progress.RequestsIssued() {
// All the expected requests have completed, there is no pending or
// in-progress work. If moreWordsChan was buffered we would need to
// check it again here to ensure no pending work was added while we
// acquired the locks
break ListenForMore
}
}
}
feederCancel()
workerCancel()
feederGroup.Wait()
workerGroup.Wait()
if err := wordlist.scanner.Err(); err != nil {
return err
}
return nil
}
// GetConfigString returns the current config as a printable string
func (g *Gobuster) GetConfigString() (string, error) {
return g.plugin.GetConfigString()
}
func (g *Gobuster) processPatterns(word string) []string {
if g.Opts.PatternFile == "" {
return nil
}
return g.applyPatterns(word, g.Opts.Patterns)
}
func (g *Gobuster) processDiscoverPatterns(word string) []string {
if g.Opts.DiscoverPatternFile == "" {
return nil
}
return g.applyPatterns(word, g.Opts.DiscoverPatterns)
}
func (g *Gobuster) applyPatterns(word string, patterns []string) []string {
pat := make([]string, len(patterns))
for i, x := range patterns {
pat[i] = strings.ReplaceAll(x, PATTERN, word)
}
return pat
}