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
This commit is contained in:
parent
80a4aa7783
commit
9aeae16a08
@ -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'
|
||||
|
||||
2
main.go
2
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")
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
85
pkg/ffuf/optionsparser_test.go
Normal file
85
pkg/ffuf/optionsparser_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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), "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
246
pkg/ffuf/request_test.go
Normal file
246
pkg/ffuf/request_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user