2018-11-08 09:26:32 +00:00
|
|
|
|
package output
|
|
|
|
|
|
|
|
|
|
import (
|
2019-12-28 15:46:44 +00:00
|
|
|
|
"crypto/md5"
|
2018-11-08 09:26:32 +00:00
|
|
|
|
"fmt"
|
2019-12-28 15:46:44 +00:00
|
|
|
|
"io/ioutil"
|
2018-11-09 12:26:55 +00:00
|
|
|
|
"os"
|
2019-12-28 15:46:44 +00:00
|
|
|
|
"path"
|
2019-06-16 21:42:42 +00:00
|
|
|
|
"strconv"
|
2019-04-27 22:08:09 +00:00
|
|
|
|
"time"
|
2018-11-08 09:26:32 +00:00
|
|
|
|
|
|
|
|
|
"github.com/ffuf/ffuf/pkg/ffuf"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
BANNER_HEADER = `
|
|
|
|
|
/'___\ /'___\ /'___\
|
|
|
|
|
/\ \__/ /\ \__/ __ __ /\ \__/
|
|
|
|
|
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
|
|
|
|
|
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
|
|
|
|
|
\ \_\ \ \_\ \ \____/ \ \_\
|
|
|
|
|
\/_/ \/_/ \/___/ \/_/
|
|
|
|
|
`
|
|
|
|
|
BANNER_SEP = "________________________________________________"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Stdoutput struct {
|
2019-03-29 23:02:41 +00:00
|
|
|
|
config *ffuf.Config
|
|
|
|
|
Results []Result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Result struct {
|
2019-11-16 13:29:09 +00:00
|
|
|
|
Input map[string][]byte `json:"input"`
|
|
|
|
|
Position int `json:"position"`
|
|
|
|
|
StatusCode int64 `json:"status"`
|
|
|
|
|
ContentLength int64 `json:"length"`
|
|
|
|
|
ContentWords int64 `json:"words"`
|
|
|
|
|
ContentLines int64 `json:"lines"`
|
|
|
|
|
RedirectLocation string `json:"redirectlocation"`
|
|
|
|
|
Url string `json:"url"`
|
2019-12-28 15:46:44 +00:00
|
|
|
|
ResultFile string `json:"resultfile"`
|
2019-11-16 13:29:09 +00:00
|
|
|
|
HTMLColor string `json:"-"`
|
2018-11-08 09:26:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewStdoutput(conf *ffuf.Config) *Stdoutput {
|
|
|
|
|
var outp Stdoutput
|
|
|
|
|
outp.config = conf
|
2019-03-29 23:02:41 +00:00
|
|
|
|
outp.Results = []Result{}
|
2018-11-08 09:26:32 +00:00
|
|
|
|
return &outp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Stdoutput) Banner() error {
|
|
|
|
|
fmt.Printf("%s\n v%s\n%s\n\n", BANNER_HEADER, ffuf.VERSION, BANNER_SEP)
|
|
|
|
|
printOption([]byte("Method"), []byte(s.config.Method))
|
|
|
|
|
printOption([]byte("URL"), []byte(s.config.Url))
|
2019-11-16 15:28:34 +00:00
|
|
|
|
// 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 {
|
|
|
|
|
printOption([]byte("Output file"), []byte(s.config.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))
|
|
|
|
|
|
|
|
|
|
// 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
|
2018-11-08 09:26:32 +00:00
|
|
|
|
for _, f := range s.config.Matchers {
|
|
|
|
|
printOption([]byte("Matcher"), []byte(f.Repr()))
|
|
|
|
|
}
|
2019-11-16 15:28:34 +00:00
|
|
|
|
// Print filters
|
2018-11-08 09:26:32 +00:00
|
|
|
|
for _, f := range s.config.Filters {
|
|
|
|
|
printOption([]byte("Filter"), []byte(f.Repr()))
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%s\n\n", BANNER_SEP)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-27 22:08:09 +00:00
|
|
|
|
func (s *Stdoutput) Progress(status ffuf.Progress) {
|
2019-03-28 17:31:02 +00:00
|
|
|
|
if s.config.Quiet {
|
|
|
|
|
// No progress for quiet mode
|
|
|
|
|
return
|
2019-04-27 22:08:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dur := time.Now().Sub(status.StartedAt)
|
|
|
|
|
runningSecs := int(dur / time.Second)
|
|
|
|
|
var reqRate int
|
|
|
|
|
if runningSecs > 0 {
|
|
|
|
|
reqRate = int(status.ReqCount / runningSecs)
|
2019-03-28 17:31:02 +00:00
|
|
|
|
} else {
|
2019-04-27 22:08:09 +00:00
|
|
|
|
reqRate = 0
|
2019-03-28 17:31:02 +00:00
|
|
|
|
}
|
2019-04-27 22:08:09 +00:00
|
|
|
|
|
|
|
|
|
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] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", TERMINAL_CLEAR_LINE, status.ReqCount, status.ReqTotal, reqRate, hours, mins, secs, status.ErrorCount)
|
2019-03-28 17:31:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 12:26:55 +00:00
|
|
|
|
func (s *Stdoutput) Error(errstring string) {
|
|
|
|
|
if s.config.Quiet {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s", errstring)
|
|
|
|
|
} else {
|
2019-03-28 17:31:02 +00:00
|
|
|
|
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", TERMINAL_CLEAR_LINE, warnstring)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s[%sWARN%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_RED, ANSI_CLEAR, warnstring)
|
|
|
|
|
}
|
2018-11-09 12:26:55 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Stdoutput) Finalize() error {
|
2019-03-29 23:02:41 +00:00
|
|
|
|
var err error
|
|
|
|
|
if s.config.OutputFile != "" {
|
|
|
|
|
if s.config.OutputFormat == "json" {
|
|
|
|
|
err = writeJSON(s.config, s.Results)
|
2019-11-15 23:48:00 +00:00
|
|
|
|
} else if s.config.OutputFormat == "ejson" {
|
|
|
|
|
err = writeEJSON(s.config, s.Results)
|
2019-11-08 14:18:27 +00:00
|
|
|
|
} else if s.config.OutputFormat == "html" {
|
|
|
|
|
err = writeHTML(s.config, s.Results)
|
|
|
|
|
} else if s.config.OutputFormat == "md" {
|
|
|
|
|
err = writeMarkdown(s.config, s.Results)
|
2019-04-03 09:51:42 +00:00
|
|
|
|
} else if s.config.OutputFormat == "csv" {
|
|
|
|
|
err = writeCSV(s.config, s.Results, false)
|
|
|
|
|
} else if s.config.OutputFormat == "ecsv" {
|
|
|
|
|
err = writeCSV(s.config, s.Results, true)
|
2019-03-29 23:02:41 +00:00
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Error(fmt.Sprintf("%s", err))
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-09 12:26:55 +00:00
|
|
|
|
fmt.Fprintf(os.Stderr, "\n")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-20 17:46:43 +00:00
|
|
|
|
func (s *Stdoutput) Result(resp ffuf.Response) {
|
2019-12-28 15:46:44 +00:00
|
|
|
|
// Do we want to write request and response to a file
|
|
|
|
|
if len(s.config.OutputDirectory) > 0 {
|
|
|
|
|
resp.ResultFile = s.writeResultToFile(resp)
|
|
|
|
|
}
|
2019-04-20 17:46:43 +00:00
|
|
|
|
// Output the result
|
2018-11-08 09:26:32 +00:00
|
|
|
|
s.printResult(resp)
|
2019-04-20 17:46:43 +00:00
|
|
|
|
// Check if we need the data later
|
2019-03-29 23:02:41 +00:00
|
|
|
|
if s.config.OutputFile != "" {
|
|
|
|
|
// No need to store results if we're not going to use them later
|
2019-11-10 21:30:54 +00:00
|
|
|
|
inputs := make(map[string][]byte, 0)
|
|
|
|
|
for k, v := range resp.Request.Input {
|
|
|
|
|
inputs[k] = v
|
|
|
|
|
}
|
2019-03-29 23:02:41 +00:00
|
|
|
|
sResult := Result{
|
2019-11-16 13:29:09 +00:00
|
|
|
|
Input: inputs,
|
|
|
|
|
Position: resp.Request.Position,
|
|
|
|
|
StatusCode: resp.StatusCode,
|
|
|
|
|
ContentLength: resp.ContentLength,
|
|
|
|
|
ContentWords: resp.ContentWords,
|
|
|
|
|
ContentLines: resp.ContentLines,
|
|
|
|
|
RedirectLocation: resp.GetRedirectLocation(),
|
|
|
|
|
Url: resp.Request.Url,
|
2019-12-28 15:46:44 +00:00
|
|
|
|
ResultFile: resp.ResultFile,
|
2019-03-29 23:02:41 +00:00
|
|
|
|
}
|
|
|
|
|
s.Results = append(s.Results, sResult)
|
|
|
|
|
}
|
2018-11-08 09:26:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-28 15:46:44 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-08 09:26:32 +00:00
|
|
|
|
func (s *Stdoutput) printResult(resp ffuf.Response) {
|
|
|
|
|
if s.config.Quiet {
|
|
|
|
|
s.resultQuiet(resp)
|
|
|
|
|
} else {
|
2019-12-28 15:46:44 +00:00
|
|
|
|
if len(resp.Request.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0 {
|
2019-11-10 21:30:54 +00:00
|
|
|
|
// Print a multi-line result (when using multiple input keywords and wordlists)
|
|
|
|
|
s.resultMultiline(resp)
|
|
|
|
|
} else {
|
|
|
|
|
s.resultNormal(resp)
|
|
|
|
|
}
|
2018-11-08 09:26:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-10 21:30:54 +00:00
|
|
|
|
func (s *Stdoutput) prepareInputsOneLine(resp ffuf.Response) string {
|
|
|
|
|
inputs := ""
|
|
|
|
|
if len(resp.Request.Input) > 1 {
|
|
|
|
|
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
|
|
|
|
|
inputs = fmt.Sprintf("%s%s : %s ", inputs, k, strconv.Itoa(resp.Request.Position))
|
|
|
|
|
} else {
|
|
|
|
|
inputs = fmt.Sprintf("%s%s : %s ", inputs, k, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-16 21:42:42 +00:00
|
|
|
|
} else {
|
2019-11-10 21:30:54 +00:00
|
|
|
|
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
|
|
|
|
|
inputs = strconv.Itoa(resp.Request.Position)
|
|
|
|
|
} else {
|
|
|
|
|
inputs = string(v)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-16 21:42:42 +00:00
|
|
|
|
}
|
2019-11-10 21:30:54 +00:00
|
|
|
|
return inputs
|
2018-11-08 09:26:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-10 21:30:54 +00:00
|
|
|
|
func (s *Stdoutput) resultQuiet(resp ffuf.Response) {
|
|
|
|
|
fmt.Println(s.prepareInputsOneLine(resp))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Stdoutput) resultMultiline(resp ffuf.Response) {
|
|
|
|
|
var res_hdr, res_str string
|
2019-11-15 23:48:00 +00:00
|
|
|
|
res_str = "%s%s * %s: %s\n"
|
2019-11-16 14:32:11 +00:00
|
|
|
|
res_hdr = fmt.Sprintf("%s[Status: %d, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, resp.StatusCode, resp.ContentLength, resp.ContentWords, resp.ContentLines)
|
2019-11-15 23:48:00 +00:00
|
|
|
|
res_hdr = s.colorize(res_hdr, resp.StatusCode)
|
|
|
|
|
reslines := ""
|
2019-11-16 14:32:11 +00:00
|
|
|
|
if s.config.Verbose {
|
|
|
|
|
reslines = fmt.Sprintf("%s%s| URL | %s\n", reslines, TERMINAL_CLEAR_LINE, resp.Request.Url)
|
|
|
|
|
redirectLocation := resp.GetRedirectLocation()
|
|
|
|
|
if redirectLocation != "" {
|
|
|
|
|
reslines = fmt.Sprintf("%s%s| --> | %s\n", reslines, TERMINAL_CLEAR_LINE, redirectLocation)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-28 15:46:44 +00:00
|
|
|
|
if resp.ResultFile != "" {
|
|
|
|
|
reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, resp.ResultFile)
|
|
|
|
|
}
|
2019-11-10 21:30:54 +00:00
|
|
|
|
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
|
2019-11-15 23:48:00 +00:00
|
|
|
|
reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, strconv.Itoa(resp.Request.Position))
|
2019-11-10 21:30:54 +00:00
|
|
|
|
} else {
|
|
|
|
|
// Wordlist input
|
2019-11-15 23:48:00 +00:00
|
|
|
|
reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, v)
|
2019-11-10 21:30:54 +00:00
|
|
|
|
}
|
2019-06-16 21:42:42 +00:00
|
|
|
|
}
|
2019-11-16 14:32:11 +00:00
|
|
|
|
fmt.Printf("%s\n%s\n", res_hdr, reslines)
|
2019-11-10 21:30:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Stdoutput) resultNormal(resp ffuf.Response) {
|
|
|
|
|
var res_str string
|
2019-11-16 14:32:11 +00:00
|
|
|
|
res_str = fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, s.prepareInputsOneLine(resp), s.colorize(fmt.Sprintf("%d", resp.StatusCode), resp.StatusCode), resp.ContentLength, resp.ContentWords, resp.ContentLines)
|
2019-11-10 21:30:54 +00:00
|
|
|
|
fmt.Println(res_str)
|
2019-10-14 08:29:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-10 21:30:54 +00:00
|
|
|
|
func (s *Stdoutput) colorize(input string, status int64) string {
|
2018-11-09 13:21:23 +00:00
|
|
|
|
if !s.config.Colors {
|
2019-11-10 21:30:54 +00:00
|
|
|
|
return fmt.Sprintf("%s", input)
|
2018-11-09 13:21:23 +00:00
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
2019-11-10 21:30:54 +00:00
|
|
|
|
return fmt.Sprintf("%s%s%s", colorCode, input, ANSI_CLEAR)
|
2018-11-09 13:21:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-08 09:26:32 +00:00
|
|
|
|
func printOption(name []byte, value []byte) {
|
2019-11-16 15:28:34 +00:00
|
|
|
|
fmt.Printf(" :: %-16s : %s\n", name, value)
|
2018-11-08 09:26:32 +00:00
|
|
|
|
}
|
2019-11-10 21:30:54 +00:00
|
|
|
|
|
|
|
|
|
func inSlice(key string, slice []string) bool {
|
|
|
|
|
for _, v := range slice {
|
|
|
|
|
if v == key {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|