Skip to content

Commit

Permalink
Added support for using ranges in size, word count or status code mat…
Browse files Browse the repository at this point in the history
…ching/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
  • Loading branch information
delic authored and joohoi committed Jun 27, 2019
1 parent cb37501 commit 08c4cb4
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 48 deletions.
7 changes: 4 additions & 3 deletions README.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions main.go
Expand Up @@ -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)")
Expand Down
38 changes: 38 additions & 0 deletions 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
}
}
21 changes: 13 additions & 8 deletions pkg/filter/size.go
Expand Up @@ -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
}
}
Expand All @@ -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, ","))
}
12 changes: 8 additions & 4 deletions pkg/filter/size_test.go
Expand Up @@ -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")
}
}

Expand All @@ -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
Expand All @@ -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}
Expand Down
26 changes: 15 additions & 11 deletions pkg/filter/status.go
Expand Up @@ -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
}
}
Expand All @@ -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, ","))
Expand Down
15 changes: 9 additions & 6 deletions pkg/filter/status_test.go
Expand Up @@ -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")
}
}

Expand All @@ -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
Expand All @@ -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)
Expand Down
20 changes: 12 additions & 8 deletions pkg/filter/words.go
Expand Up @@ -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
}
}
Expand All @@ -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, ","))
}
13 changes: 8 additions & 5 deletions pkg/filter/words_test.go
Expand Up @@ -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")
}
}

Expand All @@ -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
Expand All @@ -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++ {
Expand Down

0 comments on commit 08c4cb4

Please sign in to comment.