package ffuf import ( "bufio" "context" "fmt" "io/ioutil" "net/textproto" "net/url" "os" "path/filepath" "runtime" "strconv" "strings" "github.com/pelletier/go-toml" ) type ConfigOptions struct { Filter FilterOptions General GeneralOptions HTTP HTTPOptions Input InputOptions Matcher MatcherOptions Output OutputOptions } type HTTPOptions struct { Cookies []string Data string FollowRedirects bool Headers []string IgnoreBody bool Method string ProxyURL string Recursion bool RecursionDepth int RecursionStrategy string ReplayProxyURL string SNI string Timeout int URL string Http2 bool } type GeneralOptions struct { AutoCalibration bool AutoCalibrationStrings []string Colors bool ConfigFile string `toml:"-"` Delay string MaxTime int MaxTimeJob int Noninteractive bool Quiet bool Rate int ShowVersion bool `toml:"-"` StopOn403 bool StopOnAll bool StopOnErrors bool Threads int Verbose bool } type InputOptions struct { DirSearchCompat bool Extensions string IgnoreWordlistComments bool InputMode string InputNum int InputShell string Inputcommands []string Request string RequestProto string Wordlists []string } type OutputOptions struct { DebugLog string OutputDirectory string OutputFile string OutputFormat string OutputSkipEmptyFile bool } type FilterOptions struct { Lines string Regexp string Size string Status string Time string Words string } type MatcherOptions struct { Lines string Regexp string Size string Status string Time string Words string } //NewConfigOptions returns a newly created ConfigOptions struct with default values func NewConfigOptions() *ConfigOptions { c := &ConfigOptions{} c.Filter.Lines = "" c.Filter.Regexp = "" c.Filter.Size = "" c.Filter.Status = "" c.Filter.Time = "" c.Filter.Words = "" c.General.AutoCalibration = false c.General.Colors = false c.General.Delay = "" c.General.MaxTime = 0 c.General.MaxTimeJob = 0 c.General.Noninteractive = false c.General.Quiet = false c.General.Rate = 0 c.General.ShowVersion = false c.General.StopOn403 = false c.General.StopOnAll = false c.General.StopOnErrors = false c.General.Threads = 40 c.General.Verbose = false c.HTTP.Data = "" c.HTTP.FollowRedirects = false c.HTTP.IgnoreBody = false c.HTTP.Method = "" c.HTTP.ProxyURL = "" c.HTTP.Recursion = false c.HTTP.RecursionDepth = 0 c.HTTP.RecursionStrategy = "default" c.HTTP.ReplayProxyURL = "" c.HTTP.Timeout = 10 c.HTTP.SNI = "" c.HTTP.URL = "" c.HTTP.Http2 = false c.Input.DirSearchCompat = false c.Input.Extensions = "" c.Input.IgnoreWordlistComments = false c.Input.InputMode = "clusterbomb" c.Input.InputNum = 100 c.Input.Request = "" c.Input.RequestProto = "https" c.Matcher.Lines = "" c.Matcher.Regexp = "" c.Matcher.Size = "" c.Matcher.Status = "200,204,301,302,307,401,403,405,500" c.Matcher.Time = "" c.Matcher.Words = "" c.Output.DebugLog = "" c.Output.OutputDirectory = "" c.Output.OutputFile = "" c.Output.OutputFormat = "json" c.Output.OutputSkipEmptyFile = false return c } //ConfigFromOptions parses the values in ConfigOptions struct, ensures that the values are sane, // and creates a Config struct out of them. func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel context.CancelFunc) (*Config, error) { //TODO: refactor in a proper flag library that can handle things like required flags errs := NewMultierror() conf := NewConfig(ctx, cancel) var err error var err2 error if len(parseOpts.HTTP.URL) == 0 && parseOpts.Input.Request == "" { errs.Add(fmt.Errorf("-u flag or -request flag is required")) } // prepare extensions if parseOpts.Input.Extensions != "" { extensions := strings.Split(parseOpts.Input.Extensions, ",") conf.Extensions = extensions } // Convert cookies to a header if len(parseOpts.HTTP.Cookies) > 0 { parseOpts.HTTP.Headers = append(parseOpts.HTTP.Headers, "Cookie: "+strings.Join(parseOpts.HTTP.Cookies, "; ")) } //Prepare inputproviders for _, v := range parseOpts.Input.Wordlists { var wl []string if runtime.GOOS == "windows" { // Try to ensure that Windows file paths like C:\path\to\wordlist.txt:KEYWORD are treated properly if FileExists(v) { // The wordlist was supplied without a keyword parameter wl = []string{v} } else { filepart := v if strings.Contains(filepart, ":") { filepart = v[:strings.LastIndex(filepart, ":")] } if FileExists(filepart) { wl = []string{filepart, v[strings.LastIndex(v, ":")+1:]} } else { // The file was not found. Use full wordlist parameter value for more concise error message down the line wl = []string{v} } } } else { wl = strings.SplitN(v, ":", 2) } if len(wl) == 2 { conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ Name: "wordlist", Value: wl[0], Keyword: wl[1], }) } else { conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ Name: "wordlist", Value: wl[0], Keyword: "FUZZ", }) } } for _, v := range parseOpts.Input.Inputcommands { ic := strings.SplitN(v, ":", 2) if len(ic) == 2 { conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ Name: "command", Value: ic[0], Keyword: ic[1], }) conf.CommandKeywords = append(conf.CommandKeywords, ic[0]) } else { conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ Name: "command", Value: ic[0], Keyword: "FUZZ", }) conf.CommandKeywords = append(conf.CommandKeywords, "FUZZ") } } if len(conf.InputProviders) == 0 { errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required")) } // Prepare the request using body if parseOpts.Input.Request != "" { err := parseRawRequest(parseOpts, &conf) if err != nil { errmsg := fmt.Sprintf("Could not parse raw request: %s", err) errs.Add(fmt.Errorf(errmsg)) } } //Prepare URL if parseOpts.HTTP.URL != "" { conf.Url = parseOpts.HTTP.URL } // Prepare SNI if parseOpts.HTTP.SNI != "" { conf.SNI = parseOpts.HTTP.SNI } //Prepare headers and make canonical for _, v := range parseOpts.HTTP.Headers { hs := strings.SplitN(v, ":", 2) if len(hs) == 2 { // trim and make canonical // except if used in custom defined header var CanonicalNeeded = true for _, a := range conf.CommandKeywords { if strings.Contains(hs[0], a) { CanonicalNeeded = false } } // check if part of InputProviders if CanonicalNeeded { for _, b := range conf.InputProviders { if strings.Contains(hs[0], b.Keyword) { CanonicalNeeded = false } } } if CanonicalNeeded { var CanonicalHeader = textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(hs[0])) conf.Headers[CanonicalHeader] = strings.TrimSpace(hs[1]) } else { conf.Headers[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")) } } //Prepare delay d := strings.Split(parseOpts.General.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.General.Delay) > 0 { conf.Delay.IsRange = false conf.Delay.HasDelay = true conf.Delay.Min, err = strconv.ParseFloat(parseOpts.General.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\"")) } } // Verify proxy url format if len(parseOpts.HTTP.ProxyURL) > 0 { _, err := url.Parse(parseOpts.HTTP.ProxyURL) if err != nil { errs.Add(fmt.Errorf("Bad proxy url (-x) format: %s", err)) } else { conf.ProxyURL = parseOpts.HTTP.ProxyURL } } // Verify replayproxy url format if len(parseOpts.HTTP.ReplayProxyURL) > 0 { _, err := url.Parse(parseOpts.HTTP.ReplayProxyURL) if err != nil { errs.Add(fmt.Errorf("Bad replay-proxy url (-replay-proxy) format: %s", err)) } else { conf.ReplayProxyURL = parseOpts.HTTP.ReplayProxyURL } } //Check the output file format option if parseOpts.Output.OutputFile != "" { //No need to check / error out if output file isn't defined outputFormats := []string{"all", "json", "ejson", "html", "md", "csv", "ecsv"} found := false for _, f := range outputFormats { if f == parseOpts.Output.OutputFormat { conf.OutputFormat = f found = true } } if !found { errs.Add(fmt.Errorf("Unknown output file format (-of): %s", parseOpts.Output.OutputFormat)) } } // Auto-calibration strings if len(parseOpts.General.AutoCalibrationStrings) > 0 { conf.AutoCalibrationStrings = parseOpts.General.AutoCalibrationStrings } // Using -acc implies -ac if len(parseOpts.General.AutoCalibrationStrings) > 0 { conf.AutoCalibration = true } if parseOpts.General.Rate < 0 { conf.Rate = 0 } else { conf.Rate = int64(parseOpts.General.Rate) } if conf.Method == "" { if parseOpts.HTTP.Method == "" { // Only set if defined on command line, because we might be reparsing the CLI after // populating it through raw request in the first iteration conf.Method = "GET" } else { conf.Method = parseOpts.HTTP.Method } } else { if parseOpts.HTTP.Method != "" { // Method overridden in CLI conf.Method = parseOpts.HTTP.Method } } if parseOpts.HTTP.Data != "" { // Only set if defined on command line, because we might be reparsing the CLI after // populating it through raw request in the first iteration conf.Data = parseOpts.HTTP.Data } // Common stuff conf.IgnoreWordlistComments = parseOpts.Input.IgnoreWordlistComments conf.DirSearchCompat = parseOpts.Input.DirSearchCompat conf.Colors = parseOpts.General.Colors conf.InputNum = parseOpts.Input.InputNum conf.InputMode = parseOpts.Input.InputMode conf.InputShell = parseOpts.Input.InputShell conf.OutputFile = parseOpts.Output.OutputFile conf.OutputDirectory = parseOpts.Output.OutputDirectory conf.OutputSkipEmptyFile = parseOpts.Output.OutputSkipEmptyFile conf.IgnoreBody = parseOpts.HTTP.IgnoreBody conf.Quiet = parseOpts.General.Quiet conf.StopOn403 = parseOpts.General.StopOn403 conf.StopOnAll = parseOpts.General.StopOnAll conf.StopOnErrors = parseOpts.General.StopOnErrors conf.FollowRedirects = parseOpts.HTTP.FollowRedirects conf.Recursion = parseOpts.HTTP.Recursion conf.RecursionDepth = parseOpts.HTTP.RecursionDepth conf.RecursionStrategy = parseOpts.HTTP.RecursionStrategy conf.AutoCalibration = parseOpts.General.AutoCalibration conf.Threads = parseOpts.General.Threads conf.Timeout = parseOpts.HTTP.Timeout conf.MaxTime = parseOpts.General.MaxTime conf.MaxTimeJob = parseOpts.General.MaxTimeJob conf.Noninteractive = parseOpts.General.Noninteractive conf.Verbose = parseOpts.General.Verbose conf.Http2 = parseOpts.HTTP.Http2 // Handle copy as curl situation where POST method is implied by --data flag. If method is set to anything but GET, NOOP if len(conf.Data) > 0 && conf.Method == "GET" && //don't modify the method automatically if a request file is being used as input len(parseOpts.Input.Request) == 0 { conf.Method = "POST" } conf.CommandLine = strings.Join(os.Args, " ") for _, provider := range conf.InputProviders { if !keywordPresent(provider.Keyword, &conf) { errmsg := fmt.Sprintf("Keyword %s defined, but not found in headers, method, URL or POST data.", provider.Keyword) errs.Add(fmt.Errorf(errmsg)) } } // Do checks for recursion mode if parseOpts.HTTP.Recursion { if !strings.HasSuffix(conf.Url, "FUZZ") { errmsg := "When using -recursion the URL (-u) must end with FUZZ keyword." errs.Add(fmt.Errorf(errmsg)) } } return &conf, errs.ErrorOrNil() } func parseRawRequest(parseOpts *ConfigOptions, conf *Config) error { file, err := os.Open(parseOpts.Input.Request) if err != nil { return fmt.Errorf("could not open request file: %s", err) } defer file.Close() r := bufio.NewReader(file) s, err := r.ReadString('\n') if err != nil { return fmt.Errorf("could not read request: %s", err) } parts := strings.Split(s, " ") if len(parts) < 3 { return fmt.Errorf("malformed request supplied") } // Set the request Method conf.Method = parts[0] for { line, err := r.ReadString('\n') line = strings.TrimSpace(line) if err != nil || line == "" { break } p := strings.SplitN(line, ":", 2) if len(p) != 2 { continue } if strings.EqualFold(p[0], "content-length") { continue } conf.Headers[strings.TrimSpace(p[0])] = strings.TrimSpace(p[1]) } // Handle case with the full http url in path. In that case, // ignore any host header that we encounter and use the path as request URL if strings.HasPrefix(parts[1], "http") { parsed, err := url.Parse(parts[1]) if err != nil { return fmt.Errorf("could not parse request URL: %s", err) } conf.Url = parts[1] conf.Headers["Host"] = parsed.Host } else { // Build the request URL from the request conf.Url = parseOpts.Input.RequestProto + "://" + conf.Headers["Host"] + parts[1] } // Set the request body b, err := ioutil.ReadAll(r) if err != nil { return fmt.Errorf("could not read request body: %s", err) } conf.Data = string(b) // Remove newline (typically added by the editor) at the end of the file if strings.HasSuffix(conf.Data, "\r\n") { conf.Data = conf.Data[:len(conf.Data)-2] } else if strings.HasSuffix(conf.Data, "\n") { conf.Data = conf.Data[:len(conf.Data)-1] } return nil } func keywordPresent(keyword string, conf *Config) bool { //Search for keyword from HTTP method, URL and POST data too if strings.Contains(conf.Method, keyword) { return true } if strings.Contains(conf.Url, keyword) { return true } if strings.Contains(conf.Data, keyword) { return true } for k, v := range conf.Headers { if strings.Contains(k, keyword) { return true } if strings.Contains(v, keyword) { return true } } return false } func ReadConfig(configFile string) (*ConfigOptions, error) { conf := NewConfigOptions() configData, err := ioutil.ReadFile(configFile) if err == nil { err = toml.Unmarshal(configData, conf) } return conf, err } func ReadDefaultConfig() (*ConfigOptions, error) { userhome, err := os.UserHomeDir() if err != nil { return NewConfigOptions(), err } defaultconf := filepath.Join(userhome, ".ffufrc") return ReadConfig(defaultconf) }