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 @@ Position Length Words - Lines + Lines + Type Resultfile @@ -84,7 +85,7 @@ {{range $result := .Results}}
-|result_raw|{{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}|{{ $value | printf "%s" }}{{ end }}|{{ $result.Url }}|{{ $result.RedirectLocation }}|{{ $result.Position }}|{{ $result.ContentLength }}|{{ $result.ContentWords }}|{{ $result.ContentLines }}| +|result_raw|{{ $result.StatusCode }}{{ range $keyword, $value := $result.Input }}|{{ $value | printf "%s" }}{{ end }}|{{ $result.Url }}|{{ $result.RedirectLocation }}|{{ $result.Position }}|{{ $result.ContentLength }}|{{ $result.ContentWords }}|{{ $result.ContentLines }}|{{ $result.ContentType }}|
{{ $result.StatusCode }} @@ -96,7 +97,8 @@ {{ $result.Position }} {{ $result.ContentLength }} {{ $result.ContentWords }} - {{ $result.ContentLines }} + {{ $result.ContentLines }} + {{ $result.ContentType }} {{ $result.ResultFile }} {{ end }} @@ -143,8 +145,8 @@ ) // colorizeResults returns a new slice with HTMLColor attribute -func colorizeResults(results []Result) []Result { - newResults := make([]Result, 0) +func colorizeResults(results []ffuf.Result) []ffuf.Result { + newResults := make([]ffuf.Result, 0) for _, r := range results { result := r @@ -174,12 +176,12 @@ return newResults } -func writeHTML(config *ffuf.Config, results []Result) error { - - if(config.OutputCreateEmptyFile && (len(results) == 0)){ +func writeHTML(filename string, config *ffuf.Config, results []ffuf.Result) error { + + if config.OutputCreateEmptyFile && (len(results) == 0) { return nil - } - + } + results = colorizeResults(results) ti := time.Now() @@ -196,7 +198,7 @@ Keys: keywords, } - f, err := os.Create(config.OutputFile) + f, err := os.Create(filename) if err != nil { return err } diff --git a/pkg/output/file_json.go b/pkg/output/file_json.go index 604b4fc..cf495b2 100644 --- a/pkg/output/file_json.go +++ b/pkg/output/file_json.go @@ -9,10 +9,10 @@ ) type ejsonFileOutput struct { - CommandLine string `json:"commandline"` - Time string `json:"time"` - Results []Result `json:"results"` - Config *ffuf.Config `json:"config"` + CommandLine string `json:"commandline"` + Time string `json:"time"` + Results []ffuf.Result `json:"results"` + Config *ffuf.Config `json:"config"` } type JsonResult struct { @@ -22,6 +22,7 @@ ContentLength int64 `json:"length"` ContentWords int64 `json:"words"` ContentLines int64 `json:"lines"` + ContentType string `json:"content-type"` RedirectLocation string `json:"redirectlocation"` ResultFile string `json:"resultfile"` Url string `json:"url"` @@ -35,12 +36,12 @@ Config *ffuf.Config `json:"config"` } -func writeEJSON(config *ffuf.Config, res []Result) error { - - if(config.OutputCreateEmptyFile && (len(res) == 0)){ +func writeEJSON(filename string, config *ffuf.Config, res []ffuf.Result) error { + + if config.OutputCreateEmptyFile && (len(res) == 0) { return nil } - + t := time.Now() outJSON := ejsonFileOutput{ CommandLine: config.CommandLine, @@ -52,14 +53,14 @@ if err != nil { return err } - err = ioutil.WriteFile(config.OutputFile, outBytes, 0644) + err = ioutil.WriteFile(filename, outBytes, 0644) if err != nil { return err } return nil } -func writeJSON(config *ffuf.Config, res []Result) error { +func writeJSON(filename string, config *ffuf.Config, res []ffuf.Result) error { t := time.Now() jsonRes := make([]JsonResult, 0) for _, r := range res { @@ -74,6 +75,7 @@ ContentLength: r.ContentLength, ContentWords: r.ContentWords, ContentLines: r.ContentLines, + ContentType: r.ContentType, RedirectLocation: r.RedirectLocation, ResultFile: r.ResultFile, Url: r.Url, @@ -90,7 +92,7 @@ if err != nil { return err } - err = ioutil.WriteFile(config.OutputFile, outBytes, 0644) + err = ioutil.WriteFile(filename, outBytes, 0644) if err != nil { return err } diff --git a/pkg/output/file_md.go b/pkg/output/file_md.go index 6e25b4a..86db0b8 100644 --- a/pkg/output/file_md.go +++ b/pkg/output/file_md.go @@ -14,15 +14,15 @@ Command line : ` + "`{{.CommandLine}}`" + ` Time: ` + "{{ .Time }}" + ` - {{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | ResultFile | - {{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | :--------- | - {{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{ .ResultFile }} | + {{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | Content Type | ResultFile | + {{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | :--------- | :----------- | + {{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{ .ContentType }} | {{ .ResultFile }} | {{end}}` // The template format is not pretty but follows the markdown guide ) -func writeMarkdown(config *ffuf.Config, res []Result) error { +func writeMarkdown(filename string, config *ffuf.Config, res []ffuf.Result) error { - if(config.OutputCreateEmptyFile && (len(res) == 0)){ + if config.OutputCreateEmptyFile && (len(res) == 0) { return nil } @@ -40,7 +40,7 @@ Keys: keywords, } - f, err := os.Create(config.OutputFile) + f, err := os.Create(filename) if err != nil { return err } diff --git a/pkg/output/stdout.go b/pkg/output/stdout.go index 8de94a1..b68cedd 100644 --- a/pkg/output/stdout.go +++ b/pkg/output/stdout.go @@ -7,6 +7,7 @@ "os" "path" "strconv" + "strings" "time" "github.com/ffuf/ffuf/pkg/ffuf" @@ -26,32 +27,19 @@ type Stdoutput struct { config *ffuf.Config - Results []Result -} - -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"` - RedirectLocation string `json:"redirectlocation"` - Url string `json:"url"` - ResultFile string `json:"resultfile"` - Host string `json:"host"` - HTMLColor string `json:"-"` + Results []ffuf.Result } func NewStdoutput(conf *ffuf.Config) *Stdoutput { var outp Stdoutput outp.config = conf - outp.Results = []Result{} + outp.Results = []ffuf.Result{} return &outp } func (s *Stdoutput) Banner() { - fmt.Fprintf(os.Stderr, "%s\n v%s\n%s\n\n", BANNER_HEADER, ffuf.VERSION, BANNER_SEP) + version := strings.ReplaceAll(ffuf.Version(), "<3", fmt.Sprintf("%s<3%s", ANSI_RED, ANSI_CLEAR)) + fmt.Fprintf(os.Stderr, "%s\n v%s\n%s\n\n", BANNER_HEADER, version, BANNER_SEP) printOption([]byte("Method"), []byte(s.config.Method)) printOption([]byte("URL"), []byte(s.config.Url)) @@ -134,13 +122,28 @@ // Print matchers for _, f := range s.config.Matchers { - printOption([]byte("Matcher"), []byte(f.Repr())) + printOption([]byte("Matcher"), []byte(f.ReprVerbose())) } // Print filters for _, f := range s.config.Filters { - printOption([]byte("Filter"), []byte(f.Repr())) + printOption([]byte("Filter"), []byte(f.ReprVerbose())) } fmt.Fprintf(os.Stderr, "%s\n\n", BANNER_SEP) +} + +// Reset resets the result slice +func (s *Stdoutput) Reset() { + s.Results = make([]ffuf.Result, 0) +} + +// GetResults returns the result slice +func (s *Stdoutput) GetResults() []ffuf.Result { + return s.Results +} + +// SetResults sets the result slice +func (s *Stdoutput) SetResults(results []ffuf.Result) { + s.Results = results } func (s *Stdoutput) Progress(status ffuf.Progress) { @@ -164,7 +167,7 @@ dur -= mins * time.Minute secs := dur / time.Second - fmt.Fprintf(os.Stderr, "%s:: Progress: [%d/%d] :: Job [%d/%d] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", TERMINAL_CLEAR_LINE, status.ReqCount, status.ReqTotal, status.QueuePos, status.QueueTotal, reqRate, hours, mins, secs, status.ErrorCount) + fmt.Fprintf(os.Stderr, "%s:: Progress: [%d/%d] :: Job [%d/%d] :: %d req/sec :: Duration: [%d:%02d:%02d] :: Errors: %d ::", TERMINAL_CLEAR_LINE, status.ReqCount, status.ReqTotal, status.QueuePos, status.QueueTotal, reqRate, hours, mins, secs, status.ErrorCount) } func (s *Stdoutput) Info(infostring string) { @@ -172,9 +175,9 @@ fmt.Fprintf(os.Stderr, "%s", infostring) } else { if !s.config.Colors { - fmt.Fprintf(os.Stderr, "%s[INFO] %s\n", TERMINAL_CLEAR_LINE, infostring) - } else { - fmt.Fprintf(os.Stderr, "%s[%sINFO%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_BLUE, ANSI_CLEAR, infostring) + fmt.Fprintf(os.Stderr, "%s[INFO] %s\n\n", TERMINAL_CLEAR_LINE, infostring) + } else { + fmt.Fprintf(os.Stderr, "%s[%sINFO%s] %s\n\n", TERMINAL_CLEAR_LINE, ANSI_BLUE, ANSI_CLEAR, infostring) } } } @@ -196,56 +199,60 @@ fmt.Fprintf(os.Stderr, "%s", warnstring) } else { if !s.config.Colors { - fmt.Fprintf(os.Stderr, "%s[WARN] %s", TERMINAL_CLEAR_LINE, warnstring) + fmt.Fprintf(os.Stderr, "%s[WARN] %s\n", TERMINAL_CLEAR_LINE, warnstring) } else { fmt.Fprintf(os.Stderr, "%s[%sWARN%s] %s\n", TERMINAL_CLEAR_LINE, ANSI_RED, ANSI_CLEAR, warnstring) } } } -func (s *Stdoutput) writeToAll(config *ffuf.Config, res []Result) error { +func (s *Stdoutput) Raw(output string) { + fmt.Fprintf(os.Stderr, "%s%s", TERMINAL_CLEAR_LINE, output) +} + +func (s *Stdoutput) writeToAll(filename string, config *ffuf.Config, res []ffuf.Result) error { var err error var BaseFilename string = s.config.OutputFile // Go through each type of write, adding // the suffix to each output file. - if(config.OutputCreateEmptyFile && (len(res) == 0)){ + if config.OutputCreateEmptyFile && (len(res) == 0) { return nil - } + } s.config.OutputFile = BaseFilename + ".json" - err = writeJSON(s.config, s.Results) + err = writeJSON(filename, s.config, s.Results) if err != nil { s.Error(err.Error()) } s.config.OutputFile = BaseFilename + ".ejson" - err = writeEJSON(s.config, s.Results) + err = writeEJSON(filename, s.config, s.Results) if err != nil { s.Error(err.Error()) } s.config.OutputFile = BaseFilename + ".html" - err = writeHTML(s.config, s.Results) + err = writeHTML(filename, s.config, s.Results) if err != nil { s.Error(err.Error()) } s.config.OutputFile = BaseFilename + ".md" - err = writeMarkdown(s.config, s.Results) + err = writeMarkdown(filename, s.config, s.Results) if err != nil { s.Error(err.Error()) } s.config.OutputFile = BaseFilename + ".csv" - err = writeCSV(s.config, s.Results, false) + err = writeCSV(filename, s.config, s.Results, false) if err != nil { s.Error(err.Error()) } s.config.OutputFile = BaseFilename + ".ecsv" - err = writeCSV(s.config, s.Results, true) + err = writeCSV(filename, s.config, s.Results, true) if err != nil { s.Error(err.Error()) } @@ -254,24 +261,33 @@ } +// SaveFile saves the current results to a file of a given type +func (s *Stdoutput) SaveFile(filename, format string) error { + var err error + switch format { + case "all": + err = s.writeToAll(filename, s.config, s.Results) + case "json": + err = writeJSON(filename, s.config, s.Results) + case "ejson": + err = writeEJSON(filename, s.config, s.Results) + case "html": + err = writeHTML(filename, s.config, s.Results) + case "md": + err = writeMarkdown(filename, s.config, s.Results) + case "csv": + err = writeCSV(filename, s.config, s.Results, false) + case "ecsv": + err = writeCSV(filename, s.config, s.Results, true) + } + return err +} + +// Finalize gets run after all the ffuf jobs are completed func (s *Stdoutput) Finalize() error { var err error if s.config.OutputFile != "" { - if s.config.OutputFormat == "all" { - err = s.writeToAll(s.config, s.Results) - } else if s.config.OutputFormat == "json" { - err = writeJSON(s.config, s.Results) - } else if s.config.OutputFormat == "ejson" { - err = writeEJSON(s.config, s.Results) - } else if s.config.OutputFormat == "html" { - err = writeHTML(s.config, s.Results) - } else if s.config.OutputFormat == "md" { - err = writeMarkdown(s.config, s.Results) - } else if s.config.OutputFormat == "csv" { - err = writeCSV(s.config, s.Results, false) - } else if s.config.OutputFormat == "ecsv" { - err = writeCSV(s.config, s.Results, true) - } + err = s.SaveFile(s.config.OutputFile, s.config.OutputFormat) if err != nil { s.Error(err.Error()) } @@ -285,29 +301,27 @@ if len(s.config.OutputDirectory) > 0 { resp.ResultFile = s.writeResultToFile(resp) } + + inputs := make(map[string][]byte, len(resp.Request.Input)) + for k, v := range resp.Request.Input { + inputs[k] = v + } + sResult := ffuf.Result{ + Input: inputs, + Position: resp.Request.Position, + StatusCode: resp.StatusCode, + ContentLength: resp.ContentLength, + ContentWords: resp.ContentWords, + ContentLines: resp.ContentLines, + ContentType: resp.ContentType, + RedirectLocation: resp.GetRedirectLocation(false), + Url: resp.Request.Url, + ResultFile: resp.ResultFile, + Host: resp.Request.Host, + } + s.Results = append(s.Results, sResult) // Output the result - s.printResult(resp) - // Check if we need the data later - if s.config.OutputFile != "" { - // No need to store results if we're not going to use them later - inputs := make(map[string][]byte, len(resp.Request.Input)) - for k, v := range resp.Request.Input { - inputs[k] = v - } - sResult := Result{ - Input: inputs, - Position: resp.Request.Position, - StatusCode: resp.StatusCode, - ContentLength: resp.ContentLength, - ContentWords: resp.ContentWords, - ContentLines: resp.ContentLines, - RedirectLocation: resp.GetRedirectLocation(false), - Url: resp.Request.Url, - ResultFile: resp.ResultFile, - Host: resp.Request.Host, - } - s.Results = append(s.Results, sResult) - } + s.PrintResult(sResult) } func (s *Stdoutput) writeResultToFile(resp ffuf.Response) string { @@ -335,35 +349,35 @@ return fileName } -func (s *Stdoutput) printResult(resp ffuf.Response) { +func (s *Stdoutput) PrintResult(res ffuf.Result) { if s.config.Quiet { - s.resultQuiet(resp) - } else { - if len(resp.Request.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0 { + s.resultQuiet(res) + } else { + if len(res.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0 { // Print a multi-line result (when using multiple input keywords and wordlists) - s.resultMultiline(resp) - } else { - s.resultNormal(resp) - } - } -} - -func (s *Stdoutput) prepareInputsOneLine(resp ffuf.Response) string { + s.resultMultiline(res) + } else { + s.resultNormal(res) + } + } +} + +func (s *Stdoutput) prepareInputsOneLine(res ffuf.Result) string { inputs := "" - if len(resp.Request.Input) > 1 { - for k, v := range resp.Request.Input { + if len(res.Input) > 1 { + for k, v := range res.Input { if inSlice(k, s.config.CommandKeywords) { // If we're using external command for input, display the position instead of input - inputs = fmt.Sprintf("%s%s : %s ", inputs, k, strconv.Itoa(resp.Request.Position)) + inputs = fmt.Sprintf("%s%s : %s ", inputs, k, strconv.Itoa(res.Position)) } else { inputs = fmt.Sprintf("%s%s : %s ", inputs, k, v) } } } else { - for k, v := range resp.Request.Input { + for k, v := range res.Input { if inSlice(k, s.config.CommandKeywords) { // If we're using external command for input, display the position instead of input - inputs = strconv.Itoa(resp.Request.Position) + inputs = strconv.Itoa(res.Position) } else { inputs = string(v) } @@ -372,30 +386,30 @@ return inputs } -func (s *Stdoutput) resultQuiet(resp ffuf.Response) { - fmt.Println(s.prepareInputsOneLine(resp)) -} - -func (s *Stdoutput) resultMultiline(resp ffuf.Response) { +func (s *Stdoutput) resultQuiet(res ffuf.Result) { + fmt.Println(s.prepareInputsOneLine(res)) +} + +func (s *Stdoutput) resultMultiline(res ffuf.Result) { var res_hdr, res_str string res_str = "%s%s * %s: %s\n" - res_hdr = fmt.Sprintf("%s[Status: %d, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, resp.StatusCode, resp.ContentLength, resp.ContentWords, resp.ContentLines) - res_hdr = s.colorize(res_hdr, resp.StatusCode) + res_hdr = fmt.Sprintf("%s[Status: %d, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, res.StatusCode, res.ContentLength, res.ContentWords, res.ContentLines) + res_hdr = s.colorize(res_hdr, res.StatusCode) reslines := "" if s.config.Verbose { - reslines = fmt.Sprintf("%s%s| URL | %s\n", reslines, TERMINAL_CLEAR_LINE, resp.Request.Url) - redirectLocation := resp.GetRedirectLocation(false) + reslines = fmt.Sprintf("%s%s| URL | %s\n", reslines, TERMINAL_CLEAR_LINE, res.Url) + redirectLocation := res.RedirectLocation if redirectLocation != "" { reslines = fmt.Sprintf("%s%s| --> | %s\n", reslines, TERMINAL_CLEAR_LINE, redirectLocation) } } - if resp.ResultFile != "" { - reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, resp.ResultFile) - } - for k, v := range resp.Request.Input { + if res.ResultFile != "" { + reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, res.ResultFile) + } + for k, v := range res.Input { if inSlice(k, s.config.CommandKeywords) { // If we're using external command for input, display the position instead of input - reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, strconv.Itoa(resp.Request.Position)) + reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, strconv.Itoa(res.Position)) } else { // Wordlist input reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, v) @@ -404,9 +418,9 @@ fmt.Printf("%s\n%s\n", res_hdr, reslines) } -func (s *Stdoutput) resultNormal(resp ffuf.Response) { - res := fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, s.prepareInputsOneLine(resp), s.colorize(fmt.Sprintf("%d", resp.StatusCode), resp.StatusCode), resp.ContentLength, resp.ContentWords, resp.ContentLines) - fmt.Println(res) +func (s *Stdoutput) resultNormal(res ffuf.Result) { + resnormal := fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, s.prepareInputsOneLine(res), s.colorize(fmt.Sprintf("%d", res.StatusCode), res.StatusCode), res.ContentLength, res.ContentWords, res.ContentLines) + fmt.Println(resnormal) } func (s *Stdoutput) colorize(input string, status int64) string { diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index 2dca7e0..e13415e 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -104,7 +104,7 @@ // set default User-Agent header if not present if _, ok := req.Headers["User-Agent"]; !ok { - req.Headers["User-Agent"] = fmt.Sprintf("%s v%s", "Fuzz Faster U Fool", ffuf.VERSION) + req.Headers["User-Agent"] = fmt.Sprintf("%s v%s", "Fuzz Faster U Fool", ffuf.Version()) } // Handle Go http.Request special cases