diff --git a/main.go b/main.go index c531a4f..6fcbf03 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.IntVar(&conf.Threads, "t", 40, "Number of concurrent threads.") flag.BoolVar(&opts.showVersion, "V", false, "Show version information.") @@ -179,6 +182,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 @@ -190,6 +211,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 a689b35..0942f31 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -25,12 +25,15 @@ type Config struct { Quiet bool Colors bool Wordlist string + OutputFile string + OutputFormat string Delay optRange Filters []FilterProvider Matchers []FilterProvider 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 cedd848..5b81082 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 } @@ -52,6 +61,15 @@ func (s *Stdoutput) Error(errstring 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 } @@ -82,6 +100,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 }