New input provider --input-cmd (#40)

* New input provider: command

* Set env var and move to Windows and POSIX constants for shell instead of CLI flag.

* Display position instead of input payload when --input-cmd is used

* Update README

* Fix README and flags help

* Add an example to README
This commit is contained in:
Joona Hoikkala 2019-06-17 00:42:42 +03:00 committed by GitHub
parent cab7657257
commit 8883aea432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 162 additions and 19 deletions

View File

@ -66,9 +66,32 @@ This is a very straightforward operation, again by using the `FUZZ` keyword. Thi
ffuf -w /path/to/postdata.txt -X POST -d "username=admin\&password=FUZZ" https://target/login.php -fc 401 ffuf -w /path/to/postdata.txt -X POST -d "username=admin\&password=FUZZ" https://target/login.php -fc 401
``` ```
### Using external mutator to produce test cases
For this example, we'll fuzz JSON data that's sent over POST. [Radamsa](https://gitlab.com/akihe/radamsa) is used as the mutator.
When `--input-cmd` is used, ffuf will display matches as their position. This same position value will be available for the callee as an environment variable `$FFUF_NUM`. We'll use this position value as the seed for the mutator. Files example1.txt and example2.txt contain valid JSON payloads. We are matching all the responses, but filtering out response code `400 - Bad request`:
```
ffuf --input-cmd 'radamsa --seed $FFUF_NUM example1.txt example2.txt' -H "Content-Type: application/json" -X POST -u https://ffuf.io.fi/ -mc all -fc 400
```
It of course isn't very efficient to call the mutator for each payload, so we can also pre-generate the payloads, still using [Radamsa](https://gitlab.com/akihe/radamsa) as an example:
```
# Generate 1000 example payloads
radamsa -n 1000 -o %n.txt example1.txt example2.txt
# This results into files 1.txt ... 1000.txt
# Now we can just read the payload data in a loop from file for ffuf
ffuf --input-cmd 'cat $FFUF_NUM.txt' -H "Content-Type: application/json" -X POST -u https://ffuf.io.fi/ -mc all -fc 400
```
## Usage ## Usage
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`).
``` ```
-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"
@ -79,8 +102,12 @@ To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-
-ac -ac
Automatically calibrate filtering options Automatically calibrate filtering options
-c Colorize output. -c Colorize output.
-compressed
Dummy flag for copy as curl functionality (ignored) (default true)
-d string -d string
POST data. POST data
-data string
POST data (alias of -d)
-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
@ -91,6 +118,10 @@ To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-
Filter HTTP response size Filter HTTP response size
-fw string -fw string
Filter by amount of words in response Filter by amount of words in response
-input-cmd string
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 -k TLS identity verification
-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")
@ -144,6 +175,8 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l
- New CLI flag: -timeout to specify custom timeouts for all HTTP requests. - New CLI flag: -timeout to specify custom timeouts for all HTTP requests.
- New CLI flag: --data for compatibility with copy as curl functionality of browsers. - New CLI flag: --data for compatibility with copy as curl functionality of browsers.
- New CLI flag: --compress, dummy flag that does nothing. for compatibility with copy as curl. - New CLI flag: --compress, dummy flag that does nothing. for compatibility with copy as curl.
- New CLI flags: --input-cmd, and --input-num to handle input generation using external commands. Mutators for example. Environment variable FFUF_NUM will be updated on every call of the command.
- When --input-cmd is used, display position instead of the payload in results. The output file (of all formats) will include the payload in addition to the position however.
- Changed - Changed
- Wordlist can also be read from standard input - Wordlist can also be read from standard input

18
main.go
View File

@ -55,7 +55,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 path") flag.StringVar(&conf.Wordlist, "w", "", "Wordlist file path or - to read from standard input")
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") flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response")
@ -66,6 +66,8 @@ func main() {
flag.StringVar(&conf.Data, "data", "", "POST data (alias of -d)") flag.StringVar(&conf.Data, "data", "", "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.IntVar(&conf.InputNum, "input-num", 100, "Number of inputs to test. Used in conjunction with --input-cmd.")
flag.StringVar(&opts.matcherStatus, "mc", "200,204,301,302,307,401,403", "Match HTTP status codes from respose, use \"all\" to match every response code.") flag.StringVar(&opts.matcherStatus, "mc", "200,204,301,302,307,401,403", "Match HTTP status codes from respose, use \"all\" to match every response code.")
flag.StringVar(&opts.matcherSize, "ms", "", "Match HTTP response size") flag.StringVar(&opts.matcherSize, "ms", "", "Match HTTP response size")
flag.StringVar(&opts.matcherRegexp, "mr", "", "Match regexp") flag.StringVar(&opts.matcherRegexp, "mr", "", "Match regexp")
@ -116,11 +118,17 @@ 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 inputprovider ffuf.InputProvider
// 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)
// We only have wordlist inputprovider right now // Initialize the correct inputprovider
inputprovider, err := input.NewInputProviderByName("wordlist", conf) if len(conf.InputCommand) > 0 {
inputprovider, err = input.NewInputProviderByName("command", conf)
} else {
inputprovider, err = input.NewInputProviderByName("wordlist", conf)
}
if err != nil { if err != nil {
errs.Add(fmt.Errorf("%s", err)) errs.Add(fmt.Errorf("%s", err))
} }
@ -189,8 +197,8 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) 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 { if len(conf.Wordlist) == 0 && len(conf.InputCommand) == 0 {
errs.Add(fmt.Errorf("-w flag is required")) errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required"))
} }
// prepare extensions // prepare extensions
if parseOpts.extensions != "" { if parseOpts.extensions != "" {

View File

@ -27,6 +27,8 @@ type Config struct {
Quiet bool Quiet bool
Colors bool Colors bool
Wordlist string Wordlist string
InputCommand string
InputNum int
OutputFile string OutputFile string
OutputFormat string OutputFormat string
StopOn403 bool StopOn403 bool
@ -59,6 +61,8 @@ func NewConfig(ctx context.Context) Config {
conf.StopOnErrors = false conf.StopOnErrors = false
conf.StopOnAll = false conf.StopOnAll = false
conf.FollowRedirects = false conf.FollowRedirects = false
conf.InputCommand = ""
conf.InputNum = 0
conf.ProxyURL = http.ProxyFromEnvironment conf.ProxyURL = http.ProxyFromEnvironment
conf.Filters = make([]FilterProvider, 0) conf.Filters = make([]FilterProvider, 0)
conf.Delay = optRange{0, 0, false, false} conf.Delay = optRange{0, 0, false, false}

View File

@ -15,6 +15,7 @@ type RunnerProvider interface {
//InputProvider interface handles the input data for RunnerProvider //InputProvider interface handles the input data for RunnerProvider
type InputProvider interface { type InputProvider interface {
Next() bool Next() bool
Position() int
Value() []byte Value() []byte
Total() int Total() int
} }

View File

@ -79,12 +79,13 @@ func (j *Job) Start() {
} }
limiter <- true limiter <- true
nextInput := j.Input.Value() nextInput := j.Input.Value()
nextPosition := j.Input.Position()
wg.Add(1) wg.Add(1)
j.Counter++ j.Counter++
go func() { go func() {
defer func() { <-limiter }() defer func() { <-limiter }()
defer wg.Done() defer wg.Done()
j.runTask([]byte(nextInput), false) j.runTask([]byte(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 {
@ -156,8 +157,9 @@ func (j *Job) isMatch(resp Response) bool {
return true return true
} }
func (j *Job) runTask(input []byte, retried bool) { func (j *Job) runTask(input []byte, position int, retried bool) {
req, err := j.Runner.Prepare(input) req, err := j.Runner.Prepare(input)
req.Position = position
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()
@ -168,7 +170,7 @@ func (j *Job) runTask(input []byte, retried bool) {
if retried { if retried {
j.incError() j.incError()
} else { } else {
j.runTask(input, true) j.runTask(input, position, true)
} }
return return
} }

View File

@ -7,6 +7,7 @@ type Request struct {
Headers map[string]string Headers map[string]string
Data []byte Data []byte
Input []byte Input []byte
Position int
} }
func NewRequest(conf *Config) Request { func NewRequest(conf *Config) Request {

54
pkg/input/command.go Normal file
View File

@ -0,0 +1,54 @@
package input
import (
"bytes"
"os"
"os/exec"
"strconv"
"github.com/ffuf/ffuf/pkg/ffuf"
)
type CommandInput struct {
config *ffuf.Config
count int
}
func NewCommandInput(conf *ffuf.Config) (*CommandInput, error) {
var cmd CommandInput
cmd.config = conf
cmd.count = -1
return &cmd, nil
}
//Position will return the current position in the input list
func (c *CommandInput) Position() int {
return c.count
}
//Next will increment the cursor position, and return a boolean telling if there's iterations left
func (c *CommandInput) Next() bool {
c.count++
if c.count >= c.config.InputNum {
return false
}
return true
}
//Value returns the input from command stdoutput
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.Stdout = &stdout
err := cmd.Run()
if err != nil {
return []byte("")
}
return stdout.Bytes()
}
//Total returns the size of wordlist
func (c *CommandInput) Total() int {
return c.config.InputNum
}

8
pkg/input/const.go Normal file
View File

@ -0,0 +1,8 @@
// +build !windows
package input
const (
SHELL_CMD = "/bin/sh"
SHELL_ARG = "-c"
)

View File

@ -0,0 +1,8 @@
// +build windows
package input
const (
SHELL_CMD = "cmd.exe"
SHELL_ARG = "/C"
)

View File

@ -5,6 +5,10 @@ import (
) )
func NewInputProviderByName(name string, conf *ffuf.Config) (ffuf.InputProvider, error) { func NewInputProviderByName(name string, conf *ffuf.Config) (ffuf.InputProvider, error) {
// We have only one inputprovider at the moment if name == "command" {
return NewCommandInput(conf)
} else {
// Default to wordlist
return NewWordlistInput(conf) return NewWordlistInput(conf)
} }
}

View File

@ -37,6 +37,11 @@ func NewWordlistInput(conf *ffuf.Config) (*WordlistInput, error) {
return &wl, err return &wl, err
} }
//Position will return the current position in the input list
func (w *WordlistInput) Position() int {
return w.position
}
//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,7 +9,7 @@ import (
"github.com/ffuf/ffuf/pkg/ffuf" "github.com/ffuf/ffuf/pkg/ffuf"
) )
var header = []string{"input", "status_code", "content_length", "content_words"} var header = []string{"input", "position", "status_code", "content_length", "content_words"}
func writeCSV(config *ffuf.Config, res []Result, encode bool) error { func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
f, err := os.Create(config.OutputFile) f, err := os.Create(config.OutputFile)
@ -44,6 +44,7 @@ func base64encode(in string) string {
func toCSV(r Result) []string { func toCSV(r Result) []string {
return []string{ return []string{
r.Input, r.Input,
strconv.Itoa(r.Position),
strconv.FormatInt(r.StatusCode, 10), strconv.FormatInt(r.StatusCode, 10),
strconv.FormatInt(r.ContentLength, 10), strconv.FormatInt(r.ContentLength, 10),
strconv.FormatInt(r.ContentWords, 10), strconv.FormatInt(r.ContentWords, 10),

View File

@ -3,6 +3,7 @@ package output
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"time" "time"
"github.com/ffuf/ffuf/pkg/ffuf" "github.com/ffuf/ffuf/pkg/ffuf"
@ -27,6 +28,7 @@ type Stdoutput struct {
type Result struct { type Result struct {
Input string `json:"input"` Input string `json:"input"`
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"`
@ -127,6 +129,7 @@ func (s *Stdoutput) Result(resp ffuf.Response) {
// 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
sResult := Result{ sResult := Result{
Input: string(resp.Request.Input), Input: string(resp.Request.Input),
Position: resp.Request.Position,
StatusCode: resp.StatusCode, StatusCode: resp.StatusCode,
ContentLength: resp.ContentLength, ContentLength: resp.ContentLength,
ContentWords: resp.ContentWords, ContentWords: resp.ContentWords,
@ -144,11 +147,22 @@ func (s *Stdoutput) printResult(resp ffuf.Response) {
} }
func (s *Stdoutput) resultQuiet(resp ffuf.Response) { 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(string(resp.Request.Input))
} }
}
func (s *Stdoutput) resultNormal(resp ffuf.Response) { func (s *Stdoutput) resultNormal(resp ffuf.Response) {
res_str := fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d]", TERMINAL_CLEAR_LINE, resp.Request.Input, s.colorizeStatus(resp.StatusCode), resp.ContentLength, resp.ContentWords) var res_str string
if len(s.config.InputCommand) > 0 {
// If we're using external command for input, display the position instead of input
res_str = fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d]", TERMINAL_CLEAR_LINE, strconv.Itoa(resp.Request.Position), s.colorizeStatus(resp.StatusCode), resp.ContentLength, resp.ContentWords)
} else {
res_str = fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d]", TERMINAL_CLEAR_LINE, resp.Request.Input, s.colorizeStatus(resp.StatusCode), resp.ContentLength, resp.ContentWords)
}
fmt.Println(res_str) fmt.Println(res_str)
} }