diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..cf4f8bf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [joohoi] 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/.goreleaser.yml b/.goreleaser.yml index 19e6330..4e8d074 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -10,7 +10,7 @@ gcflags: - all=-trimpath={{.Env.GOPATH}} ldflags: | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -extldflags '-static' + -s -w -X github.com/ffuf/ffuf/pkg/ffuf.VERSION_APPENDIX= -extldflags '-static' goos: - linux - windows diff --git a/CHANGELOG.md b/CHANGELOG.md index 31bd4b4..063d7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ - master - New - Changed + +- v1.3.0 + - New + - All output file formats now include the `Content-Type`. + - New CLI flag `-recursion-strategy` that allows adding new queued recursion jobs for non-redirect responses. + - Ability to enter interactive mode by pressing `ENTER` during the ffuf execution. The interactive mode allows + user to change filters, manage recursion queue, save snapshot of matches to a file etc. + - Changed + - Fix a badchar in progress output + +- v1.2.1 + - Changed + - Fixed a build breaking bug in `input-shell` parameter - v1.2.0 - New diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c7a78ab..2bdd441 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -23,9 +23,11 @@ * [Kiblyn11](https://github.com/Kiblyn11) * [lc](https://github.com/lc) * [nnwakelam](https://twitter.com/nnwakelam) +* [noraj](https://pwn.by/noraj) * [oh6hay](https://github.com/oh6hay) * [putsi](https://github.com/putsi) * [SakiiR](https://github.com/SakiiR) * [seblw](https://github.com/seblw) * [Shaked](https://github.com/Shaked) * [SolomonSklash](https://github.com/SolomonSklash) +* [l4yton](https://github.com/l4yton) diff --git a/README.md b/README.md index 66f743a..30d287d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,32 @@ # ffuf - Fuzz Faster U Fool A fast web fuzzer written in Go. + +- [Installation](https://github.com/ffuf/ffuf#installation) +- [Example usage](https://github.com/ffuf/ffuf#example-usage) + - [Content discovery](https://github.com/ffuf/ffuf#typical-directory-discovery) + - [Vhost discovery](https://github.com/ffuf/ffuf#virtual-host-discovery-without-dns-records) + - [Parameter fuzzing](https://github.com/ffuf/ffuf#get-parameter-fuzzing) + - [POST data fuzzing](https://github.com/ffuf/ffuf#post-data-fuzzing) + - [Using external mutator](https://github.com/ffuf/ffuf#using-external-mutator-to-produce-test-cases) + - [Configuration files](https://github.com/ffuf/ffuf#configuration-files) +- [Help](https://github.com/ffuf/ffuf#usage) + - [Interactive mode](https://github.com/ffuf/ffuf#interactive-mode) +- [Sponsorware?](https://github.com/ffuf/ffuf#sponsorware) + +## Sponsors +[![Offensive Security](_img/offsec-logo.png)](https://www.offensive-security.com/) + +## Official Discord Channel + +ffuf has a channel at Porchetta Industries Discord server alongside of channels for many other tools. + +Come to hang out & to discuss about ffuf, it's usage and development! + +[![Porchetta Industries](https://discordapp.com/api/guilds/736724457258745996/widget.png?style=banner2)](https://discord.gg/VWcdZCUsQP) + + + ## Installation @@ -135,18 +161,19 @@ Fuzz Faster U Fool - v1.2.0-git HTTP OPTIONS: - -H Header `"Name: Value"`, separated by colon. Multiple -H flags are accepted. - -X HTTP method to use (default: GET) - -b Cookie data `"NAME1=VALUE1; NAME2=VALUE2"` for copy as curl functionality. - -d POST data - -ignore-body Do not fetch the response content. (default: false) - -r Follow redirects (default: false) - -recursion Scan recursively. Only FUZZ keyword is supported, and URL (-u) has to end in it. (default: false) - -recursion-depth Maximum recursion depth. (default: 0) - -replay-proxy Replay matched requests using this proxy. - -timeout HTTP request timeout in seconds. (default: 10) - -u Target URL - -x HTTP Proxy URL + -H Header `"Name: Value"`, separated by colon. Multiple -H flags are accepted. + -X HTTP method to use + -b Cookie data `"NAME1=VALUE1; NAME2=VALUE2"` for copy as curl functionality. + -d POST data + -ignore-body Do not fetch the response content. (default: false) + -r Follow redirects (default: false) + -recursion Scan recursively. Only FUZZ keyword is supported, and URL (-u) has to end in it. (default: false) + -recursion-depth Maximum recursion depth. (default: 0) + -recursion-strategy Recursion strategy: "default" for a redirect based, and "greedy" to recurse on all matches (default: default) + -replay-proxy Replay matched requests using this proxy. + -timeout HTTP request timeout in seconds. (default: 10) + -u Target URL + -x Proxy URL (SOCKS5 or HTTP). For example: http://127.0.0.1:8080 or socks5://127.0.0.1:8080 GENERAL OPTIONS: -V Show version information. (default: false) @@ -216,6 +243,61 @@ ``` +### Interactive mode + +By pressing `ENTER` during ffuf execution, the process is paused and user is dropped to a shell-like interactive mode: +``` +entering interactive mode +type "help" for a list of commands, or ENTER to resume. +> help + +available commands: + fc [value] - (re)configure status code filter + fl [value] - (re)configure line count filter + fw [value] - (re)configure word count filter + fs [value] - (re)configure size filter + queueshow - show recursive job queue + queuedel [number] - delete a recursion job in the queue + queueskip - advance to the next queued recursion job + restart - restart and resume the current ffuf job + resume - resume current ffuf job (or: ENTER) + show - show results + savejson [filename] - save current matches to a file + help - you are looking at it +> +``` + +in this mode, filters can be reconfigured, queue managed and the current state saved to disk. + +When (re)configuring the filters, they get applied posthumously and all the false positive matches from memory that +would have been filtered out by the newly added filters get deleted. + +The new state of matches can be printed out with a command `show` that will print out all the matches as like they +would have been found by `ffuf`. + +As "negative" matches are not stored to memory, relaxing the filters cannot unfortunately bring back the lost matches. +For this kind of scenario, the user is able to use the command `restart`, which resets the state and starts the current +job from the beginning. + + +## Sponsorware + +`ffuf` employs a sponsorware model. This means that all new features developed by its author are initially exclusively +available for their sponsors. 30 days after the exclusive release, all the new features will be released at the freely +available open source repository at https://github.com/ffuf/ffuf . + +This model enables me to provide concrete benefits for the generous individuals and companies that enable me to work on +`ffuf`. The different sponsorship tiers can be seen [here](https://github.com/sponsors/joohoi). + +All the community contributions are and will be available directly in the freely available open source repository. The +exclusive version benefits only include new features created by [@joohoi](https://github.com/joohoi) + +### Access the sponsorware through code contributions + +People that create significant contributions to the `ffuf` project itself should and will have access to the sponsorware +as well. If you are planning to create such a contribution, please contact [@joohoi](https://github.com/joohoi) +first to ensure that there aren't other people working on the same feature. + ## Helper scripts and advanced payloads See [ffuf-scripts](https://github.com/ffuf/ffuf-scripts) repository for helper scripts and payload generators diff --git a/_img/offsec-logo.png b/_img/offsec-logo.png new file mode 100644 index 0000000..c03d4fa Binary files /dev/null and b/_img/offsec-logo.png differ diff --git a/ffufrc.example b/ffufrc.example index f3684be..c14f84e 100644 --- a/ffufrc.example +++ b/ffufrc.example @@ -15,7 +15,8 @@ method = "GET" proxyurl = "http://127.0.0.1:8080" recursion = false - recursiondepth = 0 + recursion_depth = 0 + recursion_strategy = "default" replayproxyurl = "http://127.0.0.1:8080" timeout = 10 url = "https://example.org/FUZZ" diff --git a/help.go b/help.go index e9db37e..cfa9b08 100644 --- a/help.go +++ b/help.go @@ -54,7 +54,7 @@ Description: "Options controlling the HTTP request and its parts.", Flags: make([]UsageFlag, 0), Hidden: false, - ExpectedFlags: []string{"H", "X", "b", "d", "r", "u", "recursion", "recursion-depth", "replay-proxy", "timeout", "ignore-body", "x"}, + ExpectedFlags: []string{"H", "X", "b", "d", "r", "u", "recursion", "recursion-depth", "recursion-strategy", "replay-proxy", "timeout", "ignore-body", "x"}, } u_general := UsageSection{ Name: "GENERAL OPTIONS", @@ -123,7 +123,7 @@ } }) - fmt.Printf("Fuzz Faster U Fool - v%s\n\n", ffuf.VERSION) + fmt.Printf("Fuzz Faster U Fool - v%s\n\n", ffuf.Version()) // Print out the sections for _, section := range sections { diff --git a/main.go b/main.go index 3301500..2e197d5 100644 --- a/main.go +++ b/main.go @@ -4,16 +4,16 @@ "context" "flag" "fmt" + "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" + "github.com/ffuf/ffuf/pkg/runner" "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/output" - "github.com/ffuf/ffuf/pkg/runner" ) type multiStringFlag []string @@ -91,8 +91,9 @@ flag.StringVar(&opts.HTTP.Data, "data-ascii", opts.HTTP.Data, "POST data (alias of -d)") flag.StringVar(&opts.HTTP.Data, "data-binary", opts.HTTP.Data, "POST data (alias of -d)") flag.StringVar(&opts.HTTP.Method, "X", opts.HTTP.Method, "HTTP method to use") - flag.StringVar(&opts.HTTP.ProxyURL, "x", opts.HTTP.ProxyURL, "HTTP Proxy URL") + flag.StringVar(&opts.HTTP.ProxyURL, "x", opts.HTTP.ProxyURL, "Proxy URL (SOCKS5 or HTTP). For example: http://127.0.0.1:8080 or socks5://127.0.0.1:8080") flag.StringVar(&opts.HTTP.ReplayProxyURL, "replay-proxy", opts.HTTP.ReplayProxyURL, "Replay matched requests using this proxy.") + flag.StringVar(&opts.HTTP.RecursionStrategy, "recursion-strategy", opts.HTTP.RecursionStrategy, "Recursion strategy: \"default\" for a redirect based, and \"greedy\" to recurse on all matches") flag.StringVar(&opts.HTTP.URL, "u", opts.HTTP.URL, "Target URL") flag.StringVar(&opts.Input.Extensions, "e", opts.Input.Extensions, "Comma separated list of extensions. Extends FUZZ keyword.") flag.StringVar(&opts.Input.InputMode, "mode", opts.Input.InputMode, "Multi-wordlist operation mode. Available modes: clusterbomb, pitchfork") @@ -136,7 +137,7 @@ opts = ParseFlags(opts) if opts.General.ShowVersion { - fmt.Printf("ffuf version: %s\n", ffuf.VERSION) + fmt.Printf("ffuf version: %s\n", ffuf.Version()) os.Exit(0) } if len(opts.Output.DebugLog) != 0 { @@ -197,6 +198,12 @@ fmt.Fprintf(os.Stderr, "Error in autocalibration, exiting: %s\n", err) os.Exit(1) } + go func() { + err := interactive.Handle(job) + if err != nil { + log.Printf("Error while trying to initialize interactive session: %s", err) + } + }() // Job handles waiting for goroutines to complete itself job.Start() diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go index 1c3fea2..39db943 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -33,13 +33,14 @@ OutputDirectory string `json:"outputdirectory"` OutputFile string `json:"outputfile"` OutputFormat string `json:"outputformat"` - OutputCreateEmptyFile bool `json:"OutputCreateEmptyFile"` + OutputCreateEmptyFile bool `json:"OutputCreateEmptyFile"` 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"` StopOn403 bool `json:"stop_403"` StopOnAll bool `json:"stop_all"` @@ -84,6 +85,7 @@ conf.Rate = 0 conf.Recursion = false conf.RecursionDepth = 0 + conf.RecursionStrategy = "default" conf.StopOn403 = false conf.StopOnAll = false conf.StopOnErrors = false diff --git a/pkg/ffuf/const.go b/pkg/ffuf/const.go deleted file mode 100644 index 145af4a..0000000 --- a/pkg/ffuf/const.go +++ /dev/null @@ -1,6 +0,0 @@ -package ffuf - -const ( - //VERSION holds the current version number - VERSION = "1.2.1" -) diff --git a/pkg/ffuf/interfaces.go b/pkg/ffuf/interfaces.go index 0aa7eed..c356444 100644 --- a/pkg/ffuf/interfaces.go +++ b/pkg/ffuf/interfaces.go @@ -4,6 +4,7 @@ type FilterProvider interface { Filter(response *Response) (bool, error) Repr() string + ReprVerbose() string } //RunnerProvider is an interface for request executors @@ -40,6 +41,27 @@ Progress(status Progress) Info(infostring string) Error(errstring string) + Raw(output string) Warning(warnstring string) Result(resp Response) + PrintResult(res Result) + SaveFile(filename, format string) error + GetResults() []Result + SetResults(results []Result) + Reset() } + +type Result struct { + Input map[string][]byte `json:"input"` + Position int `json:"position"` + StatusCode int64 `json:"status"` + ContentLength int64 `json:"length"` + ContentWords int64 `json:"words"` + ContentLines int64 `json:"lines"` + ContentType string `json:"content-type"` + RedirectLocation string `json:"redirectlocation"` + Url string `json:"url"` + ResultFile string `json:"resultfile"` + Host string `json:"host"` + HTMLColor string `json:"-"` +} diff --git a/pkg/ffuf/job.go b/pkg/ffuf/job.go index 5c74f54..22c0da3 100644 --- a/pkg/ffuf/job.go +++ b/pkg/ffuf/job.go @@ -25,6 +25,7 @@ Total int Running bool RunningJob bool + Paused bool Count403 int Count429 int Error string @@ -33,7 +34,9 @@ startTimeJob time.Time queuejobs []QueueJob queuepos int + skipQueue bool currentDepth int + pauseWg sync.WaitGroup } type QueueJob struct { @@ -49,10 +52,12 @@ j.SpuriousErrorCounter = 0 j.Running = false j.RunningJob = false + j.Paused = false j.queuepos = 0 j.queuejobs = make([]QueueJob, 0) j.currentDepth = 0 j.Rate = NewRateThrottle(conf) + j.skipQueue = false return &j } @@ -85,6 +90,17 @@ j.SpuriousErrorCounter = 0 } +//DeleteQueueItem deletes a recursion job from the queue by its index in the slice +func (j *Job) DeleteQueueItem(index int) { + index = j.queuepos + index - 1 + j.queuejobs = append(j.queuejobs[:index], j.queuejobs[index+1:]...) +} + +//QueuedJobs returns the slice of queued recursive jobs +func (j *Job) QueuedJobs() []QueueJob { + return j.queuejobs[j.queuepos-1:] +} + //Start the execution of the Job func (j *Job) Start() { if j.startTime.IsZero() { @@ -107,15 +123,8 @@ j.interruptMonitor() for j.jobsInQueue() { j.prepareQueueJob() - - if j.queuepos > 1 && !j.RunningJob { - // Print info for queued recursive jobs - j.Output.Info(fmt.Sprintf("Scanning: %s", j.Config.Url)) - } - j.Input.Reset() - j.startTimeJob = time.Now() + j.Reset() j.RunningJob = true - j.Counter = 0 j.startExecution() } @@ -123,6 +132,15 @@ if err != nil { j.Output.Error(err.Error()) } +} + +// Reset resets the counters and wordlist position for a job +func (j *Job) Reset() { + j.Input.Reset() + j.Counter = 0 + j.skipQueue = false + j.startTimeJob = time.Now() + j.Output.Reset() } func (j *Job) jobsInQueue() bool { @@ -133,6 +151,11 @@ j.Config.Url = j.queuejobs[j.queuepos].Url j.currentDepth = j.queuejobs[j.queuepos].depth j.queuepos += 1 +} + +//SkipQueue allows to skip the current job and advance to the next queued recursion job +func (j *Job) SkipQueue() { + j.skipQueue = true } func (j *Job) sleepIfNeeded() { @@ -153,14 +176,38 @@ } } +// Pause pauses the job process +func (j *Job) Pause() { + if !j.Paused { + j.Paused = true + j.pauseWg.Add(1) + j.Output.Info("------ PAUSING ------") + } +} + +// Resume resumes the job process +func (j *Job) Resume() { + if j.Paused { + j.Paused = false + j.Output.Info("------ RESUMING -----") + j.pauseWg.Done() + } +} + func (j *Job) startExecution() { var wg sync.WaitGroup wg.Add(1) go j.runBackgroundTasks(&wg) + + // Print the base URL when starting a new recursion queue job + if j.queuepos > 1 { + j.Output.Info(fmt.Sprintf("Starting queued job on target: %s", j.Config.Url)) + } + //Limiter blocks after reaching the buffer, ensuring limited concurrency limiter := make(chan bool, j.Config.Threads) - for j.Input.Next() { + for j.Input.Next() && !j.skipQueue { // Check if we should stop the process j.CheckStop() @@ -168,6 +215,7 @@ defer j.Output.Warning(j.Error) break } + j.pauseWg.Wait() limiter <- true nextInput := j.Input.Value() nextPosition := j.Input.Position() @@ -200,6 +248,11 @@ go func() { for range sigChan { j.Error = "Caught keyboard interrupt (Ctrl-C)\n" + // resume if paused + if j.Paused { + j.pauseWg.Done() + } + // Stop the job j.Stop() } }() @@ -208,8 +261,8 @@ func (j *Job) runBackgroundTasks(wg *sync.WaitGroup) { defer wg.Done() totalProgress := j.Input.Total() - for j.Counter <= totalProgress { - + for j.Counter <= totalProgress && !j.skipQueue { + j.pauseWg.Wait() if !j.Running { break } @@ -315,22 +368,39 @@ j.Output.Result(resp) // Refresh the progress indicator as we printed something out j.updateProgress() - } - - if j.Config.Recursion && len(resp.GetRedirectLocation(false)) > 0 { - j.handleRecursionJob(resp) - } -} - -//handleRecursionJob adds a new recursion job to the job queue if a new directory is found -func (j *Job) handleRecursionJob(resp Response) { + if j.Config.Recursion && j.Config.RecursionStrategy == "greedy" { + j.handleGreedyRecursionJob(resp) + } + } + + if j.Config.Recursion && j.Config.RecursionStrategy == "default" && len(resp.GetRedirectLocation(false)) > 0 { + j.handleDefaultRecursionJob(resp) + } +} + +//handleGreedyRecursionJob adds a recursion job to the queue if the maximum depth has not been reached +func (j *Job) handleGreedyRecursionJob(resp Response) { + // Handle greedy recursion strategy. Match has been determined before calling handleRecursionJob + if j.Config.RecursionDepth == 0 || j.currentDepth < j.Config.RecursionDepth { + recUrl := resp.Request.Url + "/" + "FUZZ" + newJob := QueueJob{Url: recUrl, depth: j.currentDepth + 1} + j.queuejobs = append(j.queuejobs, newJob) + j.Output.Info(fmt.Sprintf("Adding a new job to the queue: %s", recUrl)) + } else { + j.Output.Warning(fmt.Sprintf("Maximum recursion depth reached. Ignoring: %s", resp.Request.Url)) + } +} + +//handleDefaultRecursionJob adds a new recursion job to the job queue if a new directory is found and maximum depth has +//not been reached +func (j *Job) handleDefaultRecursionJob(resp Response) { + recUrl := resp.Request.Url + "/" + "FUZZ" if (resp.Request.Url + "/") != resp.GetRedirectLocation(true) { // Not a directory, return early return } if j.Config.RecursionDepth == 0 || j.currentDepth < j.Config.RecursionDepth { // We have yet to reach the maximum recursion depth - recUrl := resp.Request.Url + "/" + "FUZZ" newJob := QueueJob{Url: recUrl, depth: j.currentDepth + 1} j.queuejobs = append(j.queuejobs, newJob) j.Output.Info(fmt.Sprintf("Adding a new job to the queue: %s", recUrl)) diff --git a/pkg/ffuf/optionsparser.go b/pkg/ffuf/optionsparser.go index fd4343d..7f73af8 100644 --- a/pkg/ffuf/optionsparser.go +++ b/pkg/ffuf/optionsparser.go @@ -26,18 +26,19 @@ } type HTTPOptions struct { - Cookies []string - Data string - FollowRedirects bool - Headers []string - IgnoreBody bool - Method string - ProxyURL string - Recursion bool - RecursionDepth int - ReplayProxyURL string - Timeout int - URL string + Cookies []string + Data string + FollowRedirects bool + Headers []string + IgnoreBody bool + Method string + ProxyURL string + Recursion bool + RecursionDepth int + RecursionStrategy string + ReplayProxyURL string + Timeout int + URL string } type GeneralOptions struct { @@ -72,11 +73,11 @@ } type OutputOptions struct { - DebugLog string - OutputDirectory string - OutputFile string - OutputFormat string - OutputCreateEmptyFile bool + DebugLog string + OutputDirectory string + OutputFile string + OutputFormat string + OutputCreateEmptyFile bool } type FilterOptions struct { @@ -123,6 +124,7 @@ c.HTTP.ProxyURL = "" c.HTTP.Recursion = false c.HTTP.RecursionDepth = 0 + c.HTTP.RecursionStrategy = "default" c.HTTP.ReplayProxyURL = "" c.HTTP.Timeout = 10 c.HTTP.URL = "" @@ -387,6 +389,7 @@ conf.FollowRedirects = parseOpts.HTTP.FollowRedirects conf.Recursion = parseOpts.HTTP.Recursion conf.RecursionDepth = parseOpts.HTTP.RecursionDepth + conf.RecursionStrategy = parseOpts.HTTP.RecursionStrategy conf.AutoCalibration = parseOpts.General.AutoCalibration conf.Threads = parseOpts.General.Threads conf.Timeout = parseOpts.HTTP.Timeout diff --git a/pkg/ffuf/response.go b/pkg/ffuf/response.go index 7329f39..aecfd2f 100644 --- a/pkg/ffuf/response.go +++ b/pkg/ffuf/response.go @@ -13,6 +13,7 @@ ContentLength int64 ContentWords int64 ContentLines int64 + ContentType string Cancelled bool Request *Request Raw string @@ -50,6 +51,7 @@ var resp Response resp.Request = req resp.StatusCode = int64(httpresp.StatusCode) + resp.ContentType = httpresp.Header.Get("Content-Type") resp.Headers = httpresp.Header resp.Cancelled = false resp.Raw = "" diff --git a/pkg/ffuf/util.go b/pkg/ffuf/util.go index 327fa6a..e9a8aa9 100644 --- a/pkg/ffuf/util.go +++ b/pkg/ffuf/util.go @@ -1,6 +1,7 @@ package ffuf import ( + "fmt" "math/rand" "os" ) @@ -41,3 +42,8 @@ return !md.IsDir() } + +//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 new file mode 100644 index 0000000..a06e92a --- /dev/null +++ b/pkg/ffuf/version.go @@ -0,0 +1,8 @@ +package ffuf + +var ( + //VERSION holds the current version number + VERSION = "1.3.0" + //VERSION_APPENDIX holds additional version definition + VERSION_APPENDIX = "-exclusive-dev" +) diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index 0475ca0..c1a1e57 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -30,21 +30,25 @@ //AddFilter adds a new filter to Config func AddFilter(conf *ffuf.Config, name string, option string) error { - newf, err := NewFilterByName(name, option) - if err == nil { - // valid filter create or append - if conf.Filters[name] == nil { - conf.Filters[name] = newf - } else { - currentfilter := conf.Filters[name].Repr() - newoption := strings.TrimSpace(strings.Split(currentfilter, ":")[1]) + "," + option - newerf, err := NewFilterByName(name, newoption) - if err == nil { - conf.Filters[name] = newerf - } - } - } - return err + newf, err := NewFilterByName(name, option) + if err == nil { + // valid filter create or append + if conf.Filters[name] == nil { + conf.Filters[name] = newf + } else { + newoption := conf.Filters[name].Repr() + "," + option + newerf, err := NewFilterByName(name, newoption) + if err == nil { + conf.Filters[name] = newerf + } + } + } + return err +} + +//RemoveFilter removes a filter of a given type +func RemoveFilter(conf *ffuf.Config, name string) { + delete(conf.Filters, name) } //AddMatcher adds a new matcher to Config diff --git a/pkg/filter/lines.go b/pkg/filter/lines.go index ab07fe5..ced350b 100644 --- a/pkg/filter/lines.go +++ b/pkg/filter/lines.go @@ -60,5 +60,9 @@ strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max))) } } - return fmt.Sprintf("Response lines: %s", strings.Join(strval, ",")) + return strings.Join(strval, ",") } + +func (f *LineFilter) ReprVerbose() string { + return fmt.Sprintf("Response lines: %s", f.Repr()) +} diff --git a/pkg/filter/regex.go b/pkg/filter/regex.go index 57291e1..8f57025 100644 --- a/pkg/filter/regex.go +++ b/pkg/filter/regex.go @@ -51,5 +51,9 @@ } func (f *RegexpFilter) Repr() string { + return f.valueRaw +} + +func (f *RegexpFilter) ReprVerbose() string { return fmt.Sprintf("Regexp: %s", f.valueRaw) } diff --git a/pkg/filter/size.go b/pkg/filter/size.go index c6490a4..c940d4e 100644 --- a/pkg/filter/size.go +++ b/pkg/filter/size.go @@ -60,5 +60,9 @@ strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max))) } } - return fmt.Sprintf("Response size: %s", strings.Join(strval, ",")) + return strings.Join(strval, ",") } + +func (f *SizeFilter) ReprVerbose() string { + return fmt.Sprintf("Response size: %s", f.Repr()) +} diff --git a/pkg/filter/status.go b/pkg/filter/status.go index 9f5852c..0beb5b9 100644 --- a/pkg/filter/status.go +++ b/pkg/filter/status.go @@ -75,5 +75,9 @@ strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max))) } } - return fmt.Sprintf("Response status: %s", strings.Join(strval, ",")) + return strings.Join(strval, ",") } + +func (f *StatusFilter) ReprVerbose() string { + return fmt.Sprintf("Response status: %s", f.Repr()) +} diff --git a/pkg/filter/words.go b/pkg/filter/words.go index ce22a3e..685e534 100644 --- a/pkg/filter/words.go +++ b/pkg/filter/words.go @@ -60,5 +60,9 @@ strval = append(strval, strconv.Itoa(int(iv.Min))+"-"+strconv.Itoa(int(iv.Max))) } } - return fmt.Sprintf("Response words: %s", strings.Join(strval, ",")) + return strings.Join(strval, ",") } + +func (f *WordFilter) ReprVerbose() string { + return fmt.Sprintf("Response words: %s", f.Repr()) +} diff --git a/pkg/interactive/posix.go b/pkg/interactive/posix.go new file mode 100644 index 0000000..85ffb1e --- /dev/null +++ b/pkg/interactive/posix.go @@ -0,0 +1,9 @@ +// +build !windows + +package interactive + +import "os" + +func termHandle() (*os.File, error) { + return os.Open("/dev/tty") +} diff --git a/pkg/interactive/termhandler.go b/pkg/interactive/termhandler.go new file mode 100644 index 0000000..24a5781 --- /dev/null +++ b/pkg/interactive/termhandler.go @@ -0,0 +1,237 @@ +package interactive + +import ( + "bufio" + "fmt" + "github.com/ffuf/ffuf/pkg/ffuf" + "github.com/ffuf/ffuf/pkg/filter" + "strconv" + "strings" + "time" +) + +type interactive struct { + Job *ffuf.Job + paused bool +} + +func Handle(job *ffuf.Job) error { + i := interactive{job, false} + tty, err := termHandle() + if err != nil { + return err + } + defer tty.Close() + inreader := bufio.NewScanner(tty) + inreader.Split(bufio.ScanLines) + for inreader.Scan() { + i.handleInput(inreader.Bytes()) + } + return nil +} + +func (i *interactive) handleInput(in []byte) { + instr := string(in) + args := strings.Split(strings.TrimSpace(instr), " ") + if len(args) == 1 && args[0] == "" { + // Enter pressed - toggle interactive state + i.paused = !i.paused + if i.paused { + i.Job.Pause() + time.Sleep(500 * time.Millisecond) + i.printBanner() + } else { + i.Job.Resume() + } + } else { + switch args[0] { + case "?": + i.printHelp() + case "help": + i.printHelp() + case "resume": + i.paused = false + i.Job.Resume() + case "restart": + i.Job.Reset() + i.paused = false + i.Job.Output.Info("Restarting the current ffuf job!") + i.Job.Resume() + case "show": + for _, r := range i.Job.Output.GetResults() { + i.Job.Output.PrintResult(r) + } + case "savejson": + if len(args) < 2 { + i.Job.Output.Error("Please define the filename") + } else if len(args) > 2 { + i.Job.Output.Error("Too many arguments for \"savejson\"") + } else { + err := i.Job.Output.SaveFile(args[1], "json") + if err != nil { + i.Job.Output.Error(fmt.Sprintf("%s", err)) + } else { + i.Job.Output.Info("Output file successfully saved!") + } + } + case "fc": + if len(args) < 2 { + i.Job.Output.Error("Please define a value for status code filter, or \"none\" for removing it") + } else if len(args) > 2 { + i.Job.Output.Error("Too many arguments for \"fc\"") + } else { + i.updateFilter("status", args[1]) + i.Job.Output.Info("New status code filter value set") + } + case "fl": + if len(args) < 2 { + i.Job.Output.Error("Please define a value for line count filter, or \"none\" for removing it") + } else if len(args) > 2 { + i.Job.Output.Error("Too many arguments for \"fl\"") + } else { + i.updateFilter("line", args[1]) + i.Job.Output.Info("New line count filter value set") + } + case "fw": + if len(args) < 2 { + i.Job.Output.Error("Please define a value for word count filter, or \"none\" for removing it") + } else if len(args) > 2 { + i.Job.Output.Error("Too many arguments for \"fw\"") + } else { + i.updateFilter("word", args[1]) + i.Job.Output.Info("New word count filter value set") + } + case "fs": + if len(args) < 2 { + i.Job.Output.Error("Please define a value for response size filter, or \"none\" for removing it") + } else if len(args) > 2 { + i.Job.Output.Error("Too many arguments for \"fs\"") + } else { + i.updateFilter("size", args[1]) + i.Job.Output.Info("New response size filter value set") + } + case "queueshow": + i.printQueue() + case "queuedel": + if len(args) < 2 { + i.Job.Output.Error("Please define the index of a queued job to remove. Use \"queueshow\" for listing of jobs.") + } else if len(args) > 2 { + i.Job.Output.Error("Too many arguments for \"queuedel\"") + } else { + i.deleteQueue(args[1]) + } + case "queueskip": + i.Job.SkipQueue() + i.Job.Output.Info("Skipping to the next queued job") + default: + if i.paused { + i.Job.Output.Warning(fmt.Sprintf("Unknown command: \"%s\". Enter \"help\" for a list of available commands", args[0])) + } else { + i.Job.Output.Error("NOPE") + } + } + } + + if i.paused { + i.printPrompt() + } +} + +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) + for _, res := range i.Job.Output.GetResults() { + fakeResp := &ffuf.Response{ + StatusCode: res.StatusCode, + ContentLines: res.ContentLength, + ContentWords: res.ContentWords, + ContentLength: res.ContentLength, + } + filterOut, _ := newFc.Filter(fakeResp) + if !filterOut { + results = append(results, res) + } + } + i.Job.Output.SetResults(results) + } +} + +func (i *interactive) printQueue() { + if len(i.Job.QueuedJobs()) > 0 { + i.Job.Output.Raw("Queued recursion jobs:\n") + for index, job := range i.Job.QueuedJobs() { + postfix := "" + if index == 0 { + postfix = " (active job)" + } + i.Job.Output.Raw(fmt.Sprintf(" [%d] : %s%s\n", index, job.Url, postfix)) + } + } else { + i.Job.Output.Info("Recursion job queue is empty") + } +} + +func (i *interactive) deleteQueue(in string) { + index, err := strconv.Atoi(in) + if err != nil { + i.Job.Output.Warning(fmt.Sprintf("Not a number: %s", in)) + } else { + if index < 0 || index > len(i.Job.QueuedJobs())-1 { + i.Job.Output.Warning("No such queued job. Use \"queueshow\" to list the jobs in queue") + } else if index == 0 { + i.Job.Output.Warning("Cannot delete the currently running job. Use \"queueskip\" to advance to the next one") + } else { + i.Job.DeleteQueueItem(index) + i.Job.Output.Info("Recursion job successfully deleted!") + } + } +} +func (i *interactive) printBanner() { + i.Job.Output.Raw("entering interactive mode\ntype \"help\" for a list of commands, or ENTER to resume.\n") +} + +func (i *interactive) printPrompt() { + i.Job.Output.Raw("> ") +} + +func (i *interactive) printHelp() { + var fc, fl, fs, fw string + for name, filter := range i.Job.Config.Filters { + switch name { + case "status": + fc = "(active: " + filter.Repr() + ")" + case "line": + fl = "(active: " + filter.Repr() + ")" + case "word": + fw = "(active: " + filter.Repr() + ")" + case "size": + fs = "(active: " + filter.Repr() + ")" + } + } + help := ` +available commands: + fc [value] - (re)configure status code filter %s + fl [value] - (re)configure line count filter %s + fw [value] - (re)configure word count filter %s + fs [value] - (re)configure size filter %s + queueshow - show recursive job queue + queuedel [number] - delete a recursion job in the queue + queueskip - advance to the next queued recursion job + restart - restart and resume the current ffuf job + resume - resume current ffuf job (or: ENTER) + show - show results + savejson [filename] - save current matches to a file + help - you are looking at it +` + i.Job.Output.Raw(fmt.Sprintf(help, fc, fl, fw, fs)) +} diff --git a/pkg/interactive/windows.go b/pkg/interactive/windows.go new file mode 100644 index 0000000..8a44a96 --- /dev/null +++ b/pkg/interactive/windows.go @@ -0,0 +1,21 @@ +// +build windows + +package interactive + +import ( + "os" + "syscall" +) + +func termHandle() (*os.File, error) { + var tty *os.File + _, err := syscall.Open("CONIN$", syscall.O_RDWR, 0) + if err != nil { + return tty, err + } + tty, err = os.Open("CONIN$") + if err != nil { + return tty, err + } + return tty, nil +} diff --git a/pkg/output/file_csv.go b/pkg/output/file_csv.go index 3024aab..70f0dee 100644 --- a/pkg/output/file_csv.go +++ b/pkg/output/file_csv.go @@ -9,16 +9,16 @@ "github.com/ffuf/ffuf/pkg/ffuf" ) -var staticheaders = []string{"url", "redirectlocation", "position", "status_code", "content_length", "content_words", "content_lines", "resultfile"} +var staticheaders = []string{"url", "redirectlocation", "position", "status_code", "content_length", "content_words", "content_lines", "content_type", "resultfile"} -func writeCSV(config *ffuf.Config, res []Result, encode bool) error { - - if(config.OutputCreateEmptyFile && (len(res) == 0)){ +func writeCSV(filename string, config *ffuf.Config, res []ffuf.Result, encode bool) error { + + if config.OutputCreateEmptyFile && (len(res) == 0) { return nil } - + header := make([]string, 0) - f, err := os.Create(config.OutputFile) + f, err := os.Create(filename) if err != nil { return err } @@ -56,7 +56,7 @@ return base64.StdEncoding.EncodeToString(in) } -func toCSV(r Result) []string { +func toCSV(r ffuf.Result) []string { res := make([]string, 0) for _, v := range r.Input { res = append(res, string(v)) @@ -68,6 +68,7 @@ res = append(res, strconv.FormatInt(r.ContentLength, 10)) res = append(res, strconv.FormatInt(r.ContentWords, 10)) res = append(res, strconv.FormatInt(r.ContentLines, 10)) + res = append(res, r.ContentType) res = append(res, r.ResultFile) return res } diff --git a/pkg/output/file_html.go b/pkg/output/file_html.go index ee5aa4d..2f6bcf2 100644 --- a/pkg/output/file_html.go +++ b/pkg/output/file_html.go @@ -12,7 +12,7 @@ CommandLine string Time string Keys []string - Results []Result + Results []ffuf.Result } const ( @@ -76,7 +76,8 @@