Codebase list ffuf / upstream/1.1.0
New upstream version 1.1.0 Marcio de Souza Oliveira 3 years ago
17 changed file(s) with 250 addition(s) and 32 deletion(s). Raw diff Collapse all Expand all
00 /ffuf
1 .idea
1010 - 386
1111 - arm
1212 - arm64
13 ignore:
14 - goos: freebsd
15 goarch: arm64
1316
1417 archives:
1518 - id: tgz
00 ## Changelog
1
21 - master
32 - New
3
44 - Changed
5
6 - v1.1.0
7 - New
8 - New CLI flag `-maxtime-job` to set max. execution time per job.
9 - Changed behaviour of `-maxtime`, can now be used for entire process.
10 - A new flag `-ignore-body` so ffuf does not fetch the response content. Default value=false.
11 - Added the wordlists to the header information.
12 - Added support to output "all" formats (specify the path/filename sans file extension and ffuf will add the appropriate suffix for the filetype)
13
14 - Changed
15 - Fixed a bug related to the autocalibration feature making the random seed initialization also to take place before autocalibration needs it.
16 - Added tls renegotiation flag to fix #193 in http.Client
17 - Fixed HTML report to display select/combo-box for rows per page (and increased default from 10 to 250 rows).
18 - Added Host information to JSON output file
19 - Fixed request method when supplying request file
20 - Fixed crash with 3XX responses that weren't redirects (304 Not Modified, 300 Multiple Choices etc)
521
622 - v1.0.2
723 - Changed
00 # Contributors
1
21 * [bjhulst](https://github.com/bjhulst)
2 * [bsysop](https://twitter.com/bsysop)
33 * [ccsplit](https://github.com/ccsplit)
44 * [codingo](https://github.com/codingo)
5 * [c_sto](https://github.com/c-sto)
6 * [Damian89](https://github.com/Damian89)
7 * [Daviey](https://github.com/Daviey)
58 * [delic](https://github.com/delic)
69 * [eur0pa](https://github.com/eur0pa)
710 * [fang0654](https://github.com/fang0654)
11 * [helpermika](https://github.com/helpermika)
812 * [Ice3man543](https://github.com/Ice3man543)
913 * [JamTookTheBait](https://github.com/JamTookTheBait)
1014 * [joohoi](https://github.com/joohoi)
6262 ffuf -w /path/to/postdata.txt -X POST -d "username=admin\&password=FUZZ" -u https://target/login.php -fc 401
6363 ```
6464
65 ### Maximum execution time
66
67 If you don't want ffuf to run indefinitely, you can use the `-maxtime`. This stops __the entire__ process after a given time (in seconds).
68
69 ```
70 ffuf -w /path/to/wordlist -u https://target/FUZZ -maxtime 60
71 ```
72
73 When working with recursion, you can control the maxtime __per job__ using `-maxtime-job`. This will stop the current job after a given time (in seconds) and continue with the next one. New jobs are created when the recursion functionality detects a subdirectory.
74
75 ```
76 ffuf -w /path/to/wordlist -u https://target/FUZZ -maxtime-job 60 -recursion -recursion-depth 2
77 ```
78
79 It is also possible to combine both flags limiting the per job maximum execution time as well as the overall execution time. If you do not use recursion then both flags behave equally.
80
6581 ### Using external mutator to produce test cases
6682
6783 For this example, we'll fuzz JSON data that's sent over POST. [Radamsa](https://gitlab.com/akihe/radamsa) is used as the mutator.
109125 -ac Automatically calibrate filtering options (default: false)
110126 -acc Custom auto-calibration string. Can be used multiple times. Implies -ac
111127 -c Colorize output. (default: false)
112 -maxtime Maximum running time in seconds. (default: 0)
128 -maxtime Maximum running time in seconds for the entire process. (default: 0)
129 -maxtime-job Maximum running time in seconds per job. (default: 0)
113130 -p Seconds of `delay` between requests, or a range of random delay. For example "0.1" or "0.1-2.0"
114131 -s Do not print additional information (silent mode) (default: false)
115132 -sa Stop on all error cases. Implies -sf and -se. (default: false)
5353 Description: "Options controlling the HTTP request and its parts.",
5454 Flags: make([]UsageFlag, 0),
5555 Hidden: false,
56 ExpectedFlags: []string{"H", "X", "b", "d", "r", "u", "recursion", "recursion-depth", "replay-proxy", "timeout", "x"},
56 ExpectedFlags: []string{"H", "X", "b", "d", "r", "u", "recursion", "recursion-depth", "replay-proxy", "timeout", "ignore-body", "x"},
5757 }
5858 u_general := UsageSection{
5959 Name: "GENERAL OPTIONS",
6060 Description: "",
6161 Flags: make([]UsageFlag, 0),
6262 Hidden: false,
63 ExpectedFlags: []string{"ac", "acc", "c", "maxtime", "p", "s", "sa", "se", "sf", "t", "v", "V"},
63 ExpectedFlags: []string{"ac", "acc", "c", "maxtime", "maxtime-job", "p", "s", "sa", "se", "sf", "t", "v", "V"},
6464 }
6565 u_compat := UsageSection{
6666 Name: "COMPATIBILITY OPTIONS",
114114 }
115115 }
116116 if !found {
117 fmt.Printf("DEBUG: Flag %s was found but not defined in usage.go.\n", f.Name)
117 fmt.Printf("DEBUG: Flag %s was found but not defined in help.go.\n", f.Name)
118118 os.Exit(1)
119119 }
120120 if len(f.Name) > max_length {
99 "net/textproto"
1010 "net/url"
1111 "os"
12 "runtime"
1213 "strconv"
1314 "strings"
1415
3839 requestProto string
3940 URL string
4041 outputFormat string
42 ignoreBody bool
4143 wordlists multiStringFlag
4244 inputcommands multiStringFlag
4345 headers multiStringFlag
99101 flag.StringVar(&opts.requestProto, "request-proto", "https", "Protocol to use along with raw request")
100102 flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use")
101103 flag.StringVar(&conf.OutputFile, "o", "", "Write output to file")
102 flag.StringVar(&opts.outputFormat, "of", "json", "Output file format. Available formats: json, ejson, html, md, csv, ecsv")
104 flag.StringVar(&opts.outputFormat, "of", "json", "Output file format. Available formats: json, ejson, html, md, csv, ecsv (or, 'all' for all formats)")
103105 flag.StringVar(&conf.OutputDirectory, "od", "", "Directory path to store matched results to.")
106 flag.BoolVar(&conf.IgnoreBody, "ignore-body", false, "Do not fetch the response content.")
104107 flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)")
105108 flag.BoolVar(&conf.StopOn403, "sf", false, "Stop when > 95% of responses return 403 Forbidden")
106109 flag.BoolVar(&conf.StopOnErrors, "se", false, "Stop on spurious errors")
113116 flag.Var(&opts.AutoCalibrationStrings, "acc", "Custom auto-calibration string. Can be used multiple times. Implies -ac")
114117 flag.IntVar(&conf.Threads, "t", 40, "Number of concurrent threads.")
115118 flag.IntVar(&conf.Timeout, "timeout", 10, "HTTP request timeout in seconds.")
116 flag.IntVar(&conf.MaxTime, "maxtime", 0, "Maximum running time in seconds.")
119 flag.IntVar(&conf.MaxTime, "maxtime", 0, "Maximum running time in seconds for entire process.")
120 flag.IntVar(&conf.MaxTimeJob, "maxtime-job", 0, "Maximum running time in seconds per job.")
117121 flag.BoolVar(&conf.Verbose, "v", false, "Verbose output, printing full URL and redirect location (if any) with the results.")
118122 flag.BoolVar(&opts.showVersion, "V", false, "Show version information.")
119123 flag.StringVar(&opts.debugLog, "debug-log", "", "Write all of the internal logging to the specified file.")
195199 // If any other matcher is set, ignore -mc default value
196200 matcherSet := false
197201 statusSet := false
202 warningIgnoreBody := false
198203 flag.Visit(func(f *flag.Flag) {
199204 if f.Name == "mc" {
200205 statusSet = true
201206 }
202207 if f.Name == "ms" {
203208 matcherSet = true
209 warningIgnoreBody = true
204210 }
205211 if f.Name == "ml" {
206212 matcherSet = true
213 warningIgnoreBody = true
207214 }
208215 if f.Name == "mr" {
209216 matcherSet = true
210217 }
211218 if f.Name == "mw" {
212219 matcherSet = true
220 warningIgnoreBody = true
213221 }
214222 })
215223 if statusSet || !matcherSet {
224232 }
225233 }
226234 if parseOpts.filterSize != "" {
235 warningIgnoreBody = true
227236 if err := filter.AddFilter(conf, "size", parseOpts.filterSize); err != nil {
228237 errs.Add(err)
229238 }
234243 }
235244 }
236245 if parseOpts.filterWords != "" {
246 warningIgnoreBody = true
237247 if err := filter.AddFilter(conf, "word", parseOpts.filterWords); err != nil {
238248 errs.Add(err)
239249 }
240250 }
241251 if parseOpts.filterLines != "" {
252 warningIgnoreBody = true
242253 if err := filter.AddFilter(conf, "line", parseOpts.filterLines); err != nil {
243254 errs.Add(err)
244255 }
262273 if err := filter.AddMatcher(conf, "line", parseOpts.matcherLines); err != nil {
263274 errs.Add(err)
264275 }
276 }
277 if conf.IgnoreBody && warningIgnoreBody {
278 fmt.Printf("*** Warning: possible undesired combination of -ignore-body and the response options: fl,fs,fw,ml,ms and mw.\n")
265279 }
266280 return errs.ErrorOrNil()
267281 }
289303
290304 //Prepare inputproviders
291305 for _, v := range parseOpts.wordlists {
292 wl := strings.SplitN(v, ":", 2)
306 var wl []string
307 if runtime.GOOS == "windows" {
308 // Try to ensure that Windows file paths like C:\path\to\wordlist.txt:KEYWORD are treated properly
309 if ffuf.FileExists(v) {
310 // The wordlist was supplied without a keyword parameter
311 wl = []string{v}
312 } else {
313 filepart := v[:strings.LastIndex(v, ":")]
314 if ffuf.FileExists(filepart) {
315 wl = []string{filepart, v[strings.LastIndex(v, ":")+1:]}
316 } else {
317 // The file was not found. Use full wordlist parameter value for more concise error message down the line
318 wl = []string{v}
319 }
320 }
321 } else {
322 wl = strings.SplitN(v, ":", 2)
323 }
293324 if len(wl) == 2 {
294325 conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
295326 Name: "wordlist",
416447 //Check the output file format option
417448 if conf.OutputFile != "" {
418449 //No need to check / error out if output file isn't defined
419 outputFormats := []string{"json", "ejson", "html", "md", "csv", "ecsv"}
450 outputFormats := []string{"all", "json", "ejson", "html", "md", "csv", "ecsv"}
420451 found := false
421452 for _, f := range outputFormats {
422453 if f == parseOpts.outputFormat {
439470 }
440471
441472 // Handle copy as curl situation where POST method is implied by --data flag. If method is set to anything but GET, NOOP
442 if conf.Method == "GET" {
443 if len(conf.Data) > 0 {
444 conf.Method = "POST"
445 }
473 if len(conf.Data) > 0 &&
474 conf.Method == "GET" &&
475 //don't modify the method automatically if a request file is being used as input
476 len(parseOpts.request) == 0 {
477
478 conf.Method = "POST"
446479 }
447480
448481 conf.CommandLine = strings.Join(os.Args, " ")
1919 OutputDirectory string `json:"outputdirectory"`
2020 OutputFile string `json:"outputfile"`
2121 OutputFormat string `json:"outputformat"`
22 IgnoreBody bool `json:"ignorebody"`
2223 IgnoreWordlistComments bool `json:"ignore_wordlist_comments"`
2324 StopOn403 bool `json:"stop_403"`
2425 StopOnErrors bool `json:"stop_errors"`
3839 CommandLine string `json:"cmdline"`
3940 Verbose bool `json:"verbose"`
4041 MaxTime int `json:"maxtime"`
42 MaxTimeJob int `json:"maxtime_job"`
4143 Recursion bool `json:"recursion"`
4244 RecursionDepth int `json:"recursion_depth"`
4345 }
7779 conf.DirSearchCompat = false
7880 conf.Verbose = false
7981 conf.MaxTime = 0
82 conf.MaxTimeJob = 0
8083 conf.Recursion = false
8184 conf.RecursionDepth = 0
8285 return conf
11
22 const (
33 //VERSION holds the current version number
4 VERSION = "1.0.2"
4 VERSION = "1.1.0"
55 )
2323 SpuriousErrorCounter int
2424 Total int
2525 Running bool
26 RunningJob bool
2627 Count403 int
2728 Count429 int
2829 Error string
2930 startTime time.Time
31 startTimeJob time.Time
3032 queuejobs []QueueJob
3133 queuepos int
3234 currentDepth int
4345 j.ErrorCounter = 0
4446 j.SpuriousErrorCounter = 0
4547 j.Running = false
48 j.RunningJob = false
4649 j.queuepos = 0
4750 j.queuejobs = make([]QueueJob, 0)
4851 j.currentDepth = 0
8083
8184 //Start the execution of the Job
8285 func (j *Job) Start() {
86 if j.startTime.IsZero() {
87 j.startTime = time.Now()
88 }
89
8390 // Add the default job to job queue
8491 j.queuejobs = append(j.queuejobs, QueueJob{Url: j.Config.Url, depth: 0})
8592 rand.Seed(time.Now().UnixNano())
8693 j.Total = j.Input.Total()
8794 defer j.Stop()
95
8896 j.Running = true
97 j.RunningJob = true
8998 //Show banner if not running in silent mode
9099 if !j.Config.Quiet {
91100 j.Output.Banner()
94103 j.interruptMonitor()
95104 for j.jobsInQueue() {
96105 j.prepareQueueJob()
97 if j.queuepos > 1 {
106
107 if j.queuepos > 1 && !j.RunningJob {
98108 // Print info for queued recursive jobs
99109 j.Output.Info(fmt.Sprintf("Scanning: %s", j.Config.Url))
100110 }
101111 j.Input.Reset()
102 j.startTime = time.Now()
112 j.startTimeJob = time.Now()
113 j.RunningJob = true
103114 j.Counter = 0
104115 j.startExecution()
105116 }
126137 go j.runProgress(&wg)
127138 //Limiter blocks after reaching the buffer, ensuring limited concurrency
128139 limiter := make(chan bool, j.Config.Threads)
140
129141 for j.Input.Next() {
130142 // Check if we should stop the process
131143 j.CheckStop()
144
132145 if !j.Running {
133146 defer j.Output.Warning(j.Error)
134147 break
135148 }
149
136150 limiter <- true
137151 nextInput := j.Input.Value()
138152 nextPosition := j.Input.Position()
153167 time.Sleep(sleepDurationMS * time.Millisecond)
154168 }
155169 }()
170
171 if !j.RunningJob {
172 defer j.Output.Warning(j.Error)
173 return
174 }
156175 }
157176 wg.Wait()
158177 j.updateProgress()
174193 defer wg.Done()
175194 totalProgress := j.Input.Total()
176195 for j.Counter <= totalProgress {
196
177197 if !j.Running {
178198 break
179199 }
200
180201 j.updateProgress()
181202 if j.Counter == totalProgress {
182203 return
183204 }
205
206 if !j.RunningJob {
207 return
208 }
209
184210 time.Sleep(time.Millisecond * time.Duration(j.Config.ProgressFrequency))
185211 }
186212 }
187213
188214 func (j *Job) updateProgress() {
189215 prog := Progress{
190 StartedAt: j.startTime,
216 StartedAt: j.startTimeJob,
191217 ReqCount: j.Counter,
192218 ReqTotal: j.Input.Total(),
193219 QueuePos: j.queuepos,
304330 //CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests
305331 func (j *Job) CalibrateResponses() ([]Response, error) {
306332 cInputs := make([]string, 0)
333 rand.Seed(time.Now().UnixNano())
307334 if len(j.Config.AutoCalibrationStrings) < 1 {
308335 cInputs = append(cInputs, "admin"+RandomString(16)+"/")
309336 cInputs = append(cInputs, ".htaccess"+RandomString(16))
366393 }
367394 }
368395
369 // check for maximum running time
396 // Check for runtime of entire process
370397 if j.Config.MaxTime > 0 {
371398 dur := time.Now().Sub(j.startTime)
372399 runningSecs := int(dur / time.Second)
373400 if runningSecs >= j.Config.MaxTime {
374 j.Error = "Maximum running time reached, exiting."
401 j.Error = "Maximum running time for entire process reached, exiting."
375402 j.Stop()
403 }
404 }
405
406 // Check for runtime of current job
407 if j.Config.MaxTimeJob > 0 {
408 dur := time.Now().Sub(j.startTimeJob)
409 runningSecs := int(dur / time.Second)
410 if runningSecs >= j.Config.MaxTimeJob {
411 j.Error = "Maximum running time for this job reached, continuing with next job if one exists."
412 j.Next()
413
376414 }
377415 }
378416 }
382420 j.Running = false
383421 return
384422 }
423
424 //Stop current, resume to next
425 func (j *Job) Next() {
426 j.RunningJob = false
427 return
428 }
22 // Request holds the meaningful data that is passed for runner for making the query
33 type Request struct {
44 Method string
5 Host string
56 Url string
67 Headers map[string]string
78 Data []byte
2323
2424 redirectLocation := ""
2525 if resp.StatusCode >= 300 && resp.StatusCode <= 399 {
26 redirectLocation = resp.Headers["Location"][0]
26 if loc, ok := resp.Headers["Location"]; ok {
27 if len(loc) > 0 {
28 redirectLocation = loc[0]
29 }
30 }
2731 }
2832
2933 if absolute {
11
22 import (
33 "math/rand"
4 "os"
45 )
56
67 //used for random string generation in calibration function
2829 }
2930 return ret
3031 }
32
33 //FileExists checks if the filepath exists and is not a directory
34 func FileExists(path string) bool {
35 md, err := os.Stat(path)
36 if os.IsNotExist(err) {
37 return false
38 }
39 return !md.IsDir()
40 }
9999 <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
100100 <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
101101 <script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.js"></script>
102 <script>
103 $(document).ready( function () {
104 $('#ffufreport').DataTable();
105 } );
106 </script>
102 <script>
103 $(document).ready(function() {
104 $('#ffufreport').DataTable(
105 {
106 "aLengthMenu": [
107 [250, 500, 1000, 2500, -1],
108 [250, 500, 1000, 2500, "All"]
109 ]
110 }
111 )
112 $('select').formSelect();
113 });
114 </script>
107115 <style>
108116 body {
109117 display: flex;
88 )
99
1010 type ejsonFileOutput struct {
11 CommandLine string `json:"commandline"`
12 Time string `json:"time"`
13 Results []Result `json:"results"`
11 CommandLine string `json:"commandline"`
12 Time string `json:"time"`
13 Results []Result `json:"results"`
14 Config *ffuf.Config `json:"config"`
1415 }
1516
1617 type JsonResult struct {
2324 RedirectLocation string `json:"redirectlocation"`
2425 ResultFile string `json:"resultfile"`
2526 Url string `json:"url"`
27 Host string `json:"host"`
2628 }
2729
2830 type jsonFileOutput struct {
6971 RedirectLocation: r.RedirectLocation,
7072 ResultFile: r.ResultFile,
7173 Url: r.Url,
74 Host: r.Host,
7275 })
7376 }
7477 outJSON := jsonFileOutput{
3838 RedirectLocation string `json:"redirectlocation"`
3939 Url string `json:"url"`
4040 ResultFile string `json:"resultfile"`
41 Host string `json:"host"`
4142 HTMLColor string `json:"-"`
4243 }
4344
5253 fmt.Printf("%s\n v%s\n%s\n\n", BANNER_HEADER, ffuf.VERSION, BANNER_SEP)
5354 printOption([]byte("Method"), []byte(s.config.Method))
5455 printOption([]byte("URL"), []byte(s.config.Url))
56
57 // Print wordlists
58 for _, provider := range s.config.InputProviders {
59 if provider.Name == "wordlist" {
60 printOption([]byte("Wordlist"), []byte(provider.Keyword+": "+provider.Value))
61 }
62 }
63
5564 // Print headers
5665 if len(s.config.Headers) > 0 {
5766 for k, v := range s.config.Headers {
7483
7584 // Output file info
7685 if len(s.config.OutputFile) > 0 {
77 printOption([]byte("Output file"), []byte(s.config.OutputFile))
86
87 // Use filename as specified by user
88 OutputFile := s.config.OutputFile
89
90 if s.config.OutputFormat == "all" {
91 // Actually... append all extensions
92 OutputFile += ".{json,ejson,html,md,csv,ecsv}"
93 }
94
95 printOption([]byte("Output file"), []byte(OutputFile))
7896 printOption([]byte("File format"), []byte(s.config.OutputFormat))
7997 }
8098
187205 }
188206 }
189207
208 func (s *Stdoutput) writeToAll(config *ffuf.Config, res []Result) error {
209 var err error
210 var BaseFilename string = s.config.OutputFile
211
212 // Go through each type of write, adding
213 // the suffix to each output file.
214
215 s.config.OutputFile = BaseFilename + ".json"
216 err = writeJSON(s.config, s.Results)
217 if err != nil {
218 s.Error(fmt.Sprintf("%s", err))
219 }
220
221 s.config.OutputFile = BaseFilename + ".ejson"
222 err = writeEJSON(s.config, s.Results)
223 if err != nil {
224 s.Error(fmt.Sprintf("%s", err))
225 }
226
227 s.config.OutputFile = BaseFilename + ".html"
228 err = writeHTML(s.config, s.Results)
229 if err != nil {
230 s.Error(fmt.Sprintf("%s", err))
231 }
232
233 s.config.OutputFile = BaseFilename + ".md"
234 err = writeMarkdown(s.config, s.Results)
235 if err != nil {
236 s.Error(fmt.Sprintf("%s", err))
237 }
238
239 s.config.OutputFile = BaseFilename + ".csv"
240 err = writeCSV(s.config, s.Results, false)
241 if err != nil {
242 s.Error(fmt.Sprintf("%s", err))
243 }
244
245 s.config.OutputFile = BaseFilename + ".ecsv"
246 err = writeCSV(s.config, s.Results, true)
247 if err != nil {
248 s.Error(fmt.Sprintf("%s", err))
249 }
250
251 return nil
252
253 }
254
190255 func (s *Stdoutput) Finalize() error {
191256 var err error
192257 if s.config.OutputFile != "" {
193 if s.config.OutputFormat == "json" {
258 if s.config.OutputFormat == "all" {
259 err = s.writeToAll(s.config, s.Results)
260 } else if s.config.OutputFormat == "json" {
194261 err = writeJSON(s.config, s.Results)
195262 } else if s.config.OutputFormat == "ejson" {
196263 err = writeEJSON(s.config, s.Results)
235302 RedirectLocation: resp.GetRedirectLocation(false),
236303 Url: resp.Request.Url,
237304 ResultFile: resp.ResultFile,
305 Host: resp.Request.Host,
238306 }
239307 s.Results = append(s.Results, sResult)
240308 }
5252 MaxConnsPerHost: 500,
5353 TLSClientConfig: &tls.Config{
5454 InsecureSkipVerify: true,
55 Renegotiation: tls.RenegotiateOnceAsClient,
5556 },
5657 }}
5758
104105 if _, ok := req.Headers["Host"]; ok {
105106 httpreq.Host = req.Headers["Host"]
106107 }
108
109 req.Host = httpreq.Host
107110 httpreq = httpreq.WithContext(r.config.Context)
108111 for k, v := range req.Headers {
109112 httpreq.Header.Set(k, v)
125128 size, err := strconv.Atoi(httpresp.Header.Get("Content-Length"))
126129 if err == nil {
127130 resp.ContentLength = int64(size)
128 if size > MAX_DOWNLOAD_SIZE {
131 if (r.config.IgnoreBody) || (size > MAX_DOWNLOAD_SIZE) {
129132 resp.Cancelled = true
130133 return resp, nil
131134 }