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