mirror of
https://github.com/ffuf/ffuf
synced 2024-12-01 15:49:18 +00:00
Write requests and responses to filesystem if requested (#126)
This commit is contained in:
parent
f5609a2d13
commit
15524003b8
11 changed files with 70 additions and 8 deletions
|
@ -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
|
||||
|
|
1
main.go
1
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")
|
||||
|
|
|
@ -29,6 +29,7 @@ type Config struct {
|
|||
CommandKeywords []string
|
||||
InputNum int
|
||||
InputMode string
|
||||
OutputDirectory string
|
||||
OutputFile string
|
||||
OutputFormat string
|
||||
StopOn403 bool
|
||||
|
|
|
@ -8,6 +8,7 @@ type Request struct {
|
|||
Data []byte
|
||||
Input map[string][]byte
|
||||
Position int
|
||||
Raw string
|
||||
}
|
||||
|
||||
func NewRequest(conf *Config) Request {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue