2018-11-08 09:26:32 +00:00
package main
import (
"context"
"flag"
"fmt"
2019-01-21 20:43:04 +00:00
"net/http"
"net/url"
2018-11-08 09:26:32 +00:00
"os"
2018-12-05 22:57:42 +00:00
"strconv"
2018-11-08 09:26:32 +00:00
"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 cliOptions struct {
2019-04-06 16:49:09 +00:00
extensions string
2018-12-05 22:57:42 +00:00
delay string
2018-11-12 17:47:49 +00:00
filterStatus string
filterSize string
filterRegexp string
filterWords string
matcherStatus string
matcherSize string
matcherRegexp string
matcherWords string
2019-01-21 20:43:04 +00:00
proxyURL string
2019-03-29 23:02:41 +00:00
outputFormat string
2018-12-05 22:57:42 +00:00
headers multiStringFlag
2018-11-15 08:47:43 +00:00
showVersion bool
2018-11-08 09:26:32 +00:00
}
2018-12-05 22:57:42 +00:00
type multiStringFlag [ ] string
2018-11-08 09:26:32 +00:00
2018-12-05 22:57:42 +00:00
func ( m * multiStringFlag ) String ( ) string {
2018-11-08 09:26:32 +00:00
return ""
}
2018-12-05 22:57:42 +00:00
func ( m * multiStringFlag ) Set ( value string ) error {
* m = append ( * m , value )
2018-11-08 09:26:32 +00:00
return nil
}
func main ( ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
conf := ffuf . NewConfig ( ctx )
opts := cliOptions { }
2019-04-27 07:47:24 +00:00
flag . StringVar ( & opts . extensions , "e" , "" , "Comma separated list of extensions to apply. Each extension provided will extend the wordlist entry once." )
2019-04-10 18:50:38 +00:00
flag . BoolVar ( & conf . DirSearchCompat , "D" , false , "DirSearch style wordlist compatibility mode. Used in conjunction with -e flag. Replaces %EXT% in wordlist entry with each of the extensions provided by -e." )
2018-11-12 21:24:37 +00:00
flag . Var ( & opts . headers , "H" , "Header `\"Name: Value\"`, separated by colon. Multiple -H flags are accepted." )
2018-11-08 09:26:32 +00:00
flag . StringVar ( & conf . Url , "u" , "" , "Target URL" )
flag . StringVar ( & conf . Wordlist , "w" , "" , "Wordlist path" )
2019-04-06 15:54:27 +00:00
flag . BoolVar ( & conf . TLSVerify , "k" , false , "TLS identity verification" )
2018-12-05 22:57:42 +00:00
flag . StringVar ( & opts . delay , "p" , "" , "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"" )
2018-11-08 09:26:32 +00:00
flag . StringVar ( & opts . filterStatus , "fc" , "" , "Filter HTTP status codes from response" )
flag . StringVar ( & opts . filterSize , "fs" , "" , "Filter HTTP response size" )
2018-11-12 17:47:49 +00:00
flag . StringVar ( & opts . filterRegexp , "fr" , "" , "Filter regexp" )
2018-11-12 17:06:49 +00:00
flag . StringVar ( & opts . filterWords , "fw" , "" , "Filter by amount of words in response" )
2018-11-08 09:49:06 +00:00
flag . StringVar ( & conf . Data , "d" , "" , "POST data." )
2018-11-09 13:21:23 +00:00
flag . BoolVar ( & conf . Colors , "c" , false , "Colorize output." )
2019-04-13 13:02:00 +00:00
flag . StringVar ( & opts . matcherStatus , "mc" , "200,204,301,302,307,401,403" , "Match HTTP status codes from respose, use \"all\" to match every response code." )
2018-11-08 09:26:32 +00:00
flag . StringVar ( & opts . matcherSize , "ms" , "" , "Match HTTP response size" )
2018-11-12 17:47:49 +00:00
flag . StringVar ( & opts . matcherRegexp , "mr" , "" , "Match regexp" )
2018-11-12 17:06:49 +00:00
flag . StringVar ( & opts . matcherWords , "mw" , "" , "Match amount of words in response" )
2019-01-21 20:43:04 +00:00
flag . StringVar ( & opts . proxyURL , "x" , "" , "HTTP Proxy URL" )
flag . StringVar ( & conf . Method , "X" , "GET" , "HTTP method to use" )
2019-03-29 23:02:41 +00:00
flag . StringVar ( & conf . OutputFile , "o" , "" , "Write output to file" )
2019-04-03 09:51:42 +00:00
flag . StringVar ( & opts . outputFormat , "of" , "json" , "Output file format. Available formats: json, csv, ecsv" )
2018-11-08 09:26:32 +00:00
flag . BoolVar ( & conf . Quiet , "s" , false , "Do not print additional information (silent mode)" )
2019-04-27 07:47:24 +00:00
flag . BoolVar ( & conf . StopOn403 , "sf" , false , "Stop when > 95% of responses return 403 Forbidden" )
2019-04-03 20:11:49 +00:00
flag . BoolVar ( & conf . StopOnErrors , "se" , false , "Stop on spurious errors" )
flag . BoolVar ( & conf . StopOnAll , "sa" , false , "Stop on all error cases. Implies -sf and -se" )
2019-04-03 09:54:32 +00:00
flag . BoolVar ( & conf . FollowRedirects , "r" , false , "Follow redirects" )
2019-04-20 17:46:43 +00:00
flag . BoolVar ( & conf . AutoCalibration , "ac" , false , "Automatically calibrate filtering options" )
2018-11-12 18:51:29 +00:00
flag . IntVar ( & conf . Threads , "t" , 40 , "Number of concurrent threads." )
2019-04-27 07:29:05 +00:00
flag . IntVar ( & conf . Timeout , "timeout" , 10 , "HTTP request timeout in seconds." )
2018-11-15 08:47:43 +00:00
flag . BoolVar ( & opts . showVersion , "V" , false , "Show version information." )
2018-11-08 09:26:32 +00:00
flag . Parse ( )
2018-11-15 08:47:43 +00:00
if opts . showVersion {
fmt . Printf ( "ffuf version: %s\n" , ffuf . VERSION )
os . Exit ( 0 )
}
2018-11-08 09:26:32 +00:00
if err := prepareConfig ( & opts , & conf ) ; err != nil {
2018-11-08 13:54:12 +00:00
fmt . Fprintf ( os . Stderr , "Encountered error(s): %s\n" , err )
2018-11-08 09:26:32 +00:00
flag . Usage ( )
os . Exit ( 1 )
}
if err := prepareFilters ( & opts , & conf ) ; err != nil {
2018-11-08 13:54:12 +00:00
fmt . Fprintf ( os . Stderr , "Encountered error(s): %s\n" , err )
2018-11-08 09:26:32 +00:00
flag . Usage ( )
os . Exit ( 1 )
}
job , err := prepareJob ( & conf )
if err != nil {
2018-11-08 13:54:12 +00:00
fmt . Fprintf ( os . Stderr , "Encountered error(s): %s\n" , err )
2018-11-08 09:26:32 +00:00
flag . Usage ( )
os . Exit ( 1 )
}
2019-04-20 17:46:43 +00:00
if conf . AutoCalibration {
// Handle the calibration
responses , err := job . CalibrateResponses ( )
if err != nil {
fmt . Fprintf ( os . Stderr , "Error in autocalibration, exiting: %s\n" , err )
os . Exit ( 1 )
}
if len ( responses ) > 0 {
calibrateFilters ( responses , & conf )
}
}
2018-11-08 09:26:32 +00:00
// Job handles waiting for goroutines to complete itself
job . Start ( )
}
2019-04-20 17:46:43 +00:00
func calibrateFilters ( responses [ ] ffuf . Response , conf * ffuf . Config ) {
sizeCalib := make ( [ ] string , 0 )
wordCalib := make ( [ ] string , 0 )
for _ , r := range responses {
if r . ContentLength > 1 {
// Only add if we have an actual size of responses
sizeCalib = append ( sizeCalib , strconv . FormatInt ( r . ContentLength , 10 ) )
}
if r . ContentWords > 1 {
// Only add if we have an actual word length of response
wordCalib = append ( wordCalib , strconv . FormatInt ( r . ContentWords , 10 ) )
}
}
if len ( sizeCalib ) > 0 {
addFilter ( conf , "size" , strings . Join ( sizeCalib , "," ) )
}
if len ( wordCalib ) > 0 {
addFilter ( conf , "word" , strings . Join ( wordCalib , "," ) )
}
}
2018-11-08 09:26:32 +00:00
func prepareJob ( conf * ffuf . Config ) ( * ffuf . Job , error ) {
2018-11-14 22:18:43 +00:00
errs := ffuf . NewMultierror ( )
2018-11-08 09:26:32 +00:00
// TODO: implement error handling for runnerprovider and outputprovider
// We only have http runner right now
runprovider := runner . NewRunnerByName ( "http" , conf )
// We only have wordlist inputprovider right now
inputprovider , err := input . NewInputProviderByName ( "wordlist" , conf )
if err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( fmt . Errorf ( "%s" , err ) )
2018-11-08 09:26:32 +00:00
}
// We only have stdout outputprovider right now
outprovider := output . NewOutputProviderByName ( "stdout" , conf )
return & ffuf . Job {
Config : conf ,
Runner : runprovider ,
Output : outprovider ,
Input : inputprovider ,
2018-11-14 22:18:43 +00:00
} , errs . ErrorOrNil ( )
2018-11-08 09:26:32 +00:00
}
func prepareConfig ( parseOpts * cliOptions , conf * ffuf . Config ) error {
//TODO: refactor in a proper flag library that can handle things like required flags
2018-11-14 22:18:43 +00:00
errs := ffuf . NewMultierror ( )
2018-11-08 09:26:32 +00:00
foundkeyword := false
2018-12-05 22:57:42 +00:00
var err error
var err2 error
2018-11-08 09:26:32 +00:00
if len ( conf . Url ) == 0 {
2018-11-14 22:18:43 +00:00
errs . Add ( fmt . Errorf ( "-u flag is required" ) )
2018-11-08 09:26:32 +00:00
}
if len ( conf . Wordlist ) == 0 {
2018-11-14 22:18:43 +00:00
errs . Add ( fmt . Errorf ( "-w flag is required" ) )
2018-11-08 09:26:32 +00:00
}
2019-04-06 16:49:09 +00:00
// prepare extensions
if parseOpts . extensions != "" {
extensions := strings . Split ( parseOpts . extensions , "," )
conf . Extensions = extensions
}
2018-11-08 09:26:32 +00:00
//Prepare headers
for _ , v := range parseOpts . headers {
hs := strings . SplitN ( v , ":" , 2 )
if len ( hs ) == 2 {
fuzzedheader := false
for _ , fv := range hs {
if strings . Index ( fv , "FUZZ" ) != - 1 {
// Add to fuzzheaders
fuzzedheader = true
}
}
if fuzzedheader {
conf . FuzzHeaders [ strings . TrimSpace ( hs [ 0 ] ) ] = strings . TrimSpace ( hs [ 1 ] )
foundkeyword = true
} else {
conf . StaticHeaders [ strings . TrimSpace ( hs [ 0 ] ) ] = strings . TrimSpace ( hs [ 1 ] )
}
} else {
2018-11-14 22:18:43 +00:00
errs . Add ( fmt . Errorf ( "Header defined by -H needs to have a value. \":\" should be used as a separator" ) )
2018-11-08 09:26:32 +00:00
}
}
2018-12-05 22:57:42 +00:00
//Prepare delay
d := strings . Split ( parseOpts . delay , "-" )
if len ( d ) > 2 {
errs . Add ( fmt . Errorf ( "Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\"" ) )
} else if len ( d ) == 2 {
conf . Delay . IsRange = true
conf . Delay . HasDelay = true
conf . Delay . Min , err = strconv . ParseFloat ( d [ 0 ] , 64 )
conf . Delay . Max , err2 = strconv . ParseFloat ( d [ 1 ] , 64 )
if err != nil || err2 != nil {
errs . Add ( fmt . Errorf ( "Delay range min and max values need to be valid floats. For example: 0.1-0.5" ) )
}
} else if len ( parseOpts . delay ) > 0 {
conf . Delay . IsRange = false
conf . Delay . HasDelay = true
conf . Delay . Min , err = strconv . ParseFloat ( parseOpts . delay , 64 )
if err != nil {
errs . Add ( fmt . Errorf ( "Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\"" ) )
}
}
2019-01-21 20:43:04 +00:00
// Verify proxy url format
if len ( parseOpts . proxyURL ) > 0 {
pu , err := url . Parse ( parseOpts . proxyURL )
if err != nil {
errs . Add ( fmt . Errorf ( "Bad proxy url (-x) format: %s" , err ) )
} else {
conf . ProxyURL = http . ProxyURL ( pu )
}
}
2019-03-29 23:02:41 +00:00
//Check the output file format option
if conf . OutputFile != "" {
//No need to check / error out if output file isn't defined
2019-04-03 09:51:42 +00:00
outputFormats := [ ] string { "json" , "csv" , "ecsv" }
2019-03-29 23:02:41 +00:00
found := false
for _ , f := range outputFormats {
if f == parseOpts . outputFormat {
conf . OutputFormat = f
found = true
}
}
if ! found {
errs . Add ( fmt . Errorf ( "Unknown output file format (-of): %s" , parseOpts . outputFormat ) )
}
}
conf . CommandLine = strings . Join ( os . Args , " " )
2018-11-08 09:49:06 +00:00
//Search for keyword from URL and POST data too
2018-11-08 09:26:32 +00:00
if strings . Index ( conf . Url , "FUZZ" ) != - 1 {
foundkeyword = true
}
2018-11-08 09:49:06 +00:00
if strings . Index ( conf . Data , "FUZZ" ) != - 1 {
foundkeyword = true
}
2018-11-08 09:26:32 +00:00
if ! foundkeyword {
2018-11-14 22:18:43 +00:00
errs . Add ( fmt . Errorf ( "No FUZZ keyword(s) found in headers, URL or POST data, nothing to do" ) )
2018-11-08 09:26:32 +00:00
}
2019-03-29 23:02:41 +00:00
2018-11-14 22:18:43 +00:00
return errs . ErrorOrNil ( )
2018-11-08 09:26:32 +00:00
}
func prepareFilters ( parseOpts * cliOptions , conf * ffuf . Config ) error {
2018-11-14 22:18:43 +00:00
errs := ffuf . NewMultierror ( )
2018-11-08 09:26:32 +00:00
if parseOpts . filterStatus != "" {
if err := addFilter ( conf , "status" , parseOpts . filterStatus ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-08 09:26:32 +00:00
}
}
if parseOpts . filterSize != "" {
if err := addFilter ( conf , "size" , parseOpts . filterSize ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-08 09:26:32 +00:00
}
}
2018-11-12 17:47:49 +00:00
if parseOpts . filterRegexp != "" {
if err := addFilter ( conf , "regexp" , parseOpts . filterRegexp ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-12 17:47:49 +00:00
}
}
2018-11-12 17:06:49 +00:00
if parseOpts . filterWords != "" {
if err := addFilter ( conf , "word" , parseOpts . filterWords ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-12 17:06:49 +00:00
}
}
2018-11-08 09:26:32 +00:00
if parseOpts . matcherStatus != "" {
if err := addMatcher ( conf , "status" , parseOpts . matcherStatus ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-08 09:26:32 +00:00
}
}
if parseOpts . matcherSize != "" {
if err := addMatcher ( conf , "size" , parseOpts . matcherSize ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-08 09:26:32 +00:00
}
}
2018-11-12 17:47:49 +00:00
if parseOpts . matcherRegexp != "" {
if err := addMatcher ( conf , "regexp" , parseOpts . matcherRegexp ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-12 17:47:49 +00:00
}
}
2018-11-12 17:06:49 +00:00
if parseOpts . matcherWords != "" {
if err := addMatcher ( conf , "word" , parseOpts . matcherWords ) ; err != nil {
2018-11-14 22:18:43 +00:00
errs . Add ( err )
2018-11-12 17:06:49 +00:00
}
}
2018-11-14 22:18:43 +00:00
return errs . ErrorOrNil ( )
2018-11-08 09:26:32 +00:00
}
func addFilter ( conf * ffuf . Config , name string , option string ) error {
newf , err := filter . NewFilterByName ( name , option )
if err == nil {
conf . Filters = append ( conf . Filters , newf )
}
return err
}
func addMatcher ( conf * ffuf . Config , name string , option string ) error {
newf , err := filter . NewFilterByName ( name , option )
if err == nil {
conf . Matchers = append ( conf . Matchers , newf )
}
return err
}