Skip to content

Commit

Permalink
Recursive jobs support (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
joohoi committed Dec 31, 2019
1 parent fef5f0c commit b4adeae
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@
- New
- New CLI flag `-od` (output directory) to enable writing requests and responses for matched results to a file for postprocessing or debugging purposes.
- New CLI flag `-maxtime` to limit the running time of ffuf
- New CLI flags `-recursion` and `-recursion-depth` to control recursive ffuf jobs if directories are found. This requires the `-u` to end with FUZZ keyword.
- Changed
- Limit the use of `-e` (extensions) to a single keyword: FUZZ
- Regexp matching and filtering (-mr/-fr) allow using keywords in patterns
Expand Down
10 changes: 10 additions & 0 deletions main.go
Expand Up @@ -99,6 +99,8 @@ func main() {
flag.BoolVar(&conf.StopOnErrors, "se", false, "Stop on spurious errors")
flag.BoolVar(&conf.StopOnAll, "sa", false, "Stop on all error cases. Implies -sf and -se. Also stops on spurious 429 response codes.")
flag.BoolVar(&conf.FollowRedirects, "r", false, "Follow redirects")
flag.BoolVar(&conf.Recursion, "recursion", false, "Scan recursively. Only FUZZ keyword is supported, and URL (-u) has to end in it.")
flag.IntVar(&conf.RecursionDepth, "recursion-depth", 0, "Maximum recursion depth.")
flag.BoolVar(&conf.AutoCalibration, "ac", false, "Automatically calibrate filtering options")
flag.Var(&opts.AutoCalibrationStrings, "acc", "Custom auto-calibration string. Can be used multiple times. Implies -ac")
flag.IntVar(&conf.Threads, "t", 40, "Number of concurrent threads.")
Expand Down Expand Up @@ -371,6 +373,14 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
}
}

// Do checks for recursion mode
if conf.Recursion {
if !strings.HasSuffix(conf.Url, "FUZZ") {
errmsg := fmt.Sprintf("When using -recursion the URL (-u) must end with FUZZ keyword.")
errs.Add(fmt.Errorf(errmsg))
}
}

return errs.ErrorOrNil()
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/ffuf/config.go
Expand Up @@ -49,6 +49,8 @@ type Config struct {
CommandLine string
Verbose bool
MaxTime int
Recursion bool
RecursionDepth int
}

type InputProviderConfig struct {
Expand Down Expand Up @@ -84,5 +86,7 @@ func NewConfig(ctx context.Context) Config {
conf.DirSearchCompat = false
conf.Verbose = false
conf.MaxTime = 0
conf.Recursion = false
conf.RecursionDepth = 0
return conf
}
2 changes: 2 additions & 0 deletions pkg/ffuf/interfaces.go
Expand Up @@ -17,6 +17,7 @@ type InputProvider interface {
AddProvider(InputProviderConfig) error
Next() bool
Position() int
Reset()
Value() map[string][]byte
Total() int
}
Expand All @@ -37,6 +38,7 @@ type OutputProvider interface {
Banner() error
Finalize() error
Progress(status Progress)
Info(infostring string)
Error(errstring string)
Warning(warnstring string)
Result(resp Response)
Expand Down
69 changes: 66 additions & 3 deletions pkg/ffuf/job.go
Expand Up @@ -27,6 +27,14 @@ type Job struct {
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 {
Expand All @@ -35,6 +43,9 @@ func NewJob(conf *Config) Job {
j.ErrorCounter = 0
j.SpuriousErrorCounter = 0
j.Running = false
j.queuepos = 0
j.queuejobs = make([]QueueJob, 0)
j.currentDepth = 0
return j
}

Expand Down Expand Up @@ -69,17 +80,47 @@ func (j *Job) resetSpuriousErrors() {

//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
j.startTime = time.Now()
//Show banner if not running in silent mode
if !j.Config.Quiet {
j.Output.Banner()
}
j.Running = true
j.startTime = time.Now()
// 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.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)
Expand Down Expand Up @@ -115,7 +156,6 @@ func (j *Job) Start() {
}
wg.Wait()
j.updateProgress()
j.Output.Finalize()
return
}

Expand Down Expand Up @@ -150,6 +190,8 @@ func (j *Job) updateProgress() {
StartedAt: j.startTime,
ReqCount: j.Counter,
ReqTotal: j.Input.Total(),
QueuePos: j.queuepos,
QueueTotal: len(j.queuejobs),
ErrorCount: j.ErrorCounter,
}
j.Output.Progress(prog)
Expand Down Expand Up @@ -223,9 +265,30 @@ func (j *Job) runTask(input map[string][]byte, position int, retried bool) {
// Refresh the progress indicator as we printed something out
j.updateProgress()
}

if j.Config.Recursion && len(resp.GetRedirectLocation()) > 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() {
// 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()))
}
}

//CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests
func (j *Job) CalibrateResponses() ([]Response, error) {
cInputs := make([]string, 0)
Expand Down
2 changes: 2 additions & 0 deletions pkg/ffuf/progress.go
Expand Up @@ -8,5 +8,7 @@ type Progress struct {
StartedAt time.Time
ReqCount int
ReqTotal int
QueuePos int
QueueTotal int
ErrorCount int
}
9 changes: 9 additions & 0 deletions pkg/input/input.go
Expand Up @@ -67,6 +67,15 @@ func (i *MainInputProvider) Value() map[string][]byte {
return retval
}

//Reset resets all the inputproviders and counters
func (i *MainInputProvider) Reset() {
for _, p := range i.Providers {
p.ResetPosition()
}
i.position = 0
i.msbIterator = 0
}

//pitchforkValue returns a map of keyword:value pairs including all inputs.
//This mode will iterate through wordlists in lockstep.
func (i *MainInputProvider) pitchforkValue() map[string][]byte {
Expand Down
14 changes: 13 additions & 1 deletion pkg/output/stdout.go
Expand Up @@ -139,7 +139,19 @@ func (s *Stdoutput) Progress(status ffuf.Progress) {
dur -= mins * time.Minute
secs := dur / time.Second

fmt.Fprintf(os.Stderr, "%s:: Progress: [%d/%d] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", TERMINAL_CLEAR_LINE, status.ReqCount, status.ReqTotal, reqRate, hours, mins, secs, status.ErrorCount)
fmt.Fprintf(os.Stderr, "%s:: Progress: [%d/%d] :: Job [%d/%d] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", TERMINAL_CLEAR_LINE, status.ReqCount, status.ReqTotal, status.QueuePos, status.QueueTotal, reqRate, hours, mins, secs, status.ErrorCount)
}

func (s *Stdoutput) Info(infostring string) {
if s.config.Quiet {
fmt.Fprintf(os.Stderr, "%s", infostring)
} else {
if !s.config.Colors {
fmt.Fprintf(os.Stderr, "%s[INFO] %s\n", TERMINAL_CLEAR_LINE, infostring)
} else {
fmt.Fprintf(os.Stderr, "%s[%sINFO%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_BLUE, ANSI_CLEAR, infostring)
}
}
}

func (s *Stdoutput) Error(errstring string) {
Expand Down

0 comments on commit b4adeae

Please sign in to comment.