From 15524003b8109d286d3bcbad343000d9f45adeae Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 28 Dec 2019 17:46:44 +0200 Subject: [PATCH] Write requests and responses to filesystem if requested (#126) --- README.md | 7 +++++-- main.go | 1 + pkg/ffuf/config.go | 1 + pkg/ffuf/request.go | 1 + pkg/ffuf/response.go | 4 ++++ pkg/output/file_csv.go | 3 ++- pkg/output/file_html.go | 3 ++- pkg/output/file_json.go | 2 ++ pkg/output/file_md.go | 6 +++--- pkg/output/stdout.go | 39 ++++++++++++++++++++++++++++++++++++++- pkg/runner/simple.go | 11 +++++++++++ 11 files changed, 70 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0045d54..6bfce9d 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ ffuf --input-cmd 'cat $FFUF_NUM.txt' -H "Content-Type: application/json" -X POST To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-u`), headers (`-H`), or POST data (`-d`). ``` -Usage of ./ffuf: +Usage of ffuf: -D DirSearch style wordlist compatibility mode. Used in conjunction with -e flag. Replaces %EXT% in wordlist entry with each of the extensions provided by -e. -H "Name: Value" Header "Name: Value", separated by colon. Multiple -H flags are accepted. @@ -122,7 +122,7 @@ Usage of ./ffuf: -debug-log string Write all of the internal logging to the specified file. -e string - Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. + Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. Only extends a wordlist with (default) FUZZ keyword. -fc string Filter HTTP status codes from response. Comma separated list of codes and ranges -fl string @@ -153,6 +153,8 @@ Usage of ./ffuf: Match amount of words in response -o string Write output to file + -od string + Directory path to store matched results to. -of string Output file format. Available formats: json, ejson, html, md, csv, ecsv (default "json") -p delay @@ -192,6 +194,7 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l - master - New + - New CLI flag `-od` (output directory) to enable writing requests and responses for matched results to a file for postprocessing or debugging purposes. - Changed - Limit the use of `-e` (extensions) to a single keyword: FUZZ - Regexp matching and filtering (-mr/-fr) allow using keywords in patterns diff --git a/main.go b/main.go index 93b81ad..349c592 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,7 @@ func main() { 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") + flag.StringVar(&conf.OutputDirectory, "od", "", "Directory path to store matched results to.") 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") diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go index 5b8233c..2e58ccf 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -29,6 +29,7 @@ type Config struct { CommandKeywords []string InputNum int InputMode string + OutputDirectory string OutputFile string OutputFormat string StopOn403 bool diff --git a/pkg/ffuf/request.go b/pkg/ffuf/request.go index 7aec09c..98d07b8 100644 --- a/pkg/ffuf/request.go +++ b/pkg/ffuf/request.go @@ -8,6 +8,7 @@ type Request struct { Data []byte Input map[string][]byte Position int + Raw string } func NewRequest(conf *Config) Request { diff --git a/pkg/ffuf/response.go b/pkg/ffuf/response.go index 678015b..f6119df 100644 --- a/pkg/ffuf/response.go +++ b/pkg/ffuf/response.go @@ -14,6 +14,8 @@ type Response struct { ContentLines int64 Cancelled bool Request *Request + Raw string + ResultFile string } // GetRedirectLocation returns the redirect location for a 3xx redirect HTTP response @@ -33,5 +35,7 @@ func NewResponse(httpresp *http.Response, req *Request) Response { resp.StatusCode = int64(httpresp.StatusCode) resp.Headers = httpresp.Header resp.Cancelled = false + resp.Raw = "" + resp.ResultFile = "" return resp } diff --git a/pkg/output/file_csv.go b/pkg/output/file_csv.go index 01cb582..3451e68 100644 --- a/pkg/output/file_csv.go +++ b/pkg/output/file_csv.go @@ -9,7 +9,7 @@ import ( "github.com/ffuf/ffuf/pkg/ffuf" ) -var staticheaders = []string{"url", "redirectlocation", "position", "status_code", "content_length", "content_words", "content_lines"} +var staticheaders = []string{"url", "redirectlocation", "position", "status_code", "content_length", "content_words", "content_lines", "resultfile"} func writeCSV(config *ffuf.Config, res []Result, encode bool) error { header := make([]string, 0) @@ -66,5 +66,6 @@ func toCSV(r Result) []string { res = append(res, strconv.FormatInt(r.ContentLength, 10)) res = append(res, strconv.FormatInt(r.ContentWords, 10)) res = append(res, strconv.FormatInt(r.ContentLines, 10)) + res = append(res, r.ResultFile) return res } diff --git a/pkg/output/file_html.go b/pkg/output/file_html.go index 7c6200d..8994b0b 100644 --- a/pkg/output/file_html.go +++ b/pkg/output/file_html.go @@ -77,6 +77,7 @@ const ( Length Words Lines + Resultfile @@ -85,7 +86,7 @@ const (
|result_raw|{{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}|{{ $value | printf "%s" }}{{ end }}|{{ $result.Url }}|{{ $result.RedirectLocation }}|{{ $result.Position }}|{{ $result.ContentLength }}|{{ $result.ContentWords }}|{{ $result.ContentLines }}|
- {{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}{{ $value | printf "%s" }}{{ end }}{{ $result.Url }}{{ $result.RedirectLocation }}{{ $result.Position }}{{ $result.ContentLength }}{{ $result.ContentWords }}{{ $result.ContentLines }} + {{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}{{ $value | printf "%s" }}{{ end }}{{ $result.Url }}{{ $result.RedirectLocation }}{{ $result.Position }}{{ $result.ContentLength }}{{ $result.ContentWords }}{{ $result.ContentLines }}{{ $result.ResultFile }} {{end}} diff --git a/pkg/output/file_json.go b/pkg/output/file_json.go index e091304..40f757d 100644 --- a/pkg/output/file_json.go +++ b/pkg/output/file_json.go @@ -22,6 +22,7 @@ type JsonResult struct { ContentWords int64 `json:"words"` ContentLines int64 `json:"lines"` RedirectLocation string `json:"redirectlocation"` + ResultFile string `json:"resultfile"` Url string `json:"url"` } @@ -66,6 +67,7 @@ func writeJSON(config *ffuf.Config, res []Result) error { ContentWords: r.ContentWords, ContentLines: r.ContentLines, RedirectLocation: r.RedirectLocation, + ResultFile: r.ResultFile, Url: r.Url, }) } diff --git a/pkg/output/file_md.go b/pkg/output/file_md.go index 0c13d7e..47e77e8 100644 --- a/pkg/output/file_md.go +++ b/pkg/output/file_md.go @@ -14,9 +14,9 @@ const ( Command line : ` + "`{{.CommandLine}}`" + ` Time: ` + "{{ .Time }}" + ` - {{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | - {{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | - {{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | + {{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | ResultFile | + {{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | :--------- | + {{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{ .ResultFile }} | {{end}}` // The template format is not pretty but follows the markdown guide ) diff --git a/pkg/output/stdout.go b/pkg/output/stdout.go index 96724c7..bd54175 100644 --- a/pkg/output/stdout.go +++ b/pkg/output/stdout.go @@ -1,8 +1,11 @@ package output import ( + "crypto/md5" "fmt" + "io/ioutil" "os" + "path" "strconv" "time" @@ -35,6 +38,7 @@ type Result struct { ContentLines int64 `json:"lines"` RedirectLocation string `json:"redirectlocation"` Url string `json:"url"` + ResultFile string `json:"resultfile"` HTMLColor string `json:"-"` } @@ -187,6 +191,10 @@ func (s *Stdoutput) Finalize() error { } func (s *Stdoutput) Result(resp ffuf.Response) { + // Do we want to write request and response to a file + if len(s.config.OutputDirectory) > 0 { + resp.ResultFile = s.writeResultToFile(resp) + } // Output the result s.printResult(resp) // Check if we need the data later @@ -205,16 +213,42 @@ func (s *Stdoutput) Result(resp ffuf.Response) { ContentLines: resp.ContentLines, RedirectLocation: resp.GetRedirectLocation(), Url: resp.Request.Url, + ResultFile: resp.ResultFile, } s.Results = append(s.Results, sResult) } } +func (s *Stdoutput) writeResultToFile(resp ffuf.Response) string { + var fileContent, fileName, filePath string + // Create directory if needed + if s.config.OutputDirectory != "" { + err := os.Mkdir(s.config.OutputDirectory, 0750) + if err != nil { + if !os.IsExist(err) { + s.Error(fmt.Sprintf("%s", err)) + return "" + } + } + } + fileContent = fmt.Sprintf("%s\n---- ↑ Request ---- Response ↓ ----\n\n%s", resp.Request.Raw, resp.Raw) + + // Create file name + fileName = fmt.Sprintf("%x", md5.Sum([]byte(fileContent))) + + filePath = path.Join(s.config.OutputDirectory, fileName) + err := ioutil.WriteFile(filePath, []byte(fileContent), 0640) + if err != nil { + s.Error(fmt.Sprintf("%s", err)) + } + return fileName +} + func (s *Stdoutput) printResult(resp ffuf.Response) { if s.config.Quiet { s.resultQuiet(resp) } else { - if len(resp.Request.Input) > 1 || s.config.Verbose { + if len(resp.Request.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0 { // Print a multi-line result (when using multiple input keywords and wordlists) s.resultMultiline(resp) } else { @@ -264,6 +298,9 @@ func (s *Stdoutput) resultMultiline(resp ffuf.Response) { reslines = fmt.Sprintf("%s%s| --> | %s\n", reslines, TERMINAL_CLEAR_LINE, redirectLocation) } } + if resp.ResultFile != "" { + reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, resp.ResultFile) + } for k, v := range resp.Request.Input { if inSlice(k, s.config.CommandKeywords) { // If we're using external command for input, display the position instead of input diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index 5aceb9b..ba75b23 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -70,6 +70,7 @@ func (r *SimpleRunner) Prepare(input map[string][]byte) (ffuf.Request, error) { func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) { var httpreq *http.Request var err error + var rawreq, rawresp strings.Builder data := bytes.NewReader(req.Data) httpreq, err = http.NewRequest(req.Method, req.Url, data) if err != nil { @@ -91,9 +92,19 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) { if err != nil { return ffuf.Response{}, err } + resp := ffuf.NewResponse(httpresp, req) defer httpresp.Body.Close() + if len(r.config.OutputDirectory) > 0 { + // store raw request + httpreq.Write(&rawreq) + resp.Request.Raw = rawreq.String() + // store raw response + httpresp.Write(&rawresp) + resp.Raw = rawresp.String() + } + // Check if we should download the resource or not size, err := strconv.Atoi(httpresp.Header.Get("Content-Length")) if err == nil {