Multiple wordlist support (#79)

* Multiple wordlist support

* Display error correctly if wordlist file could not be opened

* Add back the redirect location

* Support multiple keywords in HTML output and fix wordlist positioning

* Support multiple wordlists for md output

* Support multiple keywords in CSV output

* Improve output for multi keyword runs

* Add changelog entry

* Switch the wordlist filename <-> keyword around to allow tab completion

* Fix the usage example in README
This commit is contained in:
Joona Hoikkala 2019-11-10 23:30:54 +02:00 committed by GitHub
parent e200bd11f7
commit 5456a37f72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 364 additions and 149 deletions

View File

@ -92,6 +92,7 @@ ffuf --input-cmd 'cat $FFUF_NUM.txt' -H "Content-Type: application/json" -X POST
To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-u`), headers (`-H`), or POST data (`-d`). To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-u`), headers (`-H`), or POST data (`-d`).
``` ```
Usage of ./ffuf:
-D DirSearch style wordlist compatibility mode. Used in conjunction with -e flag. Replaces %EXT% in wordlist entry with each of the extensions provided by -e. -D DirSearch style wordlist compatibility mode. Used in conjunction with -e flag. Replaces %EXT% in wordlist entry with each of the extensions provided by -e.
-H "Name: Value" -H "Name: Value"
Header "Name: Value", separated by colon. Multiple -H flags are accepted. Header "Name: Value", separated by colon. Multiple -H flags are accepted.
@ -100,57 +101,59 @@ To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-
HTTP method to use (default "GET") HTTP method to use (default "GET")
-ac -ac
Automatically calibrate filtering options Automatically calibrate filtering options
-acc -acc value
Custom auto-calibration string. Can be used multiple times. Implies -ac Custom auto-calibration string. Can be used multiple times. Implies -ac
-i
Dummy flag for copy as curl functionality (ignored)
-b "NAME1=VALUE1; NAME2=VALUE2" -b "NAME1=VALUE1; NAME2=VALUE2"
Cookie data "NAME1=VALUE1; NAME2=VALUE2" for copy as curl functionality. Cookie data "NAME1=VALUE1; NAME2=VALUE2" for copy as curl functionality.
Results unpredictable when combined with -H "Cookie: ..." Results unpredictable when combined with -H "Cookie: ..."
-cookie
Cookie data (alias of -b)
-c Colorize output. -c Colorize output.
-compressed -compressed
Dummy flag for copy as curl functionality (ignored) (default true) Dummy flag for copy as curl functionality (ignored) (default true)
-cookie value
Cookie data (alias of -b)
-d string -d string
POST data POST data
-data-ascii
POST data (alias of -d)
-data-binary
POST data (alias of -d)
-data string -data string
POST data (alias of -d) POST data (alias of -d)
-data-ascii string
POST data (alias of -d)
-data-binary string
POST data (alias of -d)
-debug-log string
Write all of the internal logging to the specified file.
-e string -e string
Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once.
-fc string -fc string
Filter HTTP status codes from response. Comma separated list of codes and ranges Filter HTTP status codes from response. Comma separated list of codes and ranges
-fl string
Filter by amount of lines in response. Comma separated list of line counts and ranges
-fr string -fr string
Filter regexp Filter regexp
-fs string -fs string
Filter HTTP response size. Comma separated list of sizes and ranges Filter HTTP response size. Comma separated list of sizes and ranges
-fw string -fw string
Filter by amount of words in response. Comma separated list of word counts and ranges Filter by amount of words in response. Comma separated list of word counts and ranges
-fl string -i Dummy flag for copy as curl functionality (ignored) (default true)
Filter by amount of lines in response. Comma separated list of line counts and ranges -input-cmd value
-input-cmd string
Command producing the input. --input-num is required when using this input method. Overrides -w. Command producing the input. --input-num is required when using this input method. Overrides -w.
-input-num int -input-num int
Number of inputs to test. Used in conjunction with --input-cmd. (default 100) Number of inputs to test. Used in conjunction with --input-cmd. (default 100)
-k TLS identity verification -k TLS identity verification
-l Show target location of redirect responses
-mc string -mc string
Match HTTP status codes from respose, use "all" to match every response code. (default "200,204,301,302,307,401,403") 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
-mr string -mr string
Match regexp Match regexp
-ms string -ms string
Match HTTP response size Match HTTP response size
-mw string -mw string
Match amount of words in response Match amount of words in response
-ml string
Match amount of lines in response
-o string -o string
Write output to file Write output to file
-of string -of string
Output file format. Available formats: json, csv, ecsv (default "json") Output file format. Available formats: json, html, md, csv, ecsv (default "json")
-p delay -p delay
Seconds of delay between requests, or a range of random delay. For example "0.1" or "0.1-2.0" Seconds of delay between requests, or a range of random delay. For example "0.1" or "0.1-2.0"
-r Follow redirects -r Follow redirects
@ -167,12 +170,10 @@ To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-
HTTP request timeout in seconds. (default 10) HTTP request timeout in seconds. (default 10)
-u string -u string
Target URL Target URL
-w string -w value
Wordlist file path or - to read from standard input Wordlist file path and (optional) custom fuzz keyword, using colon as delimiter. Use file path '-' to read from standard input. Can be supplied multiple times. Format: '/path/to/wordlist:KEYWORD'
-x string -x string
HTTP Proxy URL HTTP Proxy URL
-debug-log string
Write the debug logging information to the specified file.
``` ```
eg. `ffuf -u https://example.org/FUZZ -w /path/to/wordlist` eg. `ffuf -u https://example.org/FUZZ -w /path/to/wordlist`
@ -195,6 +196,7 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l
- New CLI flac: -acc, custom auto-calibration strings - New CLI flac: -acc, custom auto-calibration strings
- New CLI flag: -debug-log, writes the debug logging to the specified file. - New CLI flag: -debug-log, writes the debug logging to the specified file.
- New CLI flags -ml and -fl, filters/matches line count in response - New CLI flags -ml and -fl, filters/matches line count in response
- Ability to use multiple wordlists / keywords by defining multiple -w command line flags. The if no keyword is defined, the default is FUZZ to keep backwards compatibility. Example: `-w "wordlists/custom.txt:CUSTOM" -H "RandomHeader: CUSTOM"`.
- Changed - Changed

119
main.go
View File

@ -34,6 +34,8 @@ type cliOptions struct {
matcherLines string matcherLines string
proxyURL string proxyURL string
outputFormat string outputFormat string
wordlists multiStringFlag
inputcommands multiStringFlag
headers multiStringFlag headers multiStringFlag
cookies multiStringFlag cookies multiStringFlag
AutoCalibrationStrings multiStringFlag AutoCalibrationStrings multiStringFlag
@ -62,7 +64,7 @@ func main() {
flag.BoolVar(&conf.DirSearchCompat, "D", false, "DirSearch style wordlist compatibility mode. Used in conjunction with -e flag. Replaces %EXT% in wordlist entry with each of the extensions provided by -e.") flag.BoolVar(&conf.DirSearchCompat, "D", false, "DirSearch style wordlist compatibility mode. Used in conjunction with -e flag. Replaces %EXT% in wordlist entry with each of the extensions provided by -e.")
flag.Var(&opts.headers, "H", "Header `\"Name: Value\"`, separated by colon. Multiple -H flags are accepted.") flag.Var(&opts.headers, "H", "Header `\"Name: Value\"`, separated by colon. Multiple -H flags are accepted.")
flag.StringVar(&conf.Url, "u", "", "Target URL") flag.StringVar(&conf.Url, "u", "", "Target URL")
flag.StringVar(&conf.Wordlist, "w", "", "Wordlist file path or - to read from standard input") flag.Var(&opts.wordlists, "w", "Wordlist file path and (optional) custom fuzz keyword, using colon as delimiter. Use file path '-' to read from standard input. Can be supplied multiple times. Format: '/path/to/wordlist:KEYWORD'")
flag.BoolVar(&conf.TLSVerify, "k", false, "TLS identity verification") flag.BoolVar(&conf.TLSVerify, "k", false, "TLS identity verification")
flag.StringVar(&opts.delay, "p", "", "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"") flag.StringVar(&opts.delay, "p", "", "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"")
flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response. Comma separated list of codes and ranges") flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response. Comma separated list of codes and ranges")
@ -76,7 +78,7 @@ func main() {
flag.StringVar(&conf.Data, "data-binary", "", "POST data (alias of -d)") flag.StringVar(&conf.Data, "data-binary", "", "POST data (alias of -d)")
flag.BoolVar(&conf.Colors, "c", false, "Colorize output.") flag.BoolVar(&conf.Colors, "c", false, "Colorize output.")
flag.BoolVar(&ignored, "compressed", true, "Dummy flag for copy as curl functionality (ignored)") flag.BoolVar(&ignored, "compressed", true, "Dummy flag for copy as curl functionality (ignored)")
flag.StringVar(&conf.InputCommand, "input-cmd", "", "Command producing the input. --input-num is required when using this input method. Overrides -w.") 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.IntVar(&conf.InputNum, "input-num", 100, "Number of inputs to test. Used in conjunction with --input-cmd.")
flag.BoolVar(&ignored, "i", true, "Dummy flag for copy as curl functionality (ignored)") 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, "b", "Cookie data `\"NAME1=VALUE1; NAME2=VALUE2\"` for copy as curl functionality.\nResults unpredictable when combined with -H \"Cookie: ...\"")
@ -148,18 +150,16 @@ func main() {
func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) { func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) {
errs := ffuf.NewMultierror() errs := ffuf.NewMultierror()
var err error var err error
var inputprovider ffuf.InputProvider inputprovider := input.NewInputProvider(conf)
// TODO: implement error handling for runnerprovider and outputprovider // TODO: implement error handling for runnerprovider and outputprovider
// We only have http runner right now // We only have http runner right now
runprovider := runner.NewRunnerByName("http", conf) runprovider := runner.NewRunnerByName("http", conf)
// Initialize the correct inputprovider // Initialize the correct inputprovider
if len(conf.InputCommand) > 0 { for _, v := range conf.InputProviders {
inputprovider, err = input.NewInputProviderByName("command", conf) err = inputprovider.AddProvider(v)
} else { if err != nil {
inputprovider, err = input.NewInputProviderByName("wordlist", conf) errs.Add(fmt.Errorf("%s", err))
} }
if err != nil {
errs.Add(fmt.Errorf("%s", err))
} }
// We only have stdout outputprovider right now // We only have stdout outputprovider right now
outprovider := output.NewOutputProviderByName("stdout", conf) outprovider := output.NewOutputProviderByName("stdout", conf)
@ -229,16 +229,12 @@ func prepareFilters(parseOpts *cliOptions, conf *ffuf.Config) error {
func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error { func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
//TODO: refactor in a proper flag library that can handle things like required flags //TODO: refactor in a proper flag library that can handle things like required flags
errs := ffuf.NewMultierror() errs := ffuf.NewMultierror()
foundkeyword := false
var err error var err error
var err2 error var err2 error
if len(conf.Url) == 0 { if len(conf.Url) == 0 {
errs.Add(fmt.Errorf("-u flag is required")) errs.Add(fmt.Errorf("-u flag is required"))
} }
if len(conf.Wordlist) == 0 && len(conf.InputCommand) == 0 {
errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required"))
}
// prepare extensions // prepare extensions
if parseOpts.extensions != "" { if parseOpts.extensions != "" {
extensions := strings.Split(parseOpts.extensions, ",") extensions := strings.Split(parseOpts.extensions, ",")
@ -249,23 +245,52 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
if len(parseOpts.cookies) > 0 { if len(parseOpts.cookies) > 0 {
parseOpts.headers.Set("Cookie: " + strings.Join(parseOpts.cookies, "; ")) parseOpts.headers.Set("Cookie: " + strings.Join(parseOpts.cookies, "; "))
} }
//Prepare inputproviders
for _, v := range parseOpts.wordlists {
wl := strings.SplitN(v, ":", 2)
if len(wl) == 2 {
conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
Name: "wordlist",
Value: wl[0],
Keyword: wl[1],
})
} else {
conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
Name: "wordlist",
Value: wl[0],
Keyword: "FUZZ",
})
}
}
for _, v := range parseOpts.inputcommands {
ic := strings.SplitN(v, ":", 2)
if len(ic) == 2 {
conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
Name: "command",
Value: ic[0],
Keyword: ic[1],
})
conf.CommandKeywords = append(conf.CommandKeywords, ic[0])
} else {
conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
Name: "command",
Value: ic[0],
Keyword: "FUZZ",
})
conf.CommandKeywords = append(conf.CommandKeywords, "FUZZ")
}
}
if len(conf.InputProviders) == 0 {
errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required"))
}
//Prepare headers //Prepare headers
for _, v := range parseOpts.headers { for _, v := range parseOpts.headers {
hs := strings.SplitN(v, ":", 2) hs := strings.SplitN(v, ":", 2)
if len(hs) == 2 { if len(hs) == 2 {
fuzzedheader := false conf.Headers[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
for _, fv := range hs {
if strings.Index(fv, "FUZZ") != -1 {
// Add to fuzzheaders
fuzzedheader = true
}
}
if fuzzedheader {
conf.FuzzHeaders[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
foundkeyword = true
} else {
conf.StaticHeaders[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
}
} else { } else {
errs.Add(fmt.Errorf("Header defined by -H needs to have a value. \":\" should be used as a separator")) errs.Add(fmt.Errorf("Header defined by -H needs to have a value. \":\" should be used as a separator"))
} }
@ -333,20 +358,34 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
conf.CommandLine = strings.Join(os.Args, " ") conf.CommandLine = strings.Join(os.Args, " ")
//Search for keyword from HTTP method, URL and POST data too for _, provider := range conf.InputProviders {
if conf.Method == "FUZZ" { if !keywordPresent(provider.Keyword, conf) {
foundkeyword = true errmsg := fmt.Sprintf("Keyword %s defined, but not found in headers, method, URL or POST data.", provider.Keyword)
} errs.Add(fmt.Errorf(errmsg))
if strings.Index(conf.Url, "FUZZ") != -1 { }
foundkeyword = true
}
if strings.Index(conf.Data, "FUZZ") != -1 {
foundkeyword = true
}
if !foundkeyword {
errs.Add(fmt.Errorf("No FUZZ keyword(s) found in headers, method, URL or POST data, nothing to do"))
} }
return errs.ErrorOrNil() return errs.ErrorOrNil()
} }
func keywordPresent(keyword string, conf *ffuf.Config) bool {
//Search for keyword from HTTP method, URL and POST data too
if strings.Index(conf.Method, keyword) != -1 {
return true
}
if strings.Index(conf.Url, keyword) != -1 {
return true
}
if strings.Index(conf.Data, keyword) != -1 {
return true
}
for k, v := range conf.Headers {
if strings.Index(k, keyword) != -1 {
return true
}
if strings.Index(v, keyword) != -1 {
return true
}
}
return false
}

View File

@ -16,8 +16,7 @@ type optRange struct {
} }
type Config struct { type Config struct {
StaticHeaders map[string]string Headers map[string]string
FuzzHeaders map[string]string
Extensions []string Extensions []string
DirSearchCompat bool DirSearchCompat bool
Method string Method string
@ -26,8 +25,8 @@ type Config struct {
Data string Data string
Quiet bool Quiet bool
Colors bool Colors bool
Wordlist string InputProviders []InputProviderConfig
InputCommand string CommandKeywords []string
InputNum int InputNum int
OutputFile string OutputFile string
OutputFormat string OutputFormat string
@ -49,11 +48,16 @@ type Config struct {
CommandLine string CommandLine string
} }
type InputProviderConfig struct {
Name string
Keyword string
Value string
}
func NewConfig(ctx context.Context) Config { func NewConfig(ctx context.Context) Config {
var conf Config var conf Config
conf.Context = ctx conf.Context = ctx
conf.StaticHeaders = make(map[string]string) conf.Headers = make(map[string]string)
conf.FuzzHeaders = make(map[string]string)
conf.Method = "GET" conf.Method = "GET"
conf.Url = "" conf.Url = ""
conf.TLSVerify = false conf.TLSVerify = false
@ -64,7 +68,8 @@ func NewConfig(ctx context.Context) Config {
conf.StopOnAll = false conf.StopOnAll = false
conf.ShowRedirectLocation = false conf.ShowRedirectLocation = false
conf.FollowRedirects = false conf.FollowRedirects = false
conf.InputCommand = "" conf.InputProviders = make([]InputProviderConfig, 0)
conf.CommandKeywords = make([]string, 0)
conf.InputNum = 0 conf.InputNum = 0
conf.ProxyURL = http.ProxyFromEnvironment conf.ProxyURL = http.ProxyFromEnvironment
conf.Filters = make([]FilterProvider, 0) conf.Filters = make([]FilterProvider, 0)

View File

@ -8,14 +8,25 @@ type FilterProvider interface {
//RunnerProvider is an interface for request executors //RunnerProvider is an interface for request executors
type RunnerProvider interface { type RunnerProvider interface {
Prepare(input []byte) (Request, error) Prepare(input map[string][]byte) (Request, error)
Execute(req *Request) (Response, error) Execute(req *Request) (Response, error)
} }
//InputProvider interface handles the input data for RunnerProvider //InputProvider interface handles the input data for RunnerProvider
type InputProvider interface { type InputProvider interface {
AddProvider(InputProviderConfig) error
Next() bool Next() bool
Position() int Position() int
Value() map[string][]byte
Total() int
}
//InternalInputProvider interface handles providing input data to InputProvider
type InternalInputProvider interface {
Keyword() string
Next() bool
Position() int
ResetPosition()
Value() []byte Value() []byte
Total() int Total() int
} }

View File

@ -85,7 +85,7 @@ func (j *Job) Start() {
go func() { go func() {
defer func() { <-limiter }() defer func() { <-limiter }()
defer wg.Done() defer wg.Done()
j.runTask([]byte(nextInput), nextPosition, false) j.runTask(nextInput, nextPosition, false)
if j.Config.Delay.HasDelay { if j.Config.Delay.HasDelay {
var sleepDurationMS time.Duration var sleepDurationMS time.Duration
if j.Config.Delay.IsRange { if j.Config.Delay.IsRange {
@ -157,7 +157,7 @@ func (j *Job) isMatch(resp Response) bool {
return true return true
} }
func (j *Job) runTask(input []byte, position int, retried bool) { func (j *Job) runTask(input map[string][]byte, position int, retried bool) {
req, err := j.Runner.Prepare(input) req, err := j.Runner.Prepare(input)
req.Position = position req.Position = position
if err != nil { if err != nil {
@ -205,7 +205,12 @@ func (j *Job) CalibrateResponses() ([]Response, error) {
results := make([]Response, 0) results := make([]Response, 0)
for _, input := range cInputs { for _, input := range cInputs {
req, err := j.Runner.Prepare([]byte(input)) inputs := make(map[string][]byte, 0)
for _, v := range j.Config.InputProviders {
inputs[v.Keyword] = []byte(input)
}
req, err := j.Runner.Prepare(inputs)
if err != nil { if err != nil {
j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err)) j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
j.incError() j.incError()

View File

@ -6,7 +6,7 @@ type Request struct {
Url string Url string
Headers map[string]string Headers map[string]string
Data []byte Data []byte
Input []byte Input map[string][]byte
Position int Position int
} }

View File

@ -10,22 +10,36 @@ import (
) )
type CommandInput struct { type CommandInput struct {
config *ffuf.Config config *ffuf.Config
count int count int
keyword string
command string
} }
func NewCommandInput(conf *ffuf.Config) (*CommandInput, error) { func NewCommandInput(keyword string, value string, conf *ffuf.Config) (*CommandInput, error) {
var cmd CommandInput var cmd CommandInput
cmd.keyword = keyword
cmd.config = conf cmd.config = conf
cmd.count = -1 cmd.count = -1
cmd.command = value
return &cmd, nil return &cmd, nil
} }
//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 { func (c *CommandInput) Position() int {
return c.count return c.count
} }
//ResetPosition will reset the current position of the InternalInputProvider
func (c *CommandInput) ResetPosition() {
c.count = 0
}
//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 { func (c *CommandInput) Next() bool {
c.count++ c.count++
@ -39,7 +53,7 @@ func (c *CommandInput) Next() bool {
func (c *CommandInput) Value() []byte { func (c *CommandInput) Value() []byte {
var stdout bytes.Buffer var stdout bytes.Buffer
os.Setenv("FFUF_NUM", strconv.Itoa(c.count)) os.Setenv("FFUF_NUM", strconv.Itoa(c.count))
cmd := exec.Command(SHELL_CMD, SHELL_ARG, c.config.InputCommand) cmd := exec.Command(SHELL_CMD, SHELL_ARG, c.command)
cmd.Stdout = &stdout cmd.Stdout = &stdout
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {

View File

@ -4,11 +4,63 @@ import (
"github.com/ffuf/ffuf/pkg/ffuf" "github.com/ffuf/ffuf/pkg/ffuf"
) )
func NewInputProviderByName(name string, conf *ffuf.Config) (ffuf.InputProvider, error) { type MainInputProvider struct {
if name == "command" { Providers []ffuf.InternalInputProvider
return NewCommandInput(conf) Config *ffuf.Config
position int
}
func NewInputProvider(conf *ffuf.Config) ffuf.InputProvider {
return &MainInputProvider{Config: conf}
}
func (i *MainInputProvider) AddProvider(provider ffuf.InputProviderConfig) error {
if provider.Name == "command" {
newcomm, _ := NewCommandInput(provider.Keyword, provider.Value, i.Config)
i.Providers = append(i.Providers, newcomm)
} else { } else {
// Default to wordlist // Default to wordlist
return NewWordlistInput(conf) newwl, err := NewWordlistInput(provider.Keyword, provider.Value, i.Config)
if err != nil {
return err
}
i.Providers = append(i.Providers, newwl)
} }
return nil
}
//Position will return the current position of progress
func (i *MainInputProvider) Position() int {
return i.position
}
//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
}
i.position++
return true
}
//Value returns a map of keyword:value pairs including all inputs
func (i *MainInputProvider) Value() map[string][]byte {
values := make(map[string][]byte)
for _, p := range i.Providers {
if !p.Next() {
// Loop to beginning if the inputprovider has been exhausted
p.ResetPosition()
}
values[p.Keyword()] = p.Value()
}
return values
}
//Total returns the amount of input combinations available
func (i *MainInputProvider) Total() int {
count := 1
for _, p := range i.Providers {
count = count * p.Total()
}
return count
} }

View File

@ -12,27 +12,29 @@ type WordlistInput struct {
config *ffuf.Config config *ffuf.Config
data [][]byte data [][]byte
position int position int
keyword string
} }
func NewWordlistInput(conf *ffuf.Config) (*WordlistInput, error) { func NewWordlistInput(keyword string, value string, conf *ffuf.Config) (*WordlistInput, error) {
var wl WordlistInput var wl WordlistInput
wl.keyword = keyword
wl.config = conf wl.config = conf
wl.position = -1 wl.position = -1
var valid bool var valid bool
var err error var err error
// stdin? // stdin?
if conf.Wordlist == "-" { if value == "-" {
// yes // yes
valid = true valid = true
} else { } else {
// no // no
valid, err = wl.validFile(conf.Wordlist) valid, err = wl.validFile(value)
} }
if err != nil { if err != nil {
return &wl, err return &wl, err
} }
if valid { if valid {
err = wl.readFile(conf.Wordlist) err = wl.readFile(value)
} }
return &wl, err return &wl, err
} }
@ -42,6 +44,16 @@ func (w *WordlistInput) Position() int {
return w.position return w.position
} }
//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
func (w *WordlistInput) Keyword() string {
return w.keyword
}
//Next will increment the cursor position, and return a boolean telling if there's words left in the list //Next will increment the cursor position, and return a boolean telling if there's words left in the list
func (w *WordlistInput) Next() bool { func (w *WordlistInput) Next() bool {
w.position++ w.position++

View File

@ -9,9 +9,10 @@ import (
"github.com/ffuf/ffuf/pkg/ffuf" "github.com/ffuf/ffuf/pkg/ffuf"
) )
var header = []string{"input", "position", "status_code", "content_length", "content_words", "content_lines"} var staticheaders = []string{"position", "status_code", "content_length", "content_words", "content_lines"}
func writeCSV(config *ffuf.Config, res []Result, encode bool) error { func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
header := make([]string, 0)
f, err := os.Create(config.OutputFile) f, err := os.Create(config.OutputFile)
if err != nil { if err != nil {
return err return err
@ -21,12 +22,24 @@ func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
w := csv.NewWriter(f) w := csv.NewWriter(f)
defer w.Flush() defer w.Flush()
for _, inputprovider := range config.InputProviders {
header = append(header, inputprovider.Keyword)
}
for _, item := range staticheaders {
header = append(header, item)
}
if err := w.Write(header); err != nil { if err := w.Write(header); err != nil {
return err return err
} }
for _, r := range res { for _, r := range res {
if encode { if encode {
r.Input = base64encode(r.Input) inputs := make(map[string][]byte, 0)
for k, v := range r.Input {
inputs[k] = []byte(base64encode(v))
}
r.Input = inputs
} }
err := w.Write(toCSV(r)) err := w.Write(toCSV(r))
@ -37,17 +50,19 @@ func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
return nil return nil
} }
func base64encode(in string) string { func base64encode(in []byte) string {
return base64.StdEncoding.EncodeToString([]byte(in)) return base64.StdEncoding.EncodeToString(in)
} }
func toCSV(r Result) []string { func toCSV(r Result) []string {
return []string{ res := make([]string, 0)
r.Input, for _, v := range r.Input {
strconv.Itoa(r.Position), res = append(res, string(v))
strconv.FormatInt(r.StatusCode, 10),
strconv.FormatInt(r.ContentLength, 10),
strconv.FormatInt(r.ContentWords, 10),
strconv.FormatInt(r.ContentLines, 10),
} }
res = append(res, strconv.Itoa(r.Position))
res = append(res, strconv.FormatInt(r.StatusCode, 10))
res = append(res, strconv.FormatInt(r.ContentLength, 10))
res = append(res, strconv.FormatInt(r.ContentWords, 10))
res = append(res, strconv.FormatInt(r.ContentLines, 10))
return res
} }

View File

@ -11,6 +11,7 @@ import (
type htmlFileOutput struct { type htmlFileOutput struct {
CommandLine string CommandLine string
Time string Time string
Keys []string
Results []Result Results []Result
} }
@ -62,7 +63,8 @@ const (
</div> </div>
<tr> <tr>
<th>Status</th> <th>Status</th>
<th>Input</th> {{ range .Keys }} <th>{{ . }}</th>
{{ end }}
<th>Position</th> <th>Position</th>
<th>Length</th> <th>Length</th>
<th>Words</th> <th>Words</th>
@ -71,11 +73,11 @@ const (
</thead> </thead>
<tbody> <tbody>
{{range .Results}} {{range $result := .Results}}
<div style="display:none"> <div style="display:none">
|result_raw|{{ .StatusCode }}|{{ .Input }}|{{ .Position }}|{{ .ContentLength }}|{{ .ContentWords }}|{{ .ContentLines }}| |result_raw|{{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}|{{ $value | printf "%s" }}{{ end }}|{{ $result.Position }}|{{ $result.ContentLength }}|{{ $result.ContentWords }}|{{ $result.ContentLines }}|
</div> </div>
<tr class="result-{{ .StatusCode }}" style="background-color: {{.HTMLColor}};"><td><font color="black" class="status-code">{{ .StatusCode }}</font></td><td>{{ .Input }}</td><td>{{ .Position }}</td><td>{{ .ContentLength }}</td><td>{{ .ContentWords }}</td><td>{{ .ContentLines }}</td></tr> <tr class="result-{{ $result.StatusCode }}" style="background-color: {{$result.HTMLColor}};"><td><font color="black" class="status-code">{{ $result.StatusCode }}</font></td>{{ range $keyword, $value := $result.Input }}<td>{{ $value | printf "%s" }}</td>{{ end }}</td><td>{{ $result.Position }}</td><td>{{ $result.ContentLength }}</td><td>{{ $result.ContentWords }}</td><td>{{ $result.ContentLines }}</td></tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
@ -142,10 +144,16 @@ func writeHTML(config *ffuf.Config, results []Result) error {
ti := time.Now() ti := time.Now()
keywords := make([]string, 0)
for _, inputprovider := range config.InputProviders {
keywords = append(keywords, inputprovider.Keyword)
}
outHTML := htmlFileOutput{ outHTML := htmlFileOutput{
CommandLine: config.CommandLine, CommandLine: config.CommandLine,
Time: ti.Format(time.RFC3339), Time: ti.Format(time.RFC3339),
Results: results, Results: results,
Keys: keywords,
} }
f, err := os.Create(config.OutputFile) f, err := os.Create(config.OutputFile)

View File

@ -8,33 +8,32 @@ import (
"github.com/ffuf/ffuf/pkg/ffuf" "github.com/ffuf/ffuf/pkg/ffuf"
) )
type markdownFileOutput struct {
CommandLine string
Time string
Results []Result
}
const ( const (
markdownTemplate = `# FFUF Report markdownTemplate = `# FFUF Report
Command line : ` + "`{{.CommandLine}}`" + ` Command line : ` + "`{{.CommandLine}}`" + `
Time: ` + "{{ .Time }}" + ` Time: ` + "{{ .Time }}" + `
| Input | Position | Status Code | Content Length | Content Words | Content Lines | {{ range .Keys }}| {{ . }} {{ end }}| Position | Status Code | Content Length | Content Words | Content Lines |
| :---- | :------- | :---------- | :------------- | :------------ | :------------ | {{ range .Keys }}| :- {{ end }}| :---- | :------- | :---------- | :------------- | :------------ | :------------ |
{{range .Results}}| {{ .Input }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} |
{{end}} {{end}}` // The template format is not pretty but follows the markdown guide
` // The template format is not pretty but follows the markdown guide
) )
func writeMarkdown(config *ffuf.Config, res []Result) error { func writeMarkdown(config *ffuf.Config, res []Result) error {
ti := time.Now() ti := time.Now()
outHTML := htmlFileOutput{ keywords := make([]string, 0)
for _, inputprovider := range config.InputProviders {
keywords = append(keywords, inputprovider.Keyword)
}
outMD := htmlFileOutput{
CommandLine: config.CommandLine, CommandLine: config.CommandLine,
Time: ti.Format(time.RFC3339), Time: ti.Format(time.RFC3339),
Results: res, Results: res,
Keys: keywords,
} }
f, err := os.Create(config.OutputFile) f, err := os.Create(config.OutputFile)
@ -46,6 +45,6 @@ func writeMarkdown(config *ffuf.Config, res []Result) error {
templateName := "output.md" templateName := "output.md"
t := template.New(templateName).Delims("{{", "}}") t := template.New(templateName).Delims("{{", "}}")
t.Parse(markdownTemplate) t.Parse(markdownTemplate)
t.Execute(f, outHTML) t.Execute(f, outMD)
return nil return nil
} }

View File

@ -27,13 +27,13 @@ type Stdoutput struct {
} }
type Result struct { type Result struct {
Input string `json:"input"` Input map[string][]byte `json:"input"`
Position int `json:"position"` Position int `json:"position"`
StatusCode int64 `json:"status"` StatusCode int64 `json:"status"`
ContentLength int64 `json:"length"` ContentLength int64 `json:"length"`
ContentWords int64 `json:"words"` ContentWords int64 `json:"words"`
ContentLines int64 `json:"lines"` ContentLines int64 `json:"lines"`
HTMLColor string `json:"html_color"` HTMLColor string `json:"-"`
} }
func NewStdoutput(conf *ffuf.Config) *Stdoutput { func NewStdoutput(conf *ffuf.Config) *Stdoutput {
@ -133,8 +133,12 @@ func (s *Stdoutput) Result(resp ffuf.Response) {
// Check if we need the data later // Check if we need the data later
if s.config.OutputFile != "" { if s.config.OutputFile != "" {
// No need to store results if we're not going to use them later // No need to store results if we're not going to use them later
inputs := make(map[string][]byte, 0)
for k, v := range resp.Request.Input {
inputs[k] = v
}
sResult := Result{ sResult := Result{
Input: string(resp.Request.Input), Input: inputs,
Position: resp.Request.Position, Position: resp.Request.Position,
StatusCode: resp.StatusCode, StatusCode: resp.StatusCode,
ContentLength: resp.ContentLength, ContentLength: resp.ContentLength,
@ -149,28 +153,64 @@ func (s *Stdoutput) printResult(resp ffuf.Response) {
if s.config.Quiet { if s.config.Quiet {
s.resultQuiet(resp) s.resultQuiet(resp)
} else { } else {
s.resultNormal(resp) if len(resp.Request.Input) > 1 {
// Print a multi-line result (when using multiple input keywords and wordlists)
s.resultMultiline(resp)
} else {
s.resultNormal(resp)
}
} }
} }
func (s *Stdoutput) prepareInputsOneLine(resp ffuf.Response) string {
inputs := ""
if len(resp.Request.Input) > 1 {
for k, v := range resp.Request.Input {
if inSlice(k, s.config.CommandKeywords) {
// If we're using external command for input, display the position instead of input
inputs = fmt.Sprintf("%s%s : %s ", inputs, k, strconv.Itoa(resp.Request.Position))
} else {
inputs = fmt.Sprintf("%s%s : %s ", inputs, k, v)
}
}
} else {
for k, v := range resp.Request.Input {
if inSlice(k, s.config.CommandKeywords) {
// If we're using external command for input, display the position instead of input
inputs = strconv.Itoa(resp.Request.Position)
} else {
inputs = string(v)
}
}
}
return inputs
}
func (s *Stdoutput) resultQuiet(resp ffuf.Response) { func (s *Stdoutput) resultQuiet(resp ffuf.Response) {
if len(s.config.InputCommand) > 0 { fmt.Println(s.prepareInputsOneLine(resp))
// If we're using external command for input, display the position instead of input }
fmt.Println(strconv.Itoa(resp.Request.Position))
} else { func (s *Stdoutput) resultMultiline(resp ffuf.Response) {
fmt.Println(string(resp.Request.Input)) var res_hdr, res_str string
res_str = "%s * %s: %s\n"
res_hdr = fmt.Sprintf("%s[Status: %d, Size: %d, Words: %d, Lines: %d%s]", TERMINAL_CLEAR_LINE, resp.StatusCode, resp.ContentLength, resp.ContentWords, resp.ContentLines, s.addRedirectLocation(resp))
fmt.Println(s.colorize(res_hdr, resp.StatusCode))
for k, v := range resp.Request.Input {
if inSlice(k, s.config.CommandKeywords) {
// If we're using external command for input, display the position instead of input
fmt.Printf(res_str, TERMINAL_CLEAR_LINE, k, strconv.Itoa(resp.Request.Position))
} else {
// Wordlist input
fmt.Printf(res_str, TERMINAL_CLEAR_LINE, k, v)
}
} }
} }
func (s *Stdoutput) resultNormal(resp ffuf.Response) { func (s *Stdoutput) resultNormal(resp ffuf.Response) {
var responseString string var res_str string
if len(s.config.InputCommand) > 0 { res_str = fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d%s]", TERMINAL_CLEAR_LINE, s.prepareInputsOneLine(resp), s.colorize(fmt.Sprintf("%d", resp.StatusCode), resp.StatusCode), resp.ContentLength, resp.ContentWords, resp.ContentLines, s.addRedirectLocation(resp))
// If we're using external command for input, display the position instead of input fmt.Println(res_str)
responseString = fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, strconv.Itoa(resp.Request.Position), s.colorizeStatus(resp.StatusCode), resp.ContentLength, resp.ContentWords, resp.ContentLines)
} else {
responseString = fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, resp.Request.Input, s.colorizeStatus(resp.StatusCode), resp.ContentLength, resp.ContentWords, resp.ContentLines)
}
fmt.Println(responseString)
} }
// addRedirectLocation returns a formatted string containing the Redirect location or returns an empty string // addRedirectLocation returns a formatted string containing the Redirect location or returns an empty string
@ -184,9 +224,9 @@ func (s *Stdoutput) addRedirectLocation(resp ffuf.Response) string {
return "" return ""
} }
func (s *Stdoutput) colorizeStatus(status int64) string { func (s *Stdoutput) colorize(input string, status int64) string {
if !s.config.Colors { if !s.config.Colors {
return fmt.Sprintf("%d", status) return fmt.Sprintf("%s", input)
} }
colorCode := ANSI_CLEAR colorCode := ANSI_CLEAR
if status >= 200 && status < 300 { if status >= 200 && status < 300 {
@ -201,9 +241,18 @@ func (s *Stdoutput) colorizeStatus(status int64) string {
if status >= 500 && status < 600 { if status >= 500 && status < 600 {
colorCode = ANSI_RED colorCode = ANSI_RED
} }
return fmt.Sprintf("%s%d%s", colorCode, status, ANSI_CLEAR) return fmt.Sprintf("%s%s%s", colorCode, input, ANSI_CLEAR)
} }
func printOption(name []byte, value []byte) { func printOption(name []byte, value []byte) {
fmt.Printf(" :: %-12s : %s\n", name, value) fmt.Printf(" :: %-12s : %s\n", name, value)
} }
func inSlice(key string, slice []string) bool {
for _, v := range slice {
if v == key {
return true
}
}
return false
}

View File

@ -44,22 +44,26 @@ func NewSimpleRunner(conf *ffuf.Config) ffuf.RunnerProvider {
return &simplerunner return &simplerunner
} }
func (r *SimpleRunner) Prepare(input []byte) (ffuf.Request, error) { func (r *SimpleRunner) Prepare(input map[string][]byte) (ffuf.Request, error) {
req := ffuf.NewRequest(r.config) req := ffuf.NewRequest(r.config)
// should we fuzz the http method
if r.config.Method == "FUZZ" { req.Headers = r.config.Headers
req.Method = string(input) req.Url = r.config.Url
req.Method = r.config.Method
req.Data = []byte(r.config.Data)
for keyword, inputitem := range input {
req.Method = strings.Replace(req.Method, keyword, string(inputitem), -1)
headers := make(map[string]string, 0)
for h, v := range req.Headers {
headers[strings.Replace(h, keyword, string(inputitem), -1)] = strings.Replace(v, keyword, string(inputitem), -1)
}
req.Headers = headers
req.Url = strings.Replace(req.Url, keyword, string(inputitem), -1)
req.Data = []byte(strings.Replace(string(req.Data), keyword, string(inputitem), -1))
} }
for h, v := range r.config.StaticHeaders {
req.Headers[h] = v
}
for h, v := range r.config.FuzzHeaders {
req.Headers[strings.Replace(h, "FUZZ", string(input), -1)] = strings.Replace(v, "FUZZ", string(input), -1)
}
req.Input = input req.Input = input
req.Url = strings.Replace(r.config.Url, "FUZZ", string(input), -1)
req.Data = []byte(strings.Replace(r.config.Data, "FUZZ", string(input), -1))
return req, nil return req, nil
} }