From ac141e5e34f1e52cde386dafa7037928684b2523 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 16 Nov 2019 00:40:04 +0200 Subject: [PATCH] Fix the multi wordlist bug and add a new mode of operation (#93) * Fix the multi wordlist bug and add a new mode * Add a README entry --- README.md | 4 ++ main.go | 8 +++- pkg/ffuf/config.go | 2 + pkg/ffuf/interfaces.go | 1 + pkg/input/command.go | 8 +++- pkg/input/input.go | 98 ++++++++++++++++++++++++++++++++++++++---- pkg/input/wordlist.go | 8 +++- 7 files changed, 114 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f210593..7e538bc 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,8 @@ Usage of ./ffuf: Match HTTP status codes from respose, use "all" to match every response code. (default "200,204,301,302,307,401,403") -ml string Match amount of lines in response + -mode string + Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork (default "clusterbomb") -mr string Match regexp -ms string @@ -190,7 +192,9 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l - master - New + - Added a new flag to select a multi wordlist operation mode: `--mode`, possible values: `clusterbomb` and `pitchfork`. - Changed + - Fixed a bug in the default multi wordlist mode - v0.11 diff --git a/main.go b/main.go index 339d950..1a6484e 100644 --- a/main.go +++ b/main.go @@ -80,6 +80,7 @@ func main() { flag.BoolVar(&ignored, "compressed", true, "Dummy flag for copy as curl functionality (ignored)") flag.Var(&opts.inputcommands, "input-cmd", "Command producing the input. --input-num is required when using this input method. Overrides -w.") flag.IntVar(&conf.InputNum, "input-num", 100, "Number of inputs to test. Used in conjunction with --input-cmd.") + flag.StringVar(&conf.InputMode, "mode", "clusterbomb", "Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork") flag.BoolVar(&ignored, "i", true, "Dummy flag for copy as curl functionality (ignored)") flag.Var(&opts.cookies, "b", "Cookie data `\"NAME1=VALUE1; NAME2=VALUE2\"` for copy as curl functionality.\nResults unpredictable when combined with -H \"Cookie: ...\"") flag.Var(&opts.cookies, "cookie", "Cookie data (alias of -b)") @@ -150,7 +151,10 @@ func main() { func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) { errs := ffuf.NewMultierror() var err error - inputprovider := input.NewInputProvider(conf) + inputprovider, err := input.NewInputProvider(conf) + if err != nil { + errs.Add(err) + } // TODO: implement error handling for runnerprovider and outputprovider // We only have http runner right now runprovider := runner.NewRunnerByName("http", conf) @@ -158,7 +162,7 @@ func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) { for _, v := range conf.InputProviders { err = inputprovider.AddProvider(v) if err != nil { - errs.Add(fmt.Errorf("%s", err)) + errs.Add(err) } } // We only have stdout outputprovider right now diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go index 06b4fd3..a49a92c 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -28,6 +28,7 @@ type Config struct { InputProviders []InputProviderConfig CommandKeywords []string InputNum int + InputMode string OutputFile string OutputFormat string StopOn403 bool @@ -71,6 +72,7 @@ func NewConfig(ctx context.Context) Config { conf.InputProviders = make([]InputProviderConfig, 0) conf.CommandKeywords = make([]string, 0) conf.InputNum = 0 + conf.InputMode = "clusterbomb" conf.ProxyURL = http.ProxyFromEnvironment conf.Filters = make([]FilterProvider, 0) conf.Delay = optRange{0, 0, false, false} diff --git a/pkg/ffuf/interfaces.go b/pkg/ffuf/interfaces.go index 7b36b70..e3fe48e 100644 --- a/pkg/ffuf/interfaces.go +++ b/pkg/ffuf/interfaces.go @@ -27,6 +27,7 @@ type InternalInputProvider interface { Next() bool Position() int ResetPosition() + IncrementPosition() Value() []byte Total() int } diff --git a/pkg/input/command.go b/pkg/input/command.go index fffa0ba..2f31020 100644 --- a/pkg/input/command.go +++ b/pkg/input/command.go @@ -20,7 +20,7 @@ func NewCommandInput(keyword string, value string, conf *ffuf.Config) (*CommandI var cmd CommandInput cmd.keyword = keyword cmd.config = conf - cmd.count = -1 + cmd.count = 0 cmd.command = value return &cmd, nil } @@ -40,9 +40,13 @@ func (c *CommandInput) ResetPosition() { c.count = 0 } +//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 func (c *CommandInput) Next() bool { - c.count++ if c.count >= c.config.InputNum { return false } diff --git a/pkg/input/input.go b/pkg/input/input.go index a3e31a4..49c3b05 100644 --- a/pkg/input/input.go +++ b/pkg/input/input.go @@ -1,17 +1,29 @@ package input import ( + "fmt" + "github.com/ffuf/ffuf/pkg/ffuf" ) type MainInputProvider struct { - Providers []ffuf.InternalInputProvider - Config *ffuf.Config - position int + Providers []ffuf.InternalInputProvider + Config *ffuf.Config + position int + msbIterator int } -func NewInputProvider(conf *ffuf.Config) ffuf.InputProvider { - return &MainInputProvider{Config: conf} +func NewInputProvider(conf *ffuf.Config) (ffuf.InputProvider, error) { + validmode := false + for _, mode := range []string{"clusterbomb", "pitchfork"} { + if conf.InputMode == mode { + validmode = true + } + } + if !validmode { + return &MainInputProvider{}, fmt.Errorf("Input mode (-mode) %s not recognized", conf.InputMode) + } + return &MainInputProvider{Config: conf, msbIterator: 0}, nil } func (i *MainInputProvider) AddProvider(provider ffuf.InputProviderConfig) error { @@ -43,8 +55,21 @@ func (i *MainInputProvider) Next() bool { return true } -//Value returns a map of keyword:value pairs including all inputs +//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" { + retval = i.clusterbombValue() + } + if i.Config.InputMode == "pitchfork" { + retval = i.pitchforkValue() + } + return retval +} + +//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 { if !p.Next() { @@ -52,15 +77,70 @@ func (i *MainInputProvider) Value() map[string][]byte { p.ResetPosition() } values[p.Keyword()] = p.Value() + p.IncrementPosition() } return values } +//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 + signalNext := false + first := true + for index, p := range i.Providers { + if signalNext { + p.IncrementPosition() + signalNext = false + } + if !p.Next() { + // No more inputs in this inputprovider + if index == i.msbIterator { + // Reset all previous wordlists and increment the msb counter + i.msbIterator += 1 + i.clusterbombIteratorReset() + // Start again + return i.clusterbombValue() + } + p.ResetPosition() + signalNext = true + } + values[p.Keyword()] = p.Value() + if first { + p.IncrementPosition() + first = false + } + } + return values +} + +func (i *MainInputProvider) clusterbombIteratorReset() { + for index, p := range i.Providers { + if index < i.msbIterator { + p.ResetPosition() + } + if index == i.msbIterator { + p.IncrementPosition() + } + } +} + //Total returns the amount of input combinations available func (i *MainInputProvider) Total() int { - count := 1 - for _, p := range i.Providers { - count = count * p.Total() + count := 0 + if i.Config.InputMode == "pitchfork" { + for _, p := range i.Providers { + if p.Total() > count { + count = p.Total() + } + } + } + if i.Config.InputMode == "clusterbomb" { + count = 1 + for _, p := range i.Providers { + count = count * p.Total() + } } return count } diff --git a/pkg/input/wordlist.go b/pkg/input/wordlist.go index 93bae62..4240652 100644 --- a/pkg/input/wordlist.go +++ b/pkg/input/wordlist.go @@ -19,7 +19,7 @@ func NewWordlistInput(keyword string, value string, conf *ffuf.Config) (*Wordlis var wl WordlistInput wl.keyword = keyword wl.config = conf - wl.position = -1 + wl.position = 0 var valid bool var err error // stdin? @@ -56,13 +56,17 @@ func (w *WordlistInput) Keyword() string { //Next will increment the cursor position, and return a boolean telling if there's words left in the list func (w *WordlistInput) Next() bool { - w.position++ if w.position >= len(w.data) { return false } return true } +//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 func (w *WordlistInput) Value() []byte { return w.data[w.position]