New upstream version 1.1.0
Marcio de Souza Oliveira
3 years ago
10 | 10 | - 386 |
11 | 11 | - arm |
12 | 12 | - arm64 |
13 | ignore: | |
14 | - goos: freebsd | |
15 | goarch: arm64 | |
13 | 16 | |
14 | 17 | archives: |
15 | 18 | - id: tgz |
0 | 0 | ## Changelog |
1 | ||
2 | 1 | - master |
3 | 2 | - New |
3 | ||
4 | 4 | - 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) | |
5 | 21 | |
6 | 22 | - v1.0.2 |
7 | 23 | - Changed |
0 | 0 | # Contributors |
1 | ||
2 | 1 | * [bjhulst](https://github.com/bjhulst) |
2 | * [bsysop](https://twitter.com/bsysop) | |
3 | 3 | * [ccsplit](https://github.com/ccsplit) |
4 | 4 | * [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) | |
5 | 8 | * [delic](https://github.com/delic) |
6 | 9 | * [eur0pa](https://github.com/eur0pa) |
7 | 10 | * [fang0654](https://github.com/fang0654) |
11 | * [helpermika](https://github.com/helpermika) | |
8 | 12 | * [Ice3man543](https://github.com/Ice3man543) |
9 | 13 | * [JamTookTheBait](https://github.com/JamTookTheBait) |
10 | 14 | * [joohoi](https://github.com/joohoi) |
62 | 62 | ffuf -w /path/to/postdata.txt -X POST -d "username=admin\&password=FUZZ" -u https://target/login.php -fc 401 |
63 | 63 | ``` |
64 | 64 | |
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 | ||
65 | 81 | ### Using external mutator to produce test cases |
66 | 82 | |
67 | 83 | For this example, we'll fuzz JSON data that's sent over POST. [Radamsa](https://gitlab.com/akihe/radamsa) is used as the mutator. |
109 | 125 | -ac Automatically calibrate filtering options (default: false) |
110 | 126 | -acc Custom auto-calibration string. Can be used multiple times. Implies -ac |
111 | 127 | -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) | |
113 | 130 | -p Seconds of `delay` between requests, or a range of random delay. For example "0.1" or "0.1-2.0" |
114 | 131 | -s Do not print additional information (silent mode) (default: false) |
115 | 132 | -sa Stop on all error cases. Implies -sf and -se. (default: false) |
53 | 53 | Description: "Options controlling the HTTP request and its parts.", |
54 | 54 | Flags: make([]UsageFlag, 0), |
55 | 55 | 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"}, | |
57 | 57 | } |
58 | 58 | u_general := UsageSection{ |
59 | 59 | Name: "GENERAL OPTIONS", |
60 | 60 | Description: "", |
61 | 61 | Flags: make([]UsageFlag, 0), |
62 | 62 | 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"}, | |
64 | 64 | } |
65 | 65 | u_compat := UsageSection{ |
66 | 66 | Name: "COMPATIBILITY OPTIONS", |
114 | 114 | } |
115 | 115 | } |
116 | 116 | 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) | |
118 | 118 | os.Exit(1) |
119 | 119 | } |
120 | 120 | if len(f.Name) > max_length { |
9 | 9 | "net/textproto" |
10 | 10 | "net/url" |
11 | 11 | "os" |
12 | "runtime" | |
12 | 13 | "strconv" |
13 | 14 | "strings" |
14 | 15 | |
38 | 39 | requestProto string |
39 | 40 | URL string |
40 | 41 | outputFormat string |
42 | ignoreBody bool | |
41 | 43 | wordlists multiStringFlag |
42 | 44 | inputcommands multiStringFlag |
43 | 45 | headers multiStringFlag |
99 | 101 | flag.StringVar(&opts.requestProto, "request-proto", "https", "Protocol to use along with raw request") |
100 | 102 | flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use") |
101 | 103 | 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)") | |
103 | 105 | 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.") | |
104 | 107 | flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)") |
105 | 108 | flag.BoolVar(&conf.StopOn403, "sf", false, "Stop when > 95% of responses return 403 Forbidden") |
106 | 109 | flag.BoolVar(&conf.StopOnErrors, "se", false, "Stop on spurious errors") |
113 | 116 | flag.Var(&opts.AutoCalibrationStrings, "acc", "Custom auto-calibration string. Can be used multiple times. Implies -ac") |
114 | 117 | flag.IntVar(&conf.Threads, "t", 40, "Number of concurrent threads.") |
115 | 118 | 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.") | |
117 | 121 | flag.BoolVar(&conf.Verbose, "v", false, "Verbose output, printing full URL and redirect location (if any) with the results.") |
118 | 122 | flag.BoolVar(&opts.showVersion, "V", false, "Show version information.") |
119 | 123 | flag.StringVar(&opts.debugLog, "debug-log", "", "Write all of the internal logging to the specified file.") |
195 | 199 | // If any other matcher is set, ignore -mc default value |
196 | 200 | matcherSet := false |
197 | 201 | statusSet := false |
202 | warningIgnoreBody := false | |
198 | 203 | flag.Visit(func(f *flag.Flag) { |
199 | 204 | if f.Name == "mc" { |
200 | 205 | statusSet = true |
201 | 206 | } |
202 | 207 | if f.Name == "ms" { |
203 | 208 | matcherSet = true |
209 | warningIgnoreBody = true | |
204 | 210 | } |
205 | 211 | if f.Name == "ml" { |
206 | 212 | matcherSet = true |
213 | warningIgnoreBody = true | |
207 | 214 | } |
208 | 215 | if f.Name == "mr" { |
209 | 216 | matcherSet = true |
210 | 217 | } |
211 | 218 | if f.Name == "mw" { |
212 | 219 | matcherSet = true |
220 | warningIgnoreBody = true | |
213 | 221 | } |
214 | 222 | }) |
215 | 223 | if statusSet || !matcherSet { |
224 | 232 | } |
225 | 233 | } |
226 | 234 | if parseOpts.filterSize != "" { |
235 | warningIgnoreBody = true | |
227 | 236 | if err := filter.AddFilter(conf, "size", parseOpts.filterSize); err != nil { |
228 | 237 | errs.Add(err) |
229 | 238 | } |
234 | 243 | } |
235 | 244 | } |
236 | 245 | if parseOpts.filterWords != "" { |
246 | warningIgnoreBody = true | |
237 | 247 | if err := filter.AddFilter(conf, "word", parseOpts.filterWords); err != nil { |
238 | 248 | errs.Add(err) |
239 | 249 | } |
240 | 250 | } |
241 | 251 | if parseOpts.filterLines != "" { |
252 | warningIgnoreBody = true | |
242 | 253 | if err := filter.AddFilter(conf, "line", parseOpts.filterLines); err != nil { |
243 | 254 | errs.Add(err) |
244 | 255 | } |
262 | 273 | if err := filter.AddMatcher(conf, "line", parseOpts.matcherLines); err != nil { |
263 | 274 | errs.Add(err) |
264 | 275 | } |
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") | |
265 | 279 | } |
266 | 280 | return errs.ErrorOrNil() |
267 | 281 | } |
289 | 303 | |
290 | 304 | //Prepare inputproviders |
291 | 305 | 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 | } | |
293 | 324 | if len(wl) == 2 { |
294 | 325 | conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{ |
295 | 326 | Name: "wordlist", |
416 | 447 | //Check the output file format option |
417 | 448 | if conf.OutputFile != "" { |
418 | 449 | //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"} | |
420 | 451 | found := false |
421 | 452 | for _, f := range outputFormats { |
422 | 453 | if f == parseOpts.outputFormat { |
439 | 470 | } |
440 | 471 | |
441 | 472 | // 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" | |
446 | 479 | } |
447 | 480 | |
448 | 481 | conf.CommandLine = strings.Join(os.Args, " ") |
19 | 19 | OutputDirectory string `json:"outputdirectory"` |
20 | 20 | OutputFile string `json:"outputfile"` |
21 | 21 | OutputFormat string `json:"outputformat"` |
22 | IgnoreBody bool `json:"ignorebody"` | |
22 | 23 | IgnoreWordlistComments bool `json:"ignore_wordlist_comments"` |
23 | 24 | StopOn403 bool `json:"stop_403"` |
24 | 25 | StopOnErrors bool `json:"stop_errors"` |
38 | 39 | CommandLine string `json:"cmdline"` |
39 | 40 | Verbose bool `json:"verbose"` |
40 | 41 | MaxTime int `json:"maxtime"` |
42 | MaxTimeJob int `json:"maxtime_job"` | |
41 | 43 | Recursion bool `json:"recursion"` |
42 | 44 | RecursionDepth int `json:"recursion_depth"` |
43 | 45 | } |
77 | 79 | conf.DirSearchCompat = false |
78 | 80 | conf.Verbose = false |
79 | 81 | conf.MaxTime = 0 |
82 | conf.MaxTimeJob = 0 | |
80 | 83 | conf.Recursion = false |
81 | 84 | conf.RecursionDepth = 0 |
82 | 85 | return conf |
1 | 1 | |
2 | 2 | const ( |
3 | 3 | //VERSION holds the current version number |
4 | VERSION = "1.0.2" | |
4 | VERSION = "1.1.0" | |
5 | 5 | ) |
23 | 23 | SpuriousErrorCounter int |
24 | 24 | Total int |
25 | 25 | Running bool |
26 | RunningJob bool | |
26 | 27 | Count403 int |
27 | 28 | Count429 int |
28 | 29 | Error string |
29 | 30 | startTime time.Time |
31 | startTimeJob time.Time | |
30 | 32 | queuejobs []QueueJob |
31 | 33 | queuepos int |
32 | 34 | currentDepth int |
43 | 45 | j.ErrorCounter = 0 |
44 | 46 | j.SpuriousErrorCounter = 0 |
45 | 47 | j.Running = false |
48 | j.RunningJob = false | |
46 | 49 | j.queuepos = 0 |
47 | 50 | j.queuejobs = make([]QueueJob, 0) |
48 | 51 | j.currentDepth = 0 |
80 | 83 | |
81 | 84 | //Start the execution of the Job |
82 | 85 | func (j *Job) Start() { |
86 | if j.startTime.IsZero() { | |
87 | j.startTime = time.Now() | |
88 | } | |
89 | ||
83 | 90 | // Add the default job to job queue |
84 | 91 | j.queuejobs = append(j.queuejobs, QueueJob{Url: j.Config.Url, depth: 0}) |
85 | 92 | rand.Seed(time.Now().UnixNano()) |
86 | 93 | j.Total = j.Input.Total() |
87 | 94 | defer j.Stop() |
95 | ||
88 | 96 | j.Running = true |
97 | j.RunningJob = true | |
89 | 98 | //Show banner if not running in silent mode |
90 | 99 | if !j.Config.Quiet { |
91 | 100 | j.Output.Banner() |
94 | 103 | j.interruptMonitor() |
95 | 104 | for j.jobsInQueue() { |
96 | 105 | j.prepareQueueJob() |
97 | if j.queuepos > 1 { | |
106 | ||
107 | if j.queuepos > 1 && !j.RunningJob { | |
98 | 108 | // Print info for queued recursive jobs |
99 | 109 | j.Output.Info(fmt.Sprintf("Scanning: %s", j.Config.Url)) |
100 | 110 | } |
101 | 111 | j.Input.Reset() |
102 | j.startTime = time.Now() | |
112 | j.startTimeJob = time.Now() | |
113 | j.RunningJob = true | |
103 | 114 | j.Counter = 0 |
104 | 115 | j.startExecution() |
105 | 116 | } |
126 | 137 | go j.runProgress(&wg) |
127 | 138 | //Limiter blocks after reaching the buffer, ensuring limited concurrency |
128 | 139 | limiter := make(chan bool, j.Config.Threads) |
140 | ||
129 | 141 | for j.Input.Next() { |
130 | 142 | // Check if we should stop the process |
131 | 143 | j.CheckStop() |
144 | ||
132 | 145 | if !j.Running { |
133 | 146 | defer j.Output.Warning(j.Error) |
134 | 147 | break |
135 | 148 | } |
149 | ||
136 | 150 | limiter <- true |
137 | 151 | nextInput := j.Input.Value() |
138 | 152 | nextPosition := j.Input.Position() |
153 | 167 | time.Sleep(sleepDurationMS * time.Millisecond) |
154 | 168 | } |
155 | 169 | }() |
170 | ||
171 | if !j.RunningJob { | |
172 | defer j.Output.Warning(j.Error) | |
173 | return | |
174 | } | |
156 | 175 | } |
157 | 176 | wg.Wait() |
158 | 177 | j.updateProgress() |
174 | 193 | defer wg.Done() |
175 | 194 | totalProgress := j.Input.Total() |
176 | 195 | for j.Counter <= totalProgress { |
196 | ||
177 | 197 | if !j.Running { |
178 | 198 | break |
179 | 199 | } |
200 | ||
180 | 201 | j.updateProgress() |
181 | 202 | if j.Counter == totalProgress { |
182 | 203 | return |
183 | 204 | } |
205 | ||
206 | if !j.RunningJob { | |
207 | return | |
208 | } | |
209 | ||
184 | 210 | time.Sleep(time.Millisecond * time.Duration(j.Config.ProgressFrequency)) |
185 | 211 | } |
186 | 212 | } |
187 | 213 | |
188 | 214 | func (j *Job) updateProgress() { |
189 | 215 | prog := Progress{ |
190 | StartedAt: j.startTime, | |
216 | StartedAt: j.startTimeJob, | |
191 | 217 | ReqCount: j.Counter, |
192 | 218 | ReqTotal: j.Input.Total(), |
193 | 219 | QueuePos: j.queuepos, |
304 | 330 | //CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests |
305 | 331 | func (j *Job) CalibrateResponses() ([]Response, error) { |
306 | 332 | cInputs := make([]string, 0) |
333 | rand.Seed(time.Now().UnixNano()) | |
307 | 334 | if len(j.Config.AutoCalibrationStrings) < 1 { |
308 | 335 | cInputs = append(cInputs, "admin"+RandomString(16)+"/") |
309 | 336 | cInputs = append(cInputs, ".htaccess"+RandomString(16)) |
366 | 393 | } |
367 | 394 | } |
368 | 395 | |
369 | // check for maximum running time | |
396 | // Check for runtime of entire process | |
370 | 397 | if j.Config.MaxTime > 0 { |
371 | 398 | dur := time.Now().Sub(j.startTime) |
372 | 399 | runningSecs := int(dur / time.Second) |
373 | 400 | if runningSecs >= j.Config.MaxTime { |
374 | j.Error = "Maximum running time reached, exiting." | |
401 | j.Error = "Maximum running time for entire process reached, exiting." | |
375 | 402 | 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 | ||
376 | 414 | } |
377 | 415 | } |
378 | 416 | } |
382 | 420 | j.Running = false |
383 | 421 | return |
384 | 422 | } |
423 | ||
424 | //Stop current, resume to next | |
425 | func (j *Job) Next() { | |
426 | j.RunningJob = false | |
427 | return | |
428 | } |
2 | 2 | // Request holds the meaningful data that is passed for runner for making the query |
3 | 3 | type Request struct { |
4 | 4 | Method string |
5 | Host string | |
5 | 6 | Url string |
6 | 7 | Headers map[string]string |
7 | 8 | Data []byte |
23 | 23 | |
24 | 24 | redirectLocation := "" |
25 | 25 | 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 | } | |
27 | 31 | } |
28 | 32 | |
29 | 33 | if absolute { |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "math/rand" |
4 | "os" | |
4 | 5 | ) |
5 | 6 | |
6 | 7 | //used for random string generation in calibration function |
28 | 29 | } |
29 | 30 | return ret |
30 | 31 | } |
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 | } |
99 | 99 | <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> |
100 | 100 | <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> |
101 | 101 | <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> | |
107 | 115 | <style> |
108 | 116 | body { |
109 | 117 | display: flex; |
8 | 8 | ) |
9 | 9 | |
10 | 10 | 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"` | |
14 | 15 | } |
15 | 16 | |
16 | 17 | type JsonResult struct { |
23 | 24 | RedirectLocation string `json:"redirectlocation"` |
24 | 25 | ResultFile string `json:"resultfile"` |
25 | 26 | Url string `json:"url"` |
27 | Host string `json:"host"` | |
26 | 28 | } |
27 | 29 | |
28 | 30 | type jsonFileOutput struct { |
69 | 71 | RedirectLocation: r.RedirectLocation, |
70 | 72 | ResultFile: r.ResultFile, |
71 | 73 | Url: r.Url, |
74 | Host: r.Host, | |
72 | 75 | }) |
73 | 76 | } |
74 | 77 | outJSON := jsonFileOutput{ |
38 | 38 | RedirectLocation string `json:"redirectlocation"` |
39 | 39 | Url string `json:"url"` |
40 | 40 | ResultFile string `json:"resultfile"` |
41 | Host string `json:"host"` | |
41 | 42 | HTMLColor string `json:"-"` |
42 | 43 | } |
43 | 44 | |
52 | 53 | fmt.Printf("%s\n v%s\n%s\n\n", BANNER_HEADER, ffuf.VERSION, BANNER_SEP) |
53 | 54 | printOption([]byte("Method"), []byte(s.config.Method)) |
54 | 55 | 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 | ||
55 | 64 | // Print headers |
56 | 65 | if len(s.config.Headers) > 0 { |
57 | 66 | for k, v := range s.config.Headers { |
74 | 83 | |
75 | 84 | // Output file info |
76 | 85 | 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)) | |
78 | 96 | printOption([]byte("File format"), []byte(s.config.OutputFormat)) |
79 | 97 | } |
80 | 98 | |
187 | 205 | } |
188 | 206 | } |
189 | 207 | |
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 | ||
190 | 255 | func (s *Stdoutput) Finalize() error { |
191 | 256 | var err error |
192 | 257 | 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" { | |
194 | 261 | err = writeJSON(s.config, s.Results) |
195 | 262 | } else if s.config.OutputFormat == "ejson" { |
196 | 263 | err = writeEJSON(s.config, s.Results) |
235 | 302 | RedirectLocation: resp.GetRedirectLocation(false), |
236 | 303 | Url: resp.Request.Url, |
237 | 304 | ResultFile: resp.ResultFile, |
305 | Host: resp.Request.Host, | |
238 | 306 | } |
239 | 307 | s.Results = append(s.Results, sResult) |
240 | 308 | } |
52 | 52 | MaxConnsPerHost: 500, |
53 | 53 | TLSClientConfig: &tls.Config{ |
54 | 54 | InsecureSkipVerify: true, |
55 | Renegotiation: tls.RenegotiateOnceAsClient, | |
55 | 56 | }, |
56 | 57 | }} |
57 | 58 | |
104 | 105 | if _, ok := req.Headers["Host"]; ok { |
105 | 106 | httpreq.Host = req.Headers["Host"] |
106 | 107 | } |
108 | ||
109 | req.Host = httpreq.Host | |
107 | 110 | httpreq = httpreq.WithContext(r.config.Context) |
108 | 111 | for k, v := range req.Headers { |
109 | 112 | httpreq.Header.Set(k, v) |
125 | 128 | size, err := strconv.Atoi(httpresp.Header.Get("Content-Length")) |
126 | 129 | if err == nil { |
127 | 130 | resp.ContentLength = int64(size) |
128 | if size > MAX_DOWNLOAD_SIZE { | |
131 | if (r.config.IgnoreBody) || (size > MAX_DOWNLOAD_SIZE) { | |
129 | 132 | resp.Cancelled = true |
130 | 133 | return resp, nil |
131 | 134 | } |