Write configuration to output JSON (#135)

* Config to json output, filters and matchers

* optRange marshaling

* Add CHANGELOG entry
This commit is contained in:
Joona Hoikkala 2020-01-07 18:27:43 +02:00 committed by GitHub
parent 1b45085191
commit ac2b447dfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 210 additions and 58 deletions

View File

@ -10,7 +10,7 @@
- Regexp matching and filtering (-mr/-fr) allow using keywords in patterns - Regexp matching and filtering (-mr/-fr) allow using keywords in patterns
- Take 429 responses into account when -sa (stop on all error cases) is used - Take 429 responses into account when -sa (stop on all error cases) is used
- Remove -k flag support, convert to dummy flag #134 - Remove -k flag support, convert to dummy flag #134
- Write configuration to output JSON
- v0.12 - v0.12
- New - New

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http"
"net/url" "net/url"
"os" "os"
"strconv" "strconv"
@ -326,11 +325,11 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
// Verify proxy url format // Verify proxy url format
if len(parseOpts.proxyURL) > 0 { if len(parseOpts.proxyURL) > 0 {
pu, err := url.Parse(parseOpts.proxyURL) _, err := url.Parse(parseOpts.proxyURL)
if err != nil { if err != nil {
errs.Add(fmt.Errorf("Bad proxy url (-x) format: %s", err)) errs.Add(fmt.Errorf("Bad proxy url (-x) format: %s", err))
} else { } else {
conf.ProxyURL = http.ProxyURL(pu) conf.ProxyURL = parseOpts.proxyURL
} }
} }
@ -351,7 +350,9 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
} }
// Auto-calibration strings // Auto-calibration strings
conf.AutoCalibrationStrings = parseOpts.AutoCalibrationStrings if len(parseOpts.AutoCalibrationStrings) > 0 {
conf.AutoCalibrationStrings = parseOpts.AutoCalibrationStrings
}
// Using -acc implies -ac // Using -acc implies -ac
if len(conf.AutoCalibrationStrings) > 0 { if len(conf.AutoCalibrationStrings) > 0 {
conf.AutoCalibration = true conf.AutoCalibration = true

View File

@ -2,60 +2,49 @@ package ffuf
import ( import (
"context" "context"
"net/http"
"net/url"
) )
//optRange stores either a single float, in which case the value is stored in min and IsRange is false,
//or a range of floats, in which case IsRange is true
type optRange struct {
Min float64
Max float64
IsRange bool
HasDelay bool
}
type Config struct { type Config struct {
Headers map[string]string Headers map[string]string `json:"headers"`
Extensions []string Extensions []string `json:"extensions"`
DirSearchCompat bool DirSearchCompat bool `json:"dirsearch_compatibility"`
Method string Method string `json:"method"`
Url string Url string `json:"url"`
Data string Data string `json:"postdata"`
Quiet bool Quiet bool `json:"quiet"`
Colors bool Colors bool `json:"colors"`
InputProviders []InputProviderConfig InputProviders []InputProviderConfig `json:"inputproviders"`
CommandKeywords []string CommandKeywords []string `json:"-"`
InputNum int InputNum int `json:"cmd_inputnum"`
InputMode string InputMode string `json:"inputmode"`
OutputDirectory string OutputDirectory string `json:"outputdirectory"`
OutputFile string OutputFile string `json:"outputfile"`
OutputFormat string OutputFormat string `json:"outputformat"`
StopOn403 bool StopOn403 bool `json:"stop_403"`
StopOnErrors bool StopOnErrors bool `json:"stop_errors"`
StopOnAll bool StopOnAll bool `json:"stop_all"`
FollowRedirects bool FollowRedirects bool `json:"follow_redirects"`
AutoCalibration bool AutoCalibration bool `json:"autocalibration"`
AutoCalibrationStrings []string AutoCalibrationStrings []string `json:"autocalibration_strings"`
Timeout int Timeout int `json:"timeout"`
ProgressFrequency int ProgressFrequency int `json:"-"`
Delay optRange Delay optRange `json:"delay"`
Filters []FilterProvider Filters map[string]FilterProvider `json:"filters"`
Matchers []FilterProvider Matchers map[string]FilterProvider `json:"matchers"`
Threads int Threads int `json:"threads"`
Context context.Context Context context.Context `json:"-"`
ProxyURL func(*http.Request) (*url.URL, error) ProxyURL string `json:"proxyurl"`
CommandLine string CommandLine string `json:"cmdline"`
Verbose bool Verbose bool `json:"verbose"`
MaxTime int MaxTime int `json:"maxtime"`
Recursion bool Recursion bool `json:"recursion"`
RecursionDepth int RecursionDepth int `json:"recursion_depth"`
} }
type InputProviderConfig struct { type InputProviderConfig struct {
Name string Name string `json:"name"`
Keyword string Keyword string `json:"keyword"`
Value string Value string `json:"value"`
} }
func NewConfig(ctx context.Context) Config { func NewConfig(ctx context.Context) Config {
@ -72,10 +61,12 @@ func NewConfig(ctx context.Context) Config {
conf.FollowRedirects = false conf.FollowRedirects = false
conf.InputProviders = make([]InputProviderConfig, 0) conf.InputProviders = make([]InputProviderConfig, 0)
conf.CommandKeywords = make([]string, 0) conf.CommandKeywords = make([]string, 0)
conf.AutoCalibrationStrings = make([]string, 0)
conf.InputNum = 0 conf.InputNum = 0
conf.InputMode = "clusterbomb" conf.InputMode = "clusterbomb"
conf.ProxyURL = http.ProxyFromEnvironment conf.ProxyURL = ""
conf.Filters = make([]FilterProvider, 0) conf.Filters = make(map[string]FilterProvider)
conf.Matchers = make(map[string]FilterProvider)
conf.Delay = optRange{0, 0, false, false} conf.Delay = optRange{0, 0, false, false}
conf.Extensions = make([]string, 0) conf.Extensions = make([]string, 0)
conf.Timeout = 10 conf.Timeout = 10

67
pkg/ffuf/optrange.go Normal file
View File

@ -0,0 +1,67 @@
package ffuf
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
//optRange stores either a single float, in which case the value is stored in min and IsRange is false,
//or a range of floats, in which case IsRange is true
type optRange struct {
Min float64
Max float64
IsRange bool
HasDelay bool
}
type optRangeJSON struct {
Value string `json:"value"`
}
func (o *optRange) MarshalJSON() ([]byte, error) {
value := ""
if o.Min == o.Max {
value = fmt.Sprintf("%.2f", o.Min)
} else {
value = fmt.Sprintf("%.2f-%.2f", o.Min, o.Max)
}
return json.Marshal(&optRangeJSON{
Value: value,
})
}
func (o *optRange) UnmarshalJSON(b []byte) error {
var inc optRangeJSON
err := json.Unmarshal(b, &inc)
if err != nil {
return err
}
return o.Initialize(inc.Value)
}
//Initialize sets up the optRange from string value
func (o *optRange) Initialize(value string) error {
var err, err2 error
d := strings.Split(value, "-")
if len(d) > 2 {
return fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\"")
} else if len(d) == 2 {
o.IsRange = true
o.HasDelay = true
o.Min, err = strconv.ParseFloat(d[0], 64)
o.Max, err2 = strconv.ParseFloat(d[1], 64)
if err != nil || err2 != nil {
return fmt.Errorf("Delay range min and max values need to be valid floats. For example: 0.1-0.5")
}
} else if len(value) > 0 {
o.IsRange = false
o.HasDelay = true
o.Min, err = strconv.ParseFloat(value, 64)
if err != nil {
return fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\"")
}
}
return nil
}

View File

@ -31,7 +31,7 @@ func NewFilterByName(name string, value string) (ffuf.FilterProvider, error) {
func AddFilter(conf *ffuf.Config, name string, option string) error { func AddFilter(conf *ffuf.Config, name string, option string) error {
newf, err := NewFilterByName(name, option) newf, err := NewFilterByName(name, option)
if err == nil { if err == nil {
conf.Filters = append(conf.Filters, newf) conf.Filters[name] = newf
} }
return err return err
} }
@ -40,7 +40,7 @@ func AddFilter(conf *ffuf.Config, name string, option string) error {
func AddMatcher(conf *ffuf.Config, name string, option string) error { func AddMatcher(conf *ffuf.Config, name string, option string) error {
newf, err := NewFilterByName(name, option) newf, err := NewFilterByName(name, option)
if err == nil { if err == nil {
conf.Matchers = append(conf.Matchers, newf) conf.Matchers[name] = newf
} }
return err return err
} }

View File

@ -1,6 +1,7 @@
package filter package filter
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -24,6 +25,22 @@ func NewLineFilter(value string) (ffuf.FilterProvider, error) {
return &LineFilter{Value: intranges}, nil return &LineFilter{Value: intranges}, nil
} }
func (f *LineFilter) MarshalJSON() ([]byte, error) {
value := make([]string, 0)
for _, v := range f.Value {
if v.Min == v.Max {
value = append(value, strconv.FormatInt(v.Min, 10))
} else {
value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
}
}
return json.Marshal(&struct {
Value string `json:"value"`
}{
Value: strings.Join(value, ","),
})
}
func (f *LineFilter) Filter(response *ffuf.Response) (bool, error) { func (f *LineFilter) Filter(response *ffuf.Response) (bool, error) {
linesSize := len(strings.Split(string(response.Data), "\n")) linesSize := len(strings.Split(string(response.Data), "\n"))
for _, iv := range f.Value { for _, iv := range f.Value {

View File

@ -1,6 +1,7 @@
package filter package filter
import ( import (
"encoding/json"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@ -21,6 +22,14 @@ func NewRegexpFilter(value string) (ffuf.FilterProvider, error) {
return &RegexpFilter{Value: re, valueRaw: value}, nil return &RegexpFilter{Value: re, valueRaw: value}, nil
} }
func (f *RegexpFilter) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Value string `json:"value"`
}{
Value: f.valueRaw,
})
}
func (f *RegexpFilter) Filter(response *ffuf.Response) (bool, error) { func (f *RegexpFilter) Filter(response *ffuf.Response) (bool, error) {
matchheaders := "" matchheaders := ""
for k, v := range response.Headers { for k, v := range response.Headers {

View File

@ -1,6 +1,7 @@
package filter package filter
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -25,6 +26,22 @@ func NewSizeFilter(value string) (ffuf.FilterProvider, error) {
return &SizeFilter{Value: intranges}, nil return &SizeFilter{Value: intranges}, nil
} }
func (f *SizeFilter) MarshalJSON() ([]byte, error) {
value := make([]string, 0)
for _, v := range f.Value {
if v.Min == v.Max {
value = append(value, strconv.FormatInt(v.Min, 10))
} else {
value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
}
}
return json.Marshal(&struct {
Value string `json:"value"`
}{
Value: strings.Join(value, ","),
})
}
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.Min <= response.ContentLength && response.ContentLength <= iv.Max { if iv.Min <= response.ContentLength && response.ContentLength <= iv.Max {

View File

@ -1,6 +1,7 @@
package filter package filter
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -30,6 +31,26 @@ func NewStatusFilter(value string) (ffuf.FilterProvider, error) {
return &StatusFilter{Value: intranges}, nil return &StatusFilter{Value: intranges}, nil
} }
func (f *StatusFilter) MarshalJSON() ([]byte, error) {
value := make([]string, 0)
for _, v := range f.Value {
if v.Min == 0 && v.Max == 0 {
value = append(value, "all")
} else {
if v.Min == v.Max {
value = append(value, strconv.FormatInt(v.Min, 10))
} else {
value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
}
}
}
return json.Marshal(&struct {
Value string `json:"value"`
}{
Value: strings.Join(value, ","),
})
}
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.Min == AllStatuses && iv.Max == AllStatuses { if iv.Min == AllStatuses && iv.Max == AllStatuses {

View File

@ -1,6 +1,7 @@
package filter package filter
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -24,6 +25,22 @@ func NewWordFilter(value string) (ffuf.FilterProvider, error) {
return &WordFilter{Value: intranges}, nil return &WordFilter{Value: intranges}, nil
} }
func (f *WordFilter) MarshalJSON() ([]byte, error) {
value := make([]string, 0)
for _, v := range f.Value {
if v.Min == v.Max {
value = append(value, strconv.FormatInt(v.Min, 10))
} else {
value = append(value, fmt.Sprintf("%d-%d", v.Min, v.Max))
}
}
return json.Marshal(&struct {
Value string `json:"value"`
}{
Value: strings.Join(value, ","),
})
}
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 {

View File

@ -30,6 +30,7 @@ type jsonFileOutput struct {
CommandLine string `json:"commandline"` CommandLine string `json:"commandline"`
Time string `json:"time"` Time string `json:"time"`
Results []JsonResult `json:"results"` Results []JsonResult `json:"results"`
Config *ffuf.Config `json:"config"`
} }
func writeEJSON(config *ffuf.Config, res []Result) error { func writeEJSON(config *ffuf.Config, res []Result) error {
@ -75,6 +76,7 @@ func writeJSON(config *ffuf.Config, res []Result) error {
CommandLine: config.CommandLine, CommandLine: config.CommandLine,
Time: t.Format(time.RFC3339), Time: t.Format(time.RFC3339),
Results: jsonRes, Results: jsonRes,
Config: config,
} }
outBytes, err := json.Marshal(outJSON) outBytes, err := json.Marshal(outJSON)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -24,12 +25,21 @@ type SimpleRunner struct {
func NewSimpleRunner(conf *ffuf.Config) ffuf.RunnerProvider { func NewSimpleRunner(conf *ffuf.Config) ffuf.RunnerProvider {
var simplerunner SimpleRunner var simplerunner SimpleRunner
proxyURL := http.ProxyFromEnvironment
if len(conf.ProxyURL) > 0 {
pu, err := url.Parse(conf.ProxyURL)
if err == nil {
proxyURL = http.ProxyURL(pu)
}
}
simplerunner.config = conf simplerunner.config = conf
simplerunner.client = &http.Client{ simplerunner.client = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
Timeout: time.Duration(time.Duration(conf.Timeout) * time.Second), Timeout: time.Duration(time.Duration(conf.Timeout) * time.Second),
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: conf.ProxyURL, Proxy: proxyURL,
MaxIdleConns: 1000, MaxIdleConns: 1000,
MaxIdleConnsPerHost: 500, MaxIdleConnsPerHost: 500,
MaxConnsPerHost: 500, MaxConnsPerHost: 500,