From 9bddff79b961804d899c68d8eea2d4c6241a011b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 2 Feb 2023 11:51:11 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 4 + go.mod | 5 +- go.sum | 15 +++ help.go | 6 +- main.go | 70 ++++++++-- pkg/ffuf/config.go | 8 ++ pkg/ffuf/configmarshaller.go | 127 +++++++++++++++++++ pkg/ffuf/{version.go => constants.go} | 7 + pkg/ffuf/history.go | 90 +++++++++++++ pkg/ffuf/interfaces.go | 19 ++- pkg/ffuf/job.go | 14 ++ pkg/ffuf/optionsparser.go | 176 ++++++++++++++------------ pkg/ffuf/util.go | 39 ++++-- pkg/input/command.go | 19 ++- pkg/input/input.go | 49 +++++-- pkg/input/wordlist.go | 29 +++-- pkg/output/file_json.go | 6 +- pkg/output/stdout.go | 14 +- pkg/runner/simple.go | 35 ++++- 19 files changed, 578 insertions(+), 154 deletions(-) create mode 100644 pkg/ffuf/configmarshaller.go rename pkg/ffuf/{version.go => constants.go} (50%) create mode 100644 pkg/ffuf/history.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 9133bec..0f73933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ ## Changelog - master - New + - Added a new, dynamic keyword `FFUFHASH` that generates hash from job configuration and wordlist position to map blind payloads back to the initial request. + - New command line parameter for searching a hash: `-search FFUFHASH` - Changed + - Multiline output prints out alphabetically sorted by keyword + - Default configuration directories now follow `XDG_CONFIG_HOME` variable (less spam in your home directory) - Fixed issue with autocalibration of line & words filter - Fixed issue with `-json` when used in conjunction with silent mode diff --git a/go.mod b/go.mod index e33c141..72121e9 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/ffuf/ffuf go 1.13 -require github.com/pelletier/go-toml v1.8.1 +require ( + github.com/adrg/xdg v0.4.0 + github.com/pelletier/go-toml v1.8.1 +) diff --git a/go.sum b/go.sum index 6537354..64eca98 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,18 @@ +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/help.go b/help.go index 4d429c4..ae96935 100644 --- a/help.go +++ b/help.go @@ -16,7 +16,7 @@ type UsageSection struct { ExpectedFlags []string } -//PrintSection prints out the section name, description and each of the flags +// PrintSection prints out the section name, description and each of the flags func (u *UsageSection) PrintSection(max_length int, extended bool) { // Do not print if extended usage not requested and section marked as hidden if !extended && u.Hidden { @@ -35,7 +35,7 @@ type UsageFlag struct { Default string } -//PrintFlag prints out the flag name, usage string and default value +// PrintFlag prints out the flag name, usage string and default value func (f *UsageFlag) PrintFlag(max_length int) { // Create format string, used for padding format := fmt.Sprintf(" -%%-%ds %%s", max_length) @@ -61,7 +61,7 @@ func Usage() { Description: "", Flags: make([]UsageFlag, 0), Hidden: false, - ExpectedFlags: []string{"ac", "acc", "ack", "ach", "acs", "c", "config", "json", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "s", "sa", "se", "sf", "t", "v", "V"}, + ExpectedFlags: []string{"ac", "acc", "ack", "ach", "acs", "c", "config", "json", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "search", "s", "sa", "se", "sf", "t", "v", "V"}, } u_compat := UsageSection{ Name: "COMPATIBILITY OPTIONS", diff --git a/main.go b/main.go index f51663f..4ed928f 100644 --- a/main.go +++ b/main.go @@ -4,17 +4,17 @@ import ( "context" "flag" "fmt" - "github.com/ffuf/ffuf/pkg/filter" - "io/ioutil" - "log" - "os" - "strings" - "github.com/ffuf/ffuf/pkg/ffuf" + "github.com/ffuf/ffuf/pkg/filter" "github.com/ffuf/ffuf/pkg/input" "github.com/ffuf/ffuf/pkg/interactive" "github.com/ffuf/ffuf/pkg/output" "github.com/ffuf/ffuf/pkg/runner" + "io" + "log" + "os" + "strings" + "time" ) type multiStringFlag []string @@ -45,7 +45,7 @@ func (m *wordlistFlag) Set(value string) error { return nil } -//ParseFlags parses the command line flags and (re)populates the ConfigOptions struct +// ParseFlags parses the command line flags and (re)populates the ConfigOptions struct func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions { var ignored bool var cookies, autocalibrationstrings, headers, inputcommands multiStringFlag @@ -96,6 +96,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions { flag.StringVar(&opts.Filter.Time, "ft", opts.Filter.Time, "Filter by number of milliseconds to the first response byte, either greater or less than. EG: >100 or <100") flag.StringVar(&opts.Filter.Words, "fw", opts.Filter.Words, "Filter by amount of words in response. Comma separated list of word counts and ranges") flag.StringVar(&opts.General.Delay, "p", opts.General.Delay, "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"") + flag.StringVar(&opts.General.Searchhash, "search", opts.General.Searchhash, "Search for a FFUFHASH payload from ffuf history") flag.StringVar(&opts.HTTP.Data, "d", opts.HTTP.Data, "POST data") flag.StringVar(&opts.HTTP.Data, "data", opts.HTTP.Data, "POST data (alias of -d)") flag.StringVar(&opts.HTTP.Data, "data-ascii", opts.HTTP.Data, "POST data (alias of -d)") @@ -142,13 +143,37 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions { func main() { var err, optserr error - + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // prepare the default config options from default config file var opts *ffuf.ConfigOptions opts, optserr = ffuf.ReadDefaultConfig() opts = ParseFlags(opts) + // Handle searchhash functionality and exit + if opts.General.Searchhash != "" { + coptions, pos, err := ffuf.SearchHash(opts.General.Searchhash) + if err != nil { + fmt.Printf("[ERR] %s\n", err) + os.Exit(1) + } + if len(coptions) > 0 { + fmt.Printf("Request candidate(s) for hash %s\n", opts.General.Searchhash) + } + for _, copt := range coptions { + conf, err := ffuf.ConfigFromOptions(&copt.ConfigOptions, ctx, cancel) + if err != nil { + continue + } + printSearchResults(conf, pos, copt.Time, opts.General.Searchhash) + } + if err != nil { + fmt.Printf("[ERR] %s\n", err) + } + os.Exit(0) + } + if opts.General.ShowVersion { fmt.Printf("ffuf version: %s\n", ffuf.Version()) os.Exit(0) @@ -157,13 +182,13 @@ func main() { f, err := os.OpenFile(opts.Output.DebugLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Fprintf(os.Stderr, "Disabling logging, encountered error(s): %s\n", err) - log.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) } else { log.SetOutput(f) defer f.Close() } } else { - log.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) } if optserr != nil { log.Printf("Error while opening default config file: %s", optserr) @@ -183,9 +208,7 @@ func main() { opts = ParseFlags(opts) } - // Prepare context and set up Config struct - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + // Set up Config struct conf, err := ffuf.ConfigFromOptions(opts, ctx, cancel) if err != nil { fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err) @@ -193,6 +216,7 @@ func main() { fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err) os.Exit(1) } + job, err := prepareJob(conf) if err != nil { fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err) @@ -335,3 +359,23 @@ func SetupFilters(parseOpts *ffuf.ConfigOptions, conf *ffuf.Config) error { } return errs.ErrorOrNil() } + +func printSearchResults(conf *ffuf.Config, pos int, exectime time.Time, hash string) { + inp, err := input.NewInputProvider(conf) + if err.ErrorOrNil() != nil { + fmt.Printf("-------------------------------------------\n") + fmt.Println("Encountered error that prevents reproduction of the request:") + fmt.Println(err.ErrorOrNil()) + return + } + inp.SetPosition(pos) + inputdata := inp.Value() + inputdata["FFUFHASH"] = []byte(hash) + basereq := ffuf.BaseRequest(conf) + dummyrunner := runner.NewRunnerByName("simple", conf, false) + ffufreq, _ := dummyrunner.Prepare(inputdata, &basereq) + rawreq, _ := dummyrunner.Dump(&ffufreq) + fmt.Printf("-------------------------------------------\n") + fmt.Printf("ffuf job started at: %s\n\n", exectime.Format(time.RFC3339)) + fmt.Printf("%s\n", string(rawreq)) +} diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go index e2f21e3..16cf325 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -17,6 +17,7 @@ type Config struct { ConfigFile string `json:"configfile"` Context context.Context `json:"-"` Data string `json:"postdata"` + Debuglog string `json:"debuglog"` Delay optRange `json:"delay"` DirSearchCompat bool `json:"dirsearch_compatibility"` Extensions []string `json:"extensions"` @@ -48,6 +49,8 @@ type Config struct { RecursionDepth int `json:"recursion_depth"` RecursionStrategy string `json:"recursion_strategy"` ReplayProxyURL string `json:"replayproxyurl"` + RequestFile string `json:"requestfile"` + RequestProto string `json:"requestproto"` SNI string `json:"sni"` StopOn403 bool `json:"stop_403"` StopOnAll bool `json:"stop_all"` @@ -56,6 +59,7 @@ type Config struct { Timeout int `json:"timeout"` Url string `json:"url"` Verbose bool `json:"verbose"` + Wordlists []string `json:"wordlists"` Http2 bool `json:"http2"` } @@ -75,6 +79,7 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config { conf.Context = ctx conf.Cancel = cancel conf.Data = "" + conf.Debuglog = "" conf.Delay = optRange{0, 0, false, false} conf.DirSearchCompat = false conf.Extensions = make([]string, 0) @@ -99,6 +104,8 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config { conf.Recursion = false conf.RecursionDepth = 0 conf.RecursionStrategy = "default" + conf.RequestFile = "" + conf.RequestProto = "https" conf.SNI = "" conf.StopOn403 = false conf.StopOnAll = false @@ -106,6 +113,7 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config { conf.Timeout = 10 conf.Url = "" conf.Verbose = false + conf.Wordlists = []string{} conf.Http2 = false return conf } diff --git a/pkg/ffuf/configmarshaller.go b/pkg/ffuf/configmarshaller.go new file mode 100644 index 0000000..7b30eec --- /dev/null +++ b/pkg/ffuf/configmarshaller.go @@ -0,0 +1,127 @@ +package ffuf + +import ( + "fmt" + "strings" +) + +func (c *Config) ToOptions() ConfigOptions { + o := ConfigOptions{} + // HTTP options + o.HTTP.Cookies = []string{} + o.HTTP.Data = c.Data + o.HTTP.FollowRedirects = c.FollowRedirects + o.HTTP.Headers = make([]string, 0) + for k, v := range c.Headers { + o.HTTP.Headers = append(o.HTTP.Headers, fmt.Sprintf("%s: %s", k, v)) + } + o.HTTP.IgnoreBody = c.IgnoreBody + o.HTTP.Method = c.Method + o.HTTP.ProxyURL = c.ProxyURL + o.HTTP.Recursion = c.Recursion + o.HTTP.RecursionDepth = c.RecursionDepth + o.HTTP.RecursionStrategy = c.RecursionStrategy + o.HTTP.ReplayProxyURL = c.ReplayProxyURL + o.HTTP.SNI = c.SNI + o.HTTP.Timeout = c.Timeout + o.HTTP.URL = c.Url + o.HTTP.Http2 = c.Http2 + + o.General.AutoCalibration = c.AutoCalibration + o.General.AutoCalibrationKeyword = c.AutoCalibrationKeyword + o.General.AutoCalibrationPerHost = c.AutoCalibrationPerHost + o.General.AutoCalibrationStrategy = c.AutoCalibrationStrategy + o.General.AutoCalibrationStrings = c.AutoCalibrationStrings + o.General.Colors = c.Colors + o.General.ConfigFile = "" + if c.Delay.HasDelay { + if c.Delay.IsRange { + o.General.Delay = fmt.Sprintf("%.2f-%.2f", c.Delay.Min, c.Delay.Max) + } else { + o.General.Delay = fmt.Sprintf("%.2f", c.Delay.Min) + } + } else { + o.General.Delay = "" + } + o.General.Json = c.Json + o.General.MaxTime = c.MaxTime + o.General.MaxTimeJob = c.MaxTimeJob + o.General.Noninteractive = c.Noninteractive + o.General.Quiet = c.Quiet + o.General.Rate = int(c.Rate) + o.General.StopOn403 = c.StopOn403 + o.General.StopOnAll = c.StopOnAll + o.General.StopOnErrors = c.StopOnErrors + o.General.Threads = c.Threads + o.General.Verbose = c.Verbose + + o.Input.DirSearchCompat = c.DirSearchCompat + o.Input.Extensions = strings.Join(c.Extensions, ",") + o.Input.IgnoreWordlistComments = c.IgnoreWordlistComments + o.Input.InputMode = c.InputMode + o.Input.InputNum = c.InputNum + o.Input.InputShell = c.InputShell + o.Input.Inputcommands = []string{} + for _, v := range c.InputProviders { + if v.Name == "command" { + o.Input.Inputcommands = append(o.Input.Inputcommands, fmt.Sprintf("%s:%s", v.Value, v.Keyword)) + } + } + o.Input.Request = c.RequestFile + o.Input.RequestProto = c.RequestProto + o.Input.Wordlists = c.Wordlists + + o.Output.DebugLog = c.Debuglog + o.Output.OutputDirectory = c.OutputDirectory + o.Output.OutputFile = c.OutputFile + o.Output.OutputFormat = c.OutputFormat + o.Output.OutputSkipEmptyFile = c.OutputSkipEmptyFile + + o.Filter.Mode = c.FilterMode + o.Filter.Lines = "" + o.Filter.Regexp = "" + o.Filter.Size = "" + o.Filter.Status = "" + o.Filter.Time = "" + o.Filter.Words = "" + for name, filter := range c.MatcherManager.GetFilters() { + switch name { + case "line": + o.Filter.Lines = filter.Repr() + case "regexp": + o.Filter.Regexp = filter.Repr() + case "size": + o.Filter.Size = filter.Repr() + case "status": + o.Filter.Status = filter.Repr() + case "time": + o.Filter.Time = filter.Repr() + case "words": + o.Filter.Words = filter.Repr() + } + } + o.Matcher.Mode = c.MatcherMode + o.Matcher.Lines = "" + o.Matcher.Regexp = "" + o.Matcher.Size = "" + o.Matcher.Status = "" + o.Matcher.Time = "" + o.Matcher.Words = "" + for name, filter := range c.MatcherManager.GetMatchers() { + switch name { + case "line": + o.Matcher.Lines = filter.Repr() + case "regexp": + o.Matcher.Regexp = filter.Repr() + case "size": + o.Matcher.Size = filter.Repr() + case "status": + o.Matcher.Status = filter.Repr() + case "time": + o.Matcher.Time = filter.Repr() + case "words": + o.Matcher.Words = filter.Repr() + } + } + return o +} diff --git a/pkg/ffuf/version.go b/pkg/ffuf/constants.go similarity index 50% rename from pkg/ffuf/version.go rename to pkg/ffuf/constants.go index eee0c07..54d8ca1 100644 --- a/pkg/ffuf/version.go +++ b/pkg/ffuf/constants.go @@ -1,8 +1,15 @@ package ffuf +import ( + "github.com/adrg/xdg" + "path/filepath" +) + var ( //VERSION holds the current version number VERSION = "1.5.0" //VERSION_APPENDIX holds additional version definition VERSION_APPENDIX = "-dev" + CONFIGDIR = filepath.Join(xdg.ConfigHome, "ffuf") + HISTORYDIR = filepath.Join(CONFIGDIR, "history") ) diff --git a/pkg/ffuf/history.go b/pkg/ffuf/history.go new file mode 100644 index 0000000..9e896ff --- /dev/null +++ b/pkg/ffuf/history.go @@ -0,0 +1,90 @@ +package ffuf + +import ( + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +type ConfigOptionsHistory struct { + ConfigOptions + Time time.Time `json:"time"` +} + +func WriteHistoryEntry(conf *Config) (string, error) { + options := ConfigOptionsHistory{ + ConfigOptions: conf.ToOptions(), + Time: time.Now(), + } + jsonoptions, err := json.Marshal(options) + if err != nil { + return "", err + } + hashstr := calculateHistoryHash(jsonoptions) + err = createConfigDir(filepath.Join(HISTORYDIR, hashstr)) + if err != nil { + return "", err + } + err = os.WriteFile(filepath.Join(HISTORYDIR, hashstr, "options"), jsonoptions, 0640) + return hashstr, err +} + +func calculateHistoryHash(options []byte) string { + return fmt.Sprintf("%x", sha256.Sum256(options)) +} + +func SearchHash(hash string) ([]ConfigOptionsHistory, int, error) { + coptions := make([]ConfigOptionsHistory, 0) + if len(hash) < 6 { + return coptions, 0, errors.New("bad FFUFHASH value") + } + historypart := hash[0:5] + position, err := strconv.ParseInt(hash[5:], 16, 32) + if err != nil { + return coptions, 0, errors.New("bad positional value in FFUFHASH") + } + all_dirs, err := os.ReadDir(HISTORYDIR) + if err != nil { + return coptions, 0, err + } + matched_dirs := make([]string, 0) + for _, filename := range all_dirs { + if filename.IsDir() { + if strings.HasPrefix(strings.ToLower(filename.Name()), strings.ToLower(historypart)) { + matched_dirs = append(matched_dirs, filename.Name()) + } + } + } + for _, dirname := range matched_dirs { + copts, err := configFromHistory(filepath.Join(HISTORYDIR, dirname)) + if err != nil { + continue + } + coptions = append(coptions, copts) + + } + return coptions, int(position), err +} + +func configFromHistory(dirname string) (ConfigOptionsHistory, error) { + jsonOptions, err := os.ReadFile(filepath.Join(dirname, "options")) + if err != nil { + return ConfigOptionsHistory{}, err + } + tmpOptions := ConfigOptionsHistory{} + err = json.Unmarshal(jsonOptions, &tmpOptions) + return tmpOptions, err + /* + // These are dummy values for this use case + ctx, cancel := context.WithCancel(context.Background()) + conf, err := ConfigFromOptions(&tmpOptions.ConfigOptions, ctx, cancel) + job.Input, errs = input.NewInputProvider(conf) + return conf, tmpOptions.Time, err + */ +} diff --git a/pkg/ffuf/interfaces.go b/pkg/ffuf/interfaces.go index 6879992..94ef22c 100644 --- a/pkg/ffuf/interfaces.go +++ b/pkg/ffuf/interfaces.go @@ -1,8 +1,10 @@ package ffuf -import "time" +import ( + "time" +) -//MatcherManager provides functions for managing matchers and filters +// MatcherManager provides functions for managing matchers and filters type MatcherManager interface { SetCalibrated(calibrated bool) SetCalibratedForHost(host string, calibrated bool) @@ -17,36 +19,39 @@ type MatcherManager interface { Calibrated() bool } -//FilterProvider is a generic interface for both Matchers and Filters +// FilterProvider is a generic interface for both Matchers and Filters type FilterProvider interface { Filter(response *Response) (bool, error) Repr() string ReprVerbose() string } -//RunnerProvider is an interface for request executors +// RunnerProvider is an interface for request executors type RunnerProvider interface { Prepare(input map[string][]byte, basereq *Request) (Request, error) Execute(req *Request) (Response, error) + Dump(req *Request) ([]byte, error) } -//InputProvider interface handles the input data for RunnerProvider +// InputProvider interface handles the input data for RunnerProvider type InputProvider interface { ActivateKeywords([]string) AddProvider(InputProviderConfig) error Keywords() []string Next() bool Position() int + SetPosition(int) Reset() Value() map[string][]byte Total() int } -//InternalInputProvider interface handles providing input data to InputProvider +// InternalInputProvider interface handles providing input data to InputProvider type InternalInputProvider interface { Keyword() string Next() bool Position() int + SetPosition(int) ResetPosition() IncrementPosition() Value() []byte @@ -56,7 +61,7 @@ type InternalInputProvider interface { Disable() } -//OutputProvider is responsible of providing output from the RunnerProvider +// OutputProvider is responsible of providing output from the RunnerProvider type OutputProvider interface { Banner() Finalize() error diff --git a/pkg/ffuf/job.go b/pkg/ffuf/job.go index 768c231..8dd5612 100644 --- a/pkg/ffuf/job.go +++ b/pkg/ffuf/job.go @@ -19,6 +19,7 @@ type Job struct { Runner RunnerProvider ReplayRunner RunnerProvider Output OutputProvider + Jobhash string Counter int ErrorCounter int SpuriousErrorCounter int @@ -180,6 +181,7 @@ func (j *Job) prepareQueueJob() { //And activate / disable inputproviders as needed j.Input.ActivateKeywords(found_kws) j.queuepos += 1 + j.Jobhash, _ = WriteHistoryEntry(j.Config) } // SkipQueue allows to skip the current job and advance to the next queued recursion job @@ -255,6 +257,8 @@ func (j *Job) startExecution() { <-j.Rate.RateLimiter.C nextInput := j.Input.Value() nextPosition := j.Input.Position() + // Add FFUFHASH and its value + nextInput["FFUFHASH"] = j.ffufHash(nextPosition) wg.Add(1) j.Counter++ @@ -377,6 +381,16 @@ func (j *Job) isMatch(resp Response) bool { return true } +func (j *Job) ffufHash(pos int) []byte { + hashstring := "" + r := []rune(j.Jobhash) + if len(r) > 5 { + hashstring = string(r[:5]) + } + hashstring += fmt.Sprintf("%x", pos) + return []byte(hashstring) +} + func (j *Job) runTask(input map[string][]byte, position int, retried bool) { basereq := j.queuejobs[j.queuepos-1].req req, err := j.Runner.Prepare(input, &basereq) diff --git a/pkg/ffuf/optionsparser.go b/pkg/ffuf/optionsparser.go index 6d2dc98..7df672c 100644 --- a/pkg/ffuf/optionsparser.go +++ b/pkg/ffuf/optionsparser.go @@ -4,7 +4,7 @@ import ( "bufio" "context" "fmt" - "io/ioutil" + "io" "net/textproto" "net/url" "os" @@ -17,97 +17,98 @@ import ( ) type ConfigOptions struct { - Filter FilterOptions - General GeneralOptions - HTTP HTTPOptions - Input InputOptions - Matcher MatcherOptions - Output OutputOptions + Filter FilterOptions `json:"filters"` + General GeneralOptions `json:"general"` + HTTP HTTPOptions `json:"http"` + Input InputOptions `json:"input"` + Matcher MatcherOptions `json:"matchers"` + Output OutputOptions `json:"output"` } type HTTPOptions struct { - Cookies []string - Data string - FollowRedirects bool - Headers []string - IgnoreBody bool - Method string - ProxyURL string - Recursion bool - RecursionDepth int - RecursionStrategy string - ReplayProxyURL string - SNI string - Timeout int - URL string - Http2 bool + Cookies []string `json:"-"` // this is appended in headers + Data string `json:"data"` + FollowRedirects bool `json:"follow_redirects"` + Headers []string `json:"headers"` + IgnoreBody bool `json:"ignore_body"` + Method string `json:"method"` + ProxyURL string `json:"proxy_url"` + Recursion bool `json:"recursion"` + RecursionDepth int `json:"recursion_depth"` + RecursionStrategy string `json:"recursion_strategy"` + ReplayProxyURL string `json:"replay_proxy_url"` + SNI string `json:"sni"` + Timeout int `json:"timeout"` + URL string `json:"url"` + Http2 bool `json:"http2"` } type GeneralOptions struct { - AutoCalibration bool - AutoCalibrationKeyword string - AutoCalibrationPerHost bool - AutoCalibrationStrategy string - AutoCalibrationStrings []string - Colors bool - ConfigFile string `toml:"-"` - Delay string - Json bool - MaxTime int - MaxTimeJob int - Noninteractive bool - Quiet bool - Rate int - ShowVersion bool `toml:"-"` - StopOn403 bool - StopOnAll bool - StopOnErrors bool - Threads int - Verbose bool + AutoCalibration bool `json:"autocalibration"` + AutoCalibrationKeyword string `json:"autocalibration_keyword"` + AutoCalibrationPerHost bool `json:"autocalibration_per_host"` + AutoCalibrationStrategy string `json:"autocalibration_strategy"` + AutoCalibrationStrings []string `json:"autocalibration_strings"` + Colors bool `json:"colors"` + ConfigFile string `toml:"-" json:"config_file"` + Delay string `json:"delay"` + Json bool `json:"json"` + MaxTime int `json:"maxtime"` + MaxTimeJob int `json:"maxtime_job"` + Noninteractive bool `json:"noninteractive"` + Quiet bool `json:"quiet"` + Rate int `json:"rate"` + Searchhash string `json:"-"` + ShowVersion bool `toml:"-" json:"-"` + StopOn403 bool `json:"stop_on_403"` + StopOnAll bool `json:"stop_on_all"` + StopOnErrors bool `json:"stop_on_errors"` + Threads int `json:"threads"` + Verbose bool `json:"verbose"` } type InputOptions struct { - DirSearchCompat bool - Extensions string - IgnoreWordlistComments bool - InputMode string - InputNum int - InputShell string - Inputcommands []string - Request string - RequestProto string - Wordlists []string + DirSearchCompat bool `json:"dirsearch_compat"` + Extensions string `json:"extensions"` + IgnoreWordlistComments bool `json:"ignore_wordlist_comments"` + InputMode string `json:"input_mode"` + InputNum int `json:"input_num"` + InputShell string `json:"input_shell"` + Inputcommands []string `json:"input_commands"` + Request string `json:"request_file"` + RequestProto string `json:"request_proto"` + Wordlists []string `json:"wordlists"` } type OutputOptions struct { - DebugLog string - OutputDirectory string - OutputFile string - OutputFormat string - OutputSkipEmptyFile bool + DebugLog string `json:"debug_log"` + OutputDirectory string `json:"output_directory"` + OutputFile string `json:"output_file"` + OutputFormat string `json:"output_format"` + OutputSkipEmptyFile bool `json:"output_skip_empty"` } type FilterOptions struct { - Mode string - Lines string - Regexp string - Size string - Status string - Time string - Words string + Mode string `json:"mode"` + Lines string `json:"lines"` + Regexp string `json:"regexp"` + Size string `json:"size"` + Status string `json:"status"` + Time string `json:"time"` + Words string `json:"words"` } type MatcherOptions struct { - Mode string - Lines string - Regexp string - Size string - Status string - Time string - Words string + Mode string `json:"mode"` + Lines string `json:"lines"` + Regexp string `json:"regexp"` + Size string `json:"size"` + Status string `json:"status"` + Time string `json:"time"` + Words string `json:"words"` } -//NewConfigOptions returns a newly created ConfigOptions struct with default values +// NewConfigOptions returns a newly created ConfigOptions struct with default values func NewConfigOptions() *ConfigOptions { c := &ConfigOptions{} c.Filter.Mode = "or" @@ -128,6 +129,7 @@ func NewConfigOptions() *ConfigOptions { c.General.Noninteractive = false c.General.Quiet = false c.General.Rate = 0 + c.General.Searchhash = "" c.General.ShowVersion = false c.General.StopOn403 = false c.General.StopOnAll = false @@ -169,7 +171,7 @@ func NewConfigOptions() *ConfigOptions { return c } -//ConfigFromOptions parses the values in ConfigOptions struct, ensures that the values are sane, +// ConfigFromOptions parses the values in ConfigOptions struct, ensures that the values are sane, // and creates a Config struct out of them. func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel context.CancelFunc) (*Config, error) { //TODO: refactor in a proper flag library that can handle things like required flags @@ -220,6 +222,7 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con } } + tmpWordlists := make([]string, 0) for _, v := range parseOpts.Input.Wordlists { var wl []string if runtime.GOOS == "windows" { @@ -243,6 +246,11 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con } else { wl = strings.SplitN(v, ":", 2) } + // Try to use absolute paths for wordlists + fullpath, err := filepath.Abs(wl[0]) + if err == nil { + wl[0] = fullpath + } if len(wl) == 2 { if conf.InputMode == "sniper" { errs.Add(fmt.Errorf("sniper mode does not support wordlist keywords")) @@ -261,7 +269,9 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con Template: template, }) } + tmpWordlists = append(tmpWordlists, strings.Join(wl, ":")) } + conf.Wordlists = tmpWordlists for _, v := range parseOpts.Input.Inputcommands { ic := strings.SplitN(v, ":", 2) @@ -535,6 +545,8 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con } func parseRawRequest(parseOpts *ConfigOptions, conf *Config) error { + conf.RequestFile = parseOpts.Input.Request + conf.RequestProto = parseOpts.Input.RequestProto file, err := os.Open(parseOpts.Input.Request) if err != nil { return fmt.Errorf("could not open request file: %s", err) @@ -589,7 +601,7 @@ func parseRawRequest(parseOpts *ConfigOptions, conf *Config) error { } // Set the request body - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return fmt.Errorf("could not read request body: %s", err) } @@ -669,7 +681,7 @@ func templatePresent(template string, conf *Config) bool { func ReadConfig(configFile string) (*ConfigOptions, error) { conf := NewConfigOptions() - configData, err := ioutil.ReadFile(configFile) + configData, err := os.ReadFile(configFile) if err == nil { err = toml.Unmarshal(configData, conf) } @@ -677,10 +689,14 @@ func ReadConfig(configFile string) (*ConfigOptions, error) { } func ReadDefaultConfig() (*ConfigOptions, error) { - userhome, err := os.UserHomeDir() - if err != nil { - return NewConfigOptions(), err + // Try to create configuration directory, ignore the potential error + _ = CheckOrCreateConfigDir() + conffile := filepath.Join(CONFIGDIR, ".ffufrc") + if !FileExists(conffile) { + userhome, err := os.UserHomeDir() + if err == nil { + conffile = filepath.Join(userhome, ".ffufrc") + } } - defaultconf := filepath.Join(userhome, ".ffufrc") - return ReadConfig(defaultconf) + return ReadConfig(conffile) } diff --git a/pkg/ffuf/util.go b/pkg/ffuf/util.go index 183c635..8b136cb 100644 --- a/pkg/ffuf/util.go +++ b/pkg/ffuf/util.go @@ -1,6 +1,7 @@ package ffuf import ( + "errors" "fmt" "math/rand" "net/url" @@ -8,10 +9,10 @@ import ( "strings" ) -//used for random string generation in calibration function +// used for random string generation in calibration function var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") -//RandomString returns a random string of length of parameter n +// RandomString returns a random string of length of parameter n func RandomString(n int) string { s := make([]rune, n) for i := range s { @@ -20,7 +21,7 @@ func RandomString(n int) string { return string(s) } -//UniqStringSlice returns an unordered slice of unique strings. The duplicates are dropped +// UniqStringSlice returns an unordered slice of unique strings. The duplicates are dropped func UniqStringSlice(inslice []string) []string { found := map[string]bool{} @@ -34,8 +35,8 @@ func UniqStringSlice(inslice []string) []string { return ret } -//FileExists checks if the filepath exists and is not a directory. -//Returns false in case it's not possible to describe the named file. +// FileExists checks if the filepath exists and is not a directory. +// Returns false in case it's not possible to describe the named file. func FileExists(path string) bool { md, err := os.Stat(path) if err != nil { @@ -45,7 +46,7 @@ func FileExists(path string) bool { return !md.IsDir() } -//RequestContainsKeyword checks if a keyword is present in any field of a request +// RequestContainsKeyword checks if a keyword is present in any field of a request func RequestContainsKeyword(req Request, kw string) bool { if strings.Contains(req.Host, kw) { return true @@ -67,7 +68,7 @@ func RequestContainsKeyword(req Request, kw string) bool { return false } -//HostURLFromRequest gets a host + path without the filename or last part of the URL path +// HostURLFromRequest gets a host + path without the filename or last part of the URL path func HostURLFromRequest(req Request) string { u, _ := url.Parse(req.Url) u.Host = req.Host @@ -76,7 +77,29 @@ func HostURLFromRequest(req Request) string { return u.Host + trimpath } -//Version returns the ffuf version string +// Version returns the ffuf version string func Version() string { return fmt.Sprintf("%s%s", VERSION, VERSION_APPENDIX) } + +func CheckOrCreateConfigDir() error { + var err error + err = createConfigDir(CONFIGDIR) + if err != nil { + return err + } + err = createConfigDir(HISTORYDIR) + return err +} + +func createConfigDir(path string) error { + _, err := os.Stat(path) + if err != nil { + var pError *os.PathError + if errors.As(err, &pError) { + return os.MkdirAll(path, 0750) + } + return err + } + return nil +} diff --git a/pkg/input/command.go b/pkg/input/command.go index c6aa059..3d290cf 100644 --- a/pkg/input/command.go +++ b/pkg/input/command.go @@ -34,32 +34,37 @@ func NewCommandInput(keyword string, value string, conf *ffuf.Config) (*CommandI return &cmd, nil } -//Keyword returns the keyword assigned to this InternalInputProvider +// Keyword returns the keyword assigned to this InternalInputProvider func (c *CommandInput) Keyword() string { return c.keyword } -//Position will return the current position in the input list +// Position will return the current position in the input list func (c *CommandInput) Position() int { return c.count } -//ResetPosition will reset the current position of the InternalInputProvider +// SetPosition will set the current position of the inputprovider +func (c *CommandInput) SetPosition(pos int) { + c.count = pos +} + +// ResetPosition will reset the current position of the InternalInputProvider func (c *CommandInput) ResetPosition() { c.count = 0 } -//IncrementPosition increments the current position in the inputprovider +// IncrementPosition increments the current position in the inputprovider func (c *CommandInput) IncrementPosition() { c.count += 1 } -//Next will increment the cursor position, and return a boolean telling if there's iterations left +// Next will increment the cursor position, and return a boolean telling if there's iterations left func (c *CommandInput) Next() bool { return c.count < c.config.InputNum } -//Value returns the input from command stdoutput +// Value returns the input from command stdoutput func (c *CommandInput) Value() []byte { var stdout bytes.Buffer os.Setenv("FFUF_NUM", strconv.Itoa(c.count)) @@ -72,7 +77,7 @@ func (c *CommandInput) Value() []byte { return stdout.Bytes() } -//Total returns the size of wordlist +// Total returns the size of wordlist func (c *CommandInput) Total() int { return c.config.InputNum } diff --git a/pkg/input/input.go b/pkg/input/input.go index f5502c2..87c78e5 100644 --- a/pkg/input/input.go +++ b/pkg/input/input.go @@ -62,12 +62,21 @@ func (i *MainInputProvider) ActivateKeywords(kws []string) { } } -//Position will return the current position of progress +// Position will return the current position of progress func (i *MainInputProvider) Position() int { return i.position } -//Keywords returns a slice of all keywords in the inputprovider +// SetPosition will reset the MainInputProvider to a specific position +func (i *MainInputProvider) SetPosition(pos int) { + if i.Config.InputMode == "clusterbomb" || i.Config.InputMode == "sniper" { + i.setclusterbombPosition(pos) + } else { + i.setpitchforkPosition(pos) + } +} + +// Keywords returns a slice of all keywords in the inputprovider func (i *MainInputProvider) Keywords() []string { kws := make([]string, 0) for _, p := range i.Providers { @@ -76,7 +85,7 @@ func (i *MainInputProvider) Keywords() []string { return kws } -//Next will increment the cursor position, and return a boolean telling if there's inputs left +// Next will increment the cursor position, and return a boolean telling if there's inputs left func (i *MainInputProvider) Next() bool { if i.position >= i.Total() { return false @@ -85,7 +94,7 @@ func (i *MainInputProvider) Next() bool { return true } -//Value returns a map of inputs for keywords +// Value returns a map of inputs for keywords func (i *MainInputProvider) Value() map[string][]byte { retval := make(map[string][]byte) if i.Config.InputMode == "clusterbomb" || i.Config.InputMode == "sniper" { @@ -97,7 +106,7 @@ func (i *MainInputProvider) Value() map[string][]byte { return retval } -//Reset resets all the inputproviders and counters +// Reset resets all the inputproviders and counters func (i *MainInputProvider) Reset() { for _, p := range i.Providers { p.ResetPosition() @@ -106,8 +115,8 @@ func (i *MainInputProvider) Reset() { i.msbIterator = 0 } -//pitchforkValue returns a map of keyword:value pairs including all inputs. -//This mode will iterate through wordlists in lockstep. +// pitchforkValue returns a map of keyword:value pairs including all inputs. +// This mode will iterate through wordlists in lockstep. func (i *MainInputProvider) pitchforkValue() map[string][]byte { values := make(map[string][]byte) for _, p := range i.Providers { @@ -125,8 +134,14 @@ func (i *MainInputProvider) pitchforkValue() map[string][]byte { return values } -//clusterbombValue returns map of keyword:value pairs including all inputs. -//this mode will iterate through all possible combinations. +func (i *MainInputProvider) setpitchforkPosition(pos int) { + for _, p := range i.Providers { + p.SetPosition(pos) + } +} + +// clusterbombValue returns map of keyword:value pairs including all inputs. +// this mode will iterate through all possible combinations. func (i *MainInputProvider) clusterbombValue() map[string][]byte { values := make(map[string][]byte) // Should we signal the next InputProvider in the slice to increment @@ -163,6 +178,18 @@ func (i *MainInputProvider) clusterbombValue() map[string][]byte { return values } +func (i *MainInputProvider) setclusterbombPosition(pos int) { + i.Reset() + if pos > i.Total() { + // noop + return + } + for i.position < pos-1 { + i.Next() + i.Value() + } +} + func (i *MainInputProvider) clusterbombIteratorReset() { index := 0 for _, p := range i.Providers { @@ -179,7 +206,7 @@ func (i *MainInputProvider) clusterbombIteratorReset() { } } -//Total returns the amount of input combinations available +// Total returns the amount of input combinations available func (i *MainInputProvider) Total() int { count := 0 if i.Config.InputMode == "pitchfork" { @@ -204,7 +231,7 @@ func (i *MainInputProvider) Total() int { return count } -//sliceContains is a helper function that returns true if a string is included in a string slice +// sliceContains is a helper function that returns true if a string is included in a string slice func sliceContains(sslice []string, str string) bool { for _, v := range sslice { if v == str { diff --git a/pkg/input/wordlist.go b/pkg/input/wordlist.go index f22dfd9..33aefbc 100644 --- a/pkg/input/wordlist.go +++ b/pkg/input/wordlist.go @@ -42,57 +42,62 @@ func NewWordlistInput(keyword string, value string, conf *ffuf.Config) (*Wordlis return &wl, err } -//Position will return the current position in the input list +// Position will return the current position in the input list func (w *WordlistInput) Position() int { return w.position } -//ResetPosition resets the position back to beginning of the wordlist. +// SetPosition sets the current position of the inputprovider +func (w *WordlistInput) SetPosition(pos int) { + w.position = pos +} + +// ResetPosition resets the position back to beginning of the wordlist. func (w *WordlistInput) ResetPosition() { w.position = 0 } -//Keyword returns the keyword assigned to this InternalInputProvider +// Keyword returns the keyword assigned to this InternalInputProvider func (w *WordlistInput) Keyword() string { return w.keyword } -//Next will return a boolean telling if there's words left in the list +// Next will return a boolean telling if there's words left in the list func (w *WordlistInput) Next() bool { return w.position < len(w.data) } -//IncrementPosition will increment the current position in the inputprovider data slice +// IncrementPosition will increment the current position in the inputprovider data slice func (w *WordlistInput) IncrementPosition() { w.position += 1 } -//Value returns the value from wordlist at current cursor position +// Value returns the value from wordlist at current cursor position func (w *WordlistInput) Value() []byte { return w.data[w.position] } -//Total returns the size of wordlist +// Total returns the size of wordlist func (w *WordlistInput) Total() int { return len(w.data) } -//Active returns boolean if the inputprovider is active +// Active returns boolean if the inputprovider is active func (w *WordlistInput) Active() bool { return w.active } -//Enable sets the inputprovider as active +// Enable sets the inputprovider as active func (w *WordlistInput) Enable() { w.active = true } -//Disable disables the inputprovider +// Disable disables the inputprovider func (w *WordlistInput) Disable() { w.active = false } -//validFile checks that the wordlist file exists and can be read +// validFile checks that the wordlist file exists and can be read func (w *WordlistInput) validFile(path string) (bool, error) { _, err := os.Stat(path) if err != nil { @@ -106,7 +111,7 @@ func (w *WordlistInput) validFile(path string) (bool, error) { return true, nil } -//readFile reads the file line by line to a byte slice +// readFile reads the file line by line to a byte slice func (w *WordlistInput) readFile(path string) error { var file *os.File var err error diff --git a/pkg/output/file_json.go b/pkg/output/file_json.go index 61f5cc9..bcbcd43 100644 --- a/pkg/output/file_json.go +++ b/pkg/output/file_json.go @@ -2,7 +2,7 @@ package output import ( "encoding/json" - "io/ioutil" + "os" "time" "github.com/ffuf/ffuf/pkg/ffuf" @@ -49,7 +49,7 @@ func writeEJSON(filename string, config *ffuf.Config, res []ffuf.Result) error { if err != nil { return err } - err = ioutil.WriteFile(filename, outBytes, 0644) + err = os.WriteFile(filename, outBytes, 0644) if err != nil { return err } @@ -89,7 +89,7 @@ func writeJSON(filename string, config *ffuf.Config, res []ffuf.Result) error { if err != nil { return err } - err = ioutil.WriteFile(filename, outBytes, 0644) + err = os.WriteFile(filename, outBytes, 0644) if err != nil { return err } diff --git a/pkg/output/stdout.go b/pkg/output/stdout.go index a073b88..8d216a4 100644 --- a/pkg/output/stdout.go +++ b/pkg/output/stdout.go @@ -4,9 +4,9 @@ import ( "crypto/md5" "encoding/json" "fmt" - "io/ioutil" "os" "path" + "sort" "strconv" "strings" "time" @@ -28,6 +28,7 @@ const ( type Stdoutput struct { config *ffuf.Config + fuzzkeywords []string Results []ffuf.Result CurrentResults []ffuf.Result } @@ -37,6 +38,11 @@ func NewStdoutput(conf *ffuf.Config) *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 } @@ -352,7 +358,7 @@ func (s *Stdoutput) writeResultToFile(resp ffuf.Response) string { fileName = fmt.Sprintf("%x", md5.Sum([]byte(fileContent))) filePath = path.Join(s.config.OutputDirectory, fileName) - err := ioutil.WriteFile(filePath, []byte(fileContent), 0640) + err := os.WriteFile(filePath, []byte(fileContent), 0640) if err != nil { s.Error(err.Error()) } @@ -416,13 +422,13 @@ func (s *Stdoutput) resultMultiline(res ffuf.Result) { if res.ResultFile != "" { reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, res.ResultFile) } - for k, v := range res.Input { + 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, v) + reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, res.Input[k]) } } fmt.Printf("%s\n%s\n", res_hdr, reslines) diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index 76ba525..b040167 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -4,7 +4,7 @@ import ( "bytes" "crypto/tls" "fmt" - "io/ioutil" + "io" "net" "net/http" "net/http/httptrace" @@ -18,7 +18,7 @@ import ( "github.com/ffuf/ffuf/pkg/ffuf" ) -//Download results < 5MB +// Download results < 5MB const MAX_DOWNLOAD_SIZE = 5242880 type SimpleRunner struct { @@ -47,7 +47,7 @@ func NewSimpleRunner(conf *ffuf.Config, replay bool) ffuf.RunnerProvider { CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: time.Duration(time.Duration(conf.Timeout) * time.Second), Transport: &http.Transport{ - ForceAttemptHTTP2: conf.Http2, + ForceAttemptHTTP2: conf.Http2, Proxy: proxyURL, MaxIdleConns: 1000, MaxIdleConnsPerHost: 500, @@ -131,7 +131,6 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) { if len(r.config.OutputDirectory) > 0 { rawreq, _ = httputil.DumpRequestOut(httpreq, true) } - httpresp, err := r.client.Do(httpreq) if err != nil { return ffuf.Response{}, err @@ -156,7 +155,7 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) { resp.Raw = string(rawresp) } - if respbody, err := ioutil.ReadAll(httpresp.Body); err == nil { + if respbody, err := io.ReadAll(httpresp.Body); err == nil { resp.ContentLength = int64(len(string(respbody))) resp.Data = respbody } @@ -169,3 +168,29 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) { return resp, nil } + +func (r *SimpleRunner) Dump(req *ffuf.Request) ([]byte, error) { + var httpreq *http.Request + var err error + data := bytes.NewReader(req.Data) + httpreq, err = http.NewRequestWithContext(r.config.Context, req.Method, req.Url, data) + if err != nil { + return []byte{}, err + } + + // set default User-Agent header if not present + if _, ok := req.Headers["User-Agent"]; !ok { + req.Headers["User-Agent"] = fmt.Sprintf("%s v%s", "Fuzz Faster U Fool", ffuf.Version()) + } + + // Handle Go http.Request special cases + if _, ok := req.Headers["Host"]; ok { + httpreq.Host = req.Headers["Host"] + } + + req.Host = httpreq.Host + for k, v := range req.Headers { + httpreq.Header.Set(k, v) + } + return httputil.DumpRequestOut(httpreq, true) +}