ffuff/pkg/ffuf/job.go

255 lines
5.8 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ffuf
import (
"fmt"
"math/rand"
"sync"
"time"
)
//Job ties together Config, Runner, Input and Output
type Job struct {
Config *Config
ErrorMutex sync.Mutex
Input InputProvider
Runner RunnerProvider
Output OutputProvider
Counter int
ErrorCounter int
SpuriousErrorCounter int
Total int
Running bool
Count403 int
Error string
startTime time.Time
}
func NewJob(conf *Config) Job {
var j Job
j.Counter = 0
j.ErrorCounter = 0
j.SpuriousErrorCounter = 0
j.Running = false
return j
}
//incError increments the error counter
func (j *Job) incError() {
j.ErrorMutex.Lock()
defer j.ErrorMutex.Unlock()
j.ErrorCounter++
j.SpuriousErrorCounter++
}
//inc403 increments the 403 response counter
func (j *Job) inc403() {
j.ErrorMutex.Lock()
defer j.ErrorMutex.Unlock()
j.Count403++
}
//resetSpuriousErrors resets the spurious error counter
func (j *Job) resetSpuriousErrors() {
j.ErrorMutex.Lock()
defer j.ErrorMutex.Unlock()
j.SpuriousErrorCounter = 0
}
//Start the execution of the Job
func (j *Job) Start() {
rand.Seed(time.Now().UnixNano())
j.Total = j.Input.Total()
defer j.Stop()
//Show banner if not running in silent mode
if !j.Config.Quiet {
j.Output.Banner()
}
j.Running = true
var wg sync.WaitGroup
wg.Add(1)
go j.runProgress(&wg)
//Limiter blocks after reaching the buffer, ensuring limited concurrency
limiter := make(chan bool, j.Config.Threads)
for j.Input.Next() {
// Check if we should stop the process
j.CheckStop()
if !j.Running {
defer j.Output.Warning(j.Error)
break
}
limiter <- true
nextInput := j.Input.Value()
wg.Add(1)
j.Counter++
go func() {
defer func() { <-limiter }()
defer wg.Done()
j.runTask([]byte(nextInput), false)
if j.Config.Delay.HasDelay {
var sleepDurationMS time.Duration
if j.Config.Delay.IsRange {
sTime := j.Config.Delay.Min + rand.Float64()*(j.Config.Delay.Max-j.Config.Delay.Min)
sleepDurationMS = time.Duration(sTime * 1000)
} else {
sleepDurationMS = time.Duration(j.Config.Delay.Min * 1000)
}
time.Sleep(sleepDurationMS * time.Millisecond)
}
}()
}
wg.Wait()
j.updateProgress()
j.Output.Finalize()
return
}
func (j *Job) runProgress(wg *sync.WaitGroup) {
defer wg.Done()
j.startTime = time.Now()
totalProgress := j.Input.Total()
for j.Counter <= totalProgress {
if !j.Running {
break
}
j.updateProgress()
if j.Counter == totalProgress {
return
}
time.Sleep(time.Millisecond * 100)
}
}
func (j *Job) updateProgress() {
runningSecs := int((time.Now().Sub(j.startTime)) / time.Second)
var reqRate int
if runningSecs > 0 {
reqRate = int(j.Counter / runningSecs)
} else {
reqRate = 0
}
dur := time.Now().Sub(j.startTime)
hours := dur / time.Hour
dur -= hours * time.Hour
mins := dur / time.Minute
dur -= mins * time.Minute
secs := dur / time.Second
progString := fmt.Sprintf(":: Progress: [%d/%d] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", j.Counter, j.Total, int(reqRate), hours, mins, secs, j.ErrorCounter)
j.Output.Progress(progString)
}
//Calibrate runs a self-calibration task for filtering options, requesting random resources and acting accordingly
func (j *Job) CalibrateResponses() ([]Response, error) {
cInputs := make([]string, 0)
cInputs = append(cInputs, "admin"+randomString(16)+"/")
cInputs = append(cInputs, ".htaccess"+randomString(16))
cInputs = append(cInputs, randomString(16)+"/")
cInputs = append(cInputs, randomString(16))
results := make([]Response, 0)
for _, input := range cInputs {
req, err := j.Runner.Prepare([]byte(input))
if err != nil {
j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
j.incError()
return results, err
}
resp, err := j.Runner.Execute(&req)
if err != nil {
return results, err
}
// Only calibrate on responses that would be matched otherwise
if j.isMatch(resp) {
results = append(results, resp)
}
}
return results, nil
}
func (j *Job) isMatch(resp Response) bool {
matched := false
for _, m := range j.Config.Matchers {
match, err := m.Filter(&resp)
if err != nil {
continue
}
if match {
matched = true
}
}
// The response was not matched, return before running filters
if !matched {
return false
}
for _, f := range j.Config.Filters {
fv, err := f.Filter(&resp)
if err != nil {
continue
}
if fv {
return false
}
}
return true
}
func (j *Job) runTask(input []byte, retried bool) {
req, err := j.Runner.Prepare(input)
if err != nil {
j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
j.incError()
return
}
resp, err := j.Runner.Execute(&req)
if err != nil {
if retried {
j.incError()
} else {
j.runTask(input, true)
}
return
}
if j.SpuriousErrorCounter > 0 {
j.resetSpuriousErrors()
}
if j.Config.StopOn403 || j.Config.StopOnAll {
// Incremnt Forbidden counter if we encountered one
if resp.StatusCode == 403 {
j.inc403()
}
}
if j.isMatch(resp) {
j.Output.Result(resp)
// Refresh the progress indicator as we printed something out
j.updateProgress()
}
return
}
func (j *Job) CheckStop() {
if j.Counter > 50 {
// We have enough samples
if j.Config.StopOn403 || j.Config.StopOnAll {
if float64(j.Count403)/float64(j.Counter) > 0.95 {
// Over 95% of requests are 403
j.Error = "Getting an unusual amount of 403 responses, exiting."
j.Stop()
}
}
if j.Config.StopOnErrors || j.Config.StopOnAll {
if j.SpuriousErrorCounter > j.Config.Threads*2 {
// Most of the requests are erroring
j.Error = "Receiving spurious errors, exiting."
j.Stop()
}
}
}
}
//Stop the execution of the Job
func (j *Job) Stop() {
j.Running = false
return
}