diff --git a/main.go b/main.go index 87e7319..57a78d5 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ type cliOptions struct { matcherRegexp string matcherWords string proxyURL string + outputFormat string headers multiStringFlag showVersion bool } @@ -65,6 +66,8 @@ func main() { flag.StringVar(&opts.matcherWords, "mw", "", "Match amount of words in response") flag.StringVar(&opts.proxyURL, "x", "", "HTTP Proxy URL") flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use") + flag.StringVar(&conf.OutputFile, "o", "", "Write output to file") + flag.StringVar(&opts.outputFormat, "of", "json", "Output file format. Available formats: json") flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)") flag.BoolVar(&conf.StopOn403, "sf", false, "Stop when > 90% of responses return 403 Forbidden") flag.IntVar(&conf.Threads, "t", 40, "Number of concurrent threads.") @@ -180,6 +183,24 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error { } } + //Check the output file format option + if conf.OutputFile != "" { + //No need to check / error out if output file isn't defined + outputFormats := []string{"json"} + found := false + for _, f := range outputFormats { + if f == parseOpts.outputFormat { + conf.OutputFormat = f + found = true + } + } + if !found { + errs.Add(fmt.Errorf("Unknown output file format (-of): %s", parseOpts.outputFormat)) + } + } + + conf.CommandLine = strings.Join(os.Args, " ") + //Search for keyword from URL and POST data too if strings.Index(conf.Url, "FUZZ") != -1 { foundkeyword = true @@ -191,6 +212,7 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error { if !foundkeyword { errs.Add(fmt.Errorf("No FUZZ keyword(s) found in headers, URL or POST data, nothing to do")) } + return errs.ErrorOrNil() } diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go index 8fbadaa..78c276a 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -25,6 +25,8 @@ type Config struct { Quiet bool Colors bool Wordlist string + OutputFile string + OutputFormat string StopOn403 bool Delay optRange Filters []FilterProvider @@ -32,6 +34,7 @@ type Config struct { Threads int Context context.Context ProxyURL func(*http.Request) (*url.URL, error) + CommandLine string } func NewConfig(ctx context.Context) Config { diff --git a/pkg/output/file_json.go b/pkg/output/file_json.go new file mode 100644 index 0000000..2313e54 --- /dev/null +++ b/pkg/output/file_json.go @@ -0,0 +1,33 @@ +package output + +import ( + "encoding/json" + "io/ioutil" + "time" + + "github.com/ffuf/ffuf/pkg/ffuf" +) + +type jsonFileOutput struct { + CommandLine string `json:"commandline"` + Time string `json:"time"` + Results []Result `json:"results"` +} + +func writeJSON(config *ffuf.Config, res []Result) error { + t := time.Now() + outJSON := jsonFileOutput{ + CommandLine: config.CommandLine, + Time: t.Format(time.RFC3339), + Results: res, + } + outBytes, err := json.Marshal(outJSON) + if err != nil { + return err + } + err = ioutil.WriteFile(config.OutputFile, outBytes, 0644) + if err != nil { + return err + } + return nil +} diff --git a/pkg/output/stdout.go b/pkg/output/stdout.go index f93dc33..1424656 100644 --- a/pkg/output/stdout.go +++ b/pkg/output/stdout.go @@ -20,12 +20,21 @@ const ( ) type Stdoutput struct { - config *ffuf.Config + config *ffuf.Config + Results []Result +} + +type Result struct { + Input string `json:"input"` + StatusCode int64 `json:"status"` + ContentLength int64 `json:"length"` + ContentWords int64 `json:"words"` } func NewStdoutput(conf *ffuf.Config) *Stdoutput { var outp Stdoutput outp.config = conf + outp.Results = []Result{} return &outp } @@ -77,6 +86,15 @@ func (s *Stdoutput) Warning(warnstring string) { } func (s *Stdoutput) Finalize() error { + var err error + if s.config.OutputFile != "" { + if s.config.OutputFormat == "json" { + err = writeJSON(s.config, s.Results) + } + if err != nil { + s.Error(fmt.Sprintf("%s", err)) + } + } fmt.Fprintf(os.Stderr, "\n") return nil } @@ -107,6 +125,16 @@ func (s *Stdoutput) Result(resp ffuf.Response) bool { } // Response survived the filtering, output the result s.printResult(resp) + if s.config.OutputFile != "" { + // No need to store results if we're not going to use them later + sResult := Result{ + Input: string(resp.Request.Input), + StatusCode: resp.StatusCode, + ContentLength: resp.ContentLength, + ContentWords: resp.ContentWords, + } + s.Results = append(s.Results, sResult) + } return true }