Added support for using ranges in size, word count or status code matching/filtering (#47)

* allow ranges on response size matching/filtering

* allow ranges on word count matching/filtering

* allow ranges on http status matching/filtering

* documentation update about using ranges in size, word count and status code filtering/matching

* moved valuerange code to ffuf main package
This commit is contained in:
Tapio Vuorinen 2019-06-27 15:26:20 +00:00 committed by Joona Hoikkala
parent cb37501616
commit 08c4cb4f6f
9 changed files with 110 additions and 48 deletions

View file

@ -122,13 +122,13 @@ To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-
-e string -e string
Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once. Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once.
-fc string -fc string
Filter HTTP status codes from response Filter HTTP status codes from response. Comma separated list of codes and ranges
-fr string -fr string
Filter regexp Filter regexp
-fs string -fs string
Filter HTTP response size Filter HTTP response size. Comma separated list of sizes and ranges
-fw string -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 -input-cmd string
Command producing the input. --input-num is required when using this input method. Overrides -w. Command producing the input. --input-num is required when using this input method. Overrides -w.
-input-num int -input-num int
@ -185,6 +185,7 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l
- Changed - Changed
- New CLI flag: -i, dummy flag that does nothing. for compatibility with copy as curl. - 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. - 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 - v0.10
- New - New

View file

@ -59,10 +59,10 @@ func main() {
flag.StringVar(&conf.Wordlist, "w", "", "Wordlist file path or - to read from standard input") flag.StringVar(&conf.Wordlist, "w", "", "Wordlist file path or - to read from standard input")
flag.BoolVar(&conf.TLSVerify, "k", false, "TLS identity verification") flag.BoolVar(&conf.TLSVerify, "k", false, "TLS identity verification")
flag.StringVar(&opts.delay, "p", "", "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"") flag.StringVar(&opts.delay, "p", "", "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"")
flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response") flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response. Comma separated list of codes and ranges")
flag.StringVar(&opts.filterSize, "fs", "", "Filter HTTP response size") 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.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, "d", "", "POST data")
flag.StringVar(&conf.Data, "data", "", "POST data (alias of -d)") flag.StringVar(&conf.Data, "data", "", "POST data (alias of -d)")
flag.StringVar(&conf.Data, "data-ascii", "", "POST data (alias of -d)") flag.StringVar(&conf.Data, "data-ascii", "", "POST data (alias of -d)")

38
pkg/ffuf/valuerange.go Normal file
View file

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

View file

@ -9,24 +9,25 @@ import (
) )
type SizeFilter struct { type SizeFilter struct {
Value []int64 Value []ffuf.ValueRange
} }
func NewSizeFilter(value string) (ffuf.FilterProvider, error) { func NewSizeFilter(value string) (ffuf.FilterProvider, error) {
var intvals []int64 var intranges []ffuf.ValueRange
for _, sv := range strings.Split(value, ",") { for _, sv := range strings.Split(value, ",") {
intval, err := strconv.ParseInt(sv, 10, 0) vr, err := ffuf.ValueRangeFromString(sv)
if err != nil { 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) { func (f *SizeFilter) Filter(response *ffuf.Response) (bool, error) {
for _, iv := range f.Value { for _, iv := range f.Value {
if iv == response.ContentLength { if iv.Min <= response.ContentLength && response.ContentLength <= iv.Max {
return true, nil return true, nil
} }
} }
@ -36,7 +37,11 @@ func (f *SizeFilter) Filter(response *ffuf.Response) (bool, error) {
func (f *SizeFilter) Repr() string { func (f *SizeFilter) Repr() string {
var strval []string var strval []string
for _, iv := range f.Value { 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, ",")) return fmt.Sprintf("Response size: %s", strings.Join(strval, ","))
} }

View file

@ -8,10 +8,10 @@ import (
) )
func TestNewSizeFilter(t *testing.T) { func TestNewSizeFilter(t *testing.T) {
f, _ := NewSizeFilter("1,2,3,444") f, _ := NewSizeFilter("1,2,3,444,5-90")
sizeRepr := f.Repr() sizeRepr := f.Repr()
if strings.Index(sizeRepr, "1,2,3,444") == -1 { if strings.Index(sizeRepr, "1,2,3,444,5-90") == -1 {
t.Errorf("Size filter was expected to have 4 values") t.Errorf("Size filter was expected to have 5 values")
} }
} }
@ -23,7 +23,7 @@ func TestNewSizeFilterError(t *testing.T) {
} }
func TestFiltering(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 { for i, test := range []struct {
input int64 input int64
output bool output bool
@ -32,6 +32,10 @@ func TestFiltering(t *testing.T) {
{2, true}, {2, true},
{3, true}, {3, true},
{4, false}, {4, false},
{5, true},
{70, true},
{90, true},
{91, false},
{444, true}, {444, true},
} { } {
resp := ffuf.Response{ContentLength: test.input} resp := ffuf.Response{ContentLength: test.input}

View file

@ -8,33 +8,35 @@ import (
"github.com/ffuf/ffuf/pkg/ffuf" "github.com/ffuf/ffuf/pkg/ffuf"
) )
const AllStatuses = 0
type StatusFilter struct { type StatusFilter struct {
Value []int64 Value []ffuf.ValueRange
} }
func NewStatusFilter(value string) (ffuf.FilterProvider, error) { func NewStatusFilter(value string) (ffuf.FilterProvider, error) {
var intvals []int64 var intranges []ffuf.ValueRange
for _, sv := range strings.Split(value, ",") { for _, sv := range strings.Split(value, ",") {
if sv == "all" { if sv == "all" {
intvals = append(intvals, 0) intranges = append(intranges, ffuf.ValueRange{AllStatuses, AllStatuses})
} else { } else {
intval, err := strconv.ParseInt(sv, 10, 0) vr, err := ffuf.ValueRangeFromString(sv)
if err != nil { 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) { func (f *StatusFilter) Filter(response *ffuf.Response) (bool, error) {
for _, iv := range f.Value { for _, iv := range f.Value {
if iv == 0 { if iv.Min == AllStatuses && iv.Max == AllStatuses {
// Handle the "all" case // Handle the "all" case
return true, nil return true, nil
} }
if iv == response.StatusCode { if iv.Min <= response.StatusCode && response.StatusCode <= iv.Max {
return true, nil return true, nil
} }
} }
@ -44,10 +46,12 @@ func (f *StatusFilter) Filter(response *ffuf.Response) (bool, error) {
func (f *StatusFilter) Repr() string { func (f *StatusFilter) Repr() string {
var strval []string var strval []string
for _, iv := range f.Value { for _, iv := range f.Value {
if iv == 0 { if iv.Min == AllStatuses && iv.Max == AllStatuses {
strval = append(strval, "all") strval = append(strval, "all")
} else if iv.Min == iv.Max {
strval = append(strval, strconv.Itoa(int(iv.Min)))
} else { } 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, ",")) return fmt.Sprintf("Response status: %s", strings.Join(strval, ","))

View file

@ -8,10 +8,10 @@ import (
) )
func TestNewStatusFilter(t *testing.T) { func TestNewStatusFilter(t *testing.T) {
f, _ := NewStatusFilter("200,301,500") f, _ := NewStatusFilter("200,301,400-410,500")
statusRepr := f.Repr() statusRepr := f.Repr()
if strings.Index(statusRepr, "200,301,500") == -1 { if strings.Index(statusRepr, "200,301,400-410,500") == -1 {
t.Errorf("Status filter was expected to have 3 values") t.Errorf("Status filter was expected to have 4 values")
} }
} }
@ -23,7 +23,7 @@ func TestNewStatusFilterError(t *testing.T) {
} }
func TestStatusFiltering(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 { for i, test := range []struct {
input int64 input int64
output bool output bool
@ -32,9 +32,12 @@ func TestStatusFiltering(t *testing.T) {
{301, true}, {301, true},
{500, true}, {500, true},
{4, false}, {4, false},
{444, false}, {399, false},
{400, true},
{444, true},
{498, true},
{499, false},
{302, false}, {302, false},
{401, false},
} { } {
resp := ffuf.Response{StatusCode: test.input} resp := ffuf.Response{StatusCode: test.input}
filterReturn, _ := f.Filter(&resp) filterReturn, _ := f.Filter(&resp)

View file

@ -9,25 +9,25 @@ import (
) )
type WordFilter struct { type WordFilter struct {
Value []int64 Value []ffuf.ValueRange
} }
func NewWordFilter(value string) (ffuf.FilterProvider, error) { func NewWordFilter(value string) (ffuf.FilterProvider, error) {
var intvals []int64 var intranges []ffuf.ValueRange
for _, sv := range strings.Split(value, ",") { for _, sv := range strings.Split(value, ",") {
intval, err := strconv.ParseInt(sv, 10, 0) vr, err := ffuf.ValueRangeFromString(sv)
if err != nil { 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) { func (f *WordFilter) Filter(response *ffuf.Response) (bool, error) {
wordsSize := len(strings.Split(string(response.Data), " ")) wordsSize := len(strings.Split(string(response.Data), " "))
for _, iv := range f.Value { for _, iv := range f.Value {
if iv == int64(wordsSize) { if iv.Min <= int64(wordsSize) && int64(wordsSize) <= iv.Max {
return true, nil return true, nil
} }
} }
@ -37,7 +37,11 @@ func (f *WordFilter) Filter(response *ffuf.Response) (bool, error) {
func (f *WordFilter) Repr() string { func (f *WordFilter) Repr() string {
var strval []string var strval []string
for _, iv := range f.Value { 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, ",")) return fmt.Sprintf("Response words: %s", strings.Join(strval, ","))
} }

View file

@ -8,10 +8,10 @@ import (
) )
func TestNewWordFilter(t *testing.T) { func TestNewWordFilter(t *testing.T) {
f, _ := NewWordFilter("200,301,500") f, _ := NewWordFilter("200,301,400-410,500")
wordsRepr := f.Repr() wordsRepr := f.Repr()
if strings.Index(wordsRepr, "200,301,500") == -1 { if strings.Index(wordsRepr, "200,301,400-410,500") == -1 {
t.Errorf("Word filter was expected to have 3 values") t.Errorf("Word filter was expected to have 4 values")
} }
} }
@ -23,7 +23,7 @@ func TestNewWordFilterError(t *testing.T) {
} }
func TestWordFiltering(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 { for i, test := range []struct {
input int64 input int64
output bool output bool
@ -32,9 +32,12 @@ func TestWordFiltering(t *testing.T) {
{301, true}, {301, true},
{500, true}, {500, true},
{4, false}, {4, false},
{444, false}, {444, true},
{302, false}, {302, false},
{401, false}, {401, false},
{402, true},
{450, true},
{451, false},
} { } {
var data []string var data []string
for i := int64(0); i < test.input; i++ { for i := int64(0); i < test.input; i++ {