Initial commit, v0.1
This commit is contained in:
commit
ad2296f55b
12
.goreleaser.yml
Normal file
12
.goreleaser.yml
Normal file
@ -0,0 +1,12 @@
|
||||
builds:
|
||||
- binary: ffuf
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
|
||||
archive:
|
||||
format: tar.gz
|
||||
|
||||
sign:
|
||||
artifacts: checksum
|
||||
37
README.md
Normal file
37
README.md
Normal file
@ -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`
|
||||
183
main.go
Normal file
183
main.go
Normal file
@ -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
|
||||
}
|
||||
34
pkg/ffuf/config.go
Normal file
34
pkg/ffuf/config.go
Normal file
@ -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
|
||||
}
|
||||
6
pkg/ffuf/const.go
Normal file
6
pkg/ffuf/const.go
Normal file
@ -0,0 +1,6 @@
|
||||
package ffuf
|
||||
|
||||
const (
|
||||
//VERSION holds the current version number
|
||||
VERSION = "0.1"
|
||||
)
|
||||
25
pkg/ffuf/interfaces.go
Normal file
25
pkg/ffuf/interfaces.go
Normal file
@ -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)
|
||||
}
|
||||
68
pkg/ffuf/job.go
Normal file
68
pkg/ffuf/job.go
Normal file
@ -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
|
||||
}
|
||||
18
pkg/ffuf/request.go
Normal file
18
pkg/ffuf/request.go
Normal file
@ -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
|
||||
}
|
||||
24
pkg/ffuf/response.go
Normal file
24
pkg/ffuf/response.go
Normal file
@ -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
|
||||
}
|
||||
15
pkg/filter/filter.go
Normal file
15
pkg/filter/filter.go
Normal file
@ -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
|
||||
}
|
||||
42
pkg/filter/size.go
Normal file
42
pkg/filter/size.go
Normal file
@ -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, ","))
|
||||
}
|
||||
42
pkg/filter/status.go
Normal file
42
pkg/filter/status.go
Normal file
@ -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, ","))
|
||||
}
|
||||
10
pkg/input/input.go
Normal file
10
pkg/input/input.go
Normal file
@ -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)
|
||||
}
|
||||
71
pkg/input/wordlist.go
Normal file
71
pkg/input/wordlist.go
Normal file
@ -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()
|
||||
}
|
||||
10
pkg/output/output.go
Normal file
10
pkg/output/output.go
Normal file
@ -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)
|
||||
}
|
||||
91
pkg/output/stdout.go
Normal file
91
pkg/output/stdout.go
Normal file
@ -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)
|
||||
}
|
||||
10
pkg/runner/runner.go
Normal file
10
pkg/runner/runner.go
Normal file
@ -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)
|
||||
}
|
||||
92
pkg/runner/simple.go
Normal file
92
pkg/runner/simple.go
Normal file
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user