这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,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")
Expand Down
7 changes: 4 additions & 3 deletions pkg/ffuf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/ffuf/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
37 changes: 28 additions & 9 deletions pkg/ffuf/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Job struct {
type QueueJob struct {
Url string
depth int
req Request
}

func NewJob(conf *Config) *Job {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
126 changes: 105 additions & 21 deletions pkg/ffuf/optionsparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,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" {
Expand All @@ -205,33 +231,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")
}
Expand Down Expand Up @@ -389,7 +426,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
Expand Down Expand Up @@ -423,9 +460,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))
}
}
}

Expand Down Expand Up @@ -531,6 +575,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)
Expand Down
85 changes: 85 additions & 0 deletions pkg/ffuf/optionsparser_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading