New Upstream Snapshot - ffuf
Ready changes
Summary
Merged new upstream version: 1.5.0+git20221207.1.1a684a9 (was: 1.4.1).
Resulting package
Built on 2023-01-01T11:35 (took 3m22s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-snapshots ffuf
Lintian Result
Diff
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index cf4f8bf..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-github: [joohoi]
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
deleted file mode 100644
index 817cba2..0000000
--- a/.github/pull_request_template.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Description
-
-Please add a short description of pull request contents.
-If this PR addresses an existing issue, please add the issue number below.
-
-Fixes: #(issue number)
-
-## Additonally
-
-- [ ] If this is the first time you are contributing to ffuf, add your name to `CONTRIBUTORS.md`.
-The file should be alphabetically ordered.
-- [ ] Add a short description of the fix to `CHANGELOG.md`
-
-Thanks for contributing to ffuf :)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 3bd58c5..0000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-name: "CodeQL"
-
-on:
- push:
- branches: [master]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [master]
- schedule:
- - cron: '0 9 * * 3'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- # Override automatic language detection by changing the below list
- # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
- language: ['go']
- # Learn more...
- # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
- with:
- # We must fetch at least the immediate parents so that if this is
- # a pull request then we can checkout the head.
- fetch-depth: 2
-
- # If this run was triggered by a pull request event, then checkout
- # the head of the pull request instead of the merge commit.
- - run: git checkout HEAD^2
- if: ${{ github.event_name == 'pull_request' }}
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v1
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v1
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
-
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
deleted file mode 100644
index ab35228..0000000
--- a/.github/workflows/golangci-lint.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: golangci-lint
-on:
- push:
- tags:
- - v*
- branches:
- - master
- pull_request:
-jobs:
- golangci:
- name: lint
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: golangci-lint
- uses: golangci/golangci-lint-action@v2
- with:
- # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
- version: v1.29
-
- # Optional: working directory, useful for monorepos
- # working-directory: somedir
-
- # Optional: golangci-lint command line arguments.
- # args: --issues-exit-code=0
-
- # Optional: show only new issues if it's a pull request. The default value is `false`.
- # only-new-issues: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 2bbc392..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/ffuf
-.idea
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 283c12d..8f9f4ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
- master
- New
- Changed
+ - Fixed issue with autocalibration of line & words filter
+
+- v1.5.0
+ - New
+ - New autocalibration options: `-ach`, `-ack` and `-acs`. Revamped the whole autocalibration process
+ - Configurable modes for matchers and filters (CLI flags: `fmode` and `mmode`): "and" and "or"
+ - Changed
- v1.4.1
- New
diff --git a/debian/changelog b/debian/changelog
index 55a1767..3d04f04 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,10 @@
-ffuf (1.4.1-2) UNRELEASED; urgency=medium
+ffuf (1.5.0+git20221207.1.1a684a9-1) UNRELEASED; urgency=medium
* Trim trailing whitespace.
* Fix day-of-week for changelog entry 1.4.1-1.
+ * New upstream snapshot.
- -- Debian Janitor <janitor@jelmer.uk> Sat, 09 Apr 2022 13:08:32 -0000
+ -- Debian Janitor <janitor@jelmer.uk> Sun, 01 Jan 2023 11:33:36 -0000
ffuf (1.4.1-1) unstable; urgency=medium
diff --git a/debian/patches/10-fix-spelling.patch b/debian/patches/10-fix-spelling.patch
index 0b265a8..a374b8b 100644
--- a/debian/patches/10-fix-spelling.patch
+++ b/debian/patches/10-fix-spelling.patch
@@ -1,10 +1,10 @@
# Author: Pedro Loami Barbosa dos Santos <pedro@loami.eng.br>
# Date: May 11 2020
# Description: Fix spelling on /pkg/ffuf/multierror.go
-Index: ffuf-1.0.2/pkg/ffuf/multierror.go
+Index: ffuf.git/pkg/ffuf/multierror.go
===================================================================
---- ffuf-1.0.2.orig/pkg/ffuf/multierror.go
-+++ ffuf-1.0.2/pkg/ffuf/multierror.go
+--- ffuf.git.orig/pkg/ffuf/multierror.go
++++ ffuf.git/pkg/ffuf/multierror.go
@@ -20,7 +20,7 @@ func (m *Multierror) Add(err error) {
func (m *Multierror) ErrorOrNil() error {
var errString string
diff --git a/ffufrc.example b/ffufrc.example
index 059a8b8..6d6b1ec 100644
--- a/ffufrc.example
+++ b/ffufrc.example
@@ -27,6 +27,9 @@
"randomtest",
"admin"
]
+ autocalibration_strategy = "basic"
+ autocalibration_keyword = "FUZZ"
+ autocalibration_perhost = false
colors = false
delay = ""
maxtime = 0
@@ -66,6 +69,7 @@
outputcreateemptyfile = false
[filter]
+ mode = "or"
lines = ""
regexp = ""
size = ""
@@ -74,6 +78,7 @@
words = ""
[matcher]
+ mode = "or"
lines = ""
regexp = ""
size = ""
diff --git a/help.go b/help.go
index 15af7d5..4d429c4 100644
--- a/help.go
+++ b/help.go
@@ -61,7 +61,7 @@ func Usage() {
Description: "",
Flags: make([]UsageFlag, 0),
Hidden: false,
- ExpectedFlags: []string{"ac", "acc", "c", "config", "json", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "s", "sa", "se", "sf", "t", "v", "V"},
+ ExpectedFlags: []string{"ac", "acc", "ack", "ach", "acs", "c", "config", "json", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "s", "sa", "se", "sf", "t", "v", "V"},
}
u_compat := UsageSection{
Name: "COMPATIBILITY OPTIONS",
@@ -75,14 +75,14 @@ func Usage() {
Description: "Matchers for the response filtering.",
Flags: make([]UsageFlag, 0),
Hidden: false,
- ExpectedFlags: []string{"mc", "ml", "mr", "ms", "mt", "mw"},
+ ExpectedFlags: []string{"mmode", "mc", "ml", "mr", "ms", "mt", "mw"},
}
u_filter := UsageSection{
Name: "FILTER OPTIONS",
Description: "Filters for the response filtering.",
Flags: make([]UsageFlag, 0),
Hidden: false,
- ExpectedFlags: []string{"fc", "fl", "fr", "fs", "ft", "fw"},
+ ExpectedFlags: []string{"fmode", "fc", "fl", "fr", "fs", "ft", "fw"},
}
u_input := UsageSection{
Name: "INPUT OPTIONS",
diff --git a/main.go b/main.go
index 48abe64..f51663f 100644
--- a/main.go
+++ b/main.go
@@ -4,13 +4,13 @@ import (
"context"
"flag"
"fmt"
+ "github.com/ffuf/ffuf/pkg/filter"
"io/ioutil"
"log"
"os"
"strings"
"github.com/ffuf/ffuf/pkg/ffuf"
- "github.com/ffuf/ffuf/pkg/filter"
"github.com/ffuf/ffuf/pkg/input"
"github.com/ffuf/ffuf/pkg/interactive"
"github.com/ffuf/ffuf/pkg/output"
@@ -62,6 +62,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
flag.BoolVar(&ignored, "k", false, "Dummy flag for backwards compatibility")
flag.BoolVar(&opts.Output.OutputSkipEmptyFile, "or", opts.Output.OutputSkipEmptyFile, "Don't create the output file if we don't have results")
flag.BoolVar(&opts.General.AutoCalibration, "ac", opts.General.AutoCalibration, "Automatically calibrate filtering options")
+ flag.BoolVar(&opts.General.AutoCalibrationPerHost, "ach", opts.General.AutoCalibration, "Per host autocalibration")
flag.BoolVar(&opts.General.Colors, "c", opts.General.Colors, "Colorize output.")
flag.BoolVar(&opts.General.Json, "json", opts.General.Json, "JSON output, printing newline-delimited JSON records")
flag.BoolVar(&opts.General.Noninteractive, "noninteractive", opts.General.Noninteractive, "Disable the interactive console functionality")
@@ -84,7 +85,10 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
flag.IntVar(&opts.HTTP.RecursionDepth, "recursion-depth", opts.HTTP.RecursionDepth, "Maximum recursion depth.")
flag.IntVar(&opts.HTTP.Timeout, "timeout", opts.HTTP.Timeout, "HTTP request timeout in seconds.")
flag.IntVar(&opts.Input.InputNum, "input-num", opts.Input.InputNum, "Number of inputs to test. Used in conjunction with --input-cmd.")
+ flag.StringVar(&opts.General.AutoCalibrationKeyword, "ack", opts.General.AutoCalibrationKeyword, "Autocalibration keyword")
+ flag.StringVar(&opts.General.AutoCalibrationStrategy, "acs", opts.General.AutoCalibrationStrategy, "Autocalibration strategy: \"basic\" or \"advanced\"")
flag.StringVar(&opts.General.ConfigFile, "config", "", "Load configuration from a file")
+ flag.StringVar(&opts.Filter.Mode, "fmode", opts.Filter.Mode, "Filter set operator. Either of: and, or")
flag.StringVar(&opts.Filter.Lines, "fl", opts.Filter.Lines, "Filter by amount of lines in response. Comma separated list of line counts and ranges")
flag.StringVar(&opts.Filter.Regexp, "fr", opts.Filter.Regexp, "Filter regexp")
flag.StringVar(&opts.Filter.Size, "fs", opts.Filter.Size, "Filter HTTP response size. Comma separated list of sizes and ranges")
@@ -107,6 +111,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
flag.StringVar(&opts.Input.InputShell, "input-shell", opts.Input.InputShell, "Shell to be used for running command")
flag.StringVar(&opts.Input.Request, "request", opts.Input.Request, "File containing the raw http request")
flag.StringVar(&opts.Input.RequestProto, "request-proto", opts.Input.RequestProto, "Protocol to use along with raw request")
+ flag.StringVar(&opts.Matcher.Mode, "mmode", opts.Matcher.Mode, "Matcher set operator. Either of: and, or")
flag.StringVar(&opts.Matcher.Lines, "ml", opts.Matcher.Lines, "Match amount of lines in response")
flag.StringVar(&opts.Matcher.Regexp, "mr", opts.Matcher.Regexp, "Match regexp")
flag.StringVar(&opts.Matcher.Size, "ms", opts.Matcher.Size, "Match HTTP response size")
@@ -195,17 +200,13 @@ func main() {
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
os.Exit(1)
}
- if err := filter.SetupFilters(opts, conf); err != nil {
+ if err := SetupFilters(opts, conf); err != nil {
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
Usage()
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
os.Exit(1)
}
- if err := filter.CalibrateIfNeeded(job); err != nil {
- fmt.Fprintf(os.Stderr, "Error in autocalibration, exiting: %s\n", err)
- os.Exit(1)
- }
if !conf.Noninteractive {
go func() {
err := interactive.Handle(job)
@@ -233,3 +234,104 @@ func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) {
job.Output = output.NewOutputProviderByName("stdout", conf)
return job, errs.ErrorOrNil()
}
+
+func SetupFilters(parseOpts *ffuf.ConfigOptions, conf *ffuf.Config) error {
+ errs := ffuf.NewMultierror()
+ conf.MatcherManager = filter.NewMatcherManager()
+ // If any other matcher is set, ignore -mc default value
+ matcherSet := false
+ statusSet := false
+ warningIgnoreBody := false
+ flag.Visit(func(f *flag.Flag) {
+ if f.Name == "mc" {
+ statusSet = true
+ }
+ if f.Name == "ms" {
+ matcherSet = true
+ warningIgnoreBody = true
+ }
+ if f.Name == "ml" {
+ matcherSet = true
+ warningIgnoreBody = true
+ }
+ if f.Name == "mr" {
+ matcherSet = true
+ }
+ if f.Name == "mt" {
+ matcherSet = true
+ }
+ if f.Name == "mw" {
+ matcherSet = true
+ warningIgnoreBody = true
+ }
+ })
+ // Only set default matchers if no
+ if statusSet || !matcherSet {
+ if err := conf.MatcherManager.AddMatcher("status", parseOpts.Matcher.Status); err != nil {
+ errs.Add(err)
+ }
+ }
+
+ if parseOpts.Filter.Status != "" {
+ if err := conf.MatcherManager.AddFilter("status", parseOpts.Filter.Status, false); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Filter.Size != "" {
+ warningIgnoreBody = true
+ if err := conf.MatcherManager.AddFilter("size", parseOpts.Filter.Size, false); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Filter.Regexp != "" {
+ if err := conf.MatcherManager.AddFilter("regexp", parseOpts.Filter.Regexp, false); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Filter.Words != "" {
+ warningIgnoreBody = true
+ if err := conf.MatcherManager.AddFilter("word", parseOpts.Filter.Words, false); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Filter.Lines != "" {
+ warningIgnoreBody = true
+ if err := conf.MatcherManager.AddFilter("line", parseOpts.Filter.Lines, false); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Filter.Time != "" {
+ if err := conf.MatcherManager.AddFilter("time", parseOpts.Filter.Time, false); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Matcher.Size != "" {
+ if err := conf.MatcherManager.AddMatcher("size", parseOpts.Matcher.Size); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Matcher.Regexp != "" {
+ if err := conf.MatcherManager.AddMatcher("regexp", parseOpts.Matcher.Regexp); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Matcher.Words != "" {
+ if err := conf.MatcherManager.AddMatcher("word", parseOpts.Matcher.Words); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Matcher.Lines != "" {
+ if err := conf.MatcherManager.AddMatcher("line", parseOpts.Matcher.Lines); err != nil {
+ errs.Add(err)
+ }
+ }
+ if parseOpts.Matcher.Time != "" {
+ if err := conf.MatcherManager.AddFilter("time", parseOpts.Matcher.Time, false); err != nil {
+ errs.Add(err)
+ }
+ }
+ if conf.IgnoreBody && warningIgnoreBody {
+ fmt.Printf("*** Warning: possible undesired combination of -ignore-body and the response options: fl,fs,fw,ml,ms and mw.\n")
+ }
+ return errs.ErrorOrNil()
+}
diff --git a/pkg/ffuf/autocalibration.go b/pkg/ffuf/autocalibration.go
new file mode 100644
index 0000000..e9a5aeb
--- /dev/null
+++ b/pkg/ffuf/autocalibration.go
@@ -0,0 +1,233 @@
+package ffuf
+
+import (
+ "fmt"
+ "log"
+ "math/rand"
+ "strconv"
+ "time"
+)
+
+func (j *Job) autoCalibrationStrings() map[string][]string {
+ rand.Seed(time.Now().UnixNano())
+ cInputs := make(map[string][]string)
+ if len(j.Config.AutoCalibrationStrings) < 1 {
+ cInputs["basic_admin"] = append(cInputs["basic_admin"], "admin"+RandomString(16))
+ cInputs["basic_admin"] = append(cInputs["basic_admin"], "admin"+RandomString(8))
+ cInputs["htaccess"] = append(cInputs["htaccess"], ".htaccess"+RandomString(16))
+ cInputs["htaccess"] = append(cInputs["htaccess"], ".htaccess"+RandomString(8))
+ cInputs["basic_random"] = append(cInputs["basic_random"], RandomString(16))
+ cInputs["basic_random"] = append(cInputs["basic_random"], RandomString(8))
+ if j.Config.AutoCalibrationStrategy == "advanced" {
+ // Add directory tests and .htaccess too
+ cInputs["admin_dir"] = append(cInputs["admin_dir"], "admin"+RandomString(16)+"/")
+ cInputs["admin_dir"] = append(cInputs["admin_dir"], "admin"+RandomString(8)+"/")
+ cInputs["random_dir"] = append(cInputs["random_dir"], RandomString(16)+"/")
+ cInputs["random_dir"] = append(cInputs["random_dir"], RandomString(8)+"/")
+ }
+ } else {
+ cInputs["custom"] = append(cInputs["custom"], j.Config.AutoCalibrationStrings...)
+ }
+ return cInputs
+}
+
+func (j *Job) calibrationRequest(inputs map[string][]byte) (Response, error) {
+ basereq := BaseRequest(j.Config)
+ req, err := j.Runner.Prepare(inputs, &basereq)
+ if err != nil {
+ j.Output.Error(fmt.Sprintf("Encountered an error while preparing autocalibration request: %s\n", err))
+ j.incError()
+ log.Printf("%s", err)
+ return Response{}, err
+ }
+ resp, err := j.Runner.Execute(&req)
+ if err != nil {
+ j.Output.Error(fmt.Sprintf("Encountered an error while executing autocalibration request: %s\n", err))
+ j.incError()
+ log.Printf("%s", err)
+ return Response{}, err
+ }
+ // Only calibrate on responses that would be matched otherwise
+ if j.isMatch(resp) {
+ return resp, nil
+ }
+ return resp, fmt.Errorf("Response wouldn't be matched")
+}
+
+// CalibrateForHost runs autocalibration for a specific host
+func (j *Job) CalibrateForHost(host string, baseinput map[string][]byte) error {
+ if j.Config.MatcherManager.CalibratedForDomain(host) {
+ return nil
+ }
+ if baseinput[j.Config.AutoCalibrationKeyword] == nil {
+ return fmt.Errorf("Autocalibration keyword \"%s\" not found in the request.", j.Config.AutoCalibrationKeyword)
+ }
+ cStrings := j.autoCalibrationStrings()
+ input := make(map[string][]byte)
+ for k, v := range baseinput {
+ input[k] = v
+ }
+ for _, v := range cStrings {
+ responses := make([]Response, 0)
+ for _, cs := range v {
+ input[j.Config.AutoCalibrationKeyword] = []byte(cs)
+ resp, err := j.calibrationRequest(input)
+ if err != nil {
+ continue
+ }
+ responses = append(responses, resp)
+ err = j.calibrateFilters(responses, true)
+ if err != nil {
+ j.Output.Error(fmt.Sprintf("%s", err))
+ }
+ }
+ }
+ j.Config.MatcherManager.SetCalibratedForHost(host, true)
+ return nil
+}
+
+// CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests
+func (j *Job) Calibrate(input map[string][]byte) error {
+ if j.Config.MatcherManager.Calibrated() {
+ return nil
+ }
+ cInputs := j.autoCalibrationStrings()
+
+ for _, v := range cInputs {
+ responses := make([]Response, 0)
+ for _, cs := range v {
+ input[j.Config.AutoCalibrationKeyword] = []byte(cs)
+ resp, err := j.calibrationRequest(input)
+ if err != nil {
+ continue
+ }
+ responses = append(responses, resp)
+ }
+ _ = j.calibrateFilters(responses, false)
+ }
+ j.Config.MatcherManager.SetCalibrated(true)
+ return nil
+}
+
+// CalibrateIfNeeded runs a self-calibration task for filtering options (if needed) by requesting random resources and
+//
+// configuring the filters accordingly
+func (j *Job) CalibrateIfNeeded(host string, input map[string][]byte) error {
+ j.calibMutex.Lock()
+ defer j.calibMutex.Unlock()
+ if !j.Config.AutoCalibration {
+ return nil
+ }
+ if j.Config.AutoCalibrationPerHost {
+ return j.CalibrateForHost(host, input)
+ }
+ return j.Calibrate(input)
+}
+
+func (j *Job) calibrateFilters(responses []Response, perHost bool) error {
+ // Work down from the most specific common denominator
+ if len(responses) > 0 {
+ // Content length
+ baselineSize := responses[0].ContentLength
+ sizeMatch := true
+ for _, r := range responses {
+ if baselineSize != r.ContentLength {
+ sizeMatch = false
+ }
+ }
+ if sizeMatch {
+ if perHost {
+ // Check if already filtered
+ for _, f := range j.Config.MatcherManager.FiltersForDomain(HostURLFromRequest(*responses[0].Request)) {
+ match, _ := f.Filter(&responses[0])
+ if match {
+ // Already filtered
+ return nil
+ }
+ }
+ _ = j.Config.MatcherManager.AddPerDomainFilter(HostURLFromRequest(*responses[0].Request), "size", strconv.FormatInt(baselineSize, 10))
+ return nil
+ } else {
+ // Check if already filtered
+ for _, f := range j.Config.MatcherManager.GetFilters() {
+ match, _ := f.Filter(&responses[0])
+ if match {
+ // Already filtered
+ return nil
+ }
+ }
+ _ = j.Config.MatcherManager.AddFilter("size", strconv.FormatInt(baselineSize, 10), false)
+ return nil
+ }
+ }
+
+ // Content words
+ baselineWords := responses[0].ContentWords
+ wordsMatch := true
+ for _, r := range responses {
+ if baselineWords != r.ContentWords {
+ wordsMatch = false
+ }
+ }
+ if wordsMatch {
+ if perHost {
+ // Check if already filtered
+ for _, f := range j.Config.MatcherManager.FiltersForDomain(HostURLFromRequest(*responses[0].Request)) {
+ match, _ := f.Filter(&responses[0])
+ if match {
+ // Already filtered
+ return nil
+ }
+ }
+ _ = j.Config.MatcherManager.AddPerDomainFilter(HostURLFromRequest(*responses[0].Request), "word", strconv.FormatInt(baselineWords, 10))
+ return nil
+ } else {
+ // Check if already filtered
+ for _, f := range j.Config.MatcherManager.GetFilters() {
+ match, _ := f.Filter(&responses[0])
+ if match {
+ // Already filtered
+ return nil
+ }
+ }
+ _ = j.Config.MatcherManager.AddFilter("word", strconv.FormatInt(baselineWords, 10), false)
+ return nil
+ }
+ }
+
+ // Content lines
+ baselineLines := responses[0].ContentLines
+ linesMatch := true
+ for _, r := range responses {
+ if baselineLines != r.ContentLines {
+ linesMatch = false
+ }
+ }
+ if linesMatch {
+ if perHost {
+ // Check if already filtered
+ for _, f := range j.Config.MatcherManager.FiltersForDomain(HostURLFromRequest(*responses[0].Request)) {
+ match, _ := f.Filter(&responses[0])
+ if match {
+ // Already filtered
+ return nil
+ }
+ }
+ _ = j.Config.MatcherManager.AddPerDomainFilter(HostURLFromRequest(*responses[0].Request), "line", strconv.FormatInt(baselineLines, 10))
+ return nil
+ } else {
+ // Check if already filtered
+ for _, f := range j.Config.MatcherManager.GetFilters() {
+ match, _ := f.Filter(&responses[0])
+ if match {
+ // Already filtered
+ return nil
+ }
+ }
+ _ = j.Config.MatcherManager.AddFilter("line", strconv.FormatInt(baselineLines, 10), false)
+ return nil
+ }
+ }
+ }
+ return fmt.Errorf("No common filtering values found")
+}
diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go
index 48fc86c..e2f21e3 100644
--- a/pkg/ffuf/config.go
+++ b/pkg/ffuf/config.go
@@ -5,54 +5,58 @@ import (
)
type Config struct {
- AutoCalibration bool `json:"autocalibration"`
- AutoCalibrationStrings []string `json:"autocalibration_strings"`
- Cancel context.CancelFunc `json:"-"`
- Colors bool `json:"colors"`
- CommandKeywords []string `json:"-"`
- CommandLine string `json:"cmdline"`
- ConfigFile string `json:"configfile"`
- Context context.Context `json:"-"`
- Data string `json:"postdata"`
- Delay optRange `json:"delay"`
- DirSearchCompat bool `json:"dirsearch_compatibility"`
- Extensions []string `json:"extensions"`
- Filters map[string]FilterProvider `json:"filters"`
- FollowRedirects bool `json:"follow_redirects"`
- Headers map[string]string `json:"headers"`
- IgnoreBody bool `json:"ignorebody"`
- IgnoreWordlistComments bool `json:"ignore_wordlist_comments"`
- InputMode string `json:"inputmode"`
- InputNum int `json:"cmd_inputnum"`
- InputProviders []InputProviderConfig `json:"inputproviders"`
- InputShell string `json:"inputshell"`
- Json bool `json:"json"`
- Matchers map[string]FilterProvider `json:"matchers"`
- MaxTime int `json:"maxtime"`
- MaxTimeJob int `json:"maxtime_job"`
- Method string `json:"method"`
- Noninteractive bool `json:"noninteractive"`
- OutputDirectory string `json:"outputdirectory"`
- OutputFile string `json:"outputfile"`
- OutputFormat string `json:"outputformat"`
- OutputSkipEmptyFile bool `json:"OutputSkipEmptyFile"`
- ProgressFrequency int `json:"-"`
- ProxyURL string `json:"proxyurl"`
- Quiet bool `json:"quiet"`
- Rate int64 `json:"rate"`
- Recursion bool `json:"recursion"`
- RecursionDepth int `json:"recursion_depth"`
- RecursionStrategy string `json:"recursion_strategy"`
- ReplayProxyURL string `json:"replayproxyurl"`
- SNI string `json:"sni"`
- StopOn403 bool `json:"stop_403"`
- StopOnAll bool `json:"stop_all"`
- StopOnErrors bool `json:"stop_errors"`
- Threads int `json:"threads"`
- Timeout int `json:"timeout"`
- Url string `json:"url"`
- Verbose bool `json:"verbose"`
- Http2 bool `json:"http2"`
+ AutoCalibration bool `json:"autocalibration"`
+ AutoCalibrationKeyword string `json:"autocalibration_keyword"`
+ AutoCalibrationPerHost bool `json:"autocalibration_perhost"`
+ AutoCalibrationStrategy string `json:"autocalibration_strategy"`
+ AutoCalibrationStrings []string `json:"autocalibration_strings"`
+ Cancel context.CancelFunc `json:"-"`
+ Colors bool `json:"colors"`
+ CommandKeywords []string `json:"-"`
+ CommandLine string `json:"cmdline"`
+ ConfigFile string `json:"configfile"`
+ Context context.Context `json:"-"`
+ Data string `json:"postdata"`
+ Delay optRange `json:"delay"`
+ DirSearchCompat bool `json:"dirsearch_compatibility"`
+ Extensions []string `json:"extensions"`
+ FilterMode string `json:"fmode"`
+ FollowRedirects bool `json:"follow_redirects"`
+ Headers map[string]string `json:"headers"`
+ IgnoreBody bool `json:"ignorebody"`
+ IgnoreWordlistComments bool `json:"ignore_wordlist_comments"`
+ InputMode string `json:"inputmode"`
+ InputNum int `json:"cmd_inputnum"`
+ InputProviders []InputProviderConfig `json:"inputproviders"`
+ InputShell string `json:"inputshell"`
+ Json bool `json:"json"`
+ MatcherManager MatcherManager `json:"matchers"`
+ MatcherMode string `json:"mmode"`
+ MaxTime int `json:"maxtime"`
+ MaxTimeJob int `json:"maxtime_job"`
+ Method string `json:"method"`
+ Noninteractive bool `json:"noninteractive"`
+ OutputDirectory string `json:"outputdirectory"`
+ OutputFile string `json:"outputfile"`
+ OutputFormat string `json:"outputformat"`
+ OutputSkipEmptyFile bool `json:"OutputSkipEmptyFile"`
+ ProgressFrequency int `json:"-"`
+ ProxyURL string `json:"proxyurl"`
+ Quiet bool `json:"quiet"`
+ Rate int64 `json:"rate"`
+ Recursion bool `json:"recursion"`
+ RecursionDepth int `json:"recursion_depth"`
+ RecursionStrategy string `json:"recursion_strategy"`
+ ReplayProxyURL string `json:"replayproxyurl"`
+ SNI string `json:"sni"`
+ StopOn403 bool `json:"stop_403"`
+ StopOnAll bool `json:"stop_all"`
+ StopOnErrors bool `json:"stop_errors"`
+ Threads int `json:"threads"`
+ Timeout int `json:"timeout"`
+ Url string `json:"url"`
+ Verbose bool `json:"verbose"`
+ Http2 bool `json:"http2"`
}
type InputProviderConfig struct {
@@ -64,6 +68,8 @@ type InputProviderConfig struct {
func NewConfig(ctx context.Context, cancel context.CancelFunc) Config {
var conf Config
+ conf.AutoCalibrationKeyword = "FUZZ"
+ conf.AutoCalibrationStrategy = "basic"
conf.AutoCalibrationStrings = make([]string, 0)
conf.CommandKeywords = make([]string, 0)
conf.Context = ctx
@@ -72,7 +78,7 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config {
conf.Delay = optRange{0, 0, false, false}
conf.DirSearchCompat = false
conf.Extensions = make([]string, 0)
- conf.Filters = make(map[string]FilterProvider)
+ conf.FilterMode = "or"
conf.FollowRedirects = false
conf.Headers = make(map[string]string)
conf.IgnoreWordlistComments = false
@@ -81,7 +87,7 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config {
conf.InputShell = ""
conf.InputProviders = make([]InputProviderConfig, 0)
conf.Json = false
- conf.Matchers = make(map[string]FilterProvider)
+ conf.MatcherMode = "or"
conf.MaxTime = 0
conf.MaxTimeJob = 0
conf.Method = "GET"
diff --git a/pkg/ffuf/interfaces.go b/pkg/ffuf/interfaces.go
index c36021f..6879992 100644
--- a/pkg/ffuf/interfaces.go
+++ b/pkg/ffuf/interfaces.go
@@ -2,6 +2,21 @@ package ffuf
import "time"
+//MatcherManager provides functions for managing matchers and filters
+type MatcherManager interface {
+ SetCalibrated(calibrated bool)
+ SetCalibratedForHost(host string, calibrated bool)
+ AddFilter(name string, option string, replace bool) error
+ AddPerDomainFilter(domain string, name string, option string) error
+ RemoveFilter(name string)
+ AddMatcher(name string, option string) error
+ GetFilters() map[string]FilterProvider
+ GetMatchers() map[string]FilterProvider
+ FiltersForDomain(domain string) map[string]FilterProvider
+ CalibratedForDomain(domain string) bool
+ Calibrated() bool
+}
+
//FilterProvider is a generic interface for both Matchers and Filters
type FilterProvider interface {
Filter(response *Response) (bool, error)
diff --git a/pkg/ffuf/job.go b/pkg/ffuf/job.go
index 539566a..5bea32c 100644
--- a/pkg/ffuf/job.go
+++ b/pkg/ffuf/job.go
@@ -36,6 +36,7 @@ type Job struct {
queuepos int
skipQueue bool
currentDepth int
+ calibMutex sync.Mutex
pauseWg sync.WaitGroup
}
@@ -325,28 +326,53 @@ func (j *Job) updateProgress() {
func (j *Job) isMatch(resp Response) bool {
matched := false
- for _, m := range j.Config.Matchers {
+ var matchers map[string]FilterProvider
+ var filters map[string]FilterProvider
+ if j.Config.AutoCalibrationPerHost {
+ filters = j.Config.MatcherManager.FiltersForDomain(HostURLFromRequest(*resp.Request))
+ } else {
+ filters = j.Config.MatcherManager.GetFilters()
+ }
+ matchers = j.Config.MatcherManager.GetMatchers()
+ for _, m := range matchers {
match, err := m.Filter(&resp)
if err != nil {
continue
}
if match {
matched = true
+ } else if j.Config.MatcherMode == "and" {
+ // we already know this isn't "and" match
+ return false
+
}
}
// The response was not matched, return before running filters
if !matched {
return false
}
- for _, f := range j.Config.Filters {
+ for _, f := range filters {
fv, err := f.Filter(&resp)
if err != nil {
continue
}
if fv {
- return false
+ // return false
+ if j.Config.FilterMode == "or" {
+ // return early, as filter matched
+ return false
+ }
+ } else {
+ if j.Config.FilterMode == "and" {
+ // return early as not all filters matched in "and" mode
+ return true
+ }
}
}
+ if len(filters) > 0 && j.Config.FilterMode == "and" {
+ // we did not return early, so all filters were matched
+ return false
+ }
return true
}
@@ -360,6 +386,7 @@ func (j *Job) runTask(input map[string][]byte, position int, retried bool) {
log.Printf("%s", err)
return
}
+
resp, err := j.Runner.Execute(&req)
if err != nil {
if retried {
@@ -386,6 +413,10 @@ func (j *Job) runTask(input map[string][]byte, position int, retried bool) {
}
}
j.pauseWg.Wait()
+
+ // Handle autocalibration, must be done after the actual request to ensure sane value in req.Host
+ _ = j.CalibrateIfNeeded(HostURLFromRequest(req), input)
+
if j.isMatch(resp) {
// Re-send request through replay-proxy if needed
if j.ReplayRunner != nil {
@@ -444,47 +475,6 @@ func (j *Job) handleDefaultRecursionJob(resp Response) {
}
}
-//CalibrateResponses returns slice of Responses for randomly generated filter autocalibration requests
-func (j *Job) CalibrateResponses() ([]Response, error) {
- basereq := BaseRequest(j.Config)
- cInputs := make([]string, 0)
- rand.Seed(time.Now().UnixNano())
- if len(j.Config.AutoCalibrationStrings) < 1 {
- cInputs = append(cInputs, "admin"+RandomString(16)+"/")
- cInputs = append(cInputs, ".htaccess"+RandomString(16))
- cInputs = append(cInputs, RandomString(16)+"/")
- cInputs = append(cInputs, RandomString(16))
- } else {
- cInputs = append(cInputs, j.Config.AutoCalibrationStrings...)
- }
-
- results := make([]Response, 0)
- for _, input := range cInputs {
- inputs := make(map[string][]byte, len(j.Config.InputProviders))
- for _, v := range j.Config.InputProviders {
- inputs[v.Keyword] = []byte(input)
- }
-
- req, err := j.Runner.Prepare(inputs, &basereq)
- if err != nil {
- j.Output.Error(fmt.Sprintf("Encountered an error while preparing request: %s\n", err))
- j.incError()
- log.Printf("%s", err)
- return results, err
- }
- resp, err := j.Runner.Execute(&req)
- if err != nil {
- return results, err
- }
-
- // Only calibrate on responses that would be matched otherwise
- if j.isMatch(resp) {
- results = append(results, resp)
- }
- }
- return results, nil
-}
-
// CheckStop stops the job if stopping conditions are met
func (j *Job) CheckStop() {
if j.Counter > 50 {
diff --git a/pkg/ffuf/optionsparser.go b/pkg/ffuf/optionsparser.go
index 83f834e..6d2dc98 100644
--- a/pkg/ffuf/optionsparser.go
+++ b/pkg/ffuf/optionsparser.go
@@ -44,23 +44,26 @@ type HTTPOptions struct {
}
type GeneralOptions struct {
- AutoCalibration bool
- AutoCalibrationStrings []string
- Colors bool
- ConfigFile string `toml:"-"`
- Delay string
- Json bool
- MaxTime int
- MaxTimeJob int
- Noninteractive bool
- Quiet bool
- Rate int
- ShowVersion bool `toml:"-"`
- StopOn403 bool
- StopOnAll bool
- StopOnErrors bool
- Threads int
- Verbose bool
+ AutoCalibration bool
+ AutoCalibrationKeyword string
+ AutoCalibrationPerHost bool
+ AutoCalibrationStrategy string
+ AutoCalibrationStrings []string
+ Colors bool
+ ConfigFile string `toml:"-"`
+ Delay string
+ Json bool
+ MaxTime int
+ MaxTimeJob int
+ Noninteractive bool
+ Quiet bool
+ Rate int
+ ShowVersion bool `toml:"-"`
+ StopOn403 bool
+ StopOnAll bool
+ StopOnErrors bool
+ Threads int
+ Verbose bool
}
type InputOptions struct {
@@ -85,6 +88,7 @@ type OutputOptions struct {
}
type FilterOptions struct {
+ Mode string
Lines string
Regexp string
Size string
@@ -94,6 +98,7 @@ type FilterOptions struct {
}
type MatcherOptions struct {
+ Mode string
Lines string
Regexp string
Size string
@@ -105,6 +110,7 @@ type MatcherOptions struct {
//NewConfigOptions returns a newly created ConfigOptions struct with default values
func NewConfigOptions() *ConfigOptions {
c := &ConfigOptions{}
+ c.Filter.Mode = "or"
c.Filter.Lines = ""
c.Filter.Regexp = ""
c.Filter.Size = ""
@@ -112,6 +118,8 @@ func NewConfigOptions() *ConfigOptions {
c.Filter.Time = ""
c.Filter.Words = ""
c.General.AutoCalibration = false
+ c.General.AutoCalibrationKeyword = "FUZZ"
+ c.General.AutoCalibrationStrategy = "basic"
c.General.Colors = false
c.General.Delay = ""
c.General.Json = false
@@ -146,6 +154,7 @@ func NewConfigOptions() *ConfigOptions {
c.Input.InputNum = 100
c.Input.Request = ""
c.Input.RequestProto = "https"
+ c.Matcher.Mode = "or"
c.Matcher.Lines = ""
c.Matcher.Regexp = ""
c.Matcher.Size = ""
@@ -445,6 +454,8 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con
conf.RecursionDepth = parseOpts.HTTP.RecursionDepth
conf.RecursionStrategy = parseOpts.HTTP.RecursionStrategy
conf.AutoCalibration = parseOpts.General.AutoCalibration
+ conf.AutoCalibrationPerHost = parseOpts.General.AutoCalibrationPerHost
+ conf.AutoCalibrationStrategy = parseOpts.General.AutoCalibrationStrategy
conf.Threads = parseOpts.General.Threads
conf.Timeout = parseOpts.HTTP.Timeout
conf.MaxTime = parseOpts.General.MaxTime
@@ -454,6 +465,34 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con
conf.Json = parseOpts.General.Json
conf.Http2 = parseOpts.HTTP.Http2
+ // Check that fmode and mmode have sane values
+ valid_opmodes := []string{"and", "or"}
+ fmode_found := false
+ mmode_found := false
+ for _, v := range valid_opmodes {
+ if v == parseOpts.Filter.Mode {
+ fmode_found = true
+ }
+ if v == parseOpts.Matcher.Mode {
+ mmode_found = true
+ }
+ }
+ if !fmode_found {
+ errmsg := fmt.Sprintf("Unrecognized value for parameter fmode: %s, valid values are: and, or", parseOpts.Filter.Mode)
+ errs.Add(fmt.Errorf(errmsg))
+ }
+ if !mmode_found {
+ errmsg := fmt.Sprintf("Unrecognized value for parameter mmode: %s, valid values are: and, or", parseOpts.Matcher.Mode)
+ errs.Add(fmt.Errorf(errmsg))
+ }
+ conf.FilterMode = parseOpts.Filter.Mode
+ conf.MatcherMode = parseOpts.Matcher.Mode
+
+ if conf.AutoCalibrationPerHost {
+ // AutoCalibrationPerHost implies AutoCalibration
+ conf.AutoCalibration = true
+ }
+
// Handle copy as curl situation where POST method is implied by --data flag. If method is set to anything but GET, NOOP
if len(conf.Data) > 0 &&
conf.Method == "GET" &&
@@ -557,6 +596,7 @@ func parseRawRequest(parseOpts *ConfigOptions, conf *Config) error {
conf.Data = string(b)
// Remove newline (typically added by the editor) at the end of the file
+ //nolint:gosimple // we specifically want to remove just a single newline, not all of them
if strings.HasSuffix(conf.Data, "\r\n") {
conf.Data = conf.Data[:len(conf.Data)-2]
} else if strings.HasSuffix(conf.Data, "\n") {
diff --git a/pkg/ffuf/util.go b/pkg/ffuf/util.go
index 1064de6..183c635 100644
--- a/pkg/ffuf/util.go
+++ b/pkg/ffuf/util.go
@@ -3,6 +3,7 @@ package ffuf
import (
"fmt"
"math/rand"
+ "net/url"
"os"
"strings"
)
@@ -66,6 +67,15 @@ func RequestContainsKeyword(req Request, kw string) bool {
return false
}
+//HostURLFromRequest gets a host + path without the filename or last part of the URL path
+func HostURLFromRequest(req Request) string {
+ u, _ := url.Parse(req.Url)
+ u.Host = req.Host
+ pathparts := strings.Split(u.Path, "/")
+ trimpath := strings.TrimSpace(strings.Join(pathparts[:len(pathparts)-1], "/"))
+ return u.Host + trimpath
+}
+
//Version returns the ffuf version string
func Version() string {
return fmt.Sprintf("%s%s", VERSION, VERSION_APPENDIX)
diff --git a/pkg/ffuf/version.go b/pkg/ffuf/version.go
index b4fd473..eee0c07 100644
--- a/pkg/ffuf/version.go
+++ b/pkg/ffuf/version.go
@@ -2,7 +2,7 @@ package ffuf
var (
//VERSION holds the current version number
- VERSION = "1.4.1"
+ VERSION = "1.5.0"
//VERSION_APPENDIX holds additional version definition
VERSION_APPENDIX = "-dev"
)
diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go
index 17234fd..72e73d6 100644
--- a/pkg/filter/filter.go
+++ b/pkg/filter/filter.go
@@ -1,14 +1,56 @@
package filter
import (
- "flag"
"fmt"
- "strconv"
- "strings"
-
"github.com/ffuf/ffuf/pkg/ffuf"
+ "sync"
)
+// MatcherManager handles both filters and matchers.
+type MatcherManager struct {
+ IsCalibrated bool
+ Mutex sync.Mutex
+ Matchers map[string]ffuf.FilterProvider
+ Filters map[string]ffuf.FilterProvider
+ PerDomainFilters map[string]*PerDomainFilter
+}
+
+type PerDomainFilter struct {
+ IsCalibrated bool
+ Filters map[string]ffuf.FilterProvider
+}
+
+func NewPerDomainFilter(globfilters map[string]ffuf.FilterProvider) *PerDomainFilter {
+ return &PerDomainFilter{IsCalibrated: false, Filters: globfilters}
+}
+
+func (p *PerDomainFilter) SetCalibrated(value bool) {
+ p.IsCalibrated = value
+}
+
+func NewMatcherManager() ffuf.MatcherManager {
+ return &MatcherManager{
+ IsCalibrated: false,
+ Matchers: make(map[string]ffuf.FilterProvider),
+ Filters: make(map[string]ffuf.FilterProvider),
+ PerDomainFilters: make(map[string]*PerDomainFilter),
+ }
+}
+
+func (f *MatcherManager) SetCalibrated(value bool) {
+ f.IsCalibrated = value
+}
+
+func (f *MatcherManager) SetCalibratedForHost(host string, value bool) {
+ if f.PerDomainFilters[host] != nil {
+ f.PerDomainFilters[host].IsCalibrated = value
+ } else {
+ newFilter := NewPerDomainFilter(f.Filters)
+ newFilter.IsCalibrated = true
+ f.PerDomainFilters[host] = newFilter
+ }
+}
+
func NewFilterByName(name string, value string) (ffuf.FilterProvider, error) {
if name == "status" {
return NewStatusFilter(value)
@@ -31,195 +73,102 @@ func NewFilterByName(name string, value string) (ffuf.FilterProvider, error) {
return nil, fmt.Errorf("Could not create filter with name %s", name)
}
-//AddFilter adds a new filter to Config
-func AddFilter(conf *ffuf.Config, name string, option string) error {
+//AddFilter adds a new filter to MatcherManager
+func (f *MatcherManager) AddFilter(name string, option string, replace bool) error {
+ f.Mutex.Lock()
+ defer f.Mutex.Unlock()
+ newf, err := NewFilterByName(name, option)
+ if err == nil {
+ // valid filter create or append
+ if f.Filters[name] == nil || replace {
+ f.Filters[name] = newf
+ } else {
+ newoption := f.Filters[name].Repr() + "," + option
+ newerf, err := NewFilterByName(name, newoption)
+ if err == nil {
+ f.Filters[name] = newerf
+ }
+ }
+ }
+ return err
+}
+
+//AddPerDomainFilter adds a new filter to PerDomainFilter configuration
+func (f *MatcherManager) AddPerDomainFilter(domain string, name string, option string) error {
+ f.Mutex.Lock()
+ defer f.Mutex.Unlock()
+ var pdFilters *PerDomainFilter
+ if filter, ok := f.PerDomainFilters[domain]; ok {
+ pdFilters = filter
+ } else {
+ pdFilters = NewPerDomainFilter(f.Filters)
+ }
newf, err := NewFilterByName(name, option)
if err == nil {
// valid filter create or append
- if conf.Filters[name] == nil {
- conf.Filters[name] = newf
+ if pdFilters.Filters[name] == nil {
+ pdFilters.Filters[name] = newf
} else {
- newoption := conf.Filters[name].Repr() + "," + option
+ newoption := pdFilters.Filters[name].Repr() + "," + option
newerf, err := NewFilterByName(name, newoption)
if err == nil {
- conf.Filters[name] = newerf
+ pdFilters.Filters[name] = newerf
}
}
}
+ f.PerDomainFilters[domain] = pdFilters
return err
}
//RemoveFilter removes a filter of a given type
-func RemoveFilter(conf *ffuf.Config, name string) {
- delete(conf.Filters, name)
+func (f *MatcherManager) RemoveFilter(name string) {
+ f.Mutex.Lock()
+ defer f.Mutex.Unlock()
+ delete(f.Filters, name)
}
//AddMatcher adds a new matcher to Config
-func AddMatcher(conf *ffuf.Config, name string, option string) error {
+func (f *MatcherManager) AddMatcher(name string, option string) error {
+ f.Mutex.Lock()
+ defer f.Mutex.Unlock()
newf, err := NewFilterByName(name, option)
if err == nil {
- conf.Matchers[name] = newf
+ // valid filter create or append
+ if f.Matchers[name] == nil {
+ f.Matchers[name] = newf
+ } else {
+ newoption := f.Matchers[name].Repr() + "," + option
+ newerf, err := NewFilterByName(name, newoption)
+ if err == nil {
+ f.Matchers[name] = newerf
+ }
+ }
}
return err
}
-//CalibrateIfNeeded runs a self-calibration task for filtering options (if needed) by requesting random resources and acting accordingly
-func CalibrateIfNeeded(j *ffuf.Job) error {
- var err error
- if !j.Config.AutoCalibration {
- return nil
- }
- // Handle the calibration
- responses, err := j.CalibrateResponses()
- if err != nil {
- return err
- }
- if len(responses) > 0 {
- err = calibrateFilters(j, responses)
- }
- return err
+func (f *MatcherManager) GetFilters() map[string]ffuf.FilterProvider {
+ return f.Filters
}
-func calibrateFilters(j *ffuf.Job, responses []ffuf.Response) error {
- sizeCalib := make([]string, 0)
- wordCalib := make([]string, 0)
- lineCalib := make([]string, 0)
- for _, r := range responses {
- if r.ContentLength > 0 {
- // Only add if we have an actual size of responses
- sizeCalib = append(sizeCalib, strconv.FormatInt(r.ContentLength, 10))
- }
- if r.ContentWords > 0 {
- // Only add if we have an actual word length of response
- wordCalib = append(wordCalib, strconv.FormatInt(r.ContentWords, 10))
- }
- if r.ContentLines > 1 {
- // Only add if we have an actual word length of response
- lineCalib = append(lineCalib, strconv.FormatInt(r.ContentLines, 10))
- }
- }
-
- //Remove duplicates
- sizeCalib = ffuf.UniqStringSlice(sizeCalib)
- wordCalib = ffuf.UniqStringSlice(wordCalib)
- lineCalib = ffuf.UniqStringSlice(lineCalib)
+func (f *MatcherManager) GetMatchers() map[string]ffuf.FilterProvider {
+ return f.Matchers
+}
- if len(sizeCalib) > 0 {
- err := AddFilter(j.Config, "size", strings.Join(sizeCalib, ","))
- if err != nil {
- return err
- }
- }
- if len(wordCalib) > 0 {
- err := AddFilter(j.Config, "word", strings.Join(wordCalib, ","))
- if err != nil {
- return err
- }
- }
- if len(lineCalib) > 0 {
- err := AddFilter(j.Config, "line", strings.Join(lineCalib, ","))
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func SetupFilters(parseOpts *ffuf.ConfigOptions, conf *ffuf.Config) error {
- errs := ffuf.NewMultierror()
- // If any other matcher is set, ignore -mc default value
- matcherSet := false
- statusSet := false
- warningIgnoreBody := false
- flag.Visit(func(f *flag.Flag) {
- if f.Name == "mc" {
- statusSet = true
- }
- if f.Name == "ms" {
- matcherSet = true
- warningIgnoreBody = true
- }
- if f.Name == "ml" {
- matcherSet = true
- warningIgnoreBody = true
- }
- if f.Name == "mr" {
- matcherSet = true
- }
- if f.Name == "mt" {
- matcherSet = true
- }
- if f.Name == "mw" {
- matcherSet = true
- warningIgnoreBody = true
- }
- })
- if statusSet || !matcherSet {
- if err := AddMatcher(conf, "status", parseOpts.Matcher.Status); err != nil {
- errs.Add(err)
- }
+func (f *MatcherManager) FiltersForDomain(domain string) map[string]ffuf.FilterProvider {
+ if f.PerDomainFilters[domain] == nil {
+ return f.Filters
}
+ return f.PerDomainFilters[domain].Filters
+}
- if parseOpts.Filter.Status != "" {
- if err := AddFilter(conf, "status", parseOpts.Filter.Status); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Filter.Size != "" {
- warningIgnoreBody = true
- if err := AddFilter(conf, "size", parseOpts.Filter.Size); err != nil {
- errs.Add(err)
- }
+func (f *MatcherManager) CalibratedForDomain(domain string) bool {
+ if f.PerDomainFilters[domain] != nil {
+ return f.PerDomainFilters[domain].IsCalibrated
}
- if parseOpts.Filter.Regexp != "" {
- if err := AddFilter(conf, "regexp", parseOpts.Filter.Regexp); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Filter.Words != "" {
- warningIgnoreBody = true
- if err := AddFilter(conf, "word", parseOpts.Filter.Words); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Filter.Lines != "" {
- warningIgnoreBody = true
- if err := AddFilter(conf, "line", parseOpts.Filter.Lines); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Filter.Time != "" {
- if err := AddFilter(conf, "time", parseOpts.Filter.Time); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Matcher.Size != "" {
- if err := AddMatcher(conf, "size", parseOpts.Matcher.Size); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Matcher.Regexp != "" {
- if err := AddMatcher(conf, "regexp", parseOpts.Matcher.Regexp); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Matcher.Words != "" {
- if err := AddMatcher(conf, "word", parseOpts.Matcher.Words); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Matcher.Lines != "" {
- if err := AddMatcher(conf, "line", parseOpts.Matcher.Lines); err != nil {
- errs.Add(err)
- }
- }
- if parseOpts.Matcher.Time != "" {
- if err := AddFilter(conf, "time", parseOpts.Matcher.Time); err != nil {
- errs.Add(err)
- }
- }
- if conf.IgnoreBody && warningIgnoreBody {
- fmt.Printf("*** Warning: possible undesired combination of -ignore-body and the response options: fl,fs,fw,ml,ms and mw.\n")
- }
- return errs.ErrorOrNil()
+ return false
+}
+
+func (f *MatcherManager) Calibrated() bool {
+ return f.IsCalibrated
}
diff --git a/pkg/interactive/termhandler.go b/pkg/interactive/termhandler.go
index 15a6c3a..5846bf4 100644
--- a/pkg/interactive/termhandler.go
+++ b/pkg/interactive/termhandler.go
@@ -8,7 +8,6 @@ import (
"time"
"github.com/ffuf/ffuf/pkg/ffuf"
- "github.com/ffuf/ffuf/pkg/filter"
)
type interactive struct {
@@ -81,7 +80,7 @@ func (i *interactive) handleInput(in []byte) {
} else if len(args) > 2 {
i.Job.Output.Error("Too many arguments for \"fc\"")
} else {
- i.updateFilter("status", args[1])
+ i.updateFilter("status", args[1], true)
i.Job.Output.Info("New status code filter value set")
}
case "afc":
@@ -99,7 +98,7 @@ func (i *interactive) handleInput(in []byte) {
} else if len(args) > 2 {
i.Job.Output.Error("Too many arguments for \"fl\"")
} else {
- i.updateFilter("line", args[1])
+ i.updateFilter("line", args[1], true)
i.Job.Output.Info("New line count filter value set")
}
case "afl":
@@ -117,7 +116,7 @@ func (i *interactive) handleInput(in []byte) {
} else if len(args) > 2 {
i.Job.Output.Error("Too many arguments for \"fw\"")
} else {
- i.updateFilter("word", args[1])
+ i.updateFilter("word", args[1], true)
i.Job.Output.Info("New word count filter value set")
}
case "afw":
@@ -135,7 +134,7 @@ func (i *interactive) handleInput(in []byte) {
} else if len(args) > 2 {
i.Job.Output.Error("Too many arguments for \"fs\"")
} else {
- i.updateFilter("size", args[1])
+ i.updateFilter("size", args[1], true)
i.Job.Output.Info("New response size filter value set")
}
case "afs":
@@ -153,7 +152,7 @@ func (i *interactive) handleInput(in []byte) {
} else if len(args) > 2 {
i.Job.Output.Error("Too many arguments for \"ft\"")
} else {
- i.updateFilter("time", args[1])
+ i.updateFilter("time", args[1], true)
i.Job.Output.Info("New response time filter value set")
}
case "aft":
@@ -192,19 +191,10 @@ func (i *interactive) handleInput(in []byte) {
}
}
-func (i *interactive) updateFilter(name, value string) {
- if value == "none" {
- filter.RemoveFilter(i.Job.Config, name)
- } else {
- newFc, err := filter.NewFilterByName(name, value)
- if err != nil {
- i.Job.Output.Error(fmt.Sprintf("Error while setting new filter value: %s", err))
- return
- } else {
- i.Job.Config.Filters[name] = newFc
- }
-
- results := make([]ffuf.Result, 0)
+func (i *interactive) refreshResults() {
+ results := make([]ffuf.Result, 0)
+ filters := i.Job.Config.MatcherManager.GetFilters()
+ for _, filter := range filters {
for _, res := range i.Job.Output.GetCurrentResults() {
fakeResp := &ffuf.Response{
StatusCode: res.StatusCode,
@@ -212,22 +202,26 @@ func (i *interactive) updateFilter(name, value string) {
ContentWords: res.ContentWords,
ContentLength: res.ContentLength,
}
- filterOut, _ := newFc.Filter(fakeResp)
+ filterOut, _ := filter.Filter(fakeResp)
if !filterOut {
results = append(results, res)
}
}
- i.Job.Output.SetCurrentResults(results)
}
+ i.Job.Output.SetCurrentResults(results)
}
-func (i *interactive) appendFilter(name, value string) {
- if oldFc, found := i.Job.Config.Filters[name]; found {
- oldVal := oldFc.Repr()
- i.updateFilter(name, strings.Join([]string{oldVal, value}, ","))
+func (i *interactive) updateFilter(name, value string, replace bool) {
+ if value == "none" {
+ i.Job.Config.MatcherManager.RemoveFilter(name)
} else {
- i.updateFilter(name, value)
+ _ = i.Job.Config.MatcherManager.AddFilter(name, value, replace)
}
+ i.refreshResults()
+}
+
+func (i *interactive) appendFilter(name, value string) {
+ i.updateFilter(name, value, false)
}
func (i *interactive) printQueue() {
@@ -270,7 +264,7 @@ func (i *interactive) printPrompt() {
func (i *interactive) printHelp() {
var fc, fl, fs, ft, fw string
- for name, filter := range i.Job.Config.Filters {
+ for name, filter := range i.Job.Config.MatcherManager.GetFilters() {
switch name {
case "status":
fc = "(active: " + filter.Repr() + ")"
diff --git a/pkg/output/stdout.go b/pkg/output/stdout.go
index 588decb..758f3bd 100644
--- a/pkg/output/stdout.go
+++ b/pkg/output/stdout.go
@@ -124,11 +124,11 @@ func (s *Stdoutput) Banner() {
}
// Print matchers
- for _, f := range s.config.Matchers {
+ for _, f := range s.config.MatcherManager.GetMatchers() {
printOption([]byte("Matcher"), []byte(f.ReprVerbose()))
}
// Print filters
- for _, f := range s.config.Filters {
+ for _, f := range s.config.MatcherManager.GetFilters() {
printOption([]byte("Filter"), []byte(f.ReprVerbose()))
}
fmt.Fprintf(os.Stderr, "%s\n\n", BANNER_SEP)
Debdiff
File lists identical (after any substitutions)
No differences were encountered in the control files