package ffuf
import (
"fmt"
"log"
"math/rand"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
//Job ties together Config, Runner, Input and Output
type Job struct {
Config *Config
ErrorMutex sync.Mutex
Input InputProvider
Runner RunnerProvider
ReplayRunner RunnerProvider
Output OutputProvider
Counter int
ErrorCounter int
SpuriousErrorCounter int
Total int
Running bool
Count403 int
Count429 int
Error string
startTime time.Time
queuejobs []QueueJob
queuepos int
currentDepth int
}
type QueueJob struct {
Url string
depth int
}
func NewJob(conf *Config) Job {
var j Job
j.Counter = 0
j.ErrorCounter = 0
j.SpuriousErrorCounter = 0
j.Running = false
j.queuepos = 0
j.queuejobs = make([]QueueJob, 0)
j.currentDepth = 0
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++
}
// inc429 increments the 429 response counter
func (j *Job) inc429() {
j.ErrorMutex.Lock()
defer j.ErrorMutex.Unlock()
j.Count429++
}
//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() {
// Add the default job to job queue
j.queuejobs = append(j.queuejobs, QueueJob{Url: j.Config.Url, depth: 0})
rand.Seed(time.Now().UnixNano())
j.Total = j.Input.Total()
defer j.Stop()
j.Running = true
//Show banner if not running in silent mode
if !j.Config.Quiet {
j.Output.Banner()
}
// Monitor for SIGTERM and do cleanup properly (writing the output files etc)
j.interruptMonitor()
for j.jobsInQueue() {
j.prepareQueueJob()
if j.queuepos > 1 {
// Print info for queued recursive jobs
j.Output.Info(fmt.Sprintf("Scanning: %s", j.Config.Url))
}
j.Input.Reset()
j.startTime = time.Now()
j.Counter = 0
j.startExecution()
}
j.Output.Finalize()
}
func (j *Job) jobsInQueue() bool {
if j.queuepos < len(j.queuejobs) {
return true
}
return false
}
func (j *Job) prepareQueueJob() {
j.Config.Url = j.queuejobs[j.queuepos].Url
j.currentDepth = j.queuejobs[j.queuepos].depth
j.queuepos += 1
}
func (j *Job) startExecution() {
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()
nextPosition := j.Input.Position()
wg.Add(1)
j.Counter++
go func() {
defer func() { <-limiter }()
defer wg.Done()
j.runTask(nextInput, nextPosition, 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()
return
}
func (j *Job) interruptMonitor() {
sigChan := make(chan os.Signal, 2)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
for _ = range sigChan {
j.Error = "Caught keyboard interrupt (Ctrl-C)\n"
j.Stop()
}
}()
}
func (j *Job) runProgress(wg *sync.WaitGroup) {
defer wg.Done()
totalProgress := j.Input.Total()
for j.Counter <= totalProgress {
if !j.Running {
break
}
j.updateProgress()
if j.Counter == totalProgress {
return
}
time.Sleep(time.Millisecond * time.Duration(j.Config.ProgressFrequency))
}
}
func (j *Job) updateProgress() {
prog := Progress{
StartedAt: j.startTime,
ReqCount: j.Counter,
ReqTotal: j.Input.Total(),
QueuePos: j.queuepos,
QueueTotal: len(j.queuejobs),
ErrorCount: j.ErrorCounter,
}
j.Output.Progress(prog)
}
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 map[string][]byte, position int, retried bool) {
req, err := j.Runner.Prepare(input)
req.Position = position
if err != nil {
j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
j.incError()
log.Printf("%s", err)
return
}
resp, err := j.Runner.Execute(&req)
if err != nil {
if retried {
j.incError()
log.Printf("%s", err)
} else {
j.runTask(input, position, true)
}
return
}
if j.SpuriousErrorCounter > 0 {
j.resetSpuriousErrors()
}
if j.Config.StopOn403 || j.Config.StopOnAll {
// Increment Forbidden counter if we encountered one
if resp.StatusCode == 403 {
j.inc403()
}
}
if j.Config.StopOnAll {
// increment 429 counter if the response code is 429
if j.Config.StopOnAll {
if resp.StatusCode == 429 {
j.inc429()
}
}
}
if j.isMatch(resp) {
// Re-send request through replay-proxy if needed
if j.ReplayRunner != nil {
replayreq, err := j.ReplayRunner.Prepare(input)
replayreq.Position = position
if err != nil {
j.Output.Error(fmt.Sprintf("Encountered an error while preparing replayproxy request: %s\n", err))
j.incError()
log.Printf("%s", err)
} else {
_, _ = j.ReplayRunner.Execute(&replayreq)
}
}
j.Output.Result(resp)
// Refresh the progress indicator as we printed something out
j.updateProgress()
}
if j.Config.Recursion && len(resp.GetRedirectLocation(false)) > 0 {
j.handleRecursionJob(resp)
}
return
}
//handleRecursionJob adds a new recursion job to the job queue if a new directory is found
func (j *Job) handleRecursionJob(resp Response) {
if (resp.Request.Url + "/") != resp.GetRedirectLocation(true) {
// Not a directory, return early
return
}
if j.Config.RecursionDepth == 0 || j.currentDepth < j.Config.RecursionDepth {
// We have yet to reach the maximum recursion depth
recUrl := resp.Request.Url + "/" + "FUZZ"
newJob := QueueJob{Url: recUrl, depth: j.currentDepth + 1}
j.queuejobs = append(j.queuejobs, newJob)
j.Output.Info(fmt.Sprintf("Adding a new job to the queue: %s", recUrl))
} else {
j.Output.Warning(fmt.Sprintf("Directory found, but recursion depth exceeded. Ignoring: %s", resp.GetRedirectLocation(true)))
}
}
//CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests
func (j *Job) CalibrateResponses() ([]Response, error) {
cInputs := make([]string, 0)
if len(j.Config.AutoCalibrationStrings) < 1 {
cInputs = append(cInputs, "admin"+RandomString(16)+"/")
cInputs = append(cInputs, ".htaccess"+RandomString(16))
cInputs = append(cInputs, RandomString(16)+"/")
cInputs = append(cInputs, RandomString(16))
} else {
cInputs = append(cInputs, j.Config.AutoCalibrationStrings...)
}
results := make([]Response, 0)
for _, input := range cInputs {
inputs := make(map[string][]byte, 0)
for _, v := range j.Config.InputProviders {
inputs[v.Keyword] = []byte(input)
}
req, err := j.Runner.Prepare(inputs)
if err != nil {
j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
j.incError()
log.Printf("%s", err)
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
}
// CheckStop stops the job if stopping conditions are met
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()
}
}
if j.Config.StopOnAll && (float64(j.Count429)/float64(j.Counter) > 0.2) {
// Over 20% of responses are 429
j.Error = "Getting an unusual amount of 429 responses, exiting."
j.Stop()
}
}
// check for maximum running time
if j.Config.MaxTime > 0 {
dur := time.Now().Sub(j.startTime)
runningSecs := int(dur / time.Second)
if runningSecs >= j.Config.MaxTime {
j.Error = "Maximum running time reached, exiting."
j.Stop()
}
}
}
//Stop the execution of the Job
func (j *Job) Stop() {
j.Running = false
return
}