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`).
```
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

View file

@ -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")

View file

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

View file

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

View file

@ -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
}

View file

@ -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
}

View file

@ -77,6 +77,7 @@ const (
<th>Length</th>
<th>Words</th>
<th>Lines</th>
<th>Resultfile</th>
</tr>
</thead>
@ -85,7 +86,7 @@ const (
<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 }}|
</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}}
</tbody>
</table>

View file

@ -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,
})
}

View file

@ -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
)

View file

@ -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

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) {
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 {