├── .gitignore
├── go.mod
├── pkg
├── ffuf
│ ├── const.go
│ ├── progress.go
│ ├── request.go
│ ├── multierror.go
│ ├── util.go
│ ├── response.go
│ ├── valuerange.go
│ ├── interfaces.go
│ ├── optrange.go
│ ├── config.go
│ └── job.go
├── input
│ ├── const.go
│ ├── const_windows.go
│ ├── command.go
│ ├── wordlist.go
│ └── input.go
├── runner
│ ├── runner.go
│ └── simple.go
├── output
│ ├── const_windows.go
│ ├── output.go
│ ├── const.go
│ ├── file_md.go
│ ├── file_csv.go
│ ├── file_json.go
│ ├── file_html.go
│ └── stdout.go
└── filter
│ ├── size_test.go
│ ├── regexp_test.go
│ ├── filter_test.go
│ ├── status_test.go
│ ├── lines_test.go
│ ├── words_test.go
│ ├── regex.go
│ ├── size.go
│ ├── words.go
│ ├── lines.go
│ ├── status.go
│ └── filter.go
├── .goreleaser.yml
├── CONTRIBUTORS.md
├── LICENSE
├── CHANGELOG.md
├── README.md
└── main.go
/.gitignore:
--------------------------------------------------------------------------------
1 | /ffuf
2 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ffuf/ffuf
2 |
3 | go 1.11
4 |
--------------------------------------------------------------------------------
/pkg/ffuf/const.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | const (
4 | //VERSION holds the current version number
5 | VERSION = "1.0-rc1"
6 | )
7 |
--------------------------------------------------------------------------------
/pkg/input/const.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package input
4 |
5 | const (
6 | SHELL_CMD = "/bin/sh"
7 | SHELL_ARG = "-c"
8 | )
9 |
--------------------------------------------------------------------------------
/pkg/input/const_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package input
4 |
5 | const (
6 | SHELL_CMD = "cmd.exe"
7 | SHELL_ARG = "/C"
8 | )
9 |
--------------------------------------------------------------------------------
/pkg/ffuf/progress.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Progress struct {
8 | StartedAt time.Time
9 | ReqCount int
10 | ReqTotal int
11 | QueuePos int
12 | QueueTotal int
13 | ErrorCount int
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/runner/runner.go:
--------------------------------------------------------------------------------
1 | package runner
2 |
3 | import (
4 | "github.com/ffuf/ffuf/pkg/ffuf"
5 | )
6 |
7 | func NewRunnerByName(name string, conf *ffuf.Config) ffuf.RunnerProvider {
8 | // We have only one Runner at the moment
9 | return NewSimpleRunner(conf)
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/output/const_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package output
4 |
5 | const (
6 | TERMINAL_CLEAR_LINE = "\r\r"
7 | ANSI_CLEAR = ""
8 | ANSI_RED = ""
9 | ANSI_GREEN = ""
10 | ANSI_BLUE = ""
11 | ANSI_YELLOW = ""
12 | )
13 |
--------------------------------------------------------------------------------
/pkg/output/output.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "github.com/ffuf/ffuf/pkg/ffuf"
5 | )
6 |
7 | func NewOutputProviderByName(name string, conf *ffuf.Config) ffuf.OutputProvider {
8 | //We have only one outputprovider at the moment
9 | return NewStdoutput(conf)
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/output/const.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package output
4 |
5 | const (
6 | TERMINAL_CLEAR_LINE = "\r\x1b[2K"
7 | ANSI_CLEAR = "\x1b[0m"
8 | ANSI_RED = "\x1b[31m"
9 | ANSI_GREEN = "\x1b[32m"
10 | ANSI_BLUE = "\x1b[34m"
11 | ANSI_YELLOW = "\x1b[33m"
12 | )
13 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | builds:
2 | - binary: ffuf
3 | goos:
4 | - linux
5 | - windows
6 | - freebsd
7 | - openbsd
8 | - darwin
9 | goarch:
10 | - amd64
11 | - 386
12 | - arm
13 | - arm64
14 |
15 |
16 | archive:
17 | format: tar.gz
18 | replacements:
19 | darwin: macOS
20 | format_overrides:
21 | - goos: windows
22 | format: zip
23 |
24 | sign:
25 | artifacts: checksum
26 |
--------------------------------------------------------------------------------
/pkg/ffuf/request.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | // Request holds the meaningful data that is passed for runner for making the query
4 | type Request struct {
5 | Method string
6 | Url string
7 | Headers map[string]string
8 | Data []byte
9 | Input map[string][]byte
10 | Position int
11 | Raw string
12 | }
13 |
14 | func NewRequest(conf *Config) Request {
15 | var req Request
16 | req.Method = conf.Method
17 | req.Url = conf.Url
18 | req.Headers = make(map[string]string)
19 | return req
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/ffuf/multierror.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type Multierror struct {
8 | errors []error
9 | }
10 |
11 | //NewMultierror returns a new Multierror
12 | func NewMultierror() Multierror {
13 | return Multierror{}
14 | }
15 |
16 | func (m *Multierror) Add(err error) {
17 | m.errors = append(m.errors, err)
18 | }
19 |
20 | func (m *Multierror) ErrorOrNil() error {
21 | var errString string
22 | if len(m.errors) > 0 {
23 | errString += fmt.Sprintf("%d errors occured.\n", len(m.errors))
24 | for _, e := range m.errors {
25 | errString += fmt.Sprintf("\t* %s\n", e)
26 | }
27 | return fmt.Errorf("%s", errString)
28 | }
29 | return nil
30 | }
31 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Contributors
2 |
3 | * [ccsplit](https://github.com/ccsplit)
4 | * [delic](https://github.com/delic)
5 | * [eur0pa](https://github.com/eur0pa)
6 | * [fang0654](https://github.com/fang0654)
7 | * [JamTookTheBait](https://github.com/JamTookTheBait)
8 | * [joohoi](https://github.com/joohoi)
9 | * [jvesiluoma](https://github.com/jvesiluoma)
10 | * [lc](https://github.com/lc)
11 | * [nnwakelam](https://twitter.com/nnwakelam)
12 | * [oh6hay](https://github.com/oh6hay)
13 | * [putsi](https://github.com/putsi)
14 | * [SakiiR](https://github.com/SakiiR)
15 | * [seblw](https://github.com/seblw)
16 | * [SolomonSklash](https://github.com/SolomonSklash)
17 | * [Shaked](https://github.com/Shaked)
18 |
--------------------------------------------------------------------------------
/pkg/ffuf/util.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "math/rand"
5 | )
6 |
7 | //used for random string generation in calibration function
8 | var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
9 |
10 | //RandomString returns a random string of length of parameter n
11 | func RandomString(n int) string {
12 | s := make([]rune, n)
13 | for i := range s {
14 | s[i] = chars[rand.Intn(len(chars))]
15 | }
16 | return string(s)
17 | }
18 |
19 | //UniqStringSlice returns an unordered slice of unique strings. The duplicates are dropped
20 | func UniqStringSlice(inslice []string) []string {
21 | found := map[string]bool{}
22 |
23 | for _, v := range inslice {
24 | found[v] = true
25 | }
26 | ret := []string{}
27 | for k, _ := range found {
28 | ret = append(ret, k)
29 | }
30 | return ret
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/ffuf/response.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | // Response struct holds the meaningful data returned from request and is meant for passing to filters
8 | type Response struct {
9 | StatusCode int64
10 | Headers map[string][]string
11 | Data []byte
12 | ContentLength int64
13 | ContentWords int64
14 | ContentLines int64
15 | Cancelled bool
16 | Request *Request
17 | Raw string
18 | ResultFile string
19 | }
20 |
21 | // GetRedirectLocation returns the redirect location for a 3xx redirect HTTP response
22 | func (resp *Response) GetRedirectLocation() string {
23 |
24 | redirectLocation := ""
25 | if resp.StatusCode >= 300 && resp.StatusCode <= 399 {
26 | redirectLocation = resp.Headers["Location"][0]
27 | }
28 |
29 | return redirectLocation
30 | }
31 |
32 | func NewResponse(httpresp *http.Response, req *Request) Response {
33 | var resp Response
34 | resp.Request = req
35 | resp.StatusCode = int64(httpresp.StatusCode)
36 | resp.Headers = httpresp.Header
37 | resp.Cancelled = false
38 | resp.Raw = ""
39 | resp.ResultFile = ""
40 | return resp
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/ffuf/valuerange.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strconv"
7 | )
8 |
9 | type ValueRange struct {
10 | Min, Max int64
11 | }
12 |
13 | func ValueRangeFromString(instr string) (ValueRange, error) {
14 | // is the value a range
15 | minmax := regexp.MustCompile("^(\\d+)\\-(\\d+)$").FindAllStringSubmatch(instr, -1)
16 | if minmax != nil {
17 | // yes
18 | minval, err := strconv.ParseInt(minmax[0][1], 10, 0)
19 | if err != nil {
20 | return ValueRange{}, fmt.Errorf("Invalid value: %s", minmax[0][1])
21 | }
22 | maxval, err := strconv.ParseInt(minmax[0][2], 10, 0)
23 | if err != nil {
24 | return ValueRange{}, fmt.Errorf("Invalid value: %s", minmax[0][2])
25 | }
26 | if minval >= maxval {
27 | return ValueRange{}, fmt.Errorf("Minimum has to be smaller than maximum")
28 | }
29 | return ValueRange{minval, maxval}, nil
30 | } else {
31 | // no, a single value or something else
32 | intval, err := strconv.ParseInt(instr, 10, 0)
33 | if err != nil {
34 | return ValueRange{}, fmt.Errorf("Invalid value: %s", instr)
35 | }
36 | return ValueRange{intval, intval}, nil
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Joona Hoikkala
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pkg/filter/size_test.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/ffuf/ffuf/pkg/ffuf"
8 | )
9 |
10 | func TestNewSizeFilter(t *testing.T) {
11 | f, _ := NewSizeFilter("1,2,3,444,5-90")
12 | sizeRepr := f.Repr()
13 | if strings.Index(sizeRepr, "1,2,3,444,5-90") == -1 {
14 | t.Errorf("Size filter was expected to have 5 values")
15 | }
16 | }
17 |
18 | func TestNewSizeFilterError(t *testing.T) {
19 | _, err := NewSizeFilter("invalid")
20 | if err == nil {
21 | t.Errorf("Was expecting an error from errenous input data")
22 | }
23 | }
24 |
25 | func TestFiltering(t *testing.T) {
26 | f, _ := NewSizeFilter("1,2,3,5-90,444")
27 | for i, test := range []struct {
28 | input int64
29 | output bool
30 | }{
31 | {1, true},
32 | {2, true},
33 | {3, true},
34 | {4, false},
35 | {5, true},
36 | {70, true},
37 | {90, true},
38 | {91, false},
39 | {444, true},
40 | } {
41 | resp := ffuf.Response{ContentLength: test.input}
42 | filterReturn, _ := f.Filter(&resp)
43 | if filterReturn != test.output {
44 | t.Errorf("Filter test %d: Was expecing filter return value of %t but got %t", i, test.output, filterReturn)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/filter/regexp_test.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/ffuf/ffuf/pkg/ffuf"
8 | )
9 |
10 | func TestNewRegexpFilter(t *testing.T) {
11 | f, _ := NewRegexpFilter("s([a-z]+)arch")
12 | statusRepr := f.Repr()
13 | if strings.Index(statusRepr, "s([a-z]+)arch") == -1 {
14 | t.Errorf("Status filter was expected to have a regexp value")
15 | }
16 | }
17 |
18 | func TestNewRegexpFilterError(t *testing.T) {
19 | _, err := NewRegexpFilter("r((")
20 | if err == nil {
21 | t.Errorf("Was expecting an error from errenous input data")
22 | }
23 | }
24 |
25 | func TestRegexpFiltering(t *testing.T) {
26 | f, _ := NewRegexpFilter("s([a-z]+)arch")
27 | for i, test := range []struct {
28 | input string
29 | output bool
30 | }{
31 | {"search", true},
32 | {"text and search", true},
33 | {"sbarch in beginning", true},
34 | {"midd scarch le", true},
35 | {"s1arch", false},
36 | {"invalid", false},
37 | } {
38 | resp := ffuf.Response{Data: []byte(test.input)}
39 | filterReturn, _ := f.Filter(&resp)
40 | if filterReturn != test.output {
41 | t.Errorf("Filter test %d: Was expecing filter return value of %t but got %t", i, test.output, filterReturn)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/filter/filter_test.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestNewFilterByName(t *testing.T) {
8 | scf, _ := NewFilterByName("status", "200")
9 | if _, ok := scf.(*StatusFilter); !ok {
10 | t.Errorf("Was expecting statusfilter")
11 | }
12 |
13 | szf, _ := NewFilterByName("size", "200")
14 | if _, ok := szf.(*SizeFilter); !ok {
15 | t.Errorf("Was expecting sizefilter")
16 | }
17 |
18 | wf, _ := NewFilterByName("word", "200")
19 | if _, ok := wf.(*WordFilter); !ok {
20 | t.Errorf("Was expecting wordfilter")
21 | }
22 |
23 | lf, _ := NewFilterByName("line", "200")
24 | if _, ok := lf.(*LineFilter); !ok {
25 | t.Errorf("Was expecting linefilter")
26 | }
27 |
28 | ref, _ := NewFilterByName("regexp", "200")
29 | if _, ok := ref.(*RegexpFilter); !ok {
30 | t.Errorf("Was expecting regexpfilter")
31 | }
32 | }
33 |
34 | func TestNewFilterByNameError(t *testing.T) {
35 | _, err := NewFilterByName("status", "invalid")
36 | if err == nil {
37 | t.Errorf("Was expecing an error")
38 | }
39 | }
40 |
41 | func TestNewFilterByNameNotFound(t *testing.T) {
42 | _, err := NewFilterByName("nonexistent", "invalid")
43 | if err == nil {
44 | t.Errorf("Was expecing an error with invalid filter name")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/filter/status_test.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/ffuf/ffuf/pkg/ffuf"
8 | )
9 |
10 | func TestNewStatusFilter(t *testing.T) {
11 | f, _ := NewStatusFilter("200,301,400-410,500")
12 | statusRepr := f.Repr()
13 | if strings.Index(statusRepr, "200,301,400-410,500") == -1 {
14 | t.Errorf("Status filter was expected to have 4 values")
15 | }
16 | }
17 |
18 | func TestNewStatusFilterError(t *testing.T) {
19 | _, err := NewStatusFilter("invalid")
20 | if err == nil {
21 | t.Errorf("Was expecting an error from errenous input data")
22 | }
23 | }
24 |
25 | func TestStatusFiltering(t *testing.T) {
26 | f, _ := NewStatusFilter("200,301,400-498,500")
27 | for i, test := range []struct {
28 | input int64
29 | output bool
30 | }{
31 | {200, true},
32 | {301, true},
33 | {500, true},
34 | {4, false},
35 | {399, false},
36 | {400, true},
37 | {444, true},
38 | {498, true},
39 | {499, false},
40 | {302, false},
41 | } {
42 | resp := ffuf.Response{StatusCode: test.input}
43 | filterReturn, _ := f.Filter(&resp)
44 | if filterReturn != test.output {
45 | t.Errorf("Filter test %d: Was expecing filter return value of %t but got %t", i, test.output, filterReturn)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/ffuf/interfaces.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | //FilterProvider is a generic interface for both Matchers and Filters
4 | type FilterProvider interface {
5 | Filter(response *Response) (bool, error)
6 | Repr() string
7 | }
8 |
9 | //RunnerProvider is an interface for request executors
10 | type RunnerProvider interface {
11 | Prepare(input map[string][]byte) (Request, error)
12 | Execute(req *Request) (Response, error)
13 | }
14 |
15 | //InputProvider interface handles the input data for RunnerProvider
16 | type InputProvider interface {
17 | AddProvider(InputProviderConfig) error
18 | Next() bool
19 | Position() int
20 | Reset()
21 | Value() map[string][]byte
22 | Total() int
23 | }
24 |
25 | //InternalInputProvider interface handles providing input data to InputProvider
26 | type InternalInputProvider interface {
27 | Keyword() string
28 | Next() bool
29 | Position() int
30 | ResetPosition()
31 | IncrementPosition()
32 | Value() []byte
33 | Total() int
34 | }
35 |
36 | //OutputProvider is responsible of providing output from the RunnerProvider
37 | type OutputProvider interface {
38 | Banner() error
39 | Finalize() error
40 | Progress(status Progress)
41 | Info(infostring string)
42 | Error(errstring string)
43 | Warning(warnstring string)
44 | Result(resp Response)
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/filter/lines_test.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/ffuf/ffuf/pkg/ffuf"
8 | )
9 |
10 | func TestNewLineFilter(t *testing.T) {
11 | f, _ := NewLineFilter("200,301,400-410,500")
12 | linesRepr := f.Repr()
13 | if strings.Index(linesRepr, "200,301,400-410,500") == -1 {
14 | t.Errorf("Word filter was expected to have 4 values")
15 | }
16 | }
17 |
18 | func TestNewLineFilterError(t *testing.T) {
19 | _, err := NewLineFilter("invalid")
20 | if err == nil {
21 | t.Errorf("Was expecting an error from errenous input data")
22 | }
23 | }
24 |
25 | func TestLineFiltering(t *testing.T) {
26 | f, _ := NewLineFilter("200,301,402-450,500")
27 | for i, test := range []struct {
28 | input int64
29 | output bool
30 | }{
31 | {200, true},
32 | {301, true},
33 | {500, true},
34 | {4, false},
35 | {444, true},
36 | {302, false},
37 | {401, false},
38 | {402, true},
39 | {450, true},
40 | {451, false},
41 | } {
42 | var data []string
43 | for i := int64(0); i < test.input; i++ {
44 | data = append(data, "A")
45 | }
46 | resp := ffuf.Response{Data: []byte(strings.Join(data, " "))}
47 | filterReturn, _ := f.Filter(&resp)
48 | if filterReturn != test.output {
49 | t.Errorf("Filter test %d: Was expecing filter return value of %t but got %t", i, test.output, filterReturn)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/filter/words_test.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/ffuf/ffuf/pkg/ffuf"
8 | )
9 |
10 | func TestNewWordFilter(t *testing.T) {
11 | f, _ := NewWordFilter("200,301,400-410,500")
12 | wordsRepr := f.Repr()
13 | if strings.Index(wordsRepr, "200,301,400-410,500") == -1 {
14 | t.Errorf("Word filter was expected to have 4 values")
15 | }
16 | }
17 |
18 | func TestNewWordFilterError(t *testing.T) {
19 | _, err := NewWordFilter("invalid")
20 | if err == nil {
21 | t.Errorf("Was expecting an error from errenous input data")
22 | }
23 | }
24 |
25 | func TestWordFiltering(t *testing.T) {
26 | f, _ := NewWordFilter("200,301,402-450,500")
27 | for i, test := range []struct {
28 | input int64
29 | output bool
30 | }{
31 | {200, true},
32 | {301, true},
33 | {500, true},
34 | {4, false},
35 | {444, true},
36 | {302, false},
37 | {401, false},
38 | {402, true},
39 | {450, true},
40 | {451, false},
41 | } {
42 | var data []string
43 | for i := int64(0); i < test.input; i++ {
44 | data = append(data, "A")
45 | }
46 | resp := ffuf.Response{Data: []byte(strings.Join(data, " "))}
47 | filterReturn, _ := f.Filter(&resp)
48 | if filterReturn != test.output {
49 | t.Errorf("Filter test %d: Was expecing filter return value of %t but got %t", i, test.output, filterReturn)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/filter/regex.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "regexp"
7 | "strings"
8 |
9 | "github.com/ffuf/ffuf/pkg/ffuf"
10 | )
11 |
12 | type RegexpFilter struct {
13 | Value *regexp.Regexp
14 | valueRaw string
15 | }
16 |
17 | func NewRegexpFilter(value string) (ffuf.FilterProvider, error) {
18 | re, err := regexp.Compile(value)
19 | if err != nil {
20 | return &RegexpFilter{}, fmt.Errorf("Regexp filter or matcher (-fr / -mr): invalid value: %s", value)
21 | }
22 | return &RegexpFilter{Value: re, valueRaw: value}, nil
23 | }
24 |
25 | func (f *RegexpFilter) MarshalJSON() ([]byte, error) {
26 | return json.Marshal(&struct {
27 | Value string `json:"value"`
28 | }{
29 | Value: f.valueRaw,
30 | })
31 | }
32 |
33 | func (f *RegexpFilter) Filter(response *ffuf.Response) (bool, error) {
34 | matchheaders := ""
35 | for k, v := range response.Headers {
36 | for _, iv := range v {
37 | matchheaders += k + ": " + iv + "\r\n"
38 | }
39 | }
40 | matchdata := []byte(matchheaders)
41 | matchdata = append(matchdata, response.Data...)
42 | pattern := f.valueRaw
43 | for keyword, inputitem := range response.Request.Input {
44 | pattern = strings.Replace(pattern, keyword, regexp.QuoteMeta(string(inputitem)), -1)
45 | }
46 | matched, err := regexp.Match(pattern, matchdata)
47 | if err != nil {
48 | return false, nil
49 | }
50 | return matched, nil
51 | }
52 |
53 | func (f *RegexpFilter) Repr() string {
54 | return fmt.Sprintf("Regexp: %s", f.valueRaw)
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/output/file_md.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "html/template"
5 | "os"
6 | "time"
7 |
8 | "github.com/ffuf/ffuf/pkg/ffuf"
9 | )
10 |
11 | const (
12 | markdownTemplate = `# FFUF Report
13 |
14 | Command line : ` + "`{{.CommandLine}}`" + `
15 | Time: ` + "{{ .Time }}" + `
16 |
17 | {{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | ResultFile |
18 | {{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | :--------- |
19 | {{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{ .ResultFile }} |
20 | {{end}}` // The template format is not pretty but follows the markdown guide
21 | )
22 |
23 | func writeMarkdown(config *ffuf.Config, res []Result) error {
24 |
25 | ti := time.Now()
26 |
27 | keywords := make([]string, 0)
28 | for _, inputprovider := range config.InputProviders {
29 | keywords = append(keywords, inputprovider.Keyword)
30 | }
31 |
32 | outMD := htmlFileOutput{
33 | CommandLine: config.CommandLine,
34 | Time: ti.Format(time.RFC3339),
35 | Results: res,
36 | Keys: keywords,
37 | }
38 |
39 | f, err := os.Create(config.OutputFile)
40 | if err != nil {
41 | return err
42 | }
43 | defer f.Close()
44 |
45 | templateName := "output.md"
46 | t := template.New(templateName).Delims("{{", "}}")
47 | t.Parse(markdownTemplate)
48 | t.Execute(f, outMD)
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/filter/size.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/ffuf/ffuf/pkg/ffuf"
10 | )
11 |
12 | type SizeFilter struct {
13 | Value []ffuf.ValueRange
14 | }
15 |
16 | func NewSizeFilter(value string) (ffuf.FilterProvider, error) {
17 | var intranges []ffuf.ValueRange
18 | for _, sv := range strings.Split(value, ",") {
19 | vr, err := ffuf.ValueRangeFromString(sv)
20 | if err != nil {
21 | return &SizeFilter{}, fmt.Errorf("Size filter or matcher (-fs / -ms): invalid value: %s", sv)
22 | }
23 |
24 | intranges = append(intranges, vr)
25 | }
26 | return &SizeFilter{Value: intranges}, nil
27 | }
28 |
29 | func (f *SizeFilter) MarshalJSON() ([]byte, error) {
30 | value := make([]string, 0)
31 | for _, v := range f.Value {
32 | if v.Min == v.Max {
33 | value = append(value, strconv.FormatInt(v.Min, 10))
34 | } else {
35 | value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
36 | }
37 | }
38 | return json.Marshal(&struct {
39 | Value string `json:"value"`
40 | }{
41 | Value: strings.Join(value, ","),
42 | })
43 | }
44 |
45 | func (f *SizeFilter) Filter(response *ffuf.Response) (bool, error) {
46 | for _, iv := range f.Value {
47 | if iv.Min <= response.ContentLength && response.ContentLength <= iv.Max {
48 | return true, nil
49 | }
50 | }
51 | return false, nil
52 | }
53 |
54 | func (f *SizeFilter) Repr() string {
55 | var strval []string
56 | for _, iv := range f.Value {
57 | if iv.Min == iv.Max {
58 | strval = append(strval, strconv.Itoa(int(iv.Min)))
59 | } else {
60 | strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max)))
61 | }
62 | }
63 | return fmt.Sprintf("Response size: %s", strings.Join(strval, ","))
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/filter/words.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/ffuf/ffuf/pkg/ffuf"
10 | )
11 |
12 | type WordFilter struct {
13 | Value []ffuf.ValueRange
14 | }
15 |
16 | func NewWordFilter(value string) (ffuf.FilterProvider, error) {
17 | var intranges []ffuf.ValueRange
18 | for _, sv := range strings.Split(value, ",") {
19 | vr, err := ffuf.ValueRangeFromString(sv)
20 | if err != nil {
21 | return &WordFilter{}, fmt.Errorf("Word filter or matcher (-fw / -mw): invalid value: %s", sv)
22 | }
23 | intranges = append(intranges, vr)
24 | }
25 | return &WordFilter{Value: intranges}, nil
26 | }
27 |
28 | func (f *WordFilter) MarshalJSON() ([]byte, error) {
29 | value := make([]string, 0)
30 | for _, v := range f.Value {
31 | if v.Min == v.Max {
32 | value = append(value, strconv.FormatInt(v.Min, 10))
33 | } else {
34 | value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
35 | }
36 | }
37 | return json.Marshal(&struct {
38 | Value string `json:"value"`
39 | }{
40 | Value: strings.Join(value, ","),
41 | })
42 | }
43 |
44 | func (f *WordFilter) Filter(response *ffuf.Response) (bool, error) {
45 | wordsSize := len(strings.Split(string(response.Data), " "))
46 | for _, iv := range f.Value {
47 | if iv.Min <= int64(wordsSize) && int64(wordsSize) <= iv.Max {
48 | return true, nil
49 | }
50 | }
51 | return false, nil
52 | }
53 |
54 | func (f *WordFilter) Repr() string {
55 | var strval []string
56 | for _, iv := range f.Value {
57 | if iv.Min == iv.Max {
58 | strval = append(strval, strconv.Itoa(int(iv.Min)))
59 | } else {
60 | strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max)))
61 | }
62 | }
63 | return fmt.Sprintf("Response words: %s", strings.Join(strval, ","))
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/filter/lines.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/ffuf/ffuf/pkg/ffuf"
10 | )
11 |
12 | type LineFilter struct {
13 | Value []ffuf.ValueRange
14 | }
15 |
16 | func NewLineFilter(value string) (ffuf.FilterProvider, error) {
17 | var intranges []ffuf.ValueRange
18 | for _, sv := range strings.Split(value, ",") {
19 | vr, err := ffuf.ValueRangeFromString(sv)
20 | if err != nil {
21 | return &LineFilter{}, fmt.Errorf("Line filter or matcher (-fl / -ml): invalid value: %s", sv)
22 | }
23 | intranges = append(intranges, vr)
24 | }
25 | return &LineFilter{Value: intranges}, nil
26 | }
27 |
28 | func (f *LineFilter) MarshalJSON() ([]byte, error) {
29 | value := make([]string, 0)
30 | for _, v := range f.Value {
31 | if v.Min == v.Max {
32 | value = append(value, strconv.FormatInt(v.Min, 10))
33 | } else {
34 | value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
35 | }
36 | }
37 | return json.Marshal(&struct {
38 | Value string `json:"value"`
39 | }{
40 | Value: strings.Join(value, ","),
41 | })
42 | }
43 |
44 | func (f *LineFilter) Filter(response *ffuf.Response) (bool, error) {
45 | linesSize := len(strings.Split(string(response.Data), "\n"))
46 | for _, iv := range f.Value {
47 | if iv.Min <= int64(linesSize) && int64(linesSize) <= iv.Max {
48 | return true, nil
49 | }
50 | }
51 | return false, nil
52 | }
53 |
54 | func (f *LineFilter) Repr() string {
55 | var strval []string
56 | for _, iv := range f.Value {
57 | if iv.Min == iv.Max {
58 | strval = append(strval, strconv.Itoa(int(iv.Min)))
59 | } else {
60 | strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max)))
61 | }
62 | }
63 | return fmt.Sprintf("Response lines: %s", strings.Join(strval, ","))
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/input/command.go:
--------------------------------------------------------------------------------
1 | package input
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "os/exec"
7 | "strconv"
8 |
9 | "github.com/ffuf/ffuf/pkg/ffuf"
10 | )
11 |
12 | type CommandInput struct {
13 | config *ffuf.Config
14 | count int
15 | keyword string
16 | command string
17 | }
18 |
19 | func NewCommandInput(keyword string, value string, conf *ffuf.Config) (*CommandInput, error) {
20 | var cmd CommandInput
21 | cmd.keyword = keyword
22 | cmd.config = conf
23 | cmd.count = 0
24 | cmd.command = value
25 | return &cmd, nil
26 | }
27 |
28 | //Keyword returns the keyword assigned to this InternalInputProvider
29 | func (c *CommandInput) Keyword() string {
30 | return c.keyword
31 | }
32 |
33 | //Position will return the current position in the input list
34 | func (c *CommandInput) Position() int {
35 | return c.count
36 | }
37 |
38 | //ResetPosition will reset the current position of the InternalInputProvider
39 | func (c *CommandInput) ResetPosition() {
40 | c.count = 0
41 | }
42 |
43 | //IncrementPosition increments the current position in the inputprovider
44 | func (c *CommandInput) IncrementPosition() {
45 | c.count += 1
46 | }
47 |
48 | //Next will increment the cursor position, and return a boolean telling if there's iterations left
49 | func (c *CommandInput) Next() bool {
50 | if c.count >= c.config.InputNum {
51 | return false
52 | }
53 | return true
54 | }
55 |
56 | //Value returns the input from command stdoutput
57 | func (c *CommandInput) Value() []byte {
58 | var stdout bytes.Buffer
59 | os.Setenv("FFUF_NUM", strconv.Itoa(c.count))
60 | cmd := exec.Command(SHELL_CMD, SHELL_ARG, c.command)
61 | cmd.Stdout = &stdout
62 | err := cmd.Run()
63 | if err != nil {
64 | return []byte("")
65 | }
66 | return stdout.Bytes()
67 | }
68 |
69 | //Total returns the size of wordlist
70 | func (c *CommandInput) Total() int {
71 | return c.config.InputNum
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/output/file_csv.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/csv"
6 | "os"
7 | "strconv"
8 |
9 | "github.com/ffuf/ffuf/pkg/ffuf"
10 | )
11 |
12 | var staticheaders = []string{"url", "redirectlocation", "position", "status_code", "content_length", "content_words", "content_lines", "resultfile"}
13 |
14 | func writeCSV(config *ffuf.Config, res []Result, encode bool) error {
15 | header := make([]string, 0)
16 | f, err := os.Create(config.OutputFile)
17 | if err != nil {
18 | return err
19 | }
20 | defer f.Close()
21 |
22 | w := csv.NewWriter(f)
23 | defer w.Flush()
24 |
25 | for _, inputprovider := range config.InputProviders {
26 | header = append(header, inputprovider.Keyword)
27 | }
28 |
29 | for _, item := range staticheaders {
30 | header = append(header, item)
31 | }
32 |
33 | if err := w.Write(header); err != nil {
34 | return err
35 | }
36 | for _, r := range res {
37 | if encode {
38 | inputs := make(map[string][]byte, 0)
39 | for k, v := range r.Input {
40 | inputs[k] = []byte(base64encode(v))
41 | }
42 | r.Input = inputs
43 | }
44 |
45 | err := w.Write(toCSV(r))
46 | if err != nil {
47 | return err
48 | }
49 | }
50 | return nil
51 | }
52 |
53 | func base64encode(in []byte) string {
54 | return base64.StdEncoding.EncodeToString(in)
55 | }
56 |
57 | func toCSV(r Result) []string {
58 | res := make([]string, 0)
59 | for _, v := range r.Input {
60 | res = append(res, string(v))
61 | }
62 | res = append(res, r.Url)
63 | res = append(res, r.RedirectLocation)
64 | res = append(res, strconv.Itoa(r.Position))
65 | res = append(res, strconv.FormatInt(r.StatusCode, 10))
66 | res = append(res, strconv.FormatInt(r.ContentLength, 10))
67 | res = append(res, strconv.FormatInt(r.ContentWords, 10))
68 | res = append(res, strconv.FormatInt(r.ContentLines, 10))
69 | res = append(res, r.ResultFile)
70 | return res
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/ffuf/optrange.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | //optRange stores either a single float, in which case the value is stored in min and IsRange is false,
11 | //or a range of floats, in which case IsRange is true
12 | type optRange struct {
13 | Min float64
14 | Max float64
15 | IsRange bool
16 | HasDelay bool
17 | }
18 |
19 | type optRangeJSON struct {
20 | Value string `json:"value"`
21 | }
22 |
23 | func (o *optRange) MarshalJSON() ([]byte, error) {
24 | value := ""
25 | if o.Min == o.Max {
26 | value = fmt.Sprintf("%.2f", o.Min)
27 | } else {
28 | value = fmt.Sprintf("%.2f-%.2f", o.Min, o.Max)
29 | }
30 | return json.Marshal(&optRangeJSON{
31 | Value: value,
32 | })
33 | }
34 |
35 | func (o *optRange) UnmarshalJSON(b []byte) error {
36 | var inc optRangeJSON
37 | err := json.Unmarshal(b, &inc)
38 | if err != nil {
39 | return err
40 | }
41 | return o.Initialize(inc.Value)
42 | }
43 |
44 | //Initialize sets up the optRange from string value
45 | func (o *optRange) Initialize(value string) error {
46 | var err, err2 error
47 | d := strings.Split(value, "-")
48 | if len(d) > 2 {
49 | return fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\"")
50 | } else if len(d) == 2 {
51 | o.IsRange = true
52 | o.HasDelay = true
53 | o.Min, err = strconv.ParseFloat(d[0], 64)
54 | o.Max, err2 = strconv.ParseFloat(d[1], 64)
55 | if err != nil || err2 != nil {
56 | return fmt.Errorf("Delay range min and max values need to be valid floats. For example: 0.1-0.5")
57 | }
58 | } else if len(value) > 0 {
59 | o.IsRange = false
60 | o.HasDelay = true
61 | o.Min, err = strconv.ParseFloat(value, 64)
62 | if err != nil {
63 | return fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\"")
64 | }
65 | }
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/filter/status.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/ffuf/ffuf/pkg/ffuf"
10 | )
11 |
12 | const AllStatuses = 0
13 |
14 | type StatusFilter struct {
15 | Value []ffuf.ValueRange
16 | }
17 |
18 | func NewStatusFilter(value string) (ffuf.FilterProvider, error) {
19 | var intranges []ffuf.ValueRange
20 | for _, sv := range strings.Split(value, ",") {
21 | if sv == "all" {
22 | intranges = append(intranges, ffuf.ValueRange{AllStatuses, AllStatuses})
23 | } else {
24 | vr, err := ffuf.ValueRangeFromString(sv)
25 | if err != nil {
26 | return &StatusFilter{}, fmt.Errorf("Status filter or matcher (-fc / -mc): invalid value %s", sv)
27 | }
28 | intranges = append(intranges, vr)
29 | }
30 | }
31 | return &StatusFilter{Value: intranges}, nil
32 | }
33 |
34 | func (f *StatusFilter) MarshalJSON() ([]byte, error) {
35 | value := make([]string, 0)
36 | for _, v := range f.Value {
37 | if v.Min == 0 && v.Max == 0 {
38 | value = append(value, "all")
39 | } else {
40 | if v.Min == v.Max {
41 | value = append(value, strconv.FormatInt(v.Min, 10))
42 | } else {
43 | value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
44 | }
45 | }
46 | }
47 | return json.Marshal(&struct {
48 | Value string `json:"value"`
49 | }{
50 | Value: strings.Join(value, ","),
51 | })
52 | }
53 |
54 | func (f *StatusFilter) Filter(response *ffuf.Response) (bool, error) {
55 | for _, iv := range f.Value {
56 | if iv.Min == AllStatuses && iv.Max == AllStatuses {
57 | // Handle the "all" case
58 | return true, nil
59 | }
60 | if iv.Min <= response.StatusCode && response.StatusCode <= iv.Max {
61 | return true, nil
62 | }
63 | }
64 | return false, nil
65 | }
66 |
67 | func (f *StatusFilter) Repr() string {
68 | var strval []string
69 | for _, iv := range f.Value {
70 | if iv.Min == AllStatuses && iv.Max == AllStatuses {
71 | strval = append(strval, "all")
72 | } else if iv.Min == iv.Max {
73 | strval = append(strval, strconv.Itoa(int(iv.Min)))
74 | } else {
75 | strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max)))
76 | }
77 | }
78 | return fmt.Sprintf("Response status: %s", strings.Join(strval, ","))
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/output/file_json.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "time"
7 |
8 | "github.com/ffuf/ffuf/pkg/ffuf"
9 | )
10 |
11 | type ejsonFileOutput struct {
12 | CommandLine string `json:"commandline"`
13 | Time string `json:"time"`
14 | Results []Result `json:"results"`
15 | }
16 |
17 | type JsonResult struct {
18 | Input map[string]string `json:"input"`
19 | Position int `json:"position"`
20 | StatusCode int64 `json:"status"`
21 | ContentLength int64 `json:"length"`
22 | ContentWords int64 `json:"words"`
23 | ContentLines int64 `json:"lines"`
24 | RedirectLocation string `json:"redirectlocation"`
25 | ResultFile string `json:"resultfile"`
26 | Url string `json:"url"`
27 | }
28 |
29 | type jsonFileOutput struct {
30 | CommandLine string `json:"commandline"`
31 | Time string `json:"time"`
32 | Results []JsonResult `json:"results"`
33 | Config *ffuf.Config `json:"config"`
34 | }
35 |
36 | func writeEJSON(config *ffuf.Config, res []Result) error {
37 | t := time.Now()
38 | outJSON := ejsonFileOutput{
39 | CommandLine: config.CommandLine,
40 | Time: t.Format(time.RFC3339),
41 | Results: res,
42 | }
43 |
44 | outBytes, err := json.Marshal(outJSON)
45 | if err != nil {
46 | return err
47 | }
48 | err = ioutil.WriteFile(config.OutputFile, outBytes, 0644)
49 | if err != nil {
50 | return err
51 | }
52 | return nil
53 | }
54 |
55 | func writeJSON(config *ffuf.Config, res []Result) error {
56 | t := time.Now()
57 | jsonRes := make([]JsonResult, 0)
58 | for _, r := range res {
59 | strinput := make(map[string]string)
60 | for k, v := range r.Input {
61 | strinput[k] = string(v)
62 | }
63 | jsonRes = append(jsonRes, JsonResult{
64 | Input: strinput,
65 | Position: r.Position,
66 | StatusCode: r.StatusCode,
67 | ContentLength: r.ContentLength,
68 | ContentWords: r.ContentWords,
69 | ContentLines: r.ContentLines,
70 | RedirectLocation: r.RedirectLocation,
71 | ResultFile: r.ResultFile,
72 | Url: r.Url,
73 | })
74 | }
75 | outJSON := jsonFileOutput{
76 | CommandLine: config.CommandLine,
77 | Time: t.Format(time.RFC3339),
78 | Results: jsonRes,
79 | Config: config,
80 | }
81 | outBytes, err := json.Marshal(outJSON)
82 | if err != nil {
83 | return err
84 | }
85 | err = ioutil.WriteFile(config.OutputFile, outBytes, 0644)
86 | if err != nil {
87 | return err
88 | }
89 | return nil
90 | }
91 |
--------------------------------------------------------------------------------
/pkg/filter/filter.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/ffuf/ffuf/pkg/ffuf"
9 | )
10 |
11 | func NewFilterByName(name string, value string) (ffuf.FilterProvider, error) {
12 | if name == "status" {
13 | return NewStatusFilter(value)
14 | }
15 | if name == "size" {
16 | return NewSizeFilter(value)
17 | }
18 | if name == "word" {
19 | return NewWordFilter(value)
20 | }
21 | if name == "line" {
22 | return NewLineFilter(value)
23 | }
24 | if name == "regexp" {
25 | return NewRegexpFilter(value)
26 | }
27 | return nil, fmt.Errorf("Could not create filter with name %s", name)
28 | }
29 |
30 | //AddFilter adds a new filter to Config
31 | func AddFilter(conf *ffuf.Config, name string, option string) error {
32 | newf, err := NewFilterByName(name, option)
33 | if err == nil {
34 | conf.Filters[name] = newf
35 | }
36 | return err
37 | }
38 |
39 | //AddMatcher adds a new matcher to Config
40 | func AddMatcher(conf *ffuf.Config, name string, option string) error {
41 | newf, err := NewFilterByName(name, option)
42 | if err == nil {
43 | conf.Matchers[name] = newf
44 | }
45 | return err
46 | }
47 |
48 | //CalibrateIfNeeded runs a self-calibration task for filtering options (if needed) by requesting random resources and acting accordingly
49 | func CalibrateIfNeeded(j *ffuf.Job) error {
50 | if !j.Config.AutoCalibration {
51 | return nil
52 | }
53 | // Handle the calibration
54 | responses, err := j.CalibrateResponses()
55 | if err != nil {
56 | return err
57 | }
58 | if len(responses) > 0 {
59 | calibrateFilters(j, responses)
60 | }
61 | return nil
62 | }
63 |
64 | func calibrateFilters(j *ffuf.Job, responses []ffuf.Response) {
65 | sizeCalib := make([]string, 0)
66 | wordCalib := make([]string, 0)
67 | lineCalib := make([]string, 0)
68 | for _, r := range responses {
69 | if r.ContentLength > 0 {
70 | // Only add if we have an actual size of responses
71 | sizeCalib = append(sizeCalib, strconv.FormatInt(r.ContentLength, 10))
72 | }
73 | if r.ContentWords > 0 {
74 | // Only add if we have an actual word length of response
75 | wordCalib = append(wordCalib, strconv.FormatInt(r.ContentWords, 10))
76 | }
77 | if r.ContentLines > 1 {
78 | // Only add if we have an actual word length of response
79 | lineCalib = append(lineCalib, strconv.FormatInt(r.ContentLines, 10))
80 | }
81 | }
82 |
83 | //Remove duplicates
84 | sizeCalib = ffuf.UniqStringSlice(sizeCalib)
85 | wordCalib = ffuf.UniqStringSlice(wordCalib)
86 | lineCalib = ffuf.UniqStringSlice(lineCalib)
87 |
88 | if len(sizeCalib) > 0 {
89 | AddFilter(j.Config, "size", strings.Join(sizeCalib, ","))
90 | }
91 | if len(wordCalib) > 0 {
92 | AddFilter(j.Config, "word", strings.Join(wordCalib, ","))
93 | }
94 | if len(lineCalib) > 0 {
95 | AddFilter(j.Config, "line", strings.Join(lineCalib, ","))
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/pkg/input/wordlist.go:
--------------------------------------------------------------------------------
1 | package input
2 |
3 | import (
4 | "bufio"
5 | "os"
6 | "regexp"
7 |
8 | "github.com/ffuf/ffuf/pkg/ffuf"
9 | )
10 |
11 | type WordlistInput struct {
12 | config *ffuf.Config
13 | data [][]byte
14 | position int
15 | keyword string
16 | }
17 |
18 | func NewWordlistInput(keyword string, value string, conf *ffuf.Config) (*WordlistInput, error) {
19 | var wl WordlistInput
20 | wl.keyword = keyword
21 | wl.config = conf
22 | wl.position = 0
23 | var valid bool
24 | var err error
25 | // stdin?
26 | if value == "-" {
27 | // yes
28 | valid = true
29 | } else {
30 | // no
31 | valid, err = wl.validFile(value)
32 | }
33 | if err != nil {
34 | return &wl, err
35 | }
36 | if valid {
37 | err = wl.readFile(value)
38 | }
39 | return &wl, err
40 | }
41 |
42 | //Position will return the current position in the input list
43 | func (w *WordlistInput) Position() int {
44 | return w.position
45 | }
46 |
47 | //ResetPosition resets the position back to beginning of the wordlist.
48 | func (w *WordlistInput) ResetPosition() {
49 | w.position = 0
50 | }
51 |
52 | //Keyword returns the keyword assigned to this InternalInputProvider
53 | func (w *WordlistInput) Keyword() string {
54 | return w.keyword
55 | }
56 |
57 | //Next will increment the cursor position, and return a boolean telling if there's words left in the list
58 | func (w *WordlistInput) Next() bool {
59 | if w.position >= len(w.data) {
60 | return false
61 | }
62 | return true
63 | }
64 |
65 | //IncrementPosition will increment the current position in the inputprovider data slice
66 | func (w *WordlistInput) IncrementPosition() {
67 | w.position += 1
68 | }
69 |
70 | //Value returns the value from wordlist at current cursor position
71 | func (w *WordlistInput) Value() []byte {
72 | return w.data[w.position]
73 | }
74 |
75 | //Total returns the size of wordlist
76 | func (w *WordlistInput) Total() int {
77 | return len(w.data)
78 | }
79 |
80 | //validFile checks that the wordlist file exists and can be read
81 | func (w *WordlistInput) validFile(path string) (bool, error) {
82 | _, err := os.Stat(path)
83 | if err != nil {
84 | return false, err
85 | }
86 | f, err := os.Open(path)
87 | if err != nil {
88 | return false, err
89 | }
90 | f.Close()
91 | return true, nil
92 | }
93 |
94 | //readFile reads the file line by line to a byte slice
95 | func (w *WordlistInput) readFile(path string) error {
96 | var file *os.File
97 | var err error
98 | if path == "-" {
99 | file = os.Stdin
100 | } else {
101 | file, err = os.Open(path)
102 | if err != nil {
103 | return err
104 | }
105 | }
106 | defer file.Close()
107 |
108 | var data [][]byte
109 | reader := bufio.NewScanner(file)
110 | re := regexp.MustCompile(`(?i)%ext%`)
111 | for reader.Scan() {
112 | if w.config.DirSearchCompat && len(w.config.Extensions) > 0 {
113 | text := []byte(reader.Text())
114 | if re.Match(text) {
115 | for _, ext := range w.config.Extensions {
116 | contnt := re.ReplaceAll(text, []byte(ext))
117 | data = append(data, []byte(contnt))
118 | }
119 | } else {
120 | data = append(data, []byte(reader.Text()))
121 | }
122 | } else {
123 | data = append(data, []byte(reader.Text()))
124 | if w.keyword == "FUZZ" && len(w.config.Extensions) > 0 {
125 | for _, ext := range w.config.Extensions {
126 | data = append(data, []byte(reader.Text()+ext))
127 | }
128 | }
129 | }
130 | }
131 | w.data = data
132 | return reader.Err()
133 | }
134 |
--------------------------------------------------------------------------------
/pkg/ffuf/config.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | type Config struct {
8 | Headers map[string]string `json:"headers"`
9 | Extensions []string `json:"extensions"`
10 | DirSearchCompat bool `json:"dirsearch_compatibility"`
11 | Method string `json:"method"`
12 | Url string `json:"url"`
13 | Data string `json:"postdata"`
14 | Quiet bool `json:"quiet"`
15 | Colors bool `json:"colors"`
16 | InputProviders []InputProviderConfig `json:"inputproviders"`
17 | CommandKeywords []string `json:"-"`
18 | InputNum int `json:"cmd_inputnum"`
19 | InputMode string `json:"inputmode"`
20 | OutputDirectory string `json:"outputdirectory"`
21 | OutputFile string `json:"outputfile"`
22 | OutputFormat string `json:"outputformat"`
23 | StopOn403 bool `json:"stop_403"`
24 | StopOnErrors bool `json:"stop_errors"`
25 | StopOnAll bool `json:"stop_all"`
26 | FollowRedirects bool `json:"follow_redirects"`
27 | AutoCalibration bool `json:"autocalibration"`
28 | AutoCalibrationStrings []string `json:"autocalibration_strings"`
29 | Timeout int `json:"timeout"`
30 | ProgressFrequency int `json:"-"`
31 | Delay optRange `json:"delay"`
32 | Filters map[string]FilterProvider `json:"filters"`
33 | Matchers map[string]FilterProvider `json:"matchers"`
34 | Threads int `json:"threads"`
35 | Context context.Context `json:"-"`
36 | ProxyURL string `json:"proxyurl"`
37 | CommandLine string `json:"cmdline"`
38 | Verbose bool `json:"verbose"`
39 | MaxTime int `json:"maxtime"`
40 | Recursion bool `json:"recursion"`
41 | RecursionDepth int `json:"recursion_depth"`
42 | }
43 |
44 | type InputProviderConfig struct {
45 | Name string `json:"name"`
46 | Keyword string `json:"keyword"`
47 | Value string `json:"value"`
48 | }
49 |
50 | func NewConfig(ctx context.Context) Config {
51 | var conf Config
52 | conf.Context = ctx
53 | conf.Headers = make(map[string]string)
54 | conf.Method = "GET"
55 | conf.Url = ""
56 | conf.Data = ""
57 | conf.Quiet = false
58 | conf.StopOn403 = false
59 | conf.StopOnErrors = false
60 | conf.StopOnAll = false
61 | conf.FollowRedirects = false
62 | conf.InputProviders = make([]InputProviderConfig, 0)
63 | conf.CommandKeywords = make([]string, 0)
64 | conf.AutoCalibrationStrings = make([]string, 0)
65 | conf.InputNum = 0
66 | conf.InputMode = "clusterbomb"
67 | conf.ProxyURL = ""
68 | conf.Filters = make(map[string]FilterProvider)
69 | conf.Matchers = make(map[string]FilterProvider)
70 | conf.Delay = optRange{0, 0, false, false}
71 | conf.Extensions = make([]string, 0)
72 | conf.Timeout = 10
73 | // Progress update frequency, in milliseconds
74 | conf.ProgressFrequency = 100
75 | conf.DirSearchCompat = false
76 | conf.Verbose = false
77 | conf.MaxTime = 0
78 | conf.Recursion = false
79 | conf.RecursionDepth = 0
80 | return conf
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/runner/simple.go:
--------------------------------------------------------------------------------
1 | package runner
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "strconv"
11 | "strings"
12 | "time"
13 | "unicode/utf8"
14 |
15 | "github.com/ffuf/ffuf/pkg/ffuf"
16 | )
17 |
18 | //Download results < 5MB
19 | const MAX_DOWNLOAD_SIZE = 5242880
20 |
21 | type SimpleRunner struct {
22 | config *ffuf.Config
23 | client *http.Client
24 | }
25 |
26 | func NewSimpleRunner(conf *ffuf.Config) ffuf.RunnerProvider {
27 | var simplerunner SimpleRunner
28 | proxyURL := http.ProxyFromEnvironment
29 |
30 | if len(conf.ProxyURL) > 0 {
31 | pu, err := url.Parse(conf.ProxyURL)
32 | if err == nil {
33 | proxyURL = http.ProxyURL(pu)
34 | }
35 | }
36 |
37 | simplerunner.config = conf
38 | simplerunner.client = &http.Client{
39 | CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
40 | Timeout: time.Duration(time.Duration(conf.Timeout) * time.Second),
41 | Transport: &http.Transport{
42 | Proxy: proxyURL,
43 | MaxIdleConns: 1000,
44 | MaxIdleConnsPerHost: 500,
45 | MaxConnsPerHost: 500,
46 | TLSClientConfig: &tls.Config{
47 | InsecureSkipVerify: true,
48 | },
49 | }}
50 |
51 | if conf.FollowRedirects {
52 | simplerunner.client.CheckRedirect = nil
53 | }
54 | return &simplerunner
55 | }
56 |
57 | func (r *SimpleRunner) Prepare(input map[string][]byte) (ffuf.Request, error) {
58 | req := ffuf.NewRequest(r.config)
59 |
60 | req.Headers = r.config.Headers
61 | req.Url = r.config.Url
62 | req.Method = r.config.Method
63 | req.Data = []byte(r.config.Data)
64 |
65 | for keyword, inputitem := range input {
66 | req.Method = strings.Replace(req.Method, keyword, string(inputitem), -1)
67 | headers := make(map[string]string, 0)
68 | for h, v := range req.Headers {
69 | headers[strings.Replace(h, keyword, string(inputitem), -1)] = strings.Replace(v, keyword, string(inputitem), -1)
70 | }
71 | req.Headers = headers
72 | req.Url = strings.Replace(req.Url, keyword, string(inputitem), -1)
73 | req.Data = []byte(strings.Replace(string(req.Data), keyword, string(inputitem), -1))
74 | }
75 |
76 | req.Input = input
77 | return req, nil
78 | }
79 |
80 | func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
81 | var httpreq *http.Request
82 | var err error
83 | var rawreq, rawresp strings.Builder
84 | data := bytes.NewReader(req.Data)
85 | httpreq, err = http.NewRequest(req.Method, req.Url, data)
86 | if err != nil {
87 | return ffuf.Response{}, err
88 | }
89 | // Add user agent string if not defined
90 | if _, ok := req.Headers["User-Agent"]; !ok {
91 | req.Headers["User-Agent"] = fmt.Sprintf("%s v%s", "Fuzz Faster U Fool", ffuf.VERSION)
92 | }
93 | // Handle Go http.Request special cases
94 | if _, ok := req.Headers["Host"]; ok {
95 | httpreq.Host = req.Headers["Host"]
96 | }
97 | httpreq = httpreq.WithContext(r.config.Context)
98 | for k, v := range req.Headers {
99 | httpreq.Header.Set(k, v)
100 | }
101 | httpresp, err := r.client.Do(httpreq)
102 | if err != nil {
103 | return ffuf.Response{}, err
104 | }
105 |
106 | resp := ffuf.NewResponse(httpresp, req)
107 | defer httpresp.Body.Close()
108 |
109 | if len(r.config.OutputDirectory) > 0 {
110 | // store raw request
111 | httpreq.Write(&rawreq)
112 | resp.Request.Raw = rawreq.String()
113 | // store raw response
114 | httpresp.Write(&rawresp)
115 | resp.Raw = rawresp.String()
116 | }
117 |
118 | // Check if we should download the resource or not
119 | size, err := strconv.Atoi(httpresp.Header.Get("Content-Length"))
120 | if err == nil {
121 | resp.ContentLength = int64(size)
122 | if size > MAX_DOWNLOAD_SIZE {
123 | resp.Cancelled = true
124 | return resp, nil
125 | }
126 | }
127 |
128 | if respbody, err := ioutil.ReadAll(httpresp.Body); err == nil {
129 | resp.ContentLength = int64(utf8.RuneCountInString(string(respbody)))
130 | resp.Data = respbody
131 | }
132 |
133 | wordsSize := len(strings.Split(string(resp.Data), " "))
134 | linesSize := len(strings.Split(string(resp.Data), "\n"))
135 | resp.ContentWords = int64(wordsSize)
136 | resp.ContentLines = int64(linesSize)
137 |
138 | return resp, nil
139 | }
140 |
--------------------------------------------------------------------------------
/pkg/input/input.go:
--------------------------------------------------------------------------------
1 | package input
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ffuf/ffuf/pkg/ffuf"
7 | )
8 |
9 | type MainInputProvider struct {
10 | Providers []ffuf.InternalInputProvider
11 | Config *ffuf.Config
12 | position int
13 | msbIterator int
14 | }
15 |
16 | func NewInputProvider(conf *ffuf.Config) (ffuf.InputProvider, error) {
17 | validmode := false
18 | for _, mode := range []string{"clusterbomb", "pitchfork"} {
19 | if conf.InputMode == mode {
20 | validmode = true
21 | }
22 | }
23 | if !validmode {
24 | return &MainInputProvider{}, fmt.Errorf("Input mode (-mode) %s not recognized", conf.InputMode)
25 | }
26 | return &MainInputProvider{Config: conf, msbIterator: 0}, nil
27 | }
28 |
29 | func (i *MainInputProvider) AddProvider(provider ffuf.InputProviderConfig) error {
30 | if provider.Name == "command" {
31 | newcomm, _ := NewCommandInput(provider.Keyword, provider.Value, i.Config)
32 | i.Providers = append(i.Providers, newcomm)
33 | } else {
34 | // Default to wordlist
35 | newwl, err := NewWordlistInput(provider.Keyword, provider.Value, i.Config)
36 | if err != nil {
37 | return err
38 | }
39 | i.Providers = append(i.Providers, newwl)
40 | }
41 | return nil
42 | }
43 |
44 | //Position will return the current position of progress
45 | func (i *MainInputProvider) Position() int {
46 | return i.position
47 | }
48 |
49 | //Next will increment the cursor position, and return a boolean telling if there's inputs left
50 | func (i *MainInputProvider) Next() bool {
51 | if i.position >= i.Total() {
52 | return false
53 | }
54 | i.position++
55 | return true
56 | }
57 |
58 | //Value returns a map of inputs for keywords
59 | func (i *MainInputProvider) Value() map[string][]byte {
60 | retval := make(map[string][]byte)
61 | if i.Config.InputMode == "clusterbomb" {
62 | retval = i.clusterbombValue()
63 | }
64 | if i.Config.InputMode == "pitchfork" {
65 | retval = i.pitchforkValue()
66 | }
67 | return retval
68 | }
69 |
70 | //Reset resets all the inputproviders and counters
71 | func (i *MainInputProvider) Reset() {
72 | for _, p := range i.Providers {
73 | p.ResetPosition()
74 | }
75 | i.position = 0
76 | i.msbIterator = 0
77 | }
78 |
79 | //pitchforkValue returns a map of keyword:value pairs including all inputs.
80 | //This mode will iterate through wordlists in lockstep.
81 | func (i *MainInputProvider) pitchforkValue() map[string][]byte {
82 | values := make(map[string][]byte)
83 | for _, p := range i.Providers {
84 | if !p.Next() {
85 | // Loop to beginning if the inputprovider has been exhausted
86 | p.ResetPosition()
87 | }
88 | values[p.Keyword()] = p.Value()
89 | p.IncrementPosition()
90 | }
91 | return values
92 | }
93 |
94 | //clusterbombValue returns map of keyword:value pairs including all inputs.
95 | //this mode will iterate through all possible combinations.
96 | func (i *MainInputProvider) clusterbombValue() map[string][]byte {
97 | values := make(map[string][]byte)
98 | // Should we signal the next InputProvider in the slice to increment
99 | signalNext := false
100 | first := true
101 | for index, p := range i.Providers {
102 | if signalNext {
103 | p.IncrementPosition()
104 | signalNext = false
105 | }
106 | if !p.Next() {
107 | // No more inputs in this inputprovider
108 | if index == i.msbIterator {
109 | // Reset all previous wordlists and increment the msb counter
110 | i.msbIterator += 1
111 | i.clusterbombIteratorReset()
112 | // Start again
113 | return i.clusterbombValue()
114 | }
115 | p.ResetPosition()
116 | signalNext = true
117 | }
118 | values[p.Keyword()] = p.Value()
119 | if first {
120 | p.IncrementPosition()
121 | first = false
122 | }
123 | }
124 | return values
125 | }
126 |
127 | func (i *MainInputProvider) clusterbombIteratorReset() {
128 | for index, p := range i.Providers {
129 | if index < i.msbIterator {
130 | p.ResetPosition()
131 | }
132 | if index == i.msbIterator {
133 | p.IncrementPosition()
134 | }
135 | }
136 | }
137 |
138 | //Total returns the amount of input combinations available
139 | func (i *MainInputProvider) Total() int {
140 | count := 0
141 | if i.Config.InputMode == "pitchfork" {
142 | for _, p := range i.Providers {
143 | if p.Total() > count {
144 | count = p.Total()
145 | }
146 | }
147 | }
148 | if i.Config.InputMode == "clusterbomb" {
149 | count = 1
150 | for _, p := range i.Providers {
151 | count = count * p.Total()
152 | }
153 | }
154 | return count
155 | }
156 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | - master
4 | - New
5 | - New CLI flag `-od` (output directory) to enable writing requests and responses for matched results to a file for postprocessing or debugging purposes.
6 | - New CLI flag `-maxtime` to limit the running time of ffuf
7 | - New CLI flags `-recursion` and `-recursion-depth` to control recursive ffuf jobs if directories are found. This requires the `-u` to end with FUZZ keyword.
8 | - Changed
9 | - Limit the use of `-e` (extensions) to a single keyword: FUZZ
10 | - Regexp matching and filtering (-mr/-fr) allow using keywords in patterns
11 | - Take 429 responses into account when -sa (stop on all error cases) is used
12 | - Remove -k flag support, convert to dummy flag #134
13 | - Write configuration to output JSON
14 |
15 | - v0.12
16 | - New
17 | - Added a new flag to select a multi wordlist operation mode: `--mode`, possible values: `clusterbomb` and `pitchfork`.
18 | - Added a new output file format eJSON, for always base64 encoding the input data.
19 | - Redirect location is always shown in the output files (when using `-o`)
20 | - Full URL is always shown in the output files (when using `-o`)
21 | - HTML output format got [DataTables](https://datatables.net/) support allowing realtime searches, sorting by column etc.
22 | - New CLI flag `-v` for verbose output. Including full URL, and redirect location.
23 | - SIGTERM monitoring, in order to catch keyboard interrupts an such, to be able to write `-o` files before exiting.
24 | - Changed
25 | - Fixed a bug in the default multi wordlist mode
26 | - Fixed JSON output regression, where all the input data was always encoded in base64
27 | - `--debug-log` no correctly logs connection errors
28 | - Removed `-l` flag in favor of `-v`
29 | - More verbose information in banner shown in startup.
30 |
31 | - v0.11
32 | - New
33 |
34 | - New CLI flag: -l, shows target location of redirect responses
35 | - New CLI flac: -acc, custom auto-calibration strings
36 | - New CLI flag: -debug-log, writes the debug logging to the specified file.
37 | - New CLI flags -ml and -fl, filters/matches line count in response
38 | - 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"`.
39 |
40 | - Changed
41 | - New CLI flag: -i, dummy flag that does nothing. for compatibility with copy as curl.
42 | - New CLI flag: -b/--cookie, cookie data for compatibility with copy as curl.
43 | - New Output format are available: HTML and Markdown table.
44 | - New CLI flag: -l, shows target location of redirect responses
45 | - Filtering and matching by status code, response size or word count now allow using ranges in addition to single values
46 | - The internal logging information to be discarded, and can be written to a file with the new `-debug-log` flag.
47 |
48 | - v0.10
49 | - New
50 | - New CLI flag: -ac to autocalibrate response size and word filters based on few preset URLs.
51 | - New CLI flag: -timeout to specify custom timeouts for all HTTP requests.
52 | - New CLI flag: --data for compatibility with copy as curl functionality of browsers.
53 | - New CLI flag: --compressed, dummy flag that does nothing. for compatibility with copy as curl.
54 | - 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.
55 | - 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.
56 |
57 | - Changed
58 | - Wordlist can also be read from standard input
59 | - Defining -d or --data implies POST method if -X doesn't set it to something else than GET
60 |
61 | - v0.9
62 | - New
63 | - New output file formats: CSV and eCSV (CSV with base64 encoded input field to avoid CSV breakage with payloads containing a comma)
64 | - New CLI flag to follow redirects
65 | - Erroring connections will be retried once
66 | - Error counter in status bar
67 | - New CLI flags: -se (stop on spurious errors) and -sa (stop on all errors, implies -se and -sf)
68 | - New CLI flags: -e to provide a list of extensions to add to wordlist entries, and -D to provide DirSearch wordlist format compatibility.
69 | - Wildcard option for response status code matcher.
70 | - v0.8
71 | - New
72 | - New CLI flag to write output to a file in JSON format
73 | - New CLI flag to stop on spurious 403 responses
74 | - Changed
75 | - Regex matching / filtering now matches the headers alongside of the response body
76 |
--------------------------------------------------------------------------------
/pkg/output/file_html.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "html/template"
5 | "os"
6 | "time"
7 |
8 | "github.com/ffuf/ffuf/pkg/ffuf"
9 | )
10 |
11 | type htmlFileOutput struct {
12 | CommandLine string
13 | Time string
14 | Keys []string
15 | Results []Result
16 | }
17 |
18 | const (
19 | htmlTemplate = `
20 |
21 |
22 |
23 |
24 |
28 | FFUF Report -
29 |
30 |
31 |
35 |
39 |
44 |
45 |
46 |
47 |
48 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
{{ .CommandLine }}
63 |
{{ .Time }}
64 |
65 |
66 |
67 |
68 | |result_raw|StatusCode|Input|Position|ContentLength|ContentWords|ContentLines|
69 |
70 |
71 | | Status |
72 | {{ range .Keys }} {{ . }} |
73 | {{ end }}
74 | URL |
75 | Redirect location |
76 | Position |
77 | Length |
78 | Words |
79 | Lines |
80 | Resultfile |
81 |
82 |
83 |
84 |
85 | {{range $result := .Results}}
86 |
87 | |result_raw|{{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}|{{ $value | printf "%s" }}{{ end }}|{{ $result.Url }}|{{ $result.RedirectLocation }}|{{ $result.Position }}|{{ $result.ContentLength }}|{{ $result.ContentWords }}|{{ $result.ContentLines }}|
88 |
89 | | {{ $result.StatusCode }} | {{ range $keyword, $value := $result.Input }}{{ $value | printf "%s" }} | {{ end }}{{ $result.Url }} | {{ $result.RedirectLocation }} | {{ $result.Position }} | {{ $result.ContentLength }} | {{ $result.ContentWords }} | {{ $result.ContentLines }} | {{ $result.ResultFile }} |
90 | {{end}}
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
108 |
119 |
120 |
121 |
122 | `
123 | )
124 |
125 | // colorizeResults returns a new slice with HTMLColor attribute
126 | func colorizeResults(results []Result) []Result {
127 | newResults := make([]Result, 0)
128 |
129 | for _, r := range results {
130 | result := r
131 | result.HTMLColor = "black"
132 |
133 | s := result.StatusCode
134 |
135 | if s >= 200 && s <= 299 {
136 | result.HTMLColor = "#adea9e"
137 | }
138 |
139 | if s >= 300 && s <= 399 {
140 | result.HTMLColor = "#bbbbe6"
141 | }
142 |
143 | if s >= 400 && s <= 499 {
144 | result.HTMLColor = "#d2cb7e"
145 | }
146 |
147 | if s >= 500 && s <= 599 {
148 | result.HTMLColor = "#de8dc1"
149 | }
150 |
151 | newResults = append(newResults, result)
152 | }
153 |
154 | return newResults
155 | }
156 |
157 | func writeHTML(config *ffuf.Config, results []Result) error {
158 |
159 | results = colorizeResults(results)
160 |
161 | ti := time.Now()
162 |
163 | keywords := make([]string, 0)
164 | for _, inputprovider := range config.InputProviders {
165 | keywords = append(keywords, inputprovider.Keyword)
166 | }
167 |
168 | outHTML := htmlFileOutput{
169 | CommandLine: config.CommandLine,
170 | Time: ti.Format(time.RFC3339),
171 | Results: results,
172 | Keys: keywords,
173 | }
174 |
175 | f, err := os.Create(config.OutputFile)
176 | if err != nil {
177 | return err
178 | }
179 | defer f.Close()
180 |
181 | templateName := "output.html"
182 | t := template.New(templateName).Delims("{{", "}}")
183 | t.Parse(htmlTemplate)
184 | t.Execute(f, outHTML)
185 | return nil
186 | }
187 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | /'___\ /'___\ /'___\
3 | /\ \__/ /\ \__/ __ __ /\ \__/
4 | \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
5 | \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
6 | \ \_\ \ \_\ \ \____/ \ \_\
7 | \/_/ \/_/ \/___/ \/_/
8 | ```
9 |
10 | # ffuf - Fuzz Faster U Fool
11 |
12 | A fast web fuzzer written in Go.
13 |
14 | ## Installation
15 |
16 | - [Download](https://github.com/ffuf/ffuf/releases/latest) a prebuilt binary from [releases page](https://github.com/ffuf/ffuf/releases/latest), unpack and run!
17 | or
18 | - If you have go compiler installed: `go get github.com/ffuf/ffuf`
19 |
20 | The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard library are needed.
21 |
22 | ## Example usage
23 |
24 | ### Typical directory discovery
25 |
26 | [](https://asciinema.org/a/211350)
27 |
28 | By using the FUZZ keyword at the end of URL (`-u`):
29 |
30 | ```
31 | ffuf -w /path/to/wordlist -u https://target/FUZZ
32 | ```
33 |
34 | ### Virtual host discovery (without DNS records)
35 |
36 | [](https://asciinema.org/a/211360)
37 |
38 | Assuming that the default virtualhost response size is 4242 bytes, we can filter out all the responses of that size (`-fs 4242`)while fuzzing the Host - header:
39 |
40 | ```
41 | ffuf -w /path/to/vhost/wordlist -u https://target -H "Host: FUZZ" -fs 4242
42 | ```
43 |
44 | ### GET parameter fuzzing
45 |
46 | GET parameter name fuzzing is very similar to directory discovery, and works by defining the `FUZZ` keyword as a part of the URL. This also assumes an response size of 4242 bytes for invalid GET parameter name.
47 |
48 | ```
49 | ffuf -w /path/to/paramnames.txt -u https://target/script.php?FUZZ=test_value -fs 4242
50 | ```
51 |
52 | If the parameter name is known, the values can be fuzzed the same way. This example assumes a wrong parameter value returning HTTP response code 401.
53 |
54 | ```
55 | ffuf -w /path/to/values.txt -u https://target/script.php?valid_name=FUZZ -fc 401
56 | ```
57 |
58 | ### POST data fuzzing
59 |
60 | This is a very straightforward operation, again by using the `FUZZ` keyword. This example is fuzzing only part of the POST request. We're again filtering out the 401 responses.
61 |
62 | ```
63 | ffuf -w /path/to/postdata.txt -X POST -d "username=admin\&password=FUZZ" -u https://target/login.php -fc 401
64 | ```
65 |
66 | ### Using external mutator to produce test cases
67 |
68 | For this example, we'll fuzz JSON data that's sent over POST. [Radamsa](https://gitlab.com/akihe/radamsa) is used as the mutator.
69 |
70 | 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`:
71 |
72 | ```
73 | 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
74 | ```
75 |
76 | 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:
77 |
78 | ```
79 | # Generate 1000 example payloads
80 | radamsa -n 1000 -o %n.txt example1.txt example2.txt
81 |
82 | # This results into files 1.txt ... 1000.txt
83 | # Now we can just read the payload data in a loop from file for ffuf
84 |
85 | ffuf --input-cmd 'cat $FFUF_NUM.txt' -H "Content-Type: application/json" -X POST -u https://ffuf.io.fi/ -mc all -fc 400
86 | ```
87 |
88 | ## Usage
89 |
90 | To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-u`), headers (`-H`), or POST data (`-d`).
91 |
92 | ```
93 | Usage of ffuf:
94 | -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.
95 | -H "Name: Value"
96 | Header "Name: Value", separated by colon. Multiple -H flags are accepted.
97 | -V Show version information.
98 | -X string
99 | HTTP method to use (default "GET")
100 | -ac
101 | Automatically calibrate filtering options
102 | -acc value
103 | Custom auto-calibration string. Can be used multiple times. Implies -ac
104 | -b "NAME1=VALUE1; NAME2=VALUE2"
105 | Cookie data "NAME1=VALUE1; NAME2=VALUE2" for copy as curl functionality.
106 | Results unpredictable when combined with -H "Cookie: ..."
107 | -c Colorize output.
108 | -compressed
109 | Dummy flag for copy as curl functionality (ignored) (default true)
110 | -cookie value
111 | Cookie data (alias of -b)
112 | -d string
113 | POST data
114 | -data string
115 | POST data (alias of -d)
116 | -data-ascii string
117 | POST data (alias of -d)
118 | -data-binary string
119 | POST data (alias of -d)
120 | -debug-log string
121 | Write all of the internal logging to the specified file.
122 | -e string
123 | Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. Only extends a wordlist with (default) FUZZ keyword.
124 | -fc string
125 | Filter HTTP status codes from response. Comma separated list of codes and ranges
126 | -fl string
127 | Filter by amount of lines in response. Comma separated list of line counts and ranges
128 | -fr string
129 | Filter regexp
130 | -fs string
131 | Filter HTTP response size. Comma separated list of sizes and ranges
132 | -fw string
133 | Filter by amount of words in response. Comma separated list of word counts and ranges
134 | -i Dummy flag for copy as curl functionality (ignored) (default true)
135 | -input-cmd value
136 | Command producing the input. --input-num is required when using this input method. Overrides -w.
137 | -input-num int
138 | Number of inputs to test. Used in conjunction with --input-cmd. (default 100)
139 | -k TLS identity verification
140 | -maxtime int
141 | Maximum running time in seconds. (default 0 = inf.)
142 | -mc string
143 | Match HTTP status codes from respose, use "all" to match every response code. (default "200,204,301,302,307,401,403")
144 | -ml string
145 | Match amount of lines in response
146 | -mode string
147 | Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork (default "clusterbomb")
148 | -mr string
149 | Match regexp
150 | -ms string
151 | Match HTTP response size
152 | -mw string
153 | Match amount of words in response
154 | -o string
155 | Write output to file
156 | -od string
157 | Directory path to store matched results to.
158 | -of string
159 | Output file format. Available formats: json, ejson, html, md, csv, ecsv (default "json")
160 | -p delay
161 | Seconds of delay between requests, or a range of random delay. For example "0.1" or "0.1-2.0"
162 | -r Follow redirects
163 | -s Do not print additional information (silent mode)
164 | -sa
165 | Stop on all error cases. Implies -sf and -se. Also stops on spurious 429 response codes.
166 | -se
167 | Stop on spurious errors
168 | -sf
169 | Stop when > 95% of responses return 403 Forbidden
170 | -t int
171 | Number of concurrent threads. (default 40)
172 | -timeout int
173 | HTTP request timeout in seconds. (default 10)
174 | -u string
175 | Target URL
176 | -v Verbose output, printing full URL and redirect location (if any) with the results.
177 | -w value
178 | 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'
179 | -x string
180 | HTTP Proxy URL
181 | ```
182 |
183 | eg. `ffuf -u https://example.org/FUZZ -w /path/to/wordlist`
184 |
185 |
186 | ## License
187 |
188 | ffuf is released under MIT license. See [LICENSE](https://github.com/ffuf/ffuf/blob/master/LICENSE).
189 |
--------------------------------------------------------------------------------
/pkg/ffuf/job.go:
--------------------------------------------------------------------------------
1 | package ffuf
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "math/rand"
7 | "os"
8 | "os/signal"
9 | "sync"
10 | "syscall"
11 | "time"
12 | )
13 |
14 | //Job ties together Config, Runner, Input and Output
15 | type Job struct {
16 | Config *Config
17 | ErrorMutex sync.Mutex
18 | Input InputProvider
19 | Runner RunnerProvider
20 | Output OutputProvider
21 | Counter int
22 | ErrorCounter int
23 | SpuriousErrorCounter int
24 | Total int
25 | Running bool
26 | Count403 int
27 | Count429 int
28 | Error string
29 | startTime time.Time
30 | queuejobs []QueueJob
31 | queuepos int
32 | currentDepth int
33 | }
34 |
35 | type QueueJob struct {
36 | Url string
37 | depth int
38 | }
39 |
40 | func NewJob(conf *Config) Job {
41 | var j Job
42 | j.Counter = 0
43 | j.ErrorCounter = 0
44 | j.SpuriousErrorCounter = 0
45 | j.Running = false
46 | j.queuepos = 0
47 | j.queuejobs = make([]QueueJob, 0)
48 | j.currentDepth = 0
49 | return j
50 | }
51 |
52 | //incError increments the error counter
53 | func (j *Job) incError() {
54 | j.ErrorMutex.Lock()
55 | defer j.ErrorMutex.Unlock()
56 | j.ErrorCounter++
57 | j.SpuriousErrorCounter++
58 | }
59 |
60 | //inc403 increments the 403 response counter
61 | func (j *Job) inc403() {
62 | j.ErrorMutex.Lock()
63 | defer j.ErrorMutex.Unlock()
64 | j.Count403++
65 | }
66 |
67 | // inc429 increments the 429 response counter
68 | func (j *Job) inc429() {
69 | j.ErrorMutex.Lock()
70 | defer j.ErrorMutex.Unlock()
71 | j.Count429++
72 | }
73 |
74 | //resetSpuriousErrors resets the spurious error counter
75 | func (j *Job) resetSpuriousErrors() {
76 | j.ErrorMutex.Lock()
77 | defer j.ErrorMutex.Unlock()
78 | j.SpuriousErrorCounter = 0
79 | }
80 |
81 | //Start the execution of the Job
82 | func (j *Job) Start() {
83 | // Add the default job to job queue
84 | j.queuejobs = append(j.queuejobs, QueueJob{Url: j.Config.Url, depth: 0})
85 | rand.Seed(time.Now().UnixNano())
86 | j.Total = j.Input.Total()
87 | defer j.Stop()
88 | j.Running = true
89 | j.startTime = time.Now()
90 | //Show banner if not running in silent mode
91 | if !j.Config.Quiet {
92 | j.Output.Banner()
93 | }
94 | // Monitor for SIGTERM and do cleanup properly (writing the output files etc)
95 | j.interruptMonitor()
96 | for j.jobsInQueue() {
97 | j.prepareQueueJob()
98 | if j.queuepos > 1 {
99 | // Print info for queued recursive jobs
100 | j.Output.Info(fmt.Sprintf("Scanning: %s", j.Config.Url))
101 | }
102 | j.Input.Reset()
103 | j.Counter = 0
104 | j.startExecution()
105 | }
106 |
107 | j.Output.Finalize()
108 | }
109 |
110 | func (j *Job) jobsInQueue() bool {
111 | if j.queuepos < len(j.queuejobs) {
112 | return true
113 | }
114 | return false
115 | }
116 |
117 | func (j *Job) prepareQueueJob() {
118 | j.Config.Url = j.queuejobs[j.queuepos].Url
119 | j.currentDepth = j.queuejobs[j.queuepos].depth
120 | j.queuepos += 1
121 | }
122 |
123 | func (j *Job) startExecution() {
124 | var wg sync.WaitGroup
125 | wg.Add(1)
126 | go j.runProgress(&wg)
127 | //Limiter blocks after reaching the buffer, ensuring limited concurrency
128 | limiter := make(chan bool, j.Config.Threads)
129 | for j.Input.Next() {
130 | // Check if we should stop the process
131 | j.CheckStop()
132 | if !j.Running {
133 | defer j.Output.Warning(j.Error)
134 | break
135 | }
136 | limiter <- true
137 | nextInput := j.Input.Value()
138 | nextPosition := j.Input.Position()
139 | wg.Add(1)
140 | j.Counter++
141 | go func() {
142 | defer func() { <-limiter }()
143 | defer wg.Done()
144 | j.runTask(nextInput, nextPosition, false)
145 | if j.Config.Delay.HasDelay {
146 | var sleepDurationMS time.Duration
147 | if j.Config.Delay.IsRange {
148 | sTime := j.Config.Delay.Min + rand.Float64()*(j.Config.Delay.Max-j.Config.Delay.Min)
149 | sleepDurationMS = time.Duration(sTime * 1000)
150 | } else {
151 | sleepDurationMS = time.Duration(j.Config.Delay.Min * 1000)
152 | }
153 | time.Sleep(sleepDurationMS * time.Millisecond)
154 | }
155 | }()
156 | }
157 | wg.Wait()
158 | j.updateProgress()
159 | return
160 | }
161 |
162 | func (j *Job) interruptMonitor() {
163 | sigChan := make(chan os.Signal, 2)
164 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
165 | go func() {
166 | for _ = range sigChan {
167 | j.Error = "Caught keyboard interrupt (Ctrl-C)\n"
168 | j.Stop()
169 | }
170 | }()
171 | }
172 |
173 | func (j *Job) runProgress(wg *sync.WaitGroup) {
174 | defer wg.Done()
175 | totalProgress := j.Input.Total()
176 | for j.Counter <= totalProgress {
177 | if !j.Running {
178 | break
179 | }
180 | j.updateProgress()
181 | if j.Counter == totalProgress {
182 | return
183 | }
184 | time.Sleep(time.Millisecond * time.Duration(j.Config.ProgressFrequency))
185 | }
186 | }
187 |
188 | func (j *Job) updateProgress() {
189 | prog := Progress{
190 | StartedAt: j.startTime,
191 | ReqCount: j.Counter,
192 | ReqTotal: j.Input.Total(),
193 | QueuePos: j.queuepos,
194 | QueueTotal: len(j.queuejobs),
195 | ErrorCount: j.ErrorCounter,
196 | }
197 | j.Output.Progress(prog)
198 | }
199 |
200 | func (j *Job) isMatch(resp Response) bool {
201 | matched := false
202 | for _, m := range j.Config.Matchers {
203 | match, err := m.Filter(&resp)
204 | if err != nil {
205 | continue
206 | }
207 | if match {
208 | matched = true
209 | }
210 | }
211 | // The response was not matched, return before running filters
212 | if !matched {
213 | return false
214 | }
215 | for _, f := range j.Config.Filters {
216 | fv, err := f.Filter(&resp)
217 | if err != nil {
218 | continue
219 | }
220 | if fv {
221 | return false
222 | }
223 | }
224 | return true
225 | }
226 |
227 | func (j *Job) runTask(input map[string][]byte, position int, retried bool) {
228 | req, err := j.Runner.Prepare(input)
229 | req.Position = position
230 | if err != nil {
231 | j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
232 | j.incError()
233 | log.Printf("%s", err)
234 | return
235 | }
236 | resp, err := j.Runner.Execute(&req)
237 | if err != nil {
238 | if retried {
239 | j.incError()
240 | log.Printf("%s", err)
241 | } else {
242 | j.runTask(input, position, true)
243 | }
244 | return
245 | }
246 | if j.SpuriousErrorCounter > 0 {
247 | j.resetSpuriousErrors()
248 | }
249 | if j.Config.StopOn403 || j.Config.StopOnAll {
250 | // Increment Forbidden counter if we encountered one
251 | if resp.StatusCode == 403 {
252 | j.inc403()
253 | }
254 | }
255 | if j.Config.StopOnAll {
256 | // increment 429 counter if the response code is 429
257 | if j.Config.StopOnAll {
258 | if resp.StatusCode == 429 {
259 | j.inc429()
260 | }
261 | }
262 | }
263 | if j.isMatch(resp) {
264 | j.Output.Result(resp)
265 | // Refresh the progress indicator as we printed something out
266 | j.updateProgress()
267 | }
268 |
269 | if j.Config.Recursion && len(resp.GetRedirectLocation()) > 0 {
270 | j.handleRecursionJob(resp)
271 | }
272 | return
273 | }
274 |
275 | //handleRecursionJob adds a new recursion job to the job queue if a new directory is found
276 | func (j *Job) handleRecursionJob(resp Response) {
277 | if (resp.Request.Url + "/") != resp.GetRedirectLocation() {
278 | // Not a directory, return early
279 | return
280 | }
281 | if j.Config.RecursionDepth == 0 || j.currentDepth < j.Config.RecursionDepth {
282 | // We have yet to reach the maximum recursion depth
283 | recUrl := resp.Request.Url + "/" + "FUZZ"
284 | newJob := QueueJob{Url: recUrl, depth: j.currentDepth + 1}
285 | j.queuejobs = append(j.queuejobs, newJob)
286 | j.Output.Info(fmt.Sprintf("Adding a new job to the queue: %s", recUrl))
287 | } else {
288 | j.Output.Warning(fmt.Sprintf("Directory found, but recursion depth exceeded. Ignoring: %s", resp.GetRedirectLocation()))
289 | }
290 | }
291 |
292 | //CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests
293 | func (j *Job) CalibrateResponses() ([]Response, error) {
294 | cInputs := make([]string, 0)
295 | if len(j.Config.AutoCalibrationStrings) < 1 {
296 | cInputs = append(cInputs, "admin"+RandomString(16)+"/")
297 | cInputs = append(cInputs, ".htaccess"+RandomString(16))
298 | cInputs = append(cInputs, RandomString(16)+"/")
299 | cInputs = append(cInputs, RandomString(16))
300 | } else {
301 | cInputs = append(cInputs, j.Config.AutoCalibrationStrings...)
302 | }
303 |
304 | results := make([]Response, 0)
305 | for _, input := range cInputs {
306 | inputs := make(map[string][]byte, 0)
307 | for _, v := range j.Config.InputProviders {
308 | inputs[v.Keyword] = []byte(input)
309 | }
310 |
311 | req, err := j.Runner.Prepare(inputs)
312 | if err != nil {
313 | j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
314 | j.incError()
315 | log.Printf("%s", err)
316 | return results, err
317 | }
318 | resp, err := j.Runner.Execute(&req)
319 | if err != nil {
320 | return results, err
321 | }
322 |
323 | // Only calibrate on responses that would be matched otherwise
324 | if j.isMatch(resp) {
325 | results = append(results, resp)
326 | }
327 | }
328 | return results, nil
329 | }
330 |
331 | // CheckStop stops the job if stopping conditions are met
332 | func (j *Job) CheckStop() {
333 | if j.Counter > 50 {
334 | // We have enough samples
335 | if j.Config.StopOn403 || j.Config.StopOnAll {
336 | if float64(j.Count403)/float64(j.Counter) > 0.95 {
337 | // Over 95% of requests are 403
338 | j.Error = "Getting an unusual amount of 403 responses, exiting."
339 | j.Stop()
340 | }
341 | }
342 | if j.Config.StopOnErrors || j.Config.StopOnAll {
343 | if j.SpuriousErrorCounter > j.Config.Threads*2 {
344 | // Most of the requests are erroring
345 | j.Error = "Receiving spurious errors, exiting."
346 | j.Stop()
347 | }
348 |
349 | }
350 | if j.Config.StopOnAll && (float64(j.Count429)/float64(j.Counter) > 0.2) {
351 | // Over 20% of responses are 429
352 | j.Error = "Getting an unusual amount of 429 responses, exiting."
353 | j.Stop()
354 | }
355 | }
356 |
357 | // check for maximum running time
358 | if j.Config.MaxTime > 0 {
359 | dur := time.Now().Sub(j.startTime)
360 | runningSecs := int(dur / time.Second)
361 | if runningSecs >= j.Config.MaxTime {
362 | j.Error = "Maximum running time reached, exiting."
363 | j.Stop()
364 | }
365 | }
366 | }
367 |
368 | //Stop the execution of the Job
369 | func (j *Job) Stop() {
370 | j.Running = false
371 | return
372 | }
373 |
--------------------------------------------------------------------------------
/pkg/output/stdout.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "crypto/md5"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path"
9 | "strconv"
10 | "time"
11 |
12 | "github.com/ffuf/ffuf/pkg/ffuf"
13 | )
14 |
15 | const (
16 | BANNER_HEADER = `
17 | /'___\ /'___\ /'___\
18 | /\ \__/ /\ \__/ __ __ /\ \__/
19 | \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
20 | \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
21 | \ \_\ \ \_\ \ \____/ \ \_\
22 | \/_/ \/_/ \/___/ \/_/
23 | `
24 | BANNER_SEP = "________________________________________________"
25 | )
26 |
27 | type Stdoutput struct {
28 | config *ffuf.Config
29 | Results []Result
30 | }
31 |
32 | type Result struct {
33 | Input map[string][]byte `json:"input"`
34 | Position int `json:"position"`
35 | StatusCode int64 `json:"status"`
36 | ContentLength int64 `json:"length"`
37 | ContentWords int64 `json:"words"`
38 | ContentLines int64 `json:"lines"`
39 | RedirectLocation string `json:"redirectlocation"`
40 | Url string `json:"url"`
41 | ResultFile string `json:"resultfile"`
42 | HTMLColor string `json:"-"`
43 | }
44 |
45 | func NewStdoutput(conf *ffuf.Config) *Stdoutput {
46 | var outp Stdoutput
47 | outp.config = conf
48 | outp.Results = []Result{}
49 | return &outp
50 | }
51 |
52 | func (s *Stdoutput) Banner() error {
53 | fmt.Printf("%s\n v%s\n%s\n\n", BANNER_HEADER, ffuf.VERSION, BANNER_SEP)
54 | printOption([]byte("Method"), []byte(s.config.Method))
55 | printOption([]byte("URL"), []byte(s.config.Url))
56 | // Print headers
57 | if len(s.config.Headers) > 0 {
58 | for k, v := range s.config.Headers {
59 | printOption([]byte("Header"), []byte(fmt.Sprintf("%s: %s", k, v)))
60 | }
61 | }
62 | // Print POST data
63 | if len(s.config.Data) > 0 {
64 | printOption([]byte("Data"), []byte(s.config.Data))
65 | }
66 |
67 | // Print extensions
68 | if len(s.config.Extensions) > 0 {
69 | exts := ""
70 | for _, ext := range s.config.Extensions {
71 | exts = fmt.Sprintf("%s%s ", exts, ext)
72 | }
73 | printOption([]byte("Extensions"), []byte(exts))
74 | }
75 |
76 | // Output file info
77 | if len(s.config.OutputFile) > 0 {
78 | printOption([]byte("Output file"), []byte(s.config.OutputFile))
79 | printOption([]byte("File format"), []byte(s.config.OutputFormat))
80 | }
81 |
82 | // Follow redirects?
83 | follow := fmt.Sprintf("%t", s.config.FollowRedirects)
84 | printOption([]byte("Follow redirects"), []byte(follow))
85 |
86 | // Autocalibration
87 | autocalib := fmt.Sprintf("%t", s.config.AutoCalibration)
88 | printOption([]byte("Calibration"), []byte(autocalib))
89 |
90 | // Timeout
91 | timeout := fmt.Sprintf("%d", s.config.Timeout)
92 | printOption([]byte("Timeout"), []byte(timeout))
93 |
94 | // Threads
95 | threads := fmt.Sprintf("%d", s.config.Threads)
96 | printOption([]byte("Threads"), []byte(threads))
97 |
98 | // Delay?
99 | if s.config.Delay.HasDelay {
100 | delay := ""
101 | if s.config.Delay.IsRange {
102 | delay = fmt.Sprintf("%.2f - %.2f seconds", s.config.Delay.Min, s.config.Delay.Max)
103 | } else {
104 | delay = fmt.Sprintf("%.2f seconds", s.config.Delay.Min)
105 | }
106 | printOption([]byte("Delay"), []byte(delay))
107 | }
108 |
109 | // Print matchers
110 | for _, f := range s.config.Matchers {
111 | printOption([]byte("Matcher"), []byte(f.Repr()))
112 | }
113 | // Print filters
114 | for _, f := range s.config.Filters {
115 | printOption([]byte("Filter"), []byte(f.Repr()))
116 | }
117 | fmt.Printf("%s\n\n", BANNER_SEP)
118 | return nil
119 | }
120 |
121 | func (s *Stdoutput) Progress(status ffuf.Progress) {
122 | if s.config.Quiet {
123 | // No progress for quiet mode
124 | return
125 | }
126 |
127 | dur := time.Now().Sub(status.StartedAt)
128 | runningSecs := int(dur / time.Second)
129 | var reqRate int
130 | if runningSecs > 0 {
131 | reqRate = int(status.ReqCount / runningSecs)
132 | } else {
133 | reqRate = 0
134 | }
135 |
136 | hours := dur / time.Hour
137 | dur -= hours * time.Hour
138 | mins := dur / time.Minute
139 | dur -= mins * time.Minute
140 | secs := dur / time.Second
141 |
142 | fmt.Fprintf(os.Stderr, "%s:: Progress: [%d/%d] :: Job [%d/%d] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", TERMINAL_CLEAR_LINE, status.ReqCount, status.ReqTotal, status.QueuePos, status.QueueTotal, reqRate, hours, mins, secs, status.ErrorCount)
143 | }
144 |
145 | func (s *Stdoutput) Info(infostring string) {
146 | if s.config.Quiet {
147 | fmt.Fprintf(os.Stderr, "%s", infostring)
148 | } else {
149 | if !s.config.Colors {
150 | fmt.Fprintf(os.Stderr, "%s[INFO] %s\n", TERMINAL_CLEAR_LINE, infostring)
151 | } else {
152 | fmt.Fprintf(os.Stderr, "%s[%sINFO%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_BLUE, ANSI_CLEAR, infostring)
153 | }
154 | }
155 | }
156 |
157 | func (s *Stdoutput) Error(errstring string) {
158 | if s.config.Quiet {
159 | fmt.Fprintf(os.Stderr, "%s", errstring)
160 | } else {
161 | if !s.config.Colors {
162 | fmt.Fprintf(os.Stderr, "%s[ERR] %s\n", TERMINAL_CLEAR_LINE, errstring)
163 | } else {
164 | fmt.Fprintf(os.Stderr, "%s[%sERR%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_RED, ANSI_CLEAR, errstring)
165 | }
166 | }
167 | }
168 |
169 | func (s *Stdoutput) Warning(warnstring string) {
170 | if s.config.Quiet {
171 | fmt.Fprintf(os.Stderr, "%s", warnstring)
172 | } else {
173 | if !s.config.Colors {
174 | fmt.Fprintf(os.Stderr, "%s[WARN] %s", TERMINAL_CLEAR_LINE, warnstring)
175 | } else {
176 | fmt.Fprintf(os.Stderr, "%s[%sWARN%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_RED, ANSI_CLEAR, warnstring)
177 | }
178 | }
179 | }
180 |
181 | func (s *Stdoutput) Finalize() error {
182 | var err error
183 | if s.config.OutputFile != "" {
184 | if s.config.OutputFormat == "json" {
185 | err = writeJSON(s.config, s.Results)
186 | } else if s.config.OutputFormat == "ejson" {
187 | err = writeEJSON(s.config, s.Results)
188 | } else if s.config.OutputFormat == "html" {
189 | err = writeHTML(s.config, s.Results)
190 | } else if s.config.OutputFormat == "md" {
191 | err = writeMarkdown(s.config, s.Results)
192 | } else if s.config.OutputFormat == "csv" {
193 | err = writeCSV(s.config, s.Results, false)
194 | } else if s.config.OutputFormat == "ecsv" {
195 | err = writeCSV(s.config, s.Results, true)
196 | }
197 | if err != nil {
198 | s.Error(fmt.Sprintf("%s", err))
199 | }
200 | }
201 | fmt.Fprintf(os.Stderr, "\n")
202 | return nil
203 | }
204 |
205 | func (s *Stdoutput) Result(resp ffuf.Response) {
206 | // Do we want to write request and response to a file
207 | if len(s.config.OutputDirectory) > 0 {
208 | resp.ResultFile = s.writeResultToFile(resp)
209 | }
210 | // Output the result
211 | s.printResult(resp)
212 | // Check if we need the data later
213 | if s.config.OutputFile != "" {
214 | // No need to store results if we're not going to use them later
215 | inputs := make(map[string][]byte, 0)
216 | for k, v := range resp.Request.Input {
217 | inputs[k] = v
218 | }
219 | sResult := Result{
220 | Input: inputs,
221 | Position: resp.Request.Position,
222 | StatusCode: resp.StatusCode,
223 | ContentLength: resp.ContentLength,
224 | ContentWords: resp.ContentWords,
225 | ContentLines: resp.ContentLines,
226 | RedirectLocation: resp.GetRedirectLocation(),
227 | Url: resp.Request.Url,
228 | ResultFile: resp.ResultFile,
229 | }
230 | s.Results = append(s.Results, sResult)
231 | }
232 | }
233 |
234 | func (s *Stdoutput) writeResultToFile(resp ffuf.Response) string {
235 | var fileContent, fileName, filePath string
236 | // Create directory if needed
237 | if s.config.OutputDirectory != "" {
238 | err := os.Mkdir(s.config.OutputDirectory, 0750)
239 | if err != nil {
240 | if !os.IsExist(err) {
241 | s.Error(fmt.Sprintf("%s", err))
242 | return ""
243 | }
244 | }
245 | }
246 | fileContent = fmt.Sprintf("%s\n---- ↑ Request ---- Response ↓ ----\n\n%s", resp.Request.Raw, resp.Raw)
247 |
248 | // Create file name
249 | fileName = fmt.Sprintf("%x", md5.Sum([]byte(fileContent)))
250 |
251 | filePath = path.Join(s.config.OutputDirectory, fileName)
252 | err := ioutil.WriteFile(filePath, []byte(fileContent), 0640)
253 | if err != nil {
254 | s.Error(fmt.Sprintf("%s", err))
255 | }
256 | return fileName
257 | }
258 |
259 | func (s *Stdoutput) printResult(resp ffuf.Response) {
260 | if s.config.Quiet {
261 | s.resultQuiet(resp)
262 | } else {
263 | if len(resp.Request.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0 {
264 | // Print a multi-line result (when using multiple input keywords and wordlists)
265 | s.resultMultiline(resp)
266 | } else {
267 | s.resultNormal(resp)
268 | }
269 | }
270 | }
271 |
272 | func (s *Stdoutput) prepareInputsOneLine(resp ffuf.Response) string {
273 | inputs := ""
274 | if len(resp.Request.Input) > 1 {
275 | for k, v := range resp.Request.Input {
276 | if inSlice(k, s.config.CommandKeywords) {
277 | // If we're using external command for input, display the position instead of input
278 | inputs = fmt.Sprintf("%s%s : %s ", inputs, k, strconv.Itoa(resp.Request.Position))
279 | } else {
280 | inputs = fmt.Sprintf("%s%s : %s ", inputs, k, v)
281 | }
282 | }
283 | } else {
284 | for k, v := range resp.Request.Input {
285 | if inSlice(k, s.config.CommandKeywords) {
286 | // If we're using external command for input, display the position instead of input
287 | inputs = strconv.Itoa(resp.Request.Position)
288 | } else {
289 | inputs = string(v)
290 | }
291 | }
292 | }
293 | return inputs
294 | }
295 |
296 | func (s *Stdoutput) resultQuiet(resp ffuf.Response) {
297 | fmt.Println(s.prepareInputsOneLine(resp))
298 | }
299 |
300 | func (s *Stdoutput) resultMultiline(resp ffuf.Response) {
301 | var res_hdr, res_str string
302 | res_str = "%s%s * %s: %s\n"
303 | res_hdr = fmt.Sprintf("%s[Status: %d, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, resp.StatusCode, resp.ContentLength, resp.ContentWords, resp.ContentLines)
304 | res_hdr = s.colorize(res_hdr, resp.StatusCode)
305 | reslines := ""
306 | if s.config.Verbose {
307 | reslines = fmt.Sprintf("%s%s| URL | %s\n", reslines, TERMINAL_CLEAR_LINE, resp.Request.Url)
308 | redirectLocation := resp.GetRedirectLocation()
309 | if redirectLocation != "" {
310 | reslines = fmt.Sprintf("%s%s| --> | %s\n", reslines, TERMINAL_CLEAR_LINE, redirectLocation)
311 | }
312 | }
313 | if resp.ResultFile != "" {
314 | reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, resp.ResultFile)
315 | }
316 | for k, v := range resp.Request.Input {
317 | if inSlice(k, s.config.CommandKeywords) {
318 | // If we're using external command for input, display the position instead of input
319 | reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, strconv.Itoa(resp.Request.Position))
320 | } else {
321 | // Wordlist input
322 | reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, v)
323 | }
324 | }
325 | fmt.Printf("%s\n%s\n", res_hdr, reslines)
326 | }
327 |
328 | func (s *Stdoutput) resultNormal(resp ffuf.Response) {
329 | var res_str string
330 | res_str = fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, s.prepareInputsOneLine(resp), s.colorize(fmt.Sprintf("%d", resp.StatusCode), resp.StatusCode), resp.ContentLength, resp.ContentWords, resp.ContentLines)
331 | fmt.Println(res_str)
332 | }
333 |
334 | func (s *Stdoutput) colorize(input string, status int64) string {
335 | if !s.config.Colors {
336 | return fmt.Sprintf("%s", input)
337 | }
338 | colorCode := ANSI_CLEAR
339 | if status >= 200 && status < 300 {
340 | colorCode = ANSI_GREEN
341 | }
342 | if status >= 300 && status < 400 {
343 | colorCode = ANSI_BLUE
344 | }
345 | if status >= 400 && status < 500 {
346 | colorCode = ANSI_YELLOW
347 | }
348 | if status >= 500 && status < 600 {
349 | colorCode = ANSI_RED
350 | }
351 | return fmt.Sprintf("%s%s%s", colorCode, input, ANSI_CLEAR)
352 | }
353 |
354 | func printOption(name []byte, value []byte) {
355 | fmt.Printf(" :: %-16s : %s\n", name, value)
356 | }
357 |
358 | func inSlice(key string, slice []string) bool {
359 | for _, v := range slice {
360 | if v == key {
361 | return true
362 | }
363 | }
364 | return false
365 | }
366 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "io/ioutil"
8 | "log"
9 | "net/url"
10 | "os"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/ffuf/ffuf/pkg/ffuf"
15 | "github.com/ffuf/ffuf/pkg/filter"
16 | "github.com/ffuf/ffuf/pkg/input"
17 | "github.com/ffuf/ffuf/pkg/output"
18 | "github.com/ffuf/ffuf/pkg/runner"
19 | )
20 |
21 | type cliOptions struct {
22 | extensions string
23 | delay string
24 | filterStatus string
25 | filterSize string
26 | filterRegexp string
27 | filterWords string
28 | filterLines string
29 | matcherStatus string
30 | matcherSize string
31 | matcherRegexp string
32 | matcherWords string
33 | matcherLines string
34 | proxyURL string
35 | outputFormat string
36 | wordlists multiStringFlag
37 | inputcommands multiStringFlag
38 | headers multiStringFlag
39 | cookies multiStringFlag
40 | AutoCalibrationStrings multiStringFlag
41 | showVersion bool
42 | debugLog string
43 | }
44 |
45 | type multiStringFlag []string
46 |
47 | func (m *multiStringFlag) String() string {
48 | return ""
49 | }
50 |
51 | func (m *multiStringFlag) Set(value string) error {
52 | *m = append(*m, value)
53 | return nil
54 | }
55 |
56 | func main() {
57 | ctx, cancel := context.WithCancel(context.Background())
58 | defer cancel()
59 | conf := ffuf.NewConfig(ctx)
60 | opts := cliOptions{}
61 | var ignored bool
62 | flag.StringVar(&opts.extensions, "e", "", "Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. Only extends a wordlist with (default) FUZZ keyword.")
63 | 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.")
64 | flag.Var(&opts.headers, "H", "Header `\"Name: Value\"`, separated by colon. Multiple -H flags are accepted.")
65 | flag.StringVar(&conf.Url, "u", "", "Target URL")
66 | 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'")
67 | flag.BoolVar(&ignored, "k", false, "Dummy flag for backwards compatibility")
68 | 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\"")
69 | flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response. Comma separated list of codes and ranges")
70 | flag.StringVar(&opts.filterSize, "fs", "", "Filter HTTP response size. Comma separated list of sizes and ranges")
71 | flag.StringVar(&opts.filterRegexp, "fr", "", "Filter regexp")
72 | flag.StringVar(&opts.filterWords, "fw", "", "Filter by amount of words in response. Comma separated list of word counts and ranges")
73 | flag.StringVar(&opts.filterLines, "fl", "", "Filter by amount of lines in response. Comma separated list of line counts and ranges")
74 | flag.StringVar(&conf.Data, "d", "", "POST data")
75 | flag.StringVar(&conf.Data, "data", "", "POST data (alias of -d)")
76 | flag.StringVar(&conf.Data, "data-ascii", "", "POST data (alias of -d)")
77 | flag.StringVar(&conf.Data, "data-binary", "", "POST data (alias of -d)")
78 | flag.BoolVar(&conf.Colors, "c", false, "Colorize output.")
79 | flag.BoolVar(&ignored, "compressed", true, "Dummy flag for copy as curl functionality (ignored)")
80 | flag.Var(&opts.inputcommands, "input-cmd", "Command producing the input. --input-num is required when using this input method. Overrides -w.")
81 | flag.IntVar(&conf.InputNum, "input-num", 100, "Number of inputs to test. Used in conjunction with --input-cmd.")
82 | flag.StringVar(&conf.InputMode, "mode", "clusterbomb", "Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork")
83 | flag.BoolVar(&ignored, "i", true, "Dummy flag for copy as curl functionality (ignored)")
84 | flag.Var(&opts.cookies, "b", "Cookie data `\"NAME1=VALUE1; NAME2=VALUE2\"` for copy as curl functionality.\nResults unpredictable when combined with -H \"Cookie: ...\"")
85 | flag.Var(&opts.cookies, "cookie", "Cookie data (alias of -b)")
86 | 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.")
87 | flag.StringVar(&opts.matcherSize, "ms", "", "Match HTTP response size")
88 | flag.StringVar(&opts.matcherRegexp, "mr", "", "Match regexp")
89 | flag.StringVar(&opts.matcherWords, "mw", "", "Match amount of words in response")
90 | flag.StringVar(&opts.matcherLines, "ml", "", "Match amount of lines in response")
91 | flag.StringVar(&opts.proxyURL, "x", "", "HTTP Proxy URL")
92 | flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use")
93 | flag.StringVar(&conf.OutputFile, "o", "", "Write output to file")
94 | flag.StringVar(&opts.outputFormat, "of", "json", "Output file format. Available formats: json, ejson, html, md, csv, ecsv")
95 | flag.StringVar(&conf.OutputDirectory, "od", "", "Directory path to store matched results to.")
96 | flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)")
97 | flag.BoolVar(&conf.StopOn403, "sf", false, "Stop when > 95% of responses return 403 Forbidden")
98 | flag.BoolVar(&conf.StopOnErrors, "se", false, "Stop on spurious errors")
99 | flag.BoolVar(&conf.StopOnAll, "sa", false, "Stop on all error cases. Implies -sf and -se. Also stops on spurious 429 response codes.")
100 | flag.BoolVar(&conf.FollowRedirects, "r", false, "Follow redirects")
101 | flag.BoolVar(&conf.Recursion, "recursion", false, "Scan recursively. Only FUZZ keyword is supported, and URL (-u) has to end in it.")
102 | flag.IntVar(&conf.RecursionDepth, "recursion-depth", 0, "Maximum recursion depth.")
103 | flag.BoolVar(&conf.AutoCalibration, "ac", false, "Automatically calibrate filtering options")
104 | flag.Var(&opts.AutoCalibrationStrings, "acc", "Custom auto-calibration string. Can be used multiple times. Implies -ac")
105 | flag.IntVar(&conf.Threads, "t", 40, "Number of concurrent threads.")
106 | flag.IntVar(&conf.Timeout, "timeout", 10, "HTTP request timeout in seconds.")
107 | flag.IntVar(&conf.MaxTime, "maxtime", 0, "Maximum running time in seconds.")
108 | flag.BoolVar(&conf.Verbose, "v", false, "Verbose output, printing full URL and redirect location (if any) with the results.")
109 | flag.BoolVar(&opts.showVersion, "V", false, "Show version information.")
110 | flag.StringVar(&opts.debugLog, "debug-log", "", "Write all of the internal logging to the specified file.")
111 | flag.Parse()
112 | if opts.showVersion {
113 | fmt.Printf("ffuf version: %s\n", ffuf.VERSION)
114 | os.Exit(0)
115 | }
116 | if len(opts.debugLog) != 0 {
117 | f, err := os.OpenFile(opts.debugLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
118 | if err != nil {
119 | fmt.Fprintf(os.Stderr, "Disabling logging, encountered error(s): %s\n", err)
120 | log.SetOutput(ioutil.Discard)
121 | } else {
122 | log.SetOutput(f)
123 | defer f.Close()
124 | }
125 | } else {
126 | log.SetOutput(ioutil.Discard)
127 | }
128 | if err := prepareConfig(&opts, &conf); err != nil {
129 | fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
130 | flag.Usage()
131 | os.Exit(1)
132 | }
133 | job, err := prepareJob(&conf)
134 | if err != nil {
135 | fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
136 | flag.Usage()
137 | os.Exit(1)
138 | }
139 | if err := prepareFilters(&opts, &conf); err != nil {
140 | fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
141 | flag.Usage()
142 | os.Exit(1)
143 | }
144 |
145 | if err := filter.CalibrateIfNeeded(job); err != nil {
146 | fmt.Fprintf(os.Stderr, "Error in autocalibration, exiting: %s\n", err)
147 | os.Exit(1)
148 | }
149 |
150 | // Job handles waiting for goroutines to complete itself
151 | job.Start()
152 | }
153 |
154 | func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) {
155 | errs := ffuf.NewMultierror()
156 | var err error
157 | inputprovider, err := input.NewInputProvider(conf)
158 | if err != nil {
159 | errs.Add(err)
160 | }
161 | // TODO: implement error handling for runnerprovider and outputprovider
162 | // We only have http runner right now
163 | runprovider := runner.NewRunnerByName("http", conf)
164 | // Initialize the correct inputprovider
165 | for _, v := range conf.InputProviders {
166 | err = inputprovider.AddProvider(v)
167 | if err != nil {
168 | errs.Add(err)
169 | }
170 | }
171 | // We only have stdout outputprovider right now
172 | outprovider := output.NewOutputProviderByName("stdout", conf)
173 | return &ffuf.Job{
174 | Config: conf,
175 | Runner: runprovider,
176 | Output: outprovider,
177 | Input: inputprovider,
178 | }, errs.ErrorOrNil()
179 | }
180 |
181 | func prepareFilters(parseOpts *cliOptions, conf *ffuf.Config) error {
182 | errs := ffuf.NewMultierror()
183 | if parseOpts.filterStatus != "" {
184 | if err := filter.AddFilter(conf, "status", parseOpts.filterStatus); err != nil {
185 | errs.Add(err)
186 | }
187 | }
188 | if parseOpts.filterSize != "" {
189 | if err := filter.AddFilter(conf, "size", parseOpts.filterSize); err != nil {
190 | errs.Add(err)
191 | }
192 | }
193 | if parseOpts.filterRegexp != "" {
194 | if err := filter.AddFilter(conf, "regexp", parseOpts.filterRegexp); err != nil {
195 | errs.Add(err)
196 | }
197 | }
198 | if parseOpts.filterWords != "" {
199 | if err := filter.AddFilter(conf, "word", parseOpts.filterWords); err != nil {
200 | errs.Add(err)
201 | }
202 | }
203 | if parseOpts.filterLines != "" {
204 | if err := filter.AddFilter(conf, "line", parseOpts.filterLines); err != nil {
205 | errs.Add(err)
206 | }
207 | }
208 | if parseOpts.matcherStatus != "" {
209 | if err := filter.AddMatcher(conf, "status", parseOpts.matcherStatus); err != nil {
210 | errs.Add(err)
211 | }
212 | }
213 | if parseOpts.matcherSize != "" {
214 | if err := filter.AddMatcher(conf, "size", parseOpts.matcherSize); err != nil {
215 | errs.Add(err)
216 | }
217 | }
218 | if parseOpts.matcherRegexp != "" {
219 | if err := filter.AddMatcher(conf, "regexp", parseOpts.matcherRegexp); err != nil {
220 | errs.Add(err)
221 | }
222 | }
223 | if parseOpts.matcherWords != "" {
224 | if err := filter.AddMatcher(conf, "word", parseOpts.matcherWords); err != nil {
225 | errs.Add(err)
226 | }
227 | }
228 | if parseOpts.matcherLines != "" {
229 | if err := filter.AddMatcher(conf, "line", parseOpts.matcherLines); err != nil {
230 | errs.Add(err)
231 | }
232 | }
233 | return errs.ErrorOrNil()
234 | }
235 |
236 | func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
237 | //TODO: refactor in a proper flag library that can handle things like required flags
238 | errs := ffuf.NewMultierror()
239 |
240 | var err error
241 | var err2 error
242 | if len(conf.Url) == 0 {
243 | errs.Add(fmt.Errorf("-u flag is required"))
244 | }
245 | // prepare extensions
246 | if parseOpts.extensions != "" {
247 | extensions := strings.Split(parseOpts.extensions, ",")
248 | conf.Extensions = extensions
249 | }
250 |
251 | // Convert cookies to a header
252 | if len(parseOpts.cookies) > 0 {
253 | parseOpts.headers.Set("Cookie: " + strings.Join(parseOpts.cookies, "; "))
254 | }
255 |
256 | //Prepare inputproviders
257 | for _, v := range parseOpts.wordlists {
258 | wl := strings.SplitN(v, ":", 2)
259 | if len(wl) == 2 {
260 | conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
261 | Name: "wordlist",
262 | Value: wl[0],
263 | Keyword: wl[1],
264 | })
265 | } else {
266 | conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
267 | Name: "wordlist",
268 | Value: wl[0],
269 | Keyword: "FUZZ",
270 | })
271 | }
272 | }
273 | for _, v := range parseOpts.inputcommands {
274 | ic := strings.SplitN(v, ":", 2)
275 | if len(ic) == 2 {
276 | conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
277 | Name: "command",
278 | Value: ic[0],
279 | Keyword: ic[1],
280 | })
281 | conf.CommandKeywords = append(conf.CommandKeywords, ic[0])
282 | } else {
283 | conf.InputProviders = append(conf.InputProviders, ffuf.InputProviderConfig{
284 | Name: "command",
285 | Value: ic[0],
286 | Keyword: "FUZZ",
287 | })
288 | conf.CommandKeywords = append(conf.CommandKeywords, "FUZZ")
289 | }
290 | }
291 |
292 | if len(conf.InputProviders) == 0 {
293 | errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required"))
294 | }
295 |
296 | //Prepare headers
297 | for _, v := range parseOpts.headers {
298 | hs := strings.SplitN(v, ":", 2)
299 | if len(hs) == 2 {
300 | conf.Headers[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
301 | } else {
302 | errs.Add(fmt.Errorf("Header defined by -H needs to have a value. \":\" should be used as a separator"))
303 | }
304 | }
305 | //Prepare delay
306 | d := strings.Split(parseOpts.delay, "-")
307 | if len(d) > 2 {
308 | errs.Add(fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\""))
309 | } else if len(d) == 2 {
310 | conf.Delay.IsRange = true
311 | conf.Delay.HasDelay = true
312 | conf.Delay.Min, err = strconv.ParseFloat(d[0], 64)
313 | conf.Delay.Max, err2 = strconv.ParseFloat(d[1], 64)
314 | if err != nil || err2 != nil {
315 | errs.Add(fmt.Errorf("Delay range min and max values need to be valid floats. For example: 0.1-0.5"))
316 | }
317 | } else if len(parseOpts.delay) > 0 {
318 | conf.Delay.IsRange = false
319 | conf.Delay.HasDelay = true
320 | conf.Delay.Min, err = strconv.ParseFloat(parseOpts.delay, 64)
321 | if err != nil {
322 | errs.Add(fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\""))
323 | }
324 | }
325 |
326 | // Verify proxy url format
327 | if len(parseOpts.proxyURL) > 0 {
328 | _, err := url.Parse(parseOpts.proxyURL)
329 | if err != nil {
330 | errs.Add(fmt.Errorf("Bad proxy url (-x) format: %s", err))
331 | } else {
332 | conf.ProxyURL = parseOpts.proxyURL
333 | }
334 | }
335 |
336 | //Check the output file format option
337 | if conf.OutputFile != "" {
338 | //No need to check / error out if output file isn't defined
339 | outputFormats := []string{"json", "ejson", "html", "md", "csv", "ecsv"}
340 | found := false
341 | for _, f := range outputFormats {
342 | if f == parseOpts.outputFormat {
343 | conf.OutputFormat = f
344 | found = true
345 | }
346 | }
347 | if !found {
348 | errs.Add(fmt.Errorf("Unknown output file format (-of): %s", parseOpts.outputFormat))
349 | }
350 | }
351 |
352 | // Auto-calibration strings
353 | if len(parseOpts.AutoCalibrationStrings) > 0 {
354 | conf.AutoCalibrationStrings = parseOpts.AutoCalibrationStrings
355 | }
356 | // Using -acc implies -ac
357 | if len(conf.AutoCalibrationStrings) > 0 {
358 | conf.AutoCalibration = true
359 | }
360 |
361 | // Handle copy as curl situation where POST method is implied by --data flag. If method is set to anything but GET, NOOP
362 | if conf.Method == "GET" {
363 | if len(conf.Data) > 0 {
364 | conf.Method = "POST"
365 | }
366 | }
367 |
368 | conf.CommandLine = strings.Join(os.Args, " ")
369 |
370 | for _, provider := range conf.InputProviders {
371 | if !keywordPresent(provider.Keyword, conf) {
372 | errmsg := fmt.Sprintf("Keyword %s defined, but not found in headers, method, URL or POST data.", provider.Keyword)
373 | errs.Add(fmt.Errorf(errmsg))
374 | }
375 | }
376 |
377 | // Do checks for recursion mode
378 | if conf.Recursion {
379 | if !strings.HasSuffix(conf.Url, "FUZZ") {
380 | errmsg := fmt.Sprintf("When using -recursion the URL (-u) must end with FUZZ keyword.")
381 | errs.Add(fmt.Errorf(errmsg))
382 | }
383 | }
384 |
385 | return errs.ErrorOrNil()
386 | }
387 |
388 | func keywordPresent(keyword string, conf *ffuf.Config) bool {
389 | //Search for keyword from HTTP method, URL and POST data too
390 | if strings.Index(conf.Method, keyword) != -1 {
391 | return true
392 | }
393 | if strings.Index(conf.Url, keyword) != -1 {
394 | return true
395 | }
396 | if strings.Index(conf.Data, keyword) != -1 {
397 | return true
398 | }
399 | for k, v := range conf.Headers {
400 | if strings.Index(k, keyword) != -1 {
401 | return true
402 | }
403 | if strings.Index(v, keyword) != -1 {
404 | return true
405 | }
406 | }
407 | return false
408 | }
409 |
--------------------------------------------------------------------------------