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

View file

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

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 {
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, ","))
}

View file

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

View file

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

View file

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

View file

@ -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, ","))
}

View file

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