diff --git a/README.md b/README.md index 1a1024bd..54af086b 100644 --- a/README.md +++ b/README.md @@ -122,13 +122,13 @@ To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`- -e string Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. -fc string - Filter HTTP status codes from response + Filter HTTP status codes from response. Comma separated list of codes and ranges -fr string Filter regexp -fs string - Filter HTTP response size + Filter HTTP response size. Comma separated list of sizes and ranges -fw string - Filter by amount of words in response + Filter by amount of words in response. Comma separated list of word counts and ranges -input-cmd string Command producing the input. --input-num is required when using this input method. Overrides -w. -input-num int @@ -185,6 +185,7 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l - Changed - New CLI flag: -i, dummy flag that does nothing. for compatibility with copy as curl. - New CLI flag: -b/--cookie, cookie data for compatibility with copy as curl. + - Filtering and matching by status code, response size or word count now allow using ranges in addition to single values - v0.10 - New diff --git a/main.go b/main.go index 226ca1ec..b76c273a 100644 --- a/main.go +++ b/main.go @@ -59,10 +59,10 @@ func main() { flag.StringVar(&conf.Wordlist, "w", "", "Wordlist file path or - to read from standard input") flag.BoolVar(&conf.TLSVerify, "k", false, "TLS identity verification") flag.StringVar(&opts.delay, "p", "", "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"") - flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response") - flag.StringVar(&opts.filterSize, "fs", "", "Filter HTTP response size") + flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response. Comma separated list of codes and ranges") + flag.StringVar(&opts.filterSize, "fs", "", "Filter HTTP response size. Comma separated list of sizes and ranges") flag.StringVar(&opts.filterRegexp, "fr", "", "Filter regexp") - flag.StringVar(&opts.filterWords, "fw", "", "Filter by amount of words in response") + flag.StringVar(&opts.filterWords, "fw", "", "Filter by amount of words in response. Comma separated list of word counts and ranges") flag.StringVar(&conf.Data, "d", "", "POST data") flag.StringVar(&conf.Data, "data", "", "POST data (alias of -d)") flag.StringVar(&conf.Data, "data-ascii", "", "POST data (alias of -d)") diff --git a/pkg/ffuf/valuerange.go b/pkg/ffuf/valuerange.go new file mode 100644 index 00000000..87b98d5f --- /dev/null +++ b/pkg/ffuf/valuerange.go @@ -0,0 +1,38 @@ +package ffuf + +import ( + "fmt" + "regexp" + "strconv" +) + +type ValueRange struct { + Min, Max int64 +} + +func ValueRangeFromString(instr string) (ValueRange, error) { + // is the value a range + minmax := regexp.MustCompile("^(\\d+)\\-(\\d+)$").FindAllStringSubmatch(instr, -1) + if minmax != nil { + // yes + minval, err := strconv.ParseInt(minmax[0][1], 10, 0) + if err != nil { + return ValueRange{}, fmt.Errorf("Invalid value: %s", minmax[0][1]) + } + maxval, err := strconv.ParseInt(minmax[0][2], 10, 0) + if err != nil { + return ValueRange{}, fmt.Errorf("Invalid value: %s", minmax[0][2]) + } + if minval >= maxval { + return ValueRange{}, fmt.Errorf("Minimum has to be smaller than maximum") + } + return ValueRange{minval, maxval}, nil + } else { + // no, a single value or something else + intval, err := strconv.ParseInt(instr, 10, 0) + if err != nil { + return ValueRange{}, fmt.Errorf("Invalid value: %s", instr) + } + return ValueRange{intval, intval}, nil + } +} diff --git a/pkg/filter/size.go b/pkg/filter/size.go index f64e9ae8..e320778e 100644 --- a/pkg/filter/size.go +++ b/pkg/filter/size.go @@ -9,24 +9,25 @@ import ( ) type SizeFilter struct { - Value []int64 + Value []ffuf.ValueRange } func NewSizeFilter(value string) (ffuf.FilterProvider, error) { - var intvals []int64 + var intranges []ffuf.ValueRange for _, sv := range strings.Split(value, ",") { - intval, err := strconv.ParseInt(sv, 10, 0) + vr, err := ffuf.ValueRangeFromString(sv) if err != nil { - return &SizeFilter{}, fmt.Errorf("Size filter or matcher (-fs / -ms): invalid value: %s", value) + return &SizeFilter{}, fmt.Errorf("Size filter or matcher (-fs / -ms): invalid value: %s", sv) } - intvals = append(intvals, intval) + + intranges = append(intranges, vr) } - return &SizeFilter{Value: intvals}, nil + return &SizeFilter{Value: intranges}, nil } func (f *SizeFilter) Filter(response *ffuf.Response) (bool, error) { for _, iv := range f.Value { - if iv == response.ContentLength { + if iv.Min <= response.ContentLength && response.ContentLength <= iv.Max { return true, nil } } @@ -36,7 +37,11 @@ func (f *SizeFilter) Filter(response *ffuf.Response) (bool, error) { func (f *SizeFilter) Repr() string { var strval []string for _, iv := range f.Value { - strval = append(strval, strconv.Itoa(int(iv))) + if iv.Min == iv.Max { + strval = append(strval, strconv.Itoa(int(iv.Min))) + } else { + strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max))) + } } return fmt.Sprintf("Response size: %s", strings.Join(strval, ",")) } diff --git a/pkg/filter/size_test.go b/pkg/filter/size_test.go index 22efeb14..8c7c88de 100644 --- a/pkg/filter/size_test.go +++ b/pkg/filter/size_test.go @@ -8,10 +8,10 @@ import ( ) func TestNewSizeFilter(t *testing.T) { - f, _ := NewSizeFilter("1,2,3,444") + f, _ := NewSizeFilter("1,2,3,444,5-90") sizeRepr := f.Repr() - if strings.Index(sizeRepr, "1,2,3,444") == -1 { - t.Errorf("Size filter was expected to have 4 values") + if strings.Index(sizeRepr, "1,2,3,444,5-90") == -1 { + t.Errorf("Size filter was expected to have 5 values") } } @@ -23,7 +23,7 @@ func TestNewSizeFilterError(t *testing.T) { } func TestFiltering(t *testing.T) { - f, _ := NewSizeFilter("1,2,3,444") + f, _ := NewSizeFilter("1,2,3,5-90,444") for i, test := range []struct { input int64 output bool @@ -32,6 +32,10 @@ func TestFiltering(t *testing.T) { {2, true}, {3, true}, {4, false}, + {5, true}, + {70, true}, + {90, true}, + {91, false}, {444, true}, } { resp := ffuf.Response{ContentLength: test.input} diff --git a/pkg/filter/status.go b/pkg/filter/status.go index 40161998..a7de52ec 100644 --- a/pkg/filter/status.go +++ b/pkg/filter/status.go @@ -8,33 +8,35 @@ import ( "github.com/ffuf/ffuf/pkg/ffuf" ) +const AllStatuses = 0 + type StatusFilter struct { - Value []int64 + Value []ffuf.ValueRange } func NewStatusFilter(value string) (ffuf.FilterProvider, error) { - var intvals []int64 + var intranges []ffuf.ValueRange for _, sv := range strings.Split(value, ",") { if sv == "all" { - intvals = append(intvals, 0) + intranges = append(intranges, ffuf.ValueRange{AllStatuses, AllStatuses}) } else { - intval, err := strconv.ParseInt(sv, 10, 0) + vr, err := ffuf.ValueRangeFromString(sv) if err != nil { - return &StatusFilter{}, fmt.Errorf("Status filter or matcher (-fc / -mc): invalid value %s", value) + return &StatusFilter{}, fmt.Errorf("Status filter or matcher (-fc / -mc): invalid value %s", sv) } - intvals = append(intvals, intval) + intranges = append(intranges, vr) } } - return &StatusFilter{Value: intvals}, nil + return &StatusFilter{Value: intranges}, nil } func (f *StatusFilter) Filter(response *ffuf.Response) (bool, error) { for _, iv := range f.Value { - if iv == 0 { + if iv.Min == AllStatuses && iv.Max == AllStatuses { // Handle the "all" case return true, nil } - if iv == response.StatusCode { + if iv.Min <= response.StatusCode && response.StatusCode <= iv.Max { return true, nil } } @@ -44,10 +46,12 @@ func (f *StatusFilter) Filter(response *ffuf.Response) (bool, error) { func (f *StatusFilter) Repr() string { var strval []string for _, iv := range f.Value { - if iv == 0 { + if iv.Min == AllStatuses && iv.Max == AllStatuses { strval = append(strval, "all") + } else if iv.Min == iv.Max { + strval = append(strval, strconv.Itoa(int(iv.Min))) } else { - strval = append(strval, strconv.Itoa(int(iv))) + strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max))) } } return fmt.Sprintf("Response status: %s", strings.Join(strval, ",")) diff --git a/pkg/filter/status_test.go b/pkg/filter/status_test.go index 823f4760..64c11d4b 100644 --- a/pkg/filter/status_test.go +++ b/pkg/filter/status_test.go @@ -8,10 +8,10 @@ import ( ) func TestNewStatusFilter(t *testing.T) { - f, _ := NewStatusFilter("200,301,500") + f, _ := NewStatusFilter("200,301,400-410,500") statusRepr := f.Repr() - if strings.Index(statusRepr, "200,301,500") == -1 { - t.Errorf("Status filter was expected to have 3 values") + if strings.Index(statusRepr, "200,301,400-410,500") == -1 { + t.Errorf("Status filter was expected to have 4 values") } } @@ -23,7 +23,7 @@ func TestNewStatusFilterError(t *testing.T) { } func TestStatusFiltering(t *testing.T) { - f, _ := NewStatusFilter("200,301,500") + f, _ := NewStatusFilter("200,301,400-498,500") for i, test := range []struct { input int64 output bool @@ -32,9 +32,12 @@ func TestStatusFiltering(t *testing.T) { {301, true}, {500, true}, {4, false}, - {444, false}, + {399, false}, + {400, true}, + {444, true}, + {498, true}, + {499, false}, {302, false}, - {401, false}, } { resp := ffuf.Response{StatusCode: test.input} filterReturn, _ := f.Filter(&resp) diff --git a/pkg/filter/words.go b/pkg/filter/words.go index 160f0d4e..7911b507 100644 --- a/pkg/filter/words.go +++ b/pkg/filter/words.go @@ -9,25 +9,25 @@ import ( ) type WordFilter struct { - Value []int64 + Value []ffuf.ValueRange } func NewWordFilter(value string) (ffuf.FilterProvider, error) { - var intvals []int64 + var intranges []ffuf.ValueRange for _, sv := range strings.Split(value, ",") { - intval, err := strconv.ParseInt(sv, 10, 0) + vr, err := ffuf.ValueRangeFromString(sv) if err != nil { - return &WordFilter{}, fmt.Errorf("Word filter or matcher (-fw / -mw): invalid value: %s", value) + return &WordFilter{}, fmt.Errorf("Word filter or matcher (-fw / -mw): invalid value: %s", sv) } - intvals = append(intvals, intval) + intranges = append(intranges, vr) } - return &WordFilter{Value: intvals}, nil + return &WordFilter{Value: intranges}, nil } func (f *WordFilter) Filter(response *ffuf.Response) (bool, error) { wordsSize := len(strings.Split(string(response.Data), " ")) for _, iv := range f.Value { - if iv == int64(wordsSize) { + if iv.Min <= int64(wordsSize) && int64(wordsSize) <= iv.Max { return true, nil } } @@ -37,7 +37,11 @@ func (f *WordFilter) Filter(response *ffuf.Response) (bool, error) { func (f *WordFilter) Repr() string { var strval []string for _, iv := range f.Value { - strval = append(strval, strconv.Itoa(int(iv))) + if iv.Min == iv.Max { + strval = append(strval, strconv.Itoa(int(iv.Min))) + } else { + strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max))) + } } return fmt.Sprintf("Response words: %s", strings.Join(strval, ",")) } diff --git a/pkg/filter/words_test.go b/pkg/filter/words_test.go index d67e23e2..c447bff3 100644 --- a/pkg/filter/words_test.go +++ b/pkg/filter/words_test.go @@ -8,10 +8,10 @@ import ( ) func TestNewWordFilter(t *testing.T) { - f, _ := NewWordFilter("200,301,500") + f, _ := NewWordFilter("200,301,400-410,500") wordsRepr := f.Repr() - if strings.Index(wordsRepr, "200,301,500") == -1 { - t.Errorf("Word filter was expected to have 3 values") + if strings.Index(wordsRepr, "200,301,400-410,500") == -1 { + t.Errorf("Word filter was expected to have 4 values") } } @@ -23,7 +23,7 @@ func TestNewWordFilterError(t *testing.T) { } func TestWordFiltering(t *testing.T) { - f, _ := NewWordFilter("200,301,500") + f, _ := NewWordFilter("200,301,402-450,500") for i, test := range []struct { input int64 output bool @@ -32,9 +32,12 @@ func TestWordFiltering(t *testing.T) { {301, true}, {500, true}, {4, false}, - {444, false}, + {444, true}, {302, false}, {401, false}, + {402, true}, + {450, true}, + {451, false}, } { var data []string for i := int64(0); i < test.input; i++ {