Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better usage information and -help. Ignore -mc default value if any matcher is set #143

Merged
merged 1 commit into from Jan 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
160 changes: 160 additions & 0 deletions 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
}
50 changes: 36 additions & 14 deletions main.go
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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.")
Expand All @@ -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)
Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down