From 9aeae16a086d315d516eedb80952a82f8be4d042 Mon Sep 17 00:00:00 2001 From: DoI <5291556+denandz@users.noreply.github.com> Date: Mon, 7 Mar 2022 03:14:45 +1300 Subject: [PATCH] Add Sniper Mode (#469) * Modify SimpleRunner to take a Request parameter, add base and copy functions for Requests * Add Request structs to run queues * Implemented sniper mode * Added request and optionsparser tests for sniper mode * Removed unneccesary print statements * Updated readme.md and terminal output * Enabled command inputs for sniper mode * correctly initialize validmode in optionsparser * Remove unnecessary print data in TestScrubTemplates * Use InputProvider for sniper template characters * Add a sniper-mode specific queue job execution log --- README.md | 2 +- main.go | 2 +- pkg/ffuf/config.go | 7 +- pkg/ffuf/interfaces.go | 2 +- pkg/ffuf/job.go | 37 +++-- pkg/ffuf/optionsparser.go | 126 ++++++++++++++--- pkg/ffuf/optionsparser_test.go | 85 ++++++++++++ pkg/ffuf/request.go | 172 +++++++++++++++++++++++ pkg/ffuf/request_test.go | 246 +++++++++++++++++++++++++++++++++ pkg/input/input.go | 6 +- pkg/interactive/termhandler.go | 12 +- pkg/runner/simple.go | 9 +- 12 files changed, 654 insertions(+), 52 deletions(-) create mode 100644 pkg/ffuf/optionsparser_test.go create mode 100644 pkg/ffuf/request_test.go diff --git a/README.md b/README.md index 01aa275..c053df9 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ INPUT OPTIONS: -input-cmd Command producing the input. --input-num is required when using this input method. Overrides -w. -input-num Number of inputs to test. Used in conjunction with --input-cmd. (default: 100) -input-shell Shell to be used for running command - -mode Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork (default: clusterbomb) + -mode Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork, sniper (default: clusterbomb) -request File containing the raw http request -request-proto Protocol to use along with raw request (default: https) -w Wordlist file path and (optional) keyword separated by colon. eg. '/path/to/wordlist:KEYWORD' diff --git a/main.go b/main.go index 97a9c9b..34f42c6 100644 --- a/main.go +++ b/main.go @@ -102,7 +102,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions { flag.StringVar(&opts.HTTP.URL, "u", opts.HTTP.URL, "Target URL") flag.StringVar(&opts.HTTP.SNI, "sni", opts.HTTP.SNI, "Target TLS SNI, does not support FUZZ keyword") flag.StringVar(&opts.Input.Extensions, "e", opts.Input.Extensions, "Comma separated list of extensions. Extends FUZZ keyword.") - flag.StringVar(&opts.Input.InputMode, "mode", opts.Input.InputMode, "Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork") + flag.StringVar(&opts.Input.InputMode, "mode", opts.Input.InputMode, "Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork, sniper") flag.StringVar(&opts.Input.InputShell, "input-shell", opts.Input.InputShell, "Shell to be used for running command") flag.StringVar(&opts.Input.Request, "request", opts.Input.Request, "File containing the raw http request") flag.StringVar(&opts.Input.RequestProto, "request-proto", opts.Input.RequestProto, "Protocol to use along with raw request") diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go index 0a756ee..3c38194 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -55,9 +55,10 @@ type Config struct { } type InputProviderConfig struct { - Name string `json:"name"` - Keyword string `json:"keyword"` - Value string `json:"value"` + Name string `json:"name"` + Keyword string `json:"keyword"` + Value string `json:"value"` + Template string `json:"template"` // the templating string used for sniper mode (usually "§") } func NewConfig(ctx context.Context, cancel context.CancelFunc) Config { diff --git a/pkg/ffuf/interfaces.go b/pkg/ffuf/interfaces.go index 3f08418..7e49a93 100644 --- a/pkg/ffuf/interfaces.go +++ b/pkg/ffuf/interfaces.go @@ -11,7 +11,7 @@ type FilterProvider interface { //RunnerProvider is an interface for request executors type RunnerProvider interface { - Prepare(input map[string][]byte) (Request, error) + Prepare(input map[string][]byte, basereq *Request) (Request, error) Execute(req *Request) (Response, error) } diff --git a/pkg/ffuf/job.go b/pkg/ffuf/job.go index 1778090..2a2a99c 100644 --- a/pkg/ffuf/job.go +++ b/pkg/ffuf/job.go @@ -42,6 +42,7 @@ type Job struct { type QueueJob struct { Url string depth int + req Request } func NewJob(conf *Config) *Job { @@ -107,10 +108,22 @@ func (j *Job) Start() { j.startTime = time.Now() } - // Add the default job to job queue - j.queuejobs = append(j.queuejobs, QueueJob{Url: j.Config.Url, depth: 0}) + basereq := BaseRequest(j.Config) + + if j.Config.InputMode == "sniper" { + // process multiple payload locations and create a queue job for each location + reqs := SniperRequests(&basereq, j.Config.InputProviders[0].Template) + for _, r := range reqs { + j.queuejobs = append(j.queuejobs, QueueJob{Url: j.Config.Url, depth: 0, req: r}) + } + j.Total = j.Input.Total() * len(reqs) + } else { + // Add the default job to job queue + j.queuejobs = append(j.queuejobs, QueueJob{Url: j.Config.Url, depth: 0, req: BaseRequest(j.Config)}) + j.Total = j.Input.Total() + } + rand.Seed(time.Now().UnixNano()) - j.Total = j.Input.Total() defer j.Stop() j.Running = true @@ -203,9 +216,13 @@ func (j *Job) startExecution() { wg.Add(1) go j.runBackgroundTasks(&wg) - // Print the base URL when starting a new recursion queue job + // Print the base URL when starting a new recursion or sniper queue job if j.queuepos > 1 { - j.Output.Info(fmt.Sprintf("Starting queued job on target: %s", j.Config.Url)) + if j.Config.InputMode == "sniper" { + j.Output.Info(fmt.Sprintf("Starting queued sniper job (%d of %d) on target: %s", j.queuepos, len(j.queuejobs), j.Config.Url)) + } else { + j.Output.Info(fmt.Sprintf("Starting queued job on target: %s", j.Config.Url)) + } } //Limiter blocks after reaching the buffer, ensuring limited concurrency @@ -323,7 +340,8 @@ func (j *Job) isMatch(resp Response) bool { } func (j *Job) runTask(input map[string][]byte, position int, retried bool) { - req, err := j.Runner.Prepare(input) + basereq := j.queuejobs[j.queuepos-1].req + req, err := j.Runner.Prepare(input, &basereq) req.Position = position if err != nil { j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err)) @@ -360,7 +378,7 @@ func (j *Job) runTask(input map[string][]byte, position int, retried bool) { if j.isMatch(resp) { // Re-send request through replay-proxy if needed if j.ReplayRunner != nil { - replayreq, err := j.ReplayRunner.Prepare(input) + replayreq, err := j.ReplayRunner.Prepare(input, &basereq) replayreq.Position = position if err != nil { j.Output.Error(fmt.Sprintf("Encountered an error while preparing replayproxy request: %s\n", err)) @@ -407,7 +425,7 @@ func (j *Job) handleDefaultRecursionJob(resp Response) { } if j.Config.RecursionDepth == 0 || j.currentDepth < j.Config.RecursionDepth { // We have yet to reach the maximum recursion depth - newJob := QueueJob{Url: recUrl, depth: j.currentDepth + 1} + newJob := QueueJob{Url: recUrl, depth: j.currentDepth + 1, req: BaseRequest(j.Config)} j.queuejobs = append(j.queuejobs, newJob) j.Output.Info(fmt.Sprintf("Adding a new job to the queue: %s", recUrl)) } else { @@ -417,6 +435,7 @@ func (j *Job) handleDefaultRecursionJob(resp Response) { //CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests func (j *Job) CalibrateResponses() ([]Response, error) { + basereq := BaseRequest(j.Config) cInputs := make([]string, 0) rand.Seed(time.Now().UnixNano()) if len(j.Config.AutoCalibrationStrings) < 1 { @@ -435,7 +454,7 @@ func (j *Job) CalibrateResponses() ([]Response, error) { inputs[v.Keyword] = []byte(input) } - req, err := j.Runner.Prepare(inputs) + req, err := j.Runner.Prepare(inputs, &basereq) if err != nil { j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err)) j.incError() diff --git a/pkg/ffuf/optionsparser.go b/pkg/ffuf/optionsparser.go index a6555a7..84b817e 100644 --- a/pkg/ffuf/optionsparser.go +++ b/pkg/ffuf/optionsparser.go @@ -183,6 +183,32 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con } //Prepare inputproviders + conf.InputMode = parseOpts.Input.InputMode + + validmode := false + for _, mode := range []string{"clusterbomb", "pitchfork", "sniper"} { + if conf.InputMode == mode { + validmode = true + } + } + if !validmode { + errs.Add(fmt.Errorf("Input mode (-mode) %s not recognized", conf.InputMode)) + } + + template := "" + // sniper mode needs some additional checking + if conf.InputMode == "sniper" { + template = "§" + + if len(parseOpts.Input.Wordlists) > 1 { + errs.Add(fmt.Errorf("sniper mode only supports one wordlist")) + } + + if len(parseOpts.Input.Inputcommands) > 1 { + errs.Add(fmt.Errorf("sniper mode only supports one input command")) + } + } + for _, v := range parseOpts.Input.Wordlists { var wl []string if runtime.GOOS == "windows" { @@ -207,33 +233,44 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con wl = strings.SplitN(v, ":", 2) } if len(wl) == 2 { - conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ - Name: "wordlist", - Value: wl[0], - Keyword: wl[1], - }) + if conf.InputMode == "sniper" { + errs.Add(fmt.Errorf("sniper mode does not support wordlist keywords")) + } else { + conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ + Name: "wordlist", + Value: wl[0], + Keyword: wl[1], + }) + } } else { conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ - Name: "wordlist", - Value: wl[0], - Keyword: "FUZZ", + Name: "wordlist", + Value: wl[0], + Keyword: "FUZZ", + Template: template, }) } } + for _, v := range parseOpts.Input.Inputcommands { ic := strings.SplitN(v, ":", 2) if len(ic) == 2 { - conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ - Name: "command", - Value: ic[0], - Keyword: ic[1], - }) - conf.CommandKeywords = append(conf.CommandKeywords, ic[0]) + if conf.InputMode == "sniper" { + errs.Add(fmt.Errorf("sniper mode does not support command keywords")) + } else { + conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ + Name: "command", + Value: ic[0], + Keyword: ic[1], + }) + conf.CommandKeywords = append(conf.CommandKeywords, ic[0]) + } } else { conf.InputProviders = append(conf.InputProviders, InputProviderConfig{ - Name: "command", - Value: ic[0], - Keyword: "FUZZ", + Name: "command", + Value: ic[0], + Keyword: "FUZZ", + Template: template, }) conf.CommandKeywords = append(conf.CommandKeywords, "FUZZ") } @@ -391,7 +428,7 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con conf.DirSearchCompat = parseOpts.Input.DirSearchCompat conf.Colors = parseOpts.General.Colors conf.InputNum = parseOpts.Input.InputNum - conf.InputMode = parseOpts.Input.InputMode + conf.InputShell = parseOpts.Input.InputShell conf.OutputFile = parseOpts.Output.OutputFile conf.OutputDirectory = parseOpts.Output.OutputDirectory @@ -426,9 +463,16 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con conf.CommandLine = strings.Join(os.Args, " ") for _, provider := range conf.InputProviders { - if !keywordPresent(provider.Keyword, &conf) { - errmsg := fmt.Sprintf("Keyword %s defined, but not found in headers, method, URL or POST data.", provider.Keyword) - errs.Add(fmt.Errorf(errmsg)) + if provider.Template != "" { + if !templatePresent(provider.Template, &conf) { + errmsg := fmt.Sprintf("Template %s defined, but not found in pairs in headers, method, URL or POST data.", provider.Template) + errs.Add(fmt.Errorf(errmsg)) + } + } else { + if !keywordPresent(provider.Keyword, &conf) { + errmsg := fmt.Sprintf("Keyword %s defined, but not found in headers, method, URL or POST data.", provider.Keyword) + errs.Add(fmt.Errorf(errmsg)) + } } } @@ -534,6 +578,46 @@ func keywordPresent(keyword string, conf *Config) bool { return false } +func templatePresent(template string, conf *Config) bool { + // Search for input location identifiers, these must exist in pairs + sane := false + + if c := strings.Count(conf.Method, template); c > 0 { + if c%2 != 0 { + return false + } + sane = true + } + if c := strings.Count(conf.Url, template); c > 0 { + if c%2 != 0 { + return false + } + sane = true + } + if c := strings.Count(conf.Data, template); c > 0 { + if c%2 != 0 { + return false + } + sane = true + } + for k, v := range conf.Headers { + if c := strings.Count(k, template); c > 0 { + if c%2 != 0 { + return false + } + sane = true + } + if c := strings.Count(v, template); c > 0 { + if c%2 != 0 { + return false + } + sane = true + } + } + + return sane +} + func ReadConfig(configFile string) (*ConfigOptions, error) { conf := NewConfigOptions() configData, err := ioutil.ReadFile(configFile) diff --git a/pkg/ffuf/optionsparser_test.go b/pkg/ffuf/optionsparser_test.go new file mode 100644 index 0000000..2e9913b --- /dev/null +++ b/pkg/ffuf/optionsparser_test.go @@ -0,0 +1,85 @@ +package ffuf + +import ( + "testing" +) + +func TestTemplatePresent(t *testing.T) { + template := "§" + + headers := make(map[string]string) + headers["foo"] = "§bar§" + headers["omg"] = "bbq" + headers["§world§"] = "Ooo" + + goodConf := Config{ + Url: "https://example.com/fooo/bar?test=§value§&order[§0§]=§foo§", + Method: "PO§ST§", + Headers: headers, + Data: "line=Can we pull back the §veil§ of §static§ and reach in to the source of §all§ being?&commit=true", + } + + if !templatePresent(template, &goodConf) { + t.Errorf("Expected-good config failed validation") + } + + badConfMethod := Config{ + Url: "https://example.com/fooo/bar?test=§value§&order[§0§]=§foo§", + Method: "POST§", + Headers: headers, + Data: "line=Can we pull back the §veil§ of §static§ and reach in to the source of §all§ being?&commit=§true§", + } + + if templatePresent(template, &badConfMethod) { + t.Errorf("Expected-bad config (Method) failed validation") + } + + badConfURL := Config{ + Url: "https://example.com/fooo/bar?test=§value§&order[0§]=§foo§", + Method: "§POST§", + Headers: headers, + Data: "line=Can we pull back the §veil§ of §static§ and reach in to the source of §all§ being?&commit=§true§", + } + + if templatePresent(template, &badConfURL) { + t.Errorf("Expected-bad config (URL) failed validation") + } + + badConfData := Config{ + Url: "https://example.com/fooo/bar?test=§value§&order[§0§]=§foo§", + Method: "§POST§", + Headers: headers, + Data: "line=Can we pull back the §veil of §static§ and reach in to the source of §all§ being?&commit=§true§", + } + + if templatePresent(template, &badConfData) { + t.Errorf("Expected-bad config (Data) failed validation") + } + + headers["kingdom"] = "§candy" + + badConfHeaderValue := Config{ + Url: "https://example.com/fooo/bar?test=§value§&order[§0§]=§foo§", + Method: "PO§ST§", + Headers: headers, + Data: "line=Can we pull back the §veil§ of §static§ and reach in to the source of §all§ being?&commit=true", + } + + if templatePresent(template, &badConfHeaderValue) { + t.Errorf("Expected-bad config (Header value) failed validation") + } + + headers["kingdom"] = "candy" + headers["§kingdom"] = "candy" + + badConfHeaderKey := Config{ + Url: "https://example.com/fooo/bar?test=§value§&order[§0§]=§foo§", + Method: "PO§ST§", + Headers: headers, + Data: "line=Can we pull back the §veil§ of §static§ and reach in to the source of §all§ being?&commit=true", + } + + if templatePresent(template, &badConfHeaderKey) { + t.Errorf("Expected-bad config (Header key) failed validation") + } +} diff --git a/pkg/ffuf/request.go b/pkg/ffuf/request.go index ed3d3c4..16fdcf1 100644 --- a/pkg/ffuf/request.go +++ b/pkg/ffuf/request.go @@ -1,5 +1,9 @@ package ffuf +import ( + "strings" +) + // Request holds the meaningful data that is passed for runner for making the query type Request struct { Method string @@ -19,3 +23,171 @@ func NewRequest(conf *Config) Request { req.Headers = make(map[string]string) return req } + +// BaseRequest returns a base request struct populated from the main config +func BaseRequest(conf *Config) Request { + req := NewRequest(conf) + req.Headers = conf.Headers + req.Data = []byte(conf.Data) + return req +} + +// CopyRequest performs a deep copy of a request and returns a new struct +func CopyRequest(basereq *Request) Request { + var req Request + req.Method = basereq.Method + req.Host = basereq.Host + req.Url = basereq.Url + + req.Headers = make(map[string]string, len(basereq.Headers)) + for h, v := range basereq.Headers { + req.Headers[h] = v + } + + req.Data = make([]byte, len(basereq.Data)) + copy(req.Data, basereq.Data) + + if len(basereq.Input) > 0 { + req.Input = make(map[string][]byte, len(basereq.Input)) + for k, v := range basereq.Input { + req.Input[k] = v + } + } + + req.Position = basereq.Position + req.Raw = basereq.Raw + + return req +} + +// SniperRequests returns an array of requests, each with one of the templated locations replaced by a keyword +func SniperRequests(basereq *Request, template string) []Request { + var reqs []Request + keyword := "FUZZ" + + // Search for input location identifiers, these must exist in pairs + if c := strings.Count(basereq.Method, template); c > 0 { + if c%2 == 0 { + tokens := templateLocations(template, basereq.Method) + + for i := 0; i < len(tokens); i = i + 2 { + newreq := CopyRequest(basereq) + newreq.Method = injectKeyword(basereq.Method, keyword, tokens[i], tokens[i+1]) + scrubTemplates(&newreq, template) + reqs = append(reqs, newreq) + } + } + } + + if c := strings.Count(basereq.Url, template); c > 0 { + if c%2 == 0 { + tokens := templateLocations(template, basereq.Url) + + for i := 0; i < len(tokens); i = i + 2 { + newreq := CopyRequest(basereq) + newreq.Url = injectKeyword(basereq.Url, keyword, tokens[i], tokens[i+1]) + scrubTemplates(&newreq, template) + reqs = append(reqs, newreq) + } + } + } + + data := string(basereq.Data) + if c := strings.Count(data, template); c > 0 { + if c%2 == 0 { + tokens := templateLocations(template, data) + + for i := 0; i < len(tokens); i = i + 2 { + newreq := CopyRequest(basereq) + newreq.Data = []byte(injectKeyword(data, keyword, tokens[i], tokens[i+1])) + scrubTemplates(&newreq, template) + reqs = append(reqs, newreq) + } + } + } + + for k, v := range basereq.Headers { + if c := strings.Count(k, template); c > 0 { + if c%2 == 0 { + tokens := templateLocations(template, k) + + for i := 0; i < len(tokens); i = i + 2 { + newreq := CopyRequest(basereq) + newreq.Headers[injectKeyword(k, keyword, tokens[i], tokens[i+1])] = v + delete(newreq.Headers, k) + scrubTemplates(&newreq, template) + reqs = append(reqs, newreq) + } + } + } + if c := strings.Count(v, template); c > 0 { + if c%2 == 0 { + tokens := templateLocations(template, v) + + for i := 0; i < len(tokens); i = i + 2 { + newreq := CopyRequest(basereq) + newreq.Headers[k] = injectKeyword(v, keyword, tokens[i], tokens[i+1]) + scrubTemplates(&newreq, template) + reqs = append(reqs, newreq) + } + } + } + } + + return reqs +} + +// templateLocations returns an array of template character locations in input +func templateLocations(template string, input string) []int { + var tokens []int + + for k, i := range []rune(input) { + if i == []rune(template)[0] { + tokens = append(tokens, k) + } + } + + return tokens +} + +// injectKeyword takes a string, a keyword, and a start/end offset. The data between +// the start/end offset in string is removed, and replaced by keyword +func injectKeyword(input string, keyword string, startOffset int, endOffset int) string { + + // some basic sanity checking, return the original string unchanged if offsets didnt make sense + if startOffset > len(input) || endOffset > len(input) || startOffset > endOffset { + return input + } + + inputslice := []rune(input) + keywordslice := []rune(keyword) + + prefix := inputslice[:startOffset] + suffix := inputslice[endOffset+1:] + + inputslice = append(prefix, keywordslice...) + inputslice = append(inputslice, suffix...) + + return string(inputslice) +} + +// scrubTemplates removes all template (§) strings from the request struct +func scrubTemplates(req *Request, template string) { + req.Method = strings.Join(strings.Split(req.Method, template), "") + req.Url = strings.Join(strings.Split(req.Url, template), "") + req.Data = []byte(strings.Join(strings.Split(string(req.Data), template), "")) + + for k, v := range req.Headers { + if c := strings.Count(k, template); c > 0 { + if c%2 == 0 { + delete(req.Headers, k) + req.Headers[strings.Join(strings.Split(k, template), "")] = v + } + } + if c := strings.Count(v, template); c > 0 { + if c%2 == 0 { + req.Headers[k] = strings.Join(strings.Split(v, template), "") + } + } + } +} diff --git a/pkg/ffuf/request_test.go b/pkg/ffuf/request_test.go new file mode 100644 index 0000000..7c55f78 --- /dev/null +++ b/pkg/ffuf/request_test.go @@ -0,0 +1,246 @@ +package ffuf + +import ( + "reflect" + "testing" +) + +func TestBaseRequest(t *testing.T) { + headers := make(map[string]string) + headers["foo"] = "bar" + headers["baz"] = "wibble" + headers["Content-Type"] = "application/json" + + data := "{\"quote\":\"I'll still be here tomorrow to high five you yesterday, my friend. Peace.\"}" + + expectedreq := Request{Method: "POST", Url: "http://example.com/aaaa", Headers: headers, Data: []byte(data)} + config := Config{Method: "POST", Url: "http://example.com/aaaa", Headers: headers, Data: data} + basereq := BaseRequest(&config) + + if !reflect.DeepEqual(basereq, expectedreq) { + t.Errorf("BaseRequest does not return a struct with expected values") + } + +} + +func TestCopyRequest(t *testing.T) { + headers := make(map[string]string) + headers["foo"] = "bar" + headers["omg"] = "bbq" + + data := "line=Is+that+where+creativity+comes+from?+From+sad+biz?" + + input := make(map[string][]byte) + input["matthew"] = []byte("If you are the head that floats atop the §ziggurat§, then the stairs that lead to you must be infinite.") + + basereq := Request{Method: "POST", + Host: "testhost.local", + Url: "http://example.com/aaaa", + Headers: headers, + Data: []byte(data), + Input: input, + Position: 2, + Raw: "We're not oil and water, we're oil and vinegar! It's good. It's yummy.", + } + + copiedreq := CopyRequest(&basereq) + + if !reflect.DeepEqual(basereq, copiedreq) { + t.Errorf("CopyRequest does not return an equal struct") + } +} + +func TestSniperRequests(t *testing.T) { + headers := make(map[string]string) + headers["foo"] = "§bar§" + headers["§omg§"] = "bbq" + + testreq := Request{ + Method: "§POST§", + Url: "http://example.com/aaaa?param=§lemony§", + Headers: headers, + Data: []byte("line=§yo yo, it's grease§"), + } + + requests := SniperRequests(&testreq, "§") + + if len(requests) != 5 { + t.Errorf("SniperRequests returned an incorrect number of requests") + } + + headers = make(map[string]string) + headers["foo"] = "bar" + headers["omg"] = "bbq" + + var expected Request + expected = Request{ // Method + Method: "FUZZ", + Url: "http://example.com/aaaa?param=lemony", + Headers: headers, + Data: []byte("line=yo yo, it's grease"), + } + + pass := false + for _, req := range requests { + if reflect.DeepEqual(req, expected) { + pass = true + } + } + + if !pass { + t.Errorf("SniperRequests does not return expected values (Method)") + } + + expected = Request{ // URL + Method: "POST", + Url: "http://example.com/aaaa?param=FUZZ", + Headers: headers, + Data: []byte("line=yo yo, it's grease"), + } + + pass = false + for _, req := range requests { + if reflect.DeepEqual(req, expected) { + pass = true + } + } + + if !pass { + t.Errorf("SniperRequests does not return expected values (Url)") + } + + expected = Request{ // Data + Method: "POST", + Url: "http://example.com/aaaa?param=lemony", + Headers: headers, + Data: []byte("line=FUZZ"), + } + + pass = false + for _, req := range requests { + if reflect.DeepEqual(req, expected) { + pass = true + } + } + + if !pass { + t.Errorf("SniperRequests does not return expected values (Data)") + } + + headers = make(map[string]string) + headers["foo"] = "FUZZ" + headers["omg"] = "bbq" + + expected = Request{ // Header value + Method: "POST", + Url: "http://example.com/aaaa?param=lemony", + Headers: headers, + Data: []byte("line=yo yo, it's grease"), + } + + pass = false + for _, req := range requests { + if reflect.DeepEqual(req, expected) { + pass = true + } + } + + if !pass { + t.Errorf("SniperRequests does not return expected values (Header value)") + } + + headers = make(map[string]string) + headers["foo"] = "bar" + headers["FUZZ"] = "bbq" + + expected = Request{ // Header key + Method: "POST", + Url: "http://example.com/aaaa?param=lemony", + Headers: headers, + Data: []byte("line=yo yo, it's grease"), + } + + pass = false + for _, req := range requests { + if reflect.DeepEqual(req, expected) { + pass = true + } + } + + if !pass { + t.Errorf("SniperRequests does not return expected values (Header key)") + } + +} + +func TestTemplateLocations(t *testing.T) { + test := "this is my 1§template locator§ test" + arr := templateLocations("§", test) + expected := []int{12, 29} + if !reflect.DeepEqual(arr, expected) { + t.Errorf("templateLocations does not return expected values") + } + + test2 := "§template locator§" + arr = templateLocations("§", test2) + expected = []int{0, 17} + if !reflect.DeepEqual(arr, expected) { + t.Errorf("templateLocations does not return expected values") + } + + if len(templateLocations("§", "te§st2")) != 1 { + t.Errorf("templateLocations does not return expected values") + } +} + +func TestInjectKeyword(t *testing.T) { + input := "§Greetings, creator§" + offsetTuple := templateLocations("§", input) + expected := "FUZZ" + + result := injectKeyword(input, "FUZZ", offsetTuple[0], offsetTuple[1]) + if result != expected { + t.Errorf("injectKeyword returned unexpected result: " + result) + } + + if injectKeyword(input, "FUZZ", -32, 44) != input { + t.Errorf("injectKeyword offset validation failed") + } + + if injectKeyword(input, "FUZZ", 12, 2) != input { + t.Errorf("injectKeyword offset validation failed") + } + + if injectKeyword(input, "FUZZ", 0, 25) != input { + t.Errorf("injectKeyword offset validation failed") + } + +} + +func TestScrubTemplates(t *testing.T) { + headers := make(map[string]string) + headers["foo"] = "§bar§" + headers["§omg§"] = "bbq" + + testreq := Request{Method: "§POST§", + Url: "http://example.com/aaaa?param=§lemony§", + Headers: headers, + Data: []byte("line=§yo yo, it's grease§"), + } + + headers = make(map[string]string) + headers["foo"] = "bar" + headers["omg"] = "bbq" + + expectedreq := Request{Method: "POST", + Url: "http://example.com/aaaa?param=lemony", + Headers: headers, + Data: []byte("line=yo yo, it's grease"), + } + + scrubTemplates(&testreq, "§") + + if !reflect.DeepEqual(testreq, expectedreq) { + t.Errorf("scrubTemplates does not return expected values") + } +} diff --git a/pkg/input/input.go b/pkg/input/input.go index 14ba58e..7e17430 100644 --- a/pkg/input/input.go +++ b/pkg/input/input.go @@ -16,7 +16,7 @@ type MainInputProvider struct { func NewInputProvider(conf *ffuf.Config) (ffuf.InputProvider, ffuf.Multierror) { validmode := false errs := ffuf.NewMultierror() - for _, mode := range []string{"clusterbomb", "pitchfork"} { + for _, mode := range []string{"clusterbomb", "pitchfork", "sniper"} { if conf.InputMode == mode { validmode = true } @@ -68,7 +68,7 @@ func (i *MainInputProvider) Next() bool { //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" { + if i.Config.InputMode == "clusterbomb" || i.Config.InputMode == "sniper" { retval = i.clusterbombValue() } if i.Config.InputMode == "pitchfork" { @@ -155,7 +155,7 @@ func (i *MainInputProvider) Total() int { } } } - if i.Config.InputMode == "clusterbomb" { + if i.Config.InputMode == "clusterbomb" || i.Config.InputMode == "sniper" { count = 1 for _, p := range i.Providers { count = count * p.Total() diff --git a/pkg/interactive/termhandler.go b/pkg/interactive/termhandler.go index a34ef66..15a6c3a 100644 --- a/pkg/interactive/termhandler.go +++ b/pkg/interactive/termhandler.go @@ -232,7 +232,7 @@ func (i *interactive) appendFilter(name, value string) { func (i *interactive) printQueue() { if len(i.Job.QueuedJobs()) > 0 { - i.Job.Output.Raw("Queued recursion jobs:\n") + i.Job.Output.Raw("Queued jobs:\n") for index, job := range i.Job.QueuedJobs() { postfix := "" if index == 0 { @@ -241,7 +241,7 @@ func (i *interactive) printQueue() { i.Job.Output.Raw(fmt.Sprintf(" [%d] : %s%s\n", index, job.Url, postfix)) } } else { - i.Job.Output.Info("Recursion job queue is empty") + i.Job.Output.Info("Job queue is empty") } } @@ -256,7 +256,7 @@ func (i *interactive) deleteQueue(in string) { i.Job.Output.Warning("Cannot delete the currently running job. Use \"queueskip\" to advance to the next one") } else { i.Job.DeleteQueueItem(index) - i.Job.Output.Info("Recursion job successfully deleted!") + i.Job.Output.Info("Job successfully deleted!") } } } @@ -296,9 +296,9 @@ available commands: fs [value] - (re)configure size filter %s aft [value] - append to time filter %s ft [value] - (re)configure time filter %s - queueshow - show recursive job queue - queuedel [number] - delete a recursion job in the queue - queueskip - advance to the next queued recursion job + queueshow - show job queue + queuedel [number] - delete a job in the queue + queueskip - advance to the next queued job restart - restart and resume the current ffuf job resume - resume current ffuf job (or: ENTER) show - show results for the current job diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index 3ea25bb..76ba525 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -69,13 +69,8 @@ func NewSimpleRunner(conf *ffuf.Config, replay bool) ffuf.RunnerProvider { return &simplerunner } -func (r *SimpleRunner) Prepare(input map[string][]byte) (ffuf.Request, error) { - req := ffuf.NewRequest(r.config) - - req.Headers = r.config.Headers - req.Url = r.config.Url - req.Method = r.config.Method - req.Data = []byte(r.config.Data) +func (r *SimpleRunner) Prepare(input map[string][]byte, basereq *ffuf.Request) (ffuf.Request, error) { + req := ffuf.CopyRequest(basereq) for keyword, inputitem := range input { req.Method = strings.ReplaceAll(req.Method, keyword, string(inputitem))