255 lines
5.8 KiB
Go
255 lines
5.8 KiB
Go
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
|
||
}
|