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:
parent
e200bd11f7
commit
5456a37f72
42
README.md
42
README.md
@ -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`).
|
||||
|
||||
```
|
||||
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.
|
||||
-H "Name: Value"
|
||||
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")
|
||||
-ac
|
||||
Automatically calibrate filtering options
|
||||
-acc
|
||||
Custom auto-calibration string. Can be used multiple times. Implies -ac
|
||||
-i
|
||||
Dummy flag for copy as curl functionality (ignored)
|
||||
-acc value
|
||||
Custom auto-calibration string. Can be used multiple times. Implies -ac
|
||||
-b "NAME1=VALUE1; NAME2=VALUE2"
|
||||
Cookie data "NAME1=VALUE1; NAME2=VALUE2" for copy as curl functionality.
|
||||
Results unpredictable when combined with -H "Cookie: ..."
|
||||
-cookie
|
||||
Cookie data (alias of -b)
|
||||
-c Colorize output.
|
||||
-compressed
|
||||
Dummy flag for copy as curl functionality (ignored) (default true)
|
||||
-cookie value
|
||||
Cookie data (alias of -b)
|
||||
-d string
|
||||
POST data
|
||||
-data-ascii
|
||||
POST data (alias of -d)
|
||||
-data-binary
|
||||
POST data (alias of -d)
|
||||
-data string
|
||||
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
|
||||
Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once.
|
||||
-fc string
|
||||
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
|
||||
Filter regexp
|
||||
-fs string
|
||||
Filter HTTP response size. Comma separated list of sizes and ranges
|
||||
-fw string
|
||||
Filter by amount of words in response. Comma separated list of word counts and ranges
|
||||
-fl string
|
||||
Filter by amount of lines in response. Comma separated list of line counts and ranges
|
||||
-input-cmd string
|
||||
-i Dummy flag for copy as curl functionality (ignored) (default true)
|
||||
-input-cmd value
|
||||
Command producing the input. --input-num is required when using this input method. Overrides -w.
|
||||
-input-num int
|
||||
Number of inputs to test. Used in conjunction with --input-cmd. (default 100)
|
||||
-k TLS identity verification
|
||||
-l Show target location of redirect responses
|
||||
-mc string
|
||||
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
|
||||
Match regexp
|
||||
-ms string
|
||||
Match HTTP response size
|
||||
-mw string
|
||||
Match amount of words in response
|
||||
-ml string
|
||||
Match amount of lines in response
|
||||
-o string
|
||||
Write output to file
|
||||
-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
|
||||
Seconds of delay between requests, or a range of random delay. For example "0.1" or "0.1-2.0"
|
||||
-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)
|
||||
-u string
|
||||
Target URL
|
||||
-w string
|
||||
Wordlist file path or - to read from standard input
|
||||
-w value
|
||||
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
|
||||
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`
|
||||
@ -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 flag: -debug-log, writes the debug logging to the specified file.
|
||||
- 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
|
||||
|
||||
|
||||
119
main.go
119
main.go
@ -34,6 +34,8 @@ type cliOptions struct {
|
||||
matcherLines string
|
||||
proxyURL string
|
||||
outputFormat string
|
||||
wordlists multiStringFlag
|
||||
inputcommands multiStringFlag
|
||||
headers multiStringFlag
|
||||
cookies 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.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.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.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")
|
||||
@ -76,7 +78,7 @@ func main() {
|
||||
flag.StringVar(&conf.Data, "data-binary", "", "POST data (alias of -d)")
|
||||
flag.BoolVar(&conf.Colors, "c", false, "Colorize output.")
|
||||
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.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: ...\"")
|
||||
@ -148,18 +150,16 @@ func main() {
|
||||
func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) {
|
||||
errs := ffuf.NewMultierror()
|
||||
var err error
|
||||
var inputprovider ffuf.InputProvider
|
||||
inputprovider := input.NewInputProvider(conf)
|
||||
// TODO: implement error handling for runnerprovider and outputprovider
|
||||
// We only have http runner right now
|
||||
runprovider := runner.NewRunnerByName("http", conf)
|
||||
// Initialize the correct inputprovider
|
||||
if len(conf.InputCommand) > 0 {
|
||||
inputprovider, err = input.NewInputProviderByName("command", conf)
|
||||
} else {
|
||||
inputprovider, err = input.NewInputProviderByName("wordlist", conf)
|
||||
}
|
||||
if err != nil {
|
||||
errs.Add(fmt.Errorf("%s", err))
|
||||
for _, v := range conf.InputProviders {
|
||||
err = inputprovider.AddProvider(v)
|
||||
if err != nil {
|
||||
errs.Add(fmt.Errorf("%s", err))
|
||||
}
|
||||
}
|
||||
// We only have stdout outputprovider right now
|
||||
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 {
|
||||
//TODO: refactor in a proper flag library that can handle things like required flags
|
||||
errs := ffuf.NewMultierror()
|
||||
foundkeyword := false
|
||||
|
||||
var err error
|
||||
var err2 error
|
||||
if len(conf.Url) == 0 {
|
||||
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
|
||||
if parseOpts.extensions != "" {
|
||||
extensions := strings.Split(parseOpts.extensions, ",")
|
||||
@ -249,23 +245,52 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
|
||||
if len(parseOpts.cookies) > 0 {
|
||||
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
|
||||
for _, v := range parseOpts.headers {
|
||||
hs := strings.SplitN(v, ":", 2)
|
||||
if len(hs) == 2 {
|
||||
fuzzedheader := false
|
||||
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])
|
||||
}
|
||||
conf.Headers[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
|
||||
} else {
|
||||
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, " ")
|
||||
|
||||
//Search for keyword from HTTP method, URL and POST data too
|
||||
if conf.Method == "FUZZ" {
|
||||
foundkeyword = true
|
||||
}
|
||||
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"))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -16,8 +16,7 @@ type optRange struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
StaticHeaders map[string]string
|
||||
FuzzHeaders map[string]string
|
||||
Headers map[string]string
|
||||
Extensions []string
|
||||
DirSearchCompat bool
|
||||
Method string
|
||||
@ -26,8 +25,8 @@ type Config struct {
|
||||
Data string
|
||||
Quiet bool
|
||||
Colors bool
|
||||
Wordlist string
|
||||
InputCommand string
|
||||
InputProviders []InputProviderConfig
|
||||
CommandKeywords []string
|
||||
InputNum int
|
||||
OutputFile string
|
||||
OutputFormat string
|
||||
@ -49,11 +48,16 @@ type Config struct {
|
||||
CommandLine string
|
||||
}
|
||||
|
||||
type InputProviderConfig struct {
|
||||
Name string
|
||||
Keyword string
|
||||
Value string
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context) Config {
|
||||
var conf Config
|
||||
conf.Context = ctx
|
||||
conf.StaticHeaders = make(map[string]string)
|
||||
conf.FuzzHeaders = make(map[string]string)
|
||||
conf.Headers = make(map[string]string)
|
||||
conf.Method = "GET"
|
||||
conf.Url = ""
|
||||
conf.TLSVerify = false
|
||||
@ -64,7 +68,8 @@ func NewConfig(ctx context.Context) Config {
|
||||
conf.StopOnAll = false
|
||||
conf.ShowRedirectLocation = false
|
||||
conf.FollowRedirects = false
|
||||
conf.InputCommand = ""
|
||||
conf.InputProviders = make([]InputProviderConfig, 0)
|
||||
conf.CommandKeywords = make([]string, 0)
|
||||
conf.InputNum = 0
|
||||
conf.ProxyURL = http.ProxyFromEnvironment
|
||||
conf.Filters = make([]FilterProvider, 0)
|
||||
|
||||
@ -8,14 +8,25 @@ type FilterProvider interface {
|
||||
|
||||
//RunnerProvider is an interface for request executors
|
||||
type RunnerProvider interface {
|
||||
Prepare(input []byte) (Request, error)
|
||||
Prepare(input map[string][]byte) (Request, error)
|
||||
Execute(req *Request) (Response, error)
|
||||
}
|
||||
|
||||
//InputProvider interface handles the input data for RunnerProvider
|
||||
type InputProvider interface {
|
||||
AddProvider(InputProviderConfig) error
|
||||
Next() bool
|
||||
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
|
||||
Total() int
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ func (j *Job) Start() {
|
||||
go func() {
|
||||
defer func() { <-limiter }()
|
||||
defer wg.Done()
|
||||
j.runTask([]byte(nextInput), nextPosition, false)
|
||||
j.runTask(nextInput, nextPosition, false)
|
||||
if j.Config.Delay.HasDelay {
|
||||
var sleepDurationMS time.Duration
|
||||
if j.Config.Delay.IsRange {
|
||||
@ -157,7 +157,7 @@ func (j *Job) isMatch(resp Response) bool {
|
||||
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.Position = position
|
||||
if err != nil {
|
||||
@ -205,7 +205,12 @@ func (j *Job) CalibrateResponses() ([]Response, error) {
|
||||
|
||||
results := make([]Response, 0)
|
||||
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 {
|
||||
j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
|
||||
j.incError()
|
||||
|
||||
@ -6,7 +6,7 @@ type Request struct {
|
||||
Url string
|
||||
Headers map[string]string
|
||||
Data []byte
|
||||
Input []byte
|
||||
Input map[string][]byte
|
||||
Position int
|
||||
}
|
||||
|
||||
|
||||
@ -10,22 +10,36 @@ import (
|
||||
)
|
||||
|
||||
type CommandInput struct {
|
||||
config *ffuf.Config
|
||||
count int
|
||||
config *ffuf.Config
|
||||
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
|
||||
cmd.keyword = keyword
|
||||
cmd.config = conf
|
||||
cmd.count = -1
|
||||
cmd.command = value
|
||||
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
|
||||
func (c *CommandInput) Position() int {
|
||||
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
|
||||
func (c *CommandInput) Next() bool {
|
||||
c.count++
|
||||
@ -39,7 +53,7 @@ func (c *CommandInput) Next() bool {
|
||||
func (c *CommandInput) Value() []byte {
|
||||
var stdout bytes.Buffer
|
||||
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
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
|
||||
@ -4,11 +4,63 @@ import (
|
||||
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||
)
|
||||
|
||||
func NewInputProviderByName(name string, conf *ffuf.Config) (ffuf.InputProvider, error) {
|
||||
if name == "command" {
|
||||
return NewCommandInput(conf)
|
||||
type MainInputProvider struct {
|
||||
Providers []ffuf.InternalInputProvider
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -12,27 +12,29 @@ type WordlistInput struct {
|
||||
config *ffuf.Config
|
||||
data [][]byte
|
||||
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
|
||||
wl.keyword = keyword
|
||||
wl.config = conf
|
||||
wl.position = -1
|
||||
var valid bool
|
||||
var err error
|
||||
// stdin?
|
||||
if conf.Wordlist == "-" {
|
||||
if value == "-" {
|
||||
// yes
|
||||
valid = true
|
||||
} else {
|
||||
// no
|
||||
valid, err = wl.validFile(conf.Wordlist)
|
||||
valid, err = wl.validFile(value)
|
||||
}
|
||||
if err != nil {
|
||||
return &wl, err
|
||||
}
|
||||
if valid {
|
||||
err = wl.readFile(conf.Wordlist)
|
||||
err = wl.readFile(value)
|
||||
}
|
||||
return &wl, err
|
||||
}
|
||||
@ -42,6 +44,16 @@ func (w *WordlistInput) Position() int {
|
||||
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
|
||||
func (w *WordlistInput) Next() bool {
|
||||
w.position++
|
||||
|
||||
@ -9,9 +9,10 @@ import (
|
||||
"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 {
|
||||
header := make([]string, 0)
|
||||
f, err := os.Create(config.OutputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -21,12 +22,24 @@ func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
|
||||
w := csv.NewWriter(f)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
for _, r := range res {
|
||||
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))
|
||||
@ -37,17 +50,19 @@ func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func base64encode(in string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(in))
|
||||
func base64encode(in []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(in)
|
||||
}
|
||||
|
||||
func toCSV(r Result) []string {
|
||||
return []string{
|
||||
r.Input,
|
||||
strconv.Itoa(r.Position),
|
||||
strconv.FormatInt(r.StatusCode, 10),
|
||||
strconv.FormatInt(r.ContentLength, 10),
|
||||
strconv.FormatInt(r.ContentWords, 10),
|
||||
strconv.FormatInt(r.ContentLines, 10),
|
||||
res := make([]string, 0)
|
||||
for _, v := range r.Input {
|
||||
res = append(res, string(v))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
type htmlFileOutput struct {
|
||||
CommandLine string
|
||||
Time string
|
||||
Keys []string
|
||||
Results []Result
|
||||
}
|
||||
|
||||
@ -62,7 +63,8 @@ const (
|
||||
</div>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Input</th>
|
||||
{{ range .Keys }} <th>{{ . }}</th>
|
||||
{{ end }}
|
||||
<th>Position</th>
|
||||
<th>Length</th>
|
||||
<th>Words</th>
|
||||
@ -71,11 +73,11 @@ const (
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{range .Results}}
|
||||
{{range $result := .Results}}
|
||||
<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>
|
||||
<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}}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -142,10 +144,16 @@ func writeHTML(config *ffuf.Config, results []Result) error {
|
||||
|
||||
ti := time.Now()
|
||||
|
||||
keywords := make([]string, 0)
|
||||
for _, inputprovider := range config.InputProviders {
|
||||
keywords = append(keywords, inputprovider.Keyword)
|
||||
}
|
||||
|
||||
outHTML := htmlFileOutput{
|
||||
CommandLine: config.CommandLine,
|
||||
Time: ti.Format(time.RFC3339),
|
||||
Results: results,
|
||||
Keys: keywords,
|
||||
}
|
||||
|
||||
f, err := os.Create(config.OutputFile)
|
||||
|
||||
@ -8,33 +8,32 @@ import (
|
||||
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||
)
|
||||
|
||||
type markdownFileOutput struct {
|
||||
CommandLine string
|
||||
Time string
|
||||
Results []Result
|
||||
}
|
||||
|
||||
const (
|
||||
markdownTemplate = `# FFUF Report
|
||||
|
||||
Command line : ` + "`{{.CommandLine}}`" + `
|
||||
Time: ` + "{{ .Time }}" + `
|
||||
|
||||
| Input | Position | Status Code | Content Length | Content Words | Content Lines |
|
||||
| :---- | :------- | :---------- | :------------- | :------------ | :------------ |
|
||||
{{range .Results}}| {{ .Input }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} |
|
||||
{{end}}
|
||||
` // The template format is not pretty but follows the markdown guide
|
||||
{{ range .Keys }}| {{ . }} {{ end }}| Position | Status Code | Content Length | Content Words | Content Lines |
|
||||
{{ range .Keys }}| :- {{ end }}| :---- | :------- | :---------- | :------------- | :------------ | :------------ |
|
||||
{{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} |
|
||||
{{end}}` // The template format is not pretty but follows the markdown guide
|
||||
)
|
||||
|
||||
func writeMarkdown(config *ffuf.Config, res []Result) error {
|
||||
|
||||
ti := time.Now()
|
||||
|
||||
outHTML := htmlFileOutput{
|
||||
keywords := make([]string, 0)
|
||||
for _, inputprovider := range config.InputProviders {
|
||||
keywords = append(keywords, inputprovider.Keyword)
|
||||
}
|
||||
|
||||
outMD := htmlFileOutput{
|
||||
CommandLine: config.CommandLine,
|
||||
Time: ti.Format(time.RFC3339),
|
||||
Results: res,
|
||||
Keys: keywords,
|
||||
}
|
||||
|
||||
f, err := os.Create(config.OutputFile)
|
||||
@ -46,6 +45,6 @@ func writeMarkdown(config *ffuf.Config, res []Result) error {
|
||||
templateName := "output.md"
|
||||
t := template.New(templateName).Delims("{{", "}}")
|
||||
t.Parse(markdownTemplate)
|
||||
t.Execute(f, outHTML)
|
||||
t.Execute(f, outMD)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -27,13 +27,13 @@ type Stdoutput struct {
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Input string `json:"input"`
|
||||
Position int `json:"position"`
|
||||
StatusCode int64 `json:"status"`
|
||||
ContentLength int64 `json:"length"`
|
||||
ContentWords int64 `json:"words"`
|
||||
ContentLines int64 `json:"lines"`
|
||||
HTMLColor string `json:"html_color"`
|
||||
Input map[string][]byte `json:"input"`
|
||||
Position int `json:"position"`
|
||||
StatusCode int64 `json:"status"`
|
||||
ContentLength int64 `json:"length"`
|
||||
ContentWords int64 `json:"words"`
|
||||
ContentLines int64 `json:"lines"`
|
||||
HTMLColor string `json:"-"`
|
||||
}
|
||||
|
||||
func NewStdoutput(conf *ffuf.Config) *Stdoutput {
|
||||
@ -133,8 +133,12 @@ func (s *Stdoutput) Result(resp ffuf.Response) {
|
||||
// Check if we need the data later
|
||||
if s.config.OutputFile != "" {
|
||||
// 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{
|
||||
Input: string(resp.Request.Input),
|
||||
Input: inputs,
|
||||
Position: resp.Request.Position,
|
||||
StatusCode: resp.StatusCode,
|
||||
ContentLength: resp.ContentLength,
|
||||
@ -149,28 +153,64 @@ func (s *Stdoutput) printResult(resp ffuf.Response) {
|
||||
if s.config.Quiet {
|
||||
s.resultQuiet(resp)
|
||||
} 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) {
|
||||
if len(s.config.InputCommand) > 0 {
|
||||
// If we're using external command for input, display the position instead of input
|
||||
fmt.Println(strconv.Itoa(resp.Request.Position))
|
||||
} else {
|
||||
fmt.Println(string(resp.Request.Input))
|
||||
fmt.Println(s.prepareInputsOneLine(resp))
|
||||
}
|
||||
|
||||
func (s *Stdoutput) resultMultiline(resp ffuf.Response) {
|
||||
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) {
|
||||
var responseString string
|
||||
if len(s.config.InputCommand) > 0 {
|
||||
// If we're using external command for input, display the position instead of input
|
||||
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)
|
||||
var res_str string
|
||||
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))
|
||||
fmt.Println(res_str)
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
|
||||
func (s *Stdoutput) colorizeStatus(status int64) string {
|
||||
func (s *Stdoutput) colorize(input string, status int64) string {
|
||||
if !s.config.Colors {
|
||||
return fmt.Sprintf("%d", status)
|
||||
return fmt.Sprintf("%s", input)
|
||||
}
|
||||
colorCode := ANSI_CLEAR
|
||||
if status >= 200 && status < 300 {
|
||||
@ -201,9 +241,18 @@ func (s *Stdoutput) colorizeStatus(status int64) string {
|
||||
if status >= 500 && status < 600 {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -44,22 +44,26 @@ func NewSimpleRunner(conf *ffuf.Config) ffuf.RunnerProvider {
|
||||
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)
|
||||
// should we fuzz the http method
|
||||
if r.config.Method == "FUZZ" {
|
||||
req.Method = string(input)
|
||||
|
||||
req.Headers = r.config.Headers
|
||||
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.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
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user