Write requests and responses to filesystem if requested (#126)

This commit is contained in:
Joona Hoikkala 2019-12-28 17:46:44 +02:00 committed by GitHub
parent f5609a2d13
commit 15524003b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 70 additions and 8 deletions

View File

@ -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`). 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. -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" -H "Name: Value"
Header "Name: Value", separated by colon. Multiple -H flags are accepted. Header "Name: Value", separated by colon. Multiple -H flags are accepted.
@ -122,7 +122,7 @@ Usage of ./ffuf:
-debug-log string -debug-log string
Write all of the internal logging to the specified file. Write all of the internal logging to the specified file.
-e string -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 -fc string
Filter HTTP status codes from response. Comma separated list of codes and ranges Filter HTTP status codes from response. Comma separated list of codes and ranges
-fl string -fl string
@ -153,6 +153,8 @@ Usage of ./ffuf:
Match amount of words in response Match amount of words in response
-o string -o string
Write output to file Write output to file
-od string
Directory path to store matched results to.
-of string -of string
Output file format. Available formats: json, ejson, html, md, csv, ecsv (default "json") Output file format. Available formats: json, ejson, html, md, csv, ecsv (default "json")
-p delay -p delay
@ -192,6 +194,7 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l
- master - master
- New - 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 - Changed
- Limit the use of `-e` (extensions) to a single keyword: FUZZ - Limit the use of `-e` (extensions) to a single keyword: FUZZ
- Regexp matching and filtering (-mr/-fr) allow using keywords in patterns - Regexp matching and filtering (-mr/-fr) allow using keywords in patterns

View File

@ -93,6 +93,7 @@ func main() {
flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use") flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use")
flag.StringVar(&conf.OutputFile, "o", "", "Write output to file") 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(&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.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.StopOn403, "sf", false, "Stop when > 95% of responses return 403 Forbidden")
flag.BoolVar(&conf.StopOnErrors, "se", false, "Stop on spurious errors") flag.BoolVar(&conf.StopOnErrors, "se", false, "Stop on spurious errors")

View File

@ -29,6 +29,7 @@ type Config struct {
CommandKeywords []string CommandKeywords []string
InputNum int InputNum int
InputMode string InputMode string
OutputDirectory string
OutputFile string OutputFile string
OutputFormat string OutputFormat string
StopOn403 bool StopOn403 bool

View File

@ -8,6 +8,7 @@ type Request struct {
Data []byte Data []byte
Input map[string][]byte Input map[string][]byte
Position int Position int
Raw string
} }
func NewRequest(conf *Config) Request { func NewRequest(conf *Config) Request {

View File

@ -14,6 +14,8 @@ type Response struct {
ContentLines int64 ContentLines int64
Cancelled bool Cancelled bool
Request *Request Request *Request
Raw string
ResultFile string
} }
// GetRedirectLocation returns the redirect location for a 3xx redirect HTTP response // 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.StatusCode = int64(httpresp.StatusCode)
resp.Headers = httpresp.Header resp.Headers = httpresp.Header
resp.Cancelled = false resp.Cancelled = false
resp.Raw = ""
resp.ResultFile = ""
return resp return resp
} }

View File

@ -9,7 +9,7 @@ import (
"github.com/ffuf/ffuf/pkg/ffuf" "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 { func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
header := make([]string, 0) 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.ContentLength, 10))
res = append(res, strconv.FormatInt(r.ContentWords, 10)) res = append(res, strconv.FormatInt(r.ContentWords, 10))
res = append(res, strconv.FormatInt(r.ContentLines, 10)) res = append(res, strconv.FormatInt(r.ContentLines, 10))
res = append(res, r.ResultFile)
return res return res
} }

View File

@ -77,6 +77,7 @@ const (
<th>Length</th> <th>Length</th>
<th>Words</th> <th>Words</th>
<th>Lines</th> <th>Lines</th>
<th>Resultfile</th>
</tr> </tr>
</thead> </thead>
@ -85,7 +86,7 @@ const (
<div style="display:none"> <div style="display:none">
|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_raw|{{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}|{{ $value | printf "%s" }}{{ end }}|{{ $result.Url }}|{{ $result.RedirectLocation }}|{{ $result.Position }}|{{ $result.ContentLength }}|{{ $result.ContentWords }}|{{ $result.ContentLines }}|
</div> </div>
<tr class="result-{{ $result.StatusCode }}" style="background-color: {{$result.HTMLColor}};"><td><font color="black" class="status-code">{{ $result.StatusCode }}</font></td>{{ range $keyword, $value := $result.Input }}<td>{{ $value | printf "%s" }}</td>{{ end }}</td><td>{{ $result.Url }}</td><td>{{ $result.RedirectLocation }}</td><td>{{ $result.Position }}</td><td>{{ $result.ContentLength }}</td><td>{{ $result.ContentWords }}</td><td>{{ $result.ContentLines }}</td></tr> <tr class="result-{{ $result.StatusCode }}" style="background-color: {{$result.HTMLColor}};"><td><font color="black" class="status-code">{{ $result.StatusCode }}</font></td>{{ range $keyword, $value := $result.Input }}<td>{{ $value | printf "%s" }}</td>{{ end }}</td><td>{{ $result.Url }}</td><td>{{ $result.RedirectLocation }}</td><td>{{ $result.Position }}</td><td>{{ $result.ContentLength }}</td><td>{{ $result.ContentWords }}</td><td>{{ $result.ContentLines }}</td><td>{{ $result.ResultFile }}</td></tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>

View File

@ -22,6 +22,7 @@ type JsonResult struct {
ContentWords int64 `json:"words"` ContentWords int64 `json:"words"`
ContentLines int64 `json:"lines"` ContentLines int64 `json:"lines"`
RedirectLocation string `json:"redirectlocation"` RedirectLocation string `json:"redirectlocation"`
ResultFile string `json:"resultfile"`
Url string `json:"url"` Url string `json:"url"`
} }
@ -66,6 +67,7 @@ func writeJSON(config *ffuf.Config, res []Result) error {
ContentWords: r.ContentWords, ContentWords: r.ContentWords,
ContentLines: r.ContentLines, ContentLines: r.ContentLines,
RedirectLocation: r.RedirectLocation, RedirectLocation: r.RedirectLocation,
ResultFile: r.ResultFile,
Url: r.Url, Url: r.Url,
}) })
} }

View File

@ -14,9 +14,9 @@ const (
Command line : ` + "`{{.CommandLine}}`" + ` Command line : ` + "`{{.CommandLine}}`" + `
Time: ` + "{{ .Time }}" + ` Time: ` + "{{ .Time }}" + `
{{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | {{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | ResultFile |
{{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | {{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | :--------- |
{{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{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 {{end}}` // The template format is not pretty but follows the markdown guide
) )

View File

@ -1,8 +1,11 @@
package output package output
import ( import (
"crypto/md5"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path"
"strconv" "strconv"
"time" "time"
@ -35,6 +38,7 @@ type Result struct {
ContentLines int64 `json:"lines"` ContentLines int64 `json:"lines"`
RedirectLocation string `json:"redirectlocation"` RedirectLocation string `json:"redirectlocation"`
Url string `json:"url"` Url string `json:"url"`
ResultFile string `json:"resultfile"`
HTMLColor string `json:"-"` HTMLColor string `json:"-"`
} }
@ -187,6 +191,10 @@ func (s *Stdoutput) Finalize() error {
} }
func (s *Stdoutput) Result(resp ffuf.Response) { 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 // Output the result
s.printResult(resp) s.printResult(resp)
// Check if we need the data later // Check if we need the data later
@ -205,16 +213,42 @@ func (s *Stdoutput) Result(resp ffuf.Response) {
ContentLines: resp.ContentLines, ContentLines: resp.ContentLines,
RedirectLocation: resp.GetRedirectLocation(), RedirectLocation: resp.GetRedirectLocation(),
Url: resp.Request.Url, Url: resp.Request.Url,
ResultFile: resp.ResultFile,
} }
s.Results = append(s.Results, sResult) 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) { func (s *Stdoutput) printResult(resp ffuf.Response) {
if s.config.Quiet { if s.config.Quiet {
s.resultQuiet(resp) s.resultQuiet(resp)
} else { } 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) // Print a multi-line result (when using multiple input keywords and wordlists)
s.resultMultiline(resp) s.resultMultiline(resp)
} else { } else {
@ -264,6 +298,9 @@ func (s *Stdoutput) resultMultiline(resp ffuf.Response) {
reslines = fmt.Sprintf("%s%s| --> | %s\n", reslines, TERMINAL_CLEAR_LINE, redirectLocation) 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 { for k, v := range resp.Request.Input {
if inSlice(k, s.config.CommandKeywords) { if inSlice(k, s.config.CommandKeywords) {
// If we're using external command for input, display the position instead of input // If we're using external command for input, display the position instead of input

View File

@ -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) { func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
var httpreq *http.Request var httpreq *http.Request
var err error var err error
var rawreq, rawresp strings.Builder
data := bytes.NewReader(req.Data) data := bytes.NewReader(req.Data)
httpreq, err = http.NewRequest(req.Method, req.Url, data) httpreq, err = http.NewRequest(req.Method, req.Url, data)
if err != nil { if err != nil {
@ -91,9 +92,19 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
if err != nil { if err != nil {
return ffuf.Response{}, err return ffuf.Response{}, err
} }
resp := ffuf.NewResponse(httpresp, req) resp := ffuf.NewResponse(httpresp, req)
defer httpresp.Body.Close() 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 // Check if we should download the resource or not
size, err := strconv.Atoi(httpresp.Header.Get("Content-Length")) size, err := strconv.Atoi(httpresp.Header.Get("Content-Length"))
if err == nil { if err == nil {