From afece7bf2bfe57cf489e4cd46e879dd767af62b3 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 30 Jan 2020 00:23:58 +0200 Subject: [PATCH] Better usage information and -help. Ignore -mc default value if any matcher is manually set (#143) --- CHANGELOG.md | 2 + help.go | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 50 +++++++++++----- 3 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 help.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 529c66c..cc9e220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Take 429 responses into account when -sa (stop on all error cases) is used - Remove -k flag support, convert to dummy flag #134 - Write configuration to output JSON + - Better help text. + - If any matcher is set, ignore -mc default value. - v0.12 - New diff --git a/help.go b/help.go new file mode 100644 index 0000000..40da8b9 --- /dev/null +++ b/help.go @@ -0,0 +1,160 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/ffuf/ffuf/pkg/ffuf" +) + +type UsageSection struct { + Name string + Description string + Flags []UsageFlag + Hidden bool + ExpectedFlags []string +} + +//PrintSection prints out the section name, description and each of the flags +func (u *UsageSection) PrintSection(max_length int, extended bool) { + // Do not print if extended usage not requested and section marked as hidden + if !extended && u.Hidden { + return + } + fmt.Printf("%s:\n", u.Name) + for _, f := range u.Flags { + f.PrintFlag(max_length) + } + fmt.Printf("\n") +} + +type UsageFlag struct { + Name string + Description string + Default string +} + +//PrintFlag prints out the flag name, usage string and default value +func (f *UsageFlag) PrintFlag(max_length int) { + // Create format string, used for padding + format := fmt.Sprintf(" -%%-%ds %%s", max_length) + if f.Default != "" { + format = format + " (default: %s)\n" + fmt.Printf(format, f.Name, f.Description, f.Default) + } else { + format = format + "\n" + fmt.Printf(format, f.Name, f.Description) + } +} + +func Usage() { + u_http := UsageSection{ + Name: "HTTP OPTIONS", + Description: "Options controlling the HTTP request and its parts.", + Flags: make([]UsageFlag, 0), + Hidden: false, + ExpectedFlags: []string{"H", "X", "b", "d", "r", "u", "recursion", "recursion-depth", "replay-proxy", "timeout", "x"}, + } + u_general := UsageSection{ + Name: "GENERAL OPTIONS", + Description: "", + Flags: make([]UsageFlag, 0), + Hidden: false, + ExpectedFlags: []string{"ac", "acc", "c", "maxtime", "p", "s", "sa", "se", "sf", "t", "v", "V"}, + } + u_compat := UsageSection{ + Name: "COMPATIBILITY OPTIONS", + Description: "Options to ensure compatibility with other pieces of software.", + Flags: make([]UsageFlag, 0), + Hidden: true, + ExpectedFlags: []string{"compressed", "cookie", "data", "data-ascii", "data-binary", "i", "k"}, + } + u_matcher := UsageSection{ + Name: "MATCHER OPTIONS", + Description: "Matchers for the response filtering.", + Flags: make([]UsageFlag, 0), + Hidden: false, + ExpectedFlags: []string{"mc", "ml", "mr", "ms", "mw"}, + } + u_filter := UsageSection{ + Name: "FILTER OPTIONS", + Description: "Filters for the response filtering.", + Flags: make([]UsageFlag, 0), + Hidden: false, + ExpectedFlags: []string{"fc", "fl", "fr", "fs", "fw"}, + } + u_input := UsageSection{ + Name: "INPUT OPTIONS", + Description: "Options for input data for fuzzing. Wordlists and input generators.", + Flags: make([]UsageFlag, 0), + Hidden: false, + ExpectedFlags: []string{"D", "ic", "input-cmd", "input-num", "mode", "request", "request-proto", "e", "w"}, + } + u_output := UsageSection{ + Name: "OUTPUT OPTIONS", + Description: "Options for output. Output file formats, file names and debug file locations.", + Flags: make([]UsageFlag, 0), + Hidden: false, + ExpectedFlags: []string{"debug-log", "o", "of", "od"}, + } + sections := []UsageSection{u_http, u_general, u_compat, u_matcher, u_filter, u_input, u_output} + + // Populate the flag sections + max_length := 0 + flag.VisitAll(func(f *flag.Flag) { + found := false + for i, section := range sections { + if strInSlice(f.Name, section.ExpectedFlags) { + sections[i].Flags = append(sections[i].Flags, UsageFlag{ + Name: f.Name, + Description: f.Usage, + Default: f.DefValue, + }) + found = true + } + } + if !found { + fmt.Printf("DEBUG: Flag %s was found but not defined in usage.go.\n", f.Name) + os.Exit(1) + } + if len(f.Name) > max_length { + max_length = len(f.Name) + } + }) + + fmt.Printf("Fuzz Faster U Fool - v%s\n\n", ffuf.VERSION) + + // Print out the sections + for _, section := range sections { + section.PrintSection(max_length, false) + } + + // Usage examples. + fmt.Printf("EXAMPLE USAGE:\n") + + fmt.Printf(" Fuzz file paths from wordlist.txt, match all responses but filter out those with content-size 42.\n") + fmt.Printf(" Colored, verbose output.\n") + fmt.Printf(" ffuf -w wordlist.txt -u https://example.org/FUZZ -mc all -fs 42 -c -v\n\n") + + fmt.Printf(" Fuzz Host-header, match HTTP 200 responses.\n") + fmt.Printf(" ffuf -w hosts.txt -u https://example.org/ -H \"Host: FUZZ\" -mc 200\n\n") + + fmt.Printf(" Fuzz POST JSON data. Match all responses not containing text \"error\".\n") + fmt.Printf(" ffuf -w entries.txt -u https://example.org/ -X POST -H \"Content-Type: application/json\" \\\n") + fmt.Printf(" -d '{\"name\": \"FUZZ\", \"anotherkey\": \"anothervalue\"}' -fr \"error\"\n\n") + + fmt.Printf(" Fuzz multiple locations. Match only responses reflecting the value of \"VAL\" keyword. Colored.\n") + fmt.Printf(" ffuf -w params.txt:PARAM -w values.txt:VAL -u https://example.org/?PARAM=VAL -mr \"VAL\" -c\n\n") + + fmt.Printf(" More information and examples: https://github.com/ffuf/ffuf\n\n") +} + +func strInSlice(val string, slice []string) bool { + for _, v := range slice { + if v == val { + return true + } + } + return false +} diff --git a/main.go b/main.go index 4c38f4b..9f7ae44 100644 --- a/main.go +++ b/main.go @@ -64,11 +64,11 @@ func main() { opts := cliOptions{} var ignored bool flag.BoolVar(&conf.IgnoreWordlistComments, "ic", false, "Ignore wordlist comments") - flag.StringVar(&opts.extensions, "e", "", "Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. Only extends a wordlist with (default) FUZZ keyword.") - flag.BoolVar(&conf.DirSearchCompat, "D", false, "DirSearch style wordlist compatibility mode. Used in conjunction with -e flag. Replaces %EXT% in wordlist entry with each of the extensions provided by -e.") + flag.StringVar(&opts.extensions, "e", "", "Comma separated list of extensions. Extends FUZZ keyword.") + flag.BoolVar(&conf.DirSearchCompat, "D", false, "DirSearch wordlist compatibility mode. Used in conjunction with -e flag.") flag.Var(&opts.headers, "H", "Header `\"Name: Value\"`, separated by colon. Multiple -H flags are accepted.") flag.StringVar(&conf.Url, "u", "", "Target URL") - flag.Var(&opts.wordlists, "w", "Wordlist file path and (optional) custom fuzz keyword, using colon as delimiter. Use file path '-' to read from standard input. Can be supplied multiple times. Format: '/path/to/wordlist:KEYWORD'") + flag.Var(&opts.wordlists, "w", "Wordlist file path and (optional) keyword separated by colon. eg. '/path/to/wordlist:KEYWORD'") flag.BoolVar(&ignored, "k", false, "Dummy flag for backwards compatibility") flag.StringVar(&opts.delay, "p", "", "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"") flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response. Comma separated list of codes and ranges") @@ -86,9 +86,9 @@ func main() { flag.IntVar(&conf.InputNum, "input-num", 100, "Number of inputs to test. Used in conjunction with --input-cmd.") flag.StringVar(&conf.InputMode, "mode", "clusterbomb", "Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork") flag.BoolVar(&ignored, "i", true, "Dummy flag for copy as curl functionality (ignored)") - flag.Var(&opts.cookies, "b", "Cookie data `\"NAME1=VALUE1; NAME2=VALUE2\"` for copy as curl functionality.\nResults unpredictable when combined with -H \"Cookie: ...\"") + flag.Var(&opts.cookies, "b", "Cookie data `\"NAME1=VALUE1; NAME2=VALUE2\"` for copy as curl functionality.") flag.Var(&opts.cookies, "cookie", "Cookie data (alias of -b)") - flag.StringVar(&opts.matcherStatus, "mc", "200,204,301,302,307,401,403", "Match HTTP status codes from respose, use \"all\" to match every response code.") + flag.StringVar(&opts.matcherStatus, "mc", "200,204,301,302,307,401,403", "Match HTTP status codes, or \"all\" for everything.") flag.StringVar(&opts.matcherSize, "ms", "", "Match HTTP response size") flag.StringVar(&opts.matcherRegexp, "mr", "", "Match regexp") flag.StringVar(&opts.matcherWords, "mw", "", "Match amount of words in response") @@ -103,7 +103,7 @@ func main() { flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)") flag.BoolVar(&conf.StopOn403, "sf", false, "Stop when > 95% of responses return 403 Forbidden") 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.StopOnAll, "sa", false, "Stop on all error cases. Implies -sf and -se.") 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.") @@ -116,6 +116,7 @@ func main() { flag.BoolVar(&conf.Verbose, "v", false, "Verbose output, printing full URL and redirect location (if any) with the results.") flag.BoolVar(&opts.showVersion, "V", false, "Show version information.") flag.StringVar(&opts.debugLog, "debug-log", "", "Write all of the internal logging to the specified file.") + flag.Usage = Usage flag.Parse() if opts.showVersion { fmt.Printf("ffuf version: %s\n", ffuf.VERSION) @@ -135,18 +136,18 @@ func main() { } if err := prepareConfig(&opts, &conf); err != nil { fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err) - flag.Usage() + Usage() os.Exit(1) } job, err := prepareJob(&conf) if err != nil { fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err) - flag.Usage() + Usage() os.Exit(1) } if err := prepareFilters(&opts, &conf); err != nil { fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err) - flag.Usage() + Usage() os.Exit(1) } @@ -190,6 +191,32 @@ func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) { func prepareFilters(parseOpts *cliOptions, conf *ffuf.Config) error { errs := ffuf.NewMultierror() + // If any other matcher is set, ignore -mc default value + matcherSet := false + statusSet := false + flag.Visit(func(f *flag.Flag) { + if f.Name == "mc" { + statusSet = true + } + if f.Name == "ms" { + matcherSet = true + } + if f.Name == "ml" { + matcherSet = true + } + if f.Name == "mr" { + matcherSet = true + } + if f.Name == "mw" { + matcherSet = true + } + }) + if statusSet || !matcherSet { + if err := filter.AddMatcher(conf, "status", parseOpts.matcherStatus); err != nil { + errs.Add(err) + } + } + if parseOpts.filterStatus != "" { if err := filter.AddFilter(conf, "status", parseOpts.filterStatus); err != nil { errs.Add(err) @@ -215,11 +242,6 @@ func prepareFilters(parseOpts *cliOptions, conf *ffuf.Config) error { errs.Add(err) } } - if parseOpts.matcherStatus != "" { - if err := filter.AddMatcher(conf, "status", parseOpts.matcherStatus); err != nil { - errs.Add(err) - } - } if parseOpts.matcherSize != "" { if err := filter.AddMatcher(conf, "size", parseOpts.matcherSize); err != nil { errs.Add(err)