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`).
```
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
View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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()

View File

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

View File

@ -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 {

View File

@ -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
}

View File

@ -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++

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}