ffuf/main.go

314 lines
11 KiB
Go
Raw Normal View History

2018-11-08 09:26:32 +00:00
package main
import (
"context"
"flag"
"fmt"
2019-01-21 20:43:04 +00:00
"net/http"
"net/url"
2018-11-08 09:26:32 +00:00
"os"
2018-12-05 22:57:42 +00:00
"strconv"
2018-11-08 09:26:32 +00:00
"strings"
"github.com/ffuf/ffuf/pkg/ffuf"
"github.com/ffuf/ffuf/pkg/filter"
"github.com/ffuf/ffuf/pkg/input"
"github.com/ffuf/ffuf/pkg/output"
"github.com/ffuf/ffuf/pkg/runner"
)
type cliOptions struct {
extensions string
2018-12-05 22:57:42 +00:00
delay string
2018-11-12 17:47:49 +00:00
filterStatus string
filterSize string
filterRegexp string
filterWords string
matcherStatus string
matcherSize string
matcherRegexp string
matcherWords string
2019-01-21 20:43:04 +00:00
proxyURL string
2019-03-29 23:02:41 +00:00
outputFormat string
2018-12-05 22:57:42 +00:00
headers multiStringFlag
cookies multiStringFlag
2018-11-15 08:47:43 +00:00
showVersion bool
2018-11-08 09:26:32 +00:00
}
2018-12-05 22:57:42 +00:00
type multiStringFlag []string
2018-11-08 09:26:32 +00:00
2018-12-05 22:57:42 +00:00
func (m *multiStringFlag) String() string {
2018-11-08 09:26:32 +00:00
return ""
}
2018-12-05 22:57:42 +00:00
func (m *multiStringFlag) Set(value string) error {
*m = append(*m, value)
2018-11-08 09:26:32 +00:00
return nil
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
conf := ffuf.NewConfig(ctx)
opts := cliOptions{}
2019-06-04 21:26:27 +00:00
var ignored bool
2019-04-27 07:47:24 +00:00
flag.StringVar(&opts.extensions, "e", "", "Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once.")
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.")
2018-11-12 21:24:37 +00:00
flag.Var(&opts.headers, "H", "Header `\"Name: Value\"`, separated by colon. Multiple -H flags are accepted.")
2018-11-08 09:26:32 +00:00
flag.StringVar(&conf.Url, "u", "", "Target URL")
flag.StringVar(&conf.Wordlist, "w", "", "Wordlist file path or - to read from standard input")
flag.BoolVar(&conf.TLSVerify, "k", false, "TLS identity verification")
2018-12-05 22:57:42 +00:00
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")
flag.StringVar(&opts.filterSize, "fs", "", "Filter HTTP response size. Comma separated list of sizes and ranges")
2018-11-12 17:47:49 +00:00
flag.StringVar(&opts.filterRegexp, "fr", "", "Filter regexp")
flag.StringVar(&opts.filterWords, "fw", "", "Filter by amount of words in response. Comma separated list of word counts and ranges")
2019-06-04 21:26:27 +00:00
flag.StringVar(&conf.Data, "d", "", "POST data")
flag.StringVar(&conf.Data, "data", "", "POST data (alias of -d)")
flag.StringVar(&conf.Data, "data-ascii", "", "POST data (alias of -d)")
flag.StringVar(&conf.Data, "data-binary", "", "POST data (alias of -d)")
2018-11-09 13:21:23 +00:00
flag.BoolVar(&conf.Colors, "c", false, "Colorize output.")
2019-06-04 21:26:27 +00:00
flag.BoolVar(&ignored, "compressed", true, "Dummy flag for copy as curl functionality (ignored)")
flag.StringVar(&conf.InputCommand, "input-cmd", "", "Command producing the input. --input-num is required when using this input method. Overrides -w.")
flag.IntVar(&conf.InputNum, "input-num", 100, "Number of inputs to test. Used in conjunction with --input-cmd.")
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, "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.")
2018-11-08 09:26:32 +00:00
flag.StringVar(&opts.matcherSize, "ms", "", "Match HTTP response size")
2018-11-12 17:47:49 +00:00
flag.StringVar(&opts.matcherRegexp, "mr", "", "Match regexp")
2018-11-12 17:06:49 +00:00
flag.StringVar(&opts.matcherWords, "mw", "", "Match amount of words in response")
2019-01-21 20:43:04 +00:00
flag.StringVar(&opts.proxyURL, "x", "", "HTTP Proxy URL")
flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use")
2019-03-29 23:02:41 +00:00
flag.StringVar(&conf.OutputFile, "o", "", "Write output to file")
2019-04-03 09:51:42 +00:00
flag.StringVar(&opts.outputFormat, "of", "json", "Output file format. Available formats: json, csv, ecsv")
flag.BoolVar(&conf.ShowRedirectLocation, "l", false, "Show target location of redirect responses")
2018-11-08 09:26:32 +00:00
flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)")
2019-04-27 07:47:24 +00:00
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")
2019-04-03 09:54:32 +00:00
flag.BoolVar(&conf.FollowRedirects, "r", false, "Follow redirects")
flag.BoolVar(&conf.AutoCalibration, "ac", false, "Automatically calibrate filtering options")
flag.IntVar(&conf.Threads, "t", 40, "Number of concurrent threads.")
flag.IntVar(&conf.Timeout, "timeout", 10, "HTTP request timeout in seconds.")
2018-11-15 08:47:43 +00:00
flag.BoolVar(&opts.showVersion, "V", false, "Show version information.")
2018-11-08 09:26:32 +00:00
flag.Parse()
2018-11-15 08:47:43 +00:00
if opts.showVersion {
fmt.Printf("ffuf version: %s\n", ffuf.VERSION)
os.Exit(0)
}
2018-11-08 09:26:32 +00:00
if err := prepareConfig(&opts, &conf); err != nil {
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
2018-11-08 09:26:32 +00:00
flag.Usage()
os.Exit(1)
}
job, err := prepareJob(&conf)
if err != nil {
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
2018-11-08 09:26:32 +00:00
flag.Usage()
os.Exit(1)
}
if err := prepareFilters(&opts, &conf); err != nil {
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
2018-11-08 09:26:32 +00:00
flag.Usage()
os.Exit(1)
}
if err := filter.CalibrateIfNeeded(job); err != nil {
fmt.Fprintf(os.Stderr, "Error in autocalibration, exiting: %s\n", err)
os.Exit(1)
}
2018-11-08 09:26:32 +00:00
// Job handles waiting for goroutines to complete itself
job.Start()
}
func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) {
errs := ffuf.NewMultierror()
var err error
var inputprovider ffuf.InputProvider
2018-11-08 09:26:32 +00:00
// TODO: implement error handling for runnerprovider and outputprovider
// We only have http runner right now
runprovider := runner.NewRunnerByName("http", conf)
// Initialize the correct inputprovider
if len(conf.InputCommand) > 0 {
inputprovider, err = input.NewInputProviderByName("command", conf)
} else {
inputprovider, err = input.NewInputProviderByName("wordlist", conf)
}
2018-11-08 09:26:32 +00:00
if err != nil {
errs.Add(fmt.Errorf("%s", err))
2018-11-08 09:26:32 +00:00
}
// We only have stdout outputprovider right now
outprovider := output.NewOutputProviderByName("stdout", conf)
return &ffuf.Job{
Config: conf,
Runner: runprovider,
Output: outprovider,
Input: inputprovider,
}, errs.ErrorOrNil()
2018-11-08 09:26:32 +00:00
}
func prepareFilters(parseOpts *cliOptions, conf *ffuf.Config) error {
errs := ffuf.NewMultierror()
if parseOpts.filterStatus != "" {
if err := filter.AddFilter(conf, "status", parseOpts.filterStatus); err != nil {
errs.Add(err)
}
}
if parseOpts.filterSize != "" {
if err := filter.AddFilter(conf, "size", parseOpts.filterSize); err != nil {
errs.Add(err)
}
}
if parseOpts.filterRegexp != "" {
if err := filter.AddFilter(conf, "regexp", parseOpts.filterRegexp); err != nil {
errs.Add(err)
}
}
if parseOpts.filterWords != "" {
if err := filter.AddFilter(conf, "word", parseOpts.filterWords); err != nil {
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)
}
}
if parseOpts.matcherRegexp != "" {
if err := filter.AddMatcher(conf, "regexp", parseOpts.matcherRegexp); err != nil {
errs.Add(err)
}
}
if parseOpts.matcherWords != "" {
if err := filter.AddMatcher(conf, "word", parseOpts.matcherWords); err != nil {
errs.Add(err)
}
}
return errs.ErrorOrNil()
}
2018-11-08 09:26:32 +00:00
func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
//TODO: refactor in a proper flag library that can handle things like required flags
errs := ffuf.NewMultierror()
2018-11-08 09:26:32 +00:00
foundkeyword := false
2018-12-05 22:57:42 +00:00
var err error
var err2 error
2018-11-08 09:26:32 +00:00
if len(conf.Url) == 0 {
errs.Add(fmt.Errorf("-u flag is required"))
2018-11-08 09:26:32 +00:00
}
if len(conf.Wordlist) == 0 && len(conf.InputCommand) == 0 {
errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required"))
2018-11-08 09:26:32 +00:00
}
// prepare extensions
if parseOpts.extensions != "" {
extensions := strings.Split(parseOpts.extensions, ",")
conf.Extensions = extensions
}
// Convert cookies to a header
if len(parseOpts.cookies) > 0 {
parseOpts.headers.Set("Cookie: " + strings.Join(parseOpts.cookies, "; "))
}
2018-11-08 09:26:32 +00:00
//Prepare headers
for _, v := range parseOpts.headers {
hs := strings.SplitN(v, ":", 2)
if len(hs) == 2 {
fuzzedheader := false
for _, fv := range hs {
if strings.Index(fv, "FUZZ") != -1 {
// Add to fuzzheaders
fuzzedheader = true
}
}
if fuzzedheader {
conf.FuzzHeaders[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
foundkeyword = true
} else {
conf.StaticHeaders[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
}
} else {
errs.Add(fmt.Errorf("Header defined by -H needs to have a value. \":\" should be used as a separator"))
2018-11-08 09:26:32 +00:00
}
}
2018-12-05 22:57:42 +00:00
//Prepare delay
d := strings.Split(parseOpts.delay, "-")
if len(d) > 2 {
errs.Add(fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\""))
} else if len(d) == 2 {
conf.Delay.IsRange = true
conf.Delay.HasDelay = true
conf.Delay.Min, err = strconv.ParseFloat(d[0], 64)
conf.Delay.Max, err2 = strconv.ParseFloat(d[1], 64)
if err != nil || err2 != nil {
errs.Add(fmt.Errorf("Delay range min and max values need to be valid floats. For example: 0.1-0.5"))
}
} else if len(parseOpts.delay) > 0 {
conf.Delay.IsRange = false
conf.Delay.HasDelay = true
conf.Delay.Min, err = strconv.ParseFloat(parseOpts.delay, 64)
if err != nil {
errs.Add(fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\""))
}
}
2019-01-21 20:43:04 +00:00
// Verify proxy url format
if len(parseOpts.proxyURL) > 0 {
pu, err := url.Parse(parseOpts.proxyURL)
if err != nil {
errs.Add(fmt.Errorf("Bad proxy url (-x) format: %s", err))
} else {
conf.ProxyURL = http.ProxyURL(pu)
}
}
2019-03-29 23:02:41 +00:00
//Check the output file format option
if conf.OutputFile != "" {
//No need to check / error out if output file isn't defined
2019-04-03 09:51:42 +00:00
outputFormats := []string{"json", "csv", "ecsv"}
2019-03-29 23:02:41 +00:00
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))
}
}
// Handle copy as curl situation where POST method is implied by --data flag. If method is set to anything but GET, NOOP
2019-06-04 21:26:27 +00:00
if conf.Method == "GET" {
if len(conf.Data) > 0 {
conf.Method = "POST"
}
}
2019-03-29 23:02:41 +00:00
conf.CommandLine = strings.Join(os.Args, " ")
//Search for keyword from HTTP method, URL and POST data too
if conf.Method == "FUZZ" {
foundkeyword = true
}
2018-11-08 09:26:32 +00:00
if strings.Index(conf.Url, "FUZZ") != -1 {
foundkeyword = true
}
2018-11-08 09:49:06 +00:00
if strings.Index(conf.Data, "FUZZ") != -1 {
foundkeyword = true
}
2018-11-08 09:26:32 +00:00
if !foundkeyword {
errs.Add(fmt.Errorf("No FUZZ keyword(s) found in headers, method, URL or POST data, nothing to do"))
2018-11-08 09:26:32 +00:00
}
2019-03-29 23:02:41 +00:00
return errs.ErrorOrNil()
2018-11-08 09:26:32 +00:00
}