mirror of
https://github.com/ffuf/ffuf
synced 2024-11-10 06:04:17 +00:00
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:
parent
cb37501616
commit
08c4cb4f6f
9 changed files with 110 additions and 48 deletions
|
@ -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
|
||||||
|
|
6
main.go
6
main.go
|
@ -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
38
pkg/ffuf/valuerange.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, ","))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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, ","))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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, ","))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++ {
|
||||||
|
|
Loading…
Reference in a new issue