diff --git a/CHANGELOG.md b/CHANGELOG.md index f4fce26..7eb33be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - master - New + - New CLI flags `-request` to specify the raw request file to build the actual request from and `-request-proto` to define the new request format. - New CLI flag `-od` (output directory) to enable writing requests and responses for matched results to a file for postprocessing or debugging purposes. - New CLI flag `-maxtime` to limit the running time of ffuf - New CLI flags `-recursion` and `-recursion-depth` to control recursive ffuf jobs if directories are found. This requires the `-u` to end with FUZZ keyword. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 166270d..f3226df 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -15,3 +15,4 @@ * [seblw](https://github.com/seblw) * [SolomonSklash](https://github.com/SolomonSklash) * [Shaked](https://github.com/Shaked) +* [Ice3man543](https://github.com/Ice3man543) diff --git a/main.go b/main.go index c22248a..3bda293 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "context" "flag" "fmt" @@ -32,6 +33,8 @@ type cliOptions struct { matcherWords string matcherLines string proxyURL string + request string + requestProto string outputFormat string wordlists multiStringFlag inputcommands multiStringFlag @@ -89,6 +92,8 @@ func main() { flag.StringVar(&opts.matcherWords, "mw", "", "Match amount of words in response") flag.StringVar(&opts.matcherLines, "ml", "", "Match amount of lines in response") flag.StringVar(&opts.proxyURL, "x", "", "HTTP Proxy URL") + flag.StringVar(&opts.request, "request", "", "File containing the raw http request") + flag.StringVar(&opts.requestProto, "request-proto", "https", "Protocol to use along with raw request") 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, ejson, html, md, csv, ecsv") @@ -239,9 +244,10 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error { var err error var err2 error - if len(conf.Url) == 0 { - errs.Add(fmt.Errorf("-u flag is required")) + if len(conf.Url) == 0 && parseOpts.request == "" { + errs.Add(fmt.Errorf("-u flag or -request flag is required")) } + // prepare extensions if parseOpts.extensions != "" { extensions := strings.Split(parseOpts.extensions, ",") @@ -293,6 +299,15 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error { errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required")) } + // Prepare the request using body + if parseOpts.request != "" { + err := parseRawRequest(parseOpts, conf) + if err != nil { + errmsg := fmt.Sprintf("Could not parse raw request: %s", err) + errs.Add(fmt.Errorf(errmsg)) + } + } + //Prepare headers for _, v := range parseOpts.headers { hs := strings.SplitN(v, ":", 2) @@ -385,6 +400,70 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error { return errs.ErrorOrNil() } +func parseRawRequest(parseOpts *cliOptions, conf *ffuf.Config) error { + file, err := os.Open(parseOpts.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.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) + + return nil +} + func keywordPresent(keyword string, conf *ffuf.Config) bool { //Search for keyword from HTTP method, URL and POST data too if strings.Index(conf.Method, keyword) != -1 {