From ad2296f55b6cd5b5c854148b0ea9affcda39685e Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 8 Nov 2018 11:26:32 +0200 Subject: [PATCH] Initial commit, v0.1 --- .goreleaser.yml | 12 +++ README.md | 37 +++++++++ main.go | 183 +++++++++++++++++++++++++++++++++++++++++ pkg/ffuf/config.go | 34 ++++++++ pkg/ffuf/const.go | 6 ++ pkg/ffuf/interfaces.go | 25 ++++++ pkg/ffuf/job.go | 68 +++++++++++++++ pkg/ffuf/request.go | 18 ++++ pkg/ffuf/response.go | 24 ++++++ pkg/filter/filter.go | 15 ++++ pkg/filter/size.go | 42 ++++++++++ pkg/filter/status.go | 42 ++++++++++ pkg/input/input.go | 10 +++ pkg/input/wordlist.go | 71 ++++++++++++++++ pkg/output/output.go | 10 +++ pkg/output/stdout.go | 91 ++++++++++++++++++++ pkg/runner/runner.go | 10 +++ pkg/runner/simple.go | 92 +++++++++++++++++++++ 18 files changed, 790 insertions(+) create mode 100644 .goreleaser.yml create mode 100644 README.md create mode 100644 main.go create mode 100644 pkg/ffuf/config.go create mode 100644 pkg/ffuf/const.go create mode 100644 pkg/ffuf/interfaces.go create mode 100644 pkg/ffuf/job.go create mode 100644 pkg/ffuf/request.go create mode 100644 pkg/ffuf/response.go create mode 100644 pkg/filter/filter.go create mode 100644 pkg/filter/size.go create mode 100644 pkg/filter/status.go create mode 100644 pkg/input/input.go create mode 100644 pkg/input/wordlist.go create mode 100644 pkg/output/output.go create mode 100644 pkg/output/stdout.go create mode 100644 pkg/runner/runner.go create mode 100644 pkg/runner/simple.go diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..1d6c374 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,12 @@ +builds: + - binary: ffuf + goos: + - linux + goarch: + - amd64 + +archive: + format: tar.gz + +sign: + artifacts: checksum diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b0565d --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# ffuf - Fuzz Faster U Fool + +A fast web fuzzer written in Go, allows fuzzing of URL and header values. + +## Usage +``` +Usage of ./ffuf: + -H value + Header name and value, separated by colon. Multiple -H flags are accepted. + -X string + HTTP method to use. (default "GET") + -fc string + Filter HTTP status codes from response + -fr string + Filter regex + -fs string + Filter HTTP response size + -k Skip TLS identity verification (insecure) + -mc string + Match HTTP status codes from respose (default "200,204,301,302,307") + -mr string + Match regex + -ms string + Match HTTP response size + -s Do not print additional information (silent mode) + -t int + Number of concurrent threads. (default 20) + -u string + Target URL + -w string + Wordlist path +``` +eg. `ffuf -u https://example.org/FUZZ -w /path/to/wordlist` + +## Installation + +Either download a prebuilt binary from Releases page or install [Go 1.9 or newer](https://golang.org/doc/install). and build the project with `go get && go build` diff --git a/main.go b/main.go new file mode 100644 index 0000000..ac440ba --- /dev/null +++ b/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "context" + "flag" + "fmt" + "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" + + "github.com/hashicorp/go-multierror" +) + +type cliOptions struct { + filterStatus string + filterSize string + filterReflect string + filterRegex string + matcherStatus string + matcherSize string + matcherReflect string + matcherRegex string + headers headerFlags +} + +type headerFlags []string + +func (h *headerFlags) String() string { + return "" +} + +func (h *headerFlags) Set(value string) error { + *h = append(*h, value) + return nil +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + conf := ffuf.NewConfig(ctx) + opts := cliOptions{} + flag.Var(&opts.headers, "H", "Header name and value, separated by colon. Multiple -H flags are accepted.") + flag.StringVar(&conf.Url, "u", "", "Target URL") + flag.StringVar(&conf.Wordlist, "w", "", "Wordlist path") + flag.BoolVar(&conf.TLSSkipVerify, "k", false, "Skip TLS identity verification (insecure)") + flag.StringVar(&opts.filterStatus, "fc", "", "Filter HTTP status codes from response") + flag.StringVar(&opts.filterSize, "fs", "", "Filter HTTP response size") + flag.StringVar(&opts.filterRegex, "fr", "", "Filter regex") + //flag.StringVar(&opts.filterReflect, "fref", "", "Filter reflected payload") + flag.StringVar(&opts.matcherStatus, "mc", "200,204,301,302,307", "Match HTTP status codes from respose") + flag.StringVar(&opts.matcherSize, "ms", "", "Match HTTP response size") + flag.StringVar(&opts.matcherRegex, "mr", "", "Match regex") + flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use.") + flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)") + flag.IntVar(&conf.Threads, "t", 20, "Number of concurrent threads.") + //flag.StringVar(&opts.matcherReflect, "mref", "", "Match reflected payload") + flag.Parse() + if err := prepareConfig(&opts, &conf); err != nil { + fmt.Printf("Encountered error(s): %s\n", err) + flag.Usage() + os.Exit(1) + } + if err := prepareFilters(&opts, &conf); err != nil { + fmt.Printf("Encountered error(s): %s\n", err) + flag.Usage() + os.Exit(1) + } + + job, err := prepareJob(&conf) + if err != nil { + fmt.Printf("Encountered error(s): %s\n", err) + flag.Usage() + os.Exit(1) + } + // Job handles waiting for goroutines to complete itself + job.Start() +} + +func prepareJob(conf *ffuf.Config) (*ffuf.Job, error) { + var errlist *multierror.Error + // 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 { + errlist = multierror.Append(errlist, fmt.Errorf("%s", err)) + } + // We only have stdout outputprovider right now + outprovider := output.NewOutputProviderByName("stdout", conf) + return &ffuf.Job{ + Config: conf, + Runner: runprovider, + Output: outprovider, + Input: inputprovider, + }, errlist.ErrorOrNil() +} + +func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error { + //TODO: refactor in a proper flag library that can handle things like required flags + var errlist *multierror.Error + foundkeyword := false + if len(conf.Url) == 0 { + errlist = multierror.Append(errlist, fmt.Errorf("-u flag is required.")) + } + if len(conf.Wordlist) == 0 { + errlist = multierror.Append(errlist, fmt.Errorf("-w flag is required.")) + } + //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 { + errlist = multierror.Append(errlist, fmt.Errorf("Header defined by -H needs to have a value. \":\" should be used as a separator.")) + } + } + if strings.Index(conf.Url, "FUZZ") != -1 { + foundkeyword = true + } + if !foundkeyword { + errlist = multierror.Append(errlist, fmt.Errorf("No FUZZ keywords found in headers or URL, nothing to do.")) + } + return errlist.ErrorOrNil() +} + +func prepareFilters(parseOpts *cliOptions, conf *ffuf.Config) error { + var errlist *multierror.Error + if parseOpts.filterStatus != "" { + if err := addFilter(conf, "status", parseOpts.filterStatus); err != nil { + errlist = multierror.Append(errlist, err) + } + } + if parseOpts.filterSize != "" { + if err := addFilter(conf, "size", parseOpts.filterSize); err != nil { + errlist = multierror.Append(errlist, err) + } + } + if parseOpts.matcherStatus != "" { + if err := addMatcher(conf, "status", parseOpts.matcherStatus); err != nil { + errlist = multierror.Append(errlist, err) + } + } + if parseOpts.matcherSize != "" { + if err := addMatcher(conf, "size", parseOpts.matcherSize); err != nil { + errlist = multierror.Append(errlist, err) + } + } + return errlist.ErrorOrNil() +} + +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 +} diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go new file mode 100644 index 0000000..c8e43c4 --- /dev/null +++ b/pkg/ffuf/config.go @@ -0,0 +1,34 @@ +package ffuf + +import ( + "context" +) + +type Config struct { + StaticHeaders map[string]string + FuzzHeaders map[string]string + Method string + Url string + TLSSkipVerify bool + Data string + Quiet bool + Wordlist string + Filters []FilterProvider + Matchers []FilterProvider + Threads int + Context context.Context +} + +func NewConfig(ctx context.Context) Config { + var conf Config + conf.Context = ctx + conf.StaticHeaders = make(map[string]string) + conf.FuzzHeaders = make(map[string]string) + conf.Method = "GET" + conf.Url = "" + conf.TLSSkipVerify = false + conf.Data = "" + conf.Quiet = false + conf.Filters = make([]FilterProvider, 0) + return conf +} diff --git a/pkg/ffuf/const.go b/pkg/ffuf/const.go new file mode 100644 index 0000000..04702fb --- /dev/null +++ b/pkg/ffuf/const.go @@ -0,0 +1,6 @@ +package ffuf + +const ( + //VERSION holds the current version number + VERSION = "0.1" +) diff --git a/pkg/ffuf/interfaces.go b/pkg/ffuf/interfaces.go new file mode 100644 index 0000000..04ac49b --- /dev/null +++ b/pkg/ffuf/interfaces.go @@ -0,0 +1,25 @@ +package ffuf + +//FilterProvider is a generic interface for both Matchers and Filters +type FilterProvider interface { + Filter(response *Response) (bool, error) + Repr() string +} + +//RunnerProvider is an interface for request executors +type RunnerProvider interface { + Prepare(input []byte) (Request, error) + Execute(req *Request) (Response, error) +} + +//InputProvider interface handles the input data for RunnerProvider +type InputProvider interface { + Next() bool + Value() []byte +} + +//OutputProvider is responsible of providing output from the RunnerProvider +type OutputProvider interface { + Banner() error + Result(resp Response) +} diff --git a/pkg/ffuf/job.go b/pkg/ffuf/job.go new file mode 100644 index 0000000..22f6d31 --- /dev/null +++ b/pkg/ffuf/job.go @@ -0,0 +1,68 @@ +package ffuf + +import ( + "fmt" + "sync" +) + +//Job ties together Config, Runner, Input and Output +type Job struct { + Config *Config + Input InputProvider + Runner RunnerProvider + Output OutputProvider + Counter int + Running bool +} + +func NewJob(conf *Config) Job { + var j Job + j.Counter = 0 + j.Running = false + return j +} + +//Start the execution of the Job +func (j *Job) Start() { + defer j.Stop() + if !j.Config.Quiet { + j.Output.Banner() + } + j.Running = true + var wg sync.WaitGroup + //Limiter blocks after reaching the buffer, ensuring limited concurrency + limiter := make(chan bool, j.Config.Threads) + for j.Input.Next() { + limiter <- true + wg.Add(1) + nextInput := j.Input.Value() + go func() { + defer func() { <-limiter }() + defer wg.Done() + j.runTask([]byte(nextInput)) + }() + } + wg.Wait() + return +} + +func (j *Job) runTask(input []byte) { + req, err := j.Runner.Prepare(input) + if err != nil { + fmt.Printf("Encountered error while preparing request: %s\n", err) + return + } + resp, err := j.Runner.Execute(&req) + if err != nil { + fmt.Printf("Error in runner: %s\n", err) + return + } + j.Output.Result(resp) + return +} + +//Stop the execution of the Job +func (j *Job) Stop() { + j.Running = false + return +} diff --git a/pkg/ffuf/request.go b/pkg/ffuf/request.go new file mode 100644 index 0000000..c414ff6 --- /dev/null +++ b/pkg/ffuf/request.go @@ -0,0 +1,18 @@ +package ffuf + +// Request holds the meaningful data that is passed for runner for making the query +type Request struct { + Method string + Url string + Headers map[string]string + Data []byte + Input []byte +} + +func NewRequest(conf *Config) Request { + var req Request + req.Method = conf.Method + req.Url = conf.Url + req.Headers = make(map[string]string) + return req +} diff --git a/pkg/ffuf/response.go b/pkg/ffuf/response.go new file mode 100644 index 0000000..e83d474 --- /dev/null +++ b/pkg/ffuf/response.go @@ -0,0 +1,24 @@ +package ffuf + +import ( + "net/http" +) + +// Response struct holds the meaningful data returned from request and is meant for passing to filters +type Response struct { + StatusCode int64 + Headers map[string][]string + Data []byte + ContentLength int64 + Cancelled bool + Request *Request +} + +func NewResponse(httpresp *http.Response, req *Request) Response { + var resp Response + resp.Request = req + resp.StatusCode = int64(httpresp.StatusCode) + resp.Headers = httpresp.Header + resp.Cancelled = false + return resp +} diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go new file mode 100644 index 0000000..fe9c8e1 --- /dev/null +++ b/pkg/filter/filter.go @@ -0,0 +1,15 @@ +package filter + +import ( + "github.com/ffuf/ffuf/pkg/ffuf" +) + +func NewFilterByName(name string, value string) (ffuf.FilterProvider, error) { + if name == "status" { + return NewStatusFilter(value) + } + if name == "size" { + return NewSizeFilter(value) + } + return nil, nil +} diff --git a/pkg/filter/size.go b/pkg/filter/size.go new file mode 100644 index 0000000..b875622 --- /dev/null +++ b/pkg/filter/size.go @@ -0,0 +1,42 @@ +package filter + +import ( + "fmt" + "strconv" + "strings" + + "github.com/ffuf/ffuf/pkg/ffuf" +) + +type SizeFilter struct { + Value []int64 +} + +func NewSizeFilter(value string) (ffuf.FilterProvider, error) { + var intvals []int64 + for _, sv := range strings.Split(value, ",") { + intval, err := strconv.ParseInt(sv, 10, 0) + if err != nil { + return &SizeFilter{}, fmt.Errorf("Size filter (-fs): invalid value: %s", value) + } + intvals = append(intvals, intval) + } + return &SizeFilter{Value: intvals}, nil +} + +func (f *SizeFilter) Filter(response *ffuf.Response) (bool, error) { + for _, iv := range f.Value { + if iv == response.ContentLength { + return true, nil + } + } + return false, nil +} + +func (f *SizeFilter) Repr() string { + var strval []string + for _, iv := range f.Value { + strval = append(strval, strconv.Itoa(int(iv))) + } + return fmt.Sprintf("Response size: %s", strings.Join(strval, ",")) +} diff --git a/pkg/filter/status.go b/pkg/filter/status.go new file mode 100644 index 0000000..b0ede66 --- /dev/null +++ b/pkg/filter/status.go @@ -0,0 +1,42 @@ +package filter + +import ( + "fmt" + "strconv" + "strings" + + "github.com/ffuf/ffuf/pkg/ffuf" +) + +type StatusFilter struct { + Value []int64 +} + +func NewStatusFilter(value string) (ffuf.FilterProvider, error) { + var intvals []int64 + for _, sv := range strings.Split(value, ",") { + intval, err := strconv.ParseInt(sv, 10, 0) + if err != nil { + return &StatusFilter{}, fmt.Errorf("Status filter (-fc): invalid value %s", value) + } + intvals = append(intvals, intval) + } + return &StatusFilter{Value: intvals}, nil +} + +func (f *StatusFilter) Filter(response *ffuf.Response) (bool, error) { + for _, iv := range f.Value { + if iv == response.StatusCode { + return true, nil + } + } + return false, nil +} + +func (f *StatusFilter) Repr() string { + var strval []string + for _, iv := range f.Value { + strval = append(strval, strconv.Itoa(int(iv))) + } + return fmt.Sprintf("Response status: %s", strings.Join(strval, ",")) +} diff --git a/pkg/input/input.go b/pkg/input/input.go new file mode 100644 index 0000000..a30bc0f --- /dev/null +++ b/pkg/input/input.go @@ -0,0 +1,10 @@ +package input + +import ( + "github.com/ffuf/ffuf/pkg/ffuf" +) + +func NewInputProviderByName(name string, conf *ffuf.Config) (ffuf.InputProvider, error) { + // We have only one inputprovider at the moment + return NewWordlistInput(conf) +} diff --git a/pkg/input/wordlist.go b/pkg/input/wordlist.go new file mode 100644 index 0000000..9ecac34 --- /dev/null +++ b/pkg/input/wordlist.go @@ -0,0 +1,71 @@ +package input + +import ( + "bufio" + "os" + + "github.com/ffuf/ffuf/pkg/ffuf" +) + +type WordlistInput struct { + config *ffuf.Config + data [][]byte + position int +} + +func NewWordlistInput(conf *ffuf.Config) (*WordlistInput, error) { + var wl WordlistInput + wl.config = conf + wl.position = -1 + valid, err := wl.validFile(conf.Wordlist) + if err != nil { + return &wl, err + } + if valid { + err = wl.readFile(conf.Wordlist) + } + return &wl, err +} + +func (w *WordlistInput) Next() bool { + w.position++ + if w.position >= len(w.data)-1 { + return false + } + return true +} + +func (w *WordlistInput) Value() []byte { + return w.data[w.position] +} + +//validFile checks that the wordlist file exists and can be read +func (w *WordlistInput) validFile(path string) (bool, error) { + _, err := os.Stat(path) + if err != nil { + return false, err + } + f, err := os.Open(path) + if err != nil { + return false, err + } + f.Close() + return true, nil +} + +//readFile reads the file line by line to a byte slice +func (w *WordlistInput) readFile(path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + var data [][]byte + reader := bufio.NewScanner(file) + for reader.Scan() { + data = append(data, []byte(reader.Text())) + } + w.data = data + return reader.Err() +} diff --git a/pkg/output/output.go b/pkg/output/output.go new file mode 100644 index 0000000..a1e0149 --- /dev/null +++ b/pkg/output/output.go @@ -0,0 +1,10 @@ +package output + +import ( + "github.com/ffuf/ffuf/pkg/ffuf" +) + +func NewOutputProviderByName(name string, conf *ffuf.Config) ffuf.OutputProvider { + //We have only one outputprovider at the moment + return NewStdoutput(conf) +} diff --git a/pkg/output/stdout.go b/pkg/output/stdout.go new file mode 100644 index 0000000..70603c6 --- /dev/null +++ b/pkg/output/stdout.go @@ -0,0 +1,91 @@ +package output + +import ( + "fmt" + + "github.com/ffuf/ffuf/pkg/ffuf" +) + +const ( + BANNER_HEADER = ` + /'___\ /'___\ /'___\ + /\ \__/ /\ \__/ __ __ /\ \__/ + \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ + \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ + \ \_\ \ \_\ \ \____/ \ \_\ + \/_/ \/_/ \/___/ \/_/ +` + BANNER_SEP = "________________________________________________" +) + +type Stdoutput struct { + config *ffuf.Config +} + +func NewStdoutput(conf *ffuf.Config) *Stdoutput { + var outp Stdoutput + outp.config = conf + return &outp +} + +func (s *Stdoutput) Banner() error { + fmt.Printf("%s\n v%s\n%s\n\n", BANNER_HEADER, ffuf.VERSION, BANNER_SEP) + printOption([]byte("Method"), []byte(s.config.Method)) + printOption([]byte("URL"), []byte(s.config.Url)) + for _, f := range s.config.Matchers { + printOption([]byte("Matcher"), []byte(f.Repr())) + } + for _, f := range s.config.Filters { + printOption([]byte("Filter"), []byte(f.Repr())) + } + fmt.Printf("%s\n\n", BANNER_SEP) + return nil +} + +func (s *Stdoutput) Result(resp ffuf.Response) { + matched := false + for _, m := range s.config.Matchers { + match, err := m.Filter(&resp) + if err != nil { + continue + } + if match { + matched = true + } + } + // The response was not matched, return before running filters + if !matched { + return + } + for _, f := range s.config.Filters { + fv, err := f.Filter(&resp) + if err != nil { + continue + } + if fv { + return + } + } + // Response survived the filtering, output the result + s.printResult(resp) +} + +func (s *Stdoutput) printResult(resp ffuf.Response) { + if s.config.Quiet { + s.resultQuiet(resp) + } else { + s.resultNormal(resp) + } +} + +func (s *Stdoutput) resultQuiet(resp ffuf.Response) { + fmt.Println(string(resp.Request.Input)) +} + +func (s *Stdoutput) resultNormal(resp ffuf.Response) { + res_str := fmt.Sprintf("%-23s [Status: %d, Size: %d]", resp.Request.Input, resp.StatusCode, resp.ContentLength) + fmt.Println(res_str) +} +func printOption(name []byte, value []byte) { + fmt.Printf(" :: %-12s : %s\n", name, value) +} diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go new file mode 100644 index 0000000..2c37890 --- /dev/null +++ b/pkg/runner/runner.go @@ -0,0 +1,10 @@ +package runner + +import ( + "github.com/ffuf/ffuf/pkg/ffuf" +) + +func NewRunnerByName(name string, conf *ffuf.Config) ffuf.RunnerProvider { + // We have only one Runner at the moment + return NewSimpleRunner(conf) +} diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go new file mode 100644 index 0000000..5254632 --- /dev/null +++ b/pkg/runner/simple.go @@ -0,0 +1,92 @@ +package runner + +import ( + "bytes" + "crypto/tls" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/ffuf/ffuf/pkg/ffuf" +) + +//Download results < 5MB +const MAX_DOWNLOAD_SIZE = 5242880 + +type SimpleRunner struct { + config *ffuf.Config + client *http.Client +} + +func NewSimpleRunner(conf *ffuf.Config) ffuf.RunnerProvider { + var simplerunner SimpleRunner + simplerunner.config = conf + + simplerunner.client = &http.Client{ + + CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, + Timeout: time.Duration(3 * time.Second), + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: conf.TLSSkipVerify, + }, + }} + return &simplerunner +} + +func (r *SimpleRunner) Prepare(input []byte) (ffuf.Request, error) { + req := ffuf.NewRequest(r.config) + for h, v := range r.config.StaticHeaders { + req.Headers[h] = v + } + for h, v := range r.config.FuzzHeaders { + req.Headers[strings.Replace(h, "FUZZ", string(input), -1)] = strings.Replace(v, "FUZZ", string(input), -1) + } + req.Input = input + req.Url = strings.Replace(r.config.Url, "FUZZ", string(input), -1) + return req, nil +} + +func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) { + var httpreq *http.Request + var err error + data := bytes.NewReader(req.Data) + httpreq, err = http.NewRequest(req.Method, req.Url, data) + if err != nil { + return ffuf.Response{}, err + } + // Add user agent string if not defined + if _, ok := req.Headers["User-Agent"]; !ok { + req.Headers["User-Agent"] = "Fuzz Faster You Fool" + } + httpreq = httpreq.WithContext(r.config.Context) + for k, v := range req.Headers { + httpreq.Header.Set(k, v) + } + httpresp, err := r.client.Do(httpreq) + if err != nil { + return ffuf.Response{}, err + } + resp := ffuf.NewResponse(httpresp, req) + defer httpresp.Body.Close() + + // Check if we should download the resource or not + size, err := strconv.Atoi(httpresp.Header.Get("Content-Length")) + if err == nil { + resp.ContentLength = int64(size) + if size > MAX_DOWNLOAD_SIZE { + resp.Cancelled = true + return resp, nil + } + } + + if respbody, err := ioutil.ReadAll(httpresp.Body); err == nil { + resp.ContentLength = int64(utf8.RuneCountInString(string(respbody))) + resp.Data = respbody + } + + return resp, nil +}