Initial commit, v0.1

This commit is contained in:
Joona Hoikkala 2018-11-08 11:26:32 +02:00
commit ad2296f55b
No known key found for this signature in database
GPG Key ID: D5AA86BBF9B29A5C
18 changed files with 790 additions and 0 deletions

12
.goreleaser.yml Normal file
View File

@ -0,0 +1,12 @@
builds:
- binary: ffuf
goos:
- linux
goarch:
- amd64
archive:
format: tar.gz
sign:
artifacts: checksum

37
README.md Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
package ffuf
const (
//VERSION holds the current version number
VERSION = "0.1"
)

25
pkg/ffuf/interfaces.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}