ffuf/pkg/output/stdout.go
Joona Hoikkala 9bddff79b9
New functionality to map fired blind payloads back to the initial request (#632)
* Fix ioutil deprecation and use xdg paths instead (wip)

* Clean up deprecated ioutil references, add config directory structure creation and run entry creation

* Add wordlist position setting and FFUFHASH variable

* Save full wordlist paths and print out a raw request when searched

* Cast from string to 32bit integer, 2billion should be enough for a position

* Use correct format strings for float
2023-02-02 11:51:11 +02:00

483 lines
13 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package output
import (
"crypto/md5"
"encoding/json"
"fmt"
"os"
"path"
"sort"
"strconv"
"strings"
"time"
"github.com/ffuf/ffuf/pkg/ffuf"
)
const (
BANNER_HEADER = `
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
`
BANNER_SEP = "________________________________________________"
)
type Stdoutput struct {
config *ffuf.Config
fuzzkeywords []string
Results []ffuf.Result
CurrentResults []ffuf.Result
}
func NewStdoutput(conf *ffuf.Config) *Stdoutput {
var outp Stdoutput
outp.config = conf
outp.Results = make([]ffuf.Result, 0)
outp.CurrentResults = make([]ffuf.Result, 0)
outp.fuzzkeywords = make([]string, 0)
for _, ip := range conf.InputProviders {
outp.fuzzkeywords = append(outp.fuzzkeywords, ip.Keyword)
}
sort.Strings(outp.fuzzkeywords)
return &outp
}
func (s *Stdoutput) Banner() {
version := strings.ReplaceAll(ffuf.Version(), "<3", fmt.Sprintf("%s<3%s", ANSI_RED, ANSI_CLEAR))
fmt.Fprintf(os.Stderr, "%s\n v%s\n%s\n\n", BANNER_HEADER, version, BANNER_SEP)
printOption([]byte("Method"), []byte(s.config.Method))
printOption([]byte("URL"), []byte(s.config.Url))
// Print wordlists
for _, provider := range s.config.InputProviders {
if provider.Name == "wordlist" {
printOption([]byte("Wordlist"), []byte(provider.Keyword+": "+provider.Value))
}
}
// Print headers
if len(s.config.Headers) > 0 {
for k, v := range s.config.Headers {
printOption([]byte("Header"), []byte(fmt.Sprintf("%s: %s", k, v)))
}
}
// Print POST data
if len(s.config.Data) > 0 {
printOption([]byte("Data"), []byte(s.config.Data))
}
// Print extensions
if len(s.config.Extensions) > 0 {
exts := ""
for _, ext := range s.config.Extensions {
exts = fmt.Sprintf("%s%s ", exts, ext)
}
printOption([]byte("Extensions"), []byte(exts))
}
// Output file info
if len(s.config.OutputFile) > 0 {
// Use filename as specified by user
OutputFile := s.config.OutputFile
if s.config.OutputFormat == "all" {
// Actually... append all extensions
OutputFile += ".{json,ejson,html,md,csv,ecsv}"
}
printOption([]byte("Output file"), []byte(OutputFile))
printOption([]byte("File format"), []byte(s.config.OutputFormat))
}
// Follow redirects?
follow := fmt.Sprintf("%t", s.config.FollowRedirects)
printOption([]byte("Follow redirects"), []byte(follow))
// Autocalibration
autocalib := fmt.Sprintf("%t", s.config.AutoCalibration)
printOption([]byte("Calibration"), []byte(autocalib))
// Proxies
if len(s.config.ProxyURL) > 0 {
printOption([]byte("Proxy"), []byte(s.config.ProxyURL))
}
if len(s.config.ReplayProxyURL) > 0 {
printOption([]byte("ReplayProxy"), []byte(s.config.ReplayProxyURL))
}
// Timeout
timeout := fmt.Sprintf("%d", s.config.Timeout)
printOption([]byte("Timeout"), []byte(timeout))
// Threads
threads := fmt.Sprintf("%d", s.config.Threads)
printOption([]byte("Threads"), []byte(threads))
// Delay?
if s.config.Delay.HasDelay {
delay := ""
if s.config.Delay.IsRange {
delay = fmt.Sprintf("%.2f - %.2f seconds", s.config.Delay.Min, s.config.Delay.Max)
} else {
delay = fmt.Sprintf("%.2f seconds", s.config.Delay.Min)
}
printOption([]byte("Delay"), []byte(delay))
}
// Print matchers
for _, f := range s.config.MatcherManager.GetMatchers() {
printOption([]byte("Matcher"), []byte(f.ReprVerbose()))
}
// Print filters
for _, f := range s.config.MatcherManager.GetFilters() {
printOption([]byte("Filter"), []byte(f.ReprVerbose()))
}
fmt.Fprintf(os.Stderr, "%s\n\n", BANNER_SEP)
}
// Reset resets the result slice
func (s *Stdoutput) Reset() {
s.CurrentResults = make([]ffuf.Result, 0)
}
// Cycle moves the CurrentResults to Results and resets the results slice
func (s *Stdoutput) Cycle() {
s.Results = append(s.Results, s.CurrentResults...)
s.Reset()
}
// GetResults returns the result slice
func (s *Stdoutput) GetCurrentResults() []ffuf.Result {
return s.CurrentResults
}
// SetResults sets the result slice
func (s *Stdoutput) SetCurrentResults(results []ffuf.Result) {
s.CurrentResults = results
}
func (s *Stdoutput) Progress(status ffuf.Progress) {
if s.config.Quiet {
// No progress for quiet mode
return
}
dur := time.Since(status.StartedAt)
runningSecs := int(dur / time.Second)
var reqRate int64
if runningSecs > 0 {
reqRate = status.ReqSec
} else {
reqRate = 0
}
hours := dur / time.Hour
dur -= hours * time.Hour
mins := dur / time.Minute
dur -= mins * time.Minute
secs := dur / time.Second
fmt.Fprintf(os.Stderr, "%s:: Progress: [%d/%d] :: Job [%d/%d] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", TERMINAL_CLEAR_LINE, status.ReqCount, status.ReqTotal, status.QueuePos, status.QueueTotal, reqRate, hours, mins, secs, status.ErrorCount)
}
func (s *Stdoutput) Info(infostring string) {
if s.config.Quiet {
fmt.Fprintf(os.Stderr, "%s", infostring)
} else {
if !s.config.Colors {
fmt.Fprintf(os.Stderr, "%s[INFO] %s\n\n", TERMINAL_CLEAR_LINE, infostring)
} else {
fmt.Fprintf(os.Stderr, "%s[%sINFO%s] %s\n\n", TERMINAL_CLEAR_LINE, ANSI_BLUE, ANSI_CLEAR, infostring)
}
}
}
func (s *Stdoutput) Error(errstring string) {
if s.config.Quiet {
fmt.Fprintf(os.Stderr, "%s", errstring)
} else {
if !s.config.Colors {
fmt.Fprintf(os.Stderr, "%s[ERR] %s\n", TERMINAL_CLEAR_LINE, errstring)
} else {
fmt.Fprintf(os.Stderr, "%s[%sERR%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_RED, ANSI_CLEAR, errstring)
}
}
}
func (s *Stdoutput) Warning(warnstring string) {
if s.config.Quiet {
fmt.Fprintf(os.Stderr, "%s", warnstring)
} else {
if !s.config.Colors {
fmt.Fprintf(os.Stderr, "%s[WARN] %s\n", TERMINAL_CLEAR_LINE, warnstring)
} else {
fmt.Fprintf(os.Stderr, "%s[%sWARN%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_RED, ANSI_CLEAR, warnstring)
}
}
}
func (s *Stdoutput) Raw(output string) {
fmt.Fprintf(os.Stderr, "%s%s", TERMINAL_CLEAR_LINE, output)
}
func (s *Stdoutput) writeToAll(filename string, config *ffuf.Config, res []ffuf.Result) error {
var err error
var BaseFilename string = s.config.OutputFile
// Go through each type of write, adding
// the suffix to each output file.
s.config.OutputFile = BaseFilename + ".json"
err = writeJSON(s.config.OutputFile, s.config, res)
if err != nil {
s.Error(err.Error())
}
s.config.OutputFile = BaseFilename + ".ejson"
err = writeEJSON(s.config.OutputFile, s.config, res)
if err != nil {
s.Error(err.Error())
}
s.config.OutputFile = BaseFilename + ".html"
err = writeHTML(s.config.OutputFile, s.config, res)
if err != nil {
s.Error(err.Error())
}
s.config.OutputFile = BaseFilename + ".md"
err = writeMarkdown(s.config.OutputFile, s.config, res)
if err != nil {
s.Error(err.Error())
}
s.config.OutputFile = BaseFilename + ".csv"
err = writeCSV(s.config.OutputFile, s.config, res, false)
if err != nil {
s.Error(err.Error())
}
s.config.OutputFile = BaseFilename + ".ecsv"
err = writeCSV(s.config.OutputFile, s.config, res, true)
if err != nil {
s.Error(err.Error())
}
return nil
}
// SaveFile saves the current results to a file of a given type
func (s *Stdoutput) SaveFile(filename, format string) error {
var err error
if s.config.OutputSkipEmptyFile && len(s.Results) == 0 {
s.Info("No results and -or defined, output file not written.")
return err
}
switch format {
case "all":
err = s.writeToAll(filename, s.config, append(s.Results, s.CurrentResults...))
case "json":
err = writeJSON(filename, s.config, append(s.Results, s.CurrentResults...))
case "ejson":
err = writeEJSON(filename, s.config, append(s.Results, s.CurrentResults...))
case "html":
err = writeHTML(filename, s.config, append(s.Results, s.CurrentResults...))
case "md":
err = writeMarkdown(filename, s.config, append(s.Results, s.CurrentResults...))
case "csv":
err = writeCSV(filename, s.config, append(s.Results, s.CurrentResults...), false)
case "ecsv":
err = writeCSV(filename, s.config, append(s.Results, s.CurrentResults...), true)
}
return err
}
// Finalize gets run after all the ffuf jobs are completed
func (s *Stdoutput) Finalize() error {
var err error
if s.config.OutputFile != "" {
err = s.SaveFile(s.config.OutputFile, s.config.OutputFormat)
if err != nil {
s.Error(err.Error())
}
}
fmt.Fprintf(os.Stderr, "\n")
return nil
}
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)
}
inputs := make(map[string][]byte, len(resp.Request.Input))
for k, v := range resp.Request.Input {
inputs[k] = v
}
sResult := ffuf.Result{
Input: inputs,
Position: resp.Request.Position,
StatusCode: resp.StatusCode,
ContentLength: resp.ContentLength,
ContentWords: resp.ContentWords,
ContentLines: resp.ContentLines,
ContentType: resp.ContentType,
RedirectLocation: resp.GetRedirectLocation(false),
Url: resp.Request.Url,
Duration: resp.Time,
ResultFile: resp.ResultFile,
Host: resp.Request.Host,
}
s.CurrentResults = append(s.CurrentResults, sResult)
// Output the result
s.PrintResult(sResult)
}
func (s *Stdoutput) writeResultToFile(resp ffuf.Response) string {
var fileContent, fileName, filePath string
// Create directory if needed
if s.config.OutputDirectory != "" {
err := os.MkdirAll(s.config.OutputDirectory, 0750)
if err != nil {
if !os.IsExist(err) {
s.Error(err.Error())
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 := os.WriteFile(filePath, []byte(fileContent), 0640)
if err != nil {
s.Error(err.Error())
}
return fileName
}
func (s *Stdoutput) PrintResult(res ffuf.Result) {
switch {
case s.config.Json:
s.resultJson(res)
case s.config.Quiet:
s.resultQuiet(res)
case len(res.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0:
// Print a multi-line result (when using multiple input keywords and wordlists)
s.resultMultiline(res)
default:
s.resultNormal(res)
}
}
func (s *Stdoutput) prepareInputsOneLine(res ffuf.Result) string {
inputs := ""
if len(res.Input) > 1 {
for k, v := range res.Input {
if inSlice(k, s.config.CommandKeywords) {
// If we're using external command for input, display the position instead of input
inputs = fmt.Sprintf("%s%s : %s ", inputs, k, strconv.Itoa(res.Position))
} else {
inputs = fmt.Sprintf("%s%s : %s ", inputs, k, v)
}
}
} else {
for k, v := range res.Input {
if inSlice(k, s.config.CommandKeywords) {
// If we're using external command for input, display the position instead of input
inputs = strconv.Itoa(res.Position)
} else {
inputs = string(v)
}
}
}
return inputs
}
func (s *Stdoutput) resultQuiet(res ffuf.Result) {
fmt.Println(s.prepareInputsOneLine(res))
}
func (s *Stdoutput) resultMultiline(res ffuf.Result) {
var res_hdr, res_str string
res_str = "%s%s * %s: %s\n"
res_hdr = fmt.Sprintf("%s%s[Status: %d, Size: %d, Words: %d, Lines: %d, Duration: %dms]%s", TERMINAL_CLEAR_LINE, s.colorize(res.StatusCode), res.StatusCode, res.ContentLength, res.ContentWords, res.ContentLines, res.Duration.Milliseconds(), ANSI_CLEAR)
reslines := ""
if s.config.Verbose {
reslines = fmt.Sprintf("%s%s| URL | %s\n", reslines, TERMINAL_CLEAR_LINE, res.Url)
redirectLocation := res.RedirectLocation
if redirectLocation != "" {
reslines = fmt.Sprintf("%s%s| --> | %s\n", reslines, TERMINAL_CLEAR_LINE, redirectLocation)
}
}
if res.ResultFile != "" {
reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, res.ResultFile)
}
for _, k := range s.fuzzkeywords {
if inSlice(k, s.config.CommandKeywords) {
// If we're using external command for input, display the position instead of input
reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, strconv.Itoa(res.Position))
} else {
// Wordlist input
reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, res.Input[k])
}
}
fmt.Printf("%s\n%s\n", res_hdr, reslines)
}
func (s *Stdoutput) resultNormal(res ffuf.Result) {
resnormal := fmt.Sprintf("%s%s%-23s [Status: %d, Size: %d, Words: %d, Lines: %d, Duration: %dms]%s", TERMINAL_CLEAR_LINE, s.colorize(res.StatusCode), s.prepareInputsOneLine(res), res.StatusCode, res.ContentLength, res.ContentWords, res.ContentLines, res.Duration.Milliseconds(), ANSI_CLEAR)
fmt.Println(resnormal)
}
func (s *Stdoutput) resultJson(res ffuf.Result) {
resBytes, err := json.Marshal(res)
if err != nil {
s.Error(err.Error())
} else {
fmt.Fprint(os.Stderr, TERMINAL_CLEAR_LINE)
fmt.Println(string(resBytes))
}
}
func (s *Stdoutput) colorize(status int64) string {
if !s.config.Colors {
return ""
}
colorCode := ANSI_CLEAR
if status >= 200 && status < 300 {
colorCode = ANSI_GREEN
}
if status >= 300 && status < 400 {
colorCode = ANSI_BLUE
}
if status >= 400 && status < 500 {
colorCode = ANSI_YELLOW
}
if status >= 500 && status < 600 {
colorCode = ANSI_RED
}
return colorCode
}
func printOption(name []byte, value []byte) {
fmt.Fprintf(os.Stderr, " :: %-16s : %s\n", name, value)
}
func inSlice(key string, slice []string) bool {
for _, v := range slice {
if v == key {
return true
}
}
return false
}