mirror of
https://github.com/ffuf/ffuf
synced 2024-11-25 05:00:23 +00:00
New functionality to map fired blind payloads back to the initial request (#632)
* Fix ioutil deprecation and use xdg paths instead (wip) * Clean up deprecated ioutil references, add config directory structure creation and run entry creation * Add wordlist position setting and FFUFHASH variable * Save full wordlist paths and print out a raw request when searched * Cast from string to 32bit integer, 2billion should be enough for a position * Use correct format strings for float
This commit is contained in:
parent
b7adc5038d
commit
9bddff79b9
19 changed files with 578 additions and 154 deletions
|
@ -1,7 +1,11 @@
|
|||
## Changelog
|
||||
- master
|
||||
- New
|
||||
- Added a new, dynamic keyword `FFUFHASH` that generates hash from job configuration and wordlist position to map blind payloads back to the initial request.
|
||||
- New command line parameter for searching a hash: `-search FFUFHASH`
|
||||
- Changed
|
||||
- Multiline output prints out alphabetically sorted by keyword
|
||||
- Default configuration directories now follow `XDG_CONFIG_HOME` variable (less spam in your home directory)
|
||||
- Fixed issue with autocalibration of line & words filter
|
||||
- Fixed issue with `-json` when used in conjunction with silent mode
|
||||
|
||||
|
|
5
go.mod
5
go.mod
|
@ -2,4 +2,7 @@ module github.com/ffuf/ffuf
|
|||
|
||||
go 1.13
|
||||
|
||||
require github.com/pelletier/go-toml v1.8.1
|
||||
require (
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/pelletier/go-toml v1.8.1
|
||||
)
|
||||
|
|
15
go.sum
15
go.sum
|
@ -1,3 +1,18 @@
|
|||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
6
help.go
6
help.go
|
@ -16,7 +16,7 @@ type UsageSection struct {
|
|||
ExpectedFlags []string
|
||||
}
|
||||
|
||||
//PrintSection prints out the section name, description and each of the flags
|
||||
// PrintSection prints out the section name, description and each of the flags
|
||||
func (u *UsageSection) PrintSection(max_length int, extended bool) {
|
||||
// Do not print if extended usage not requested and section marked as hidden
|
||||
if !extended && u.Hidden {
|
||||
|
@ -35,7 +35,7 @@ type UsageFlag struct {
|
|||
Default string
|
||||
}
|
||||
|
||||
//PrintFlag prints out the flag name, usage string and default value
|
||||
// PrintFlag prints out the flag name, usage string and default value
|
||||
func (f *UsageFlag) PrintFlag(max_length int) {
|
||||
// Create format string, used for padding
|
||||
format := fmt.Sprintf(" -%%-%ds %%s", max_length)
|
||||
|
@ -61,7 +61,7 @@ func Usage() {
|
|||
Description: "",
|
||||
Flags: make([]UsageFlag, 0),
|
||||
Hidden: false,
|
||||
ExpectedFlags: []string{"ac", "acc", "ack", "ach", "acs", "c", "config", "json", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "s", "sa", "se", "sf", "t", "v", "V"},
|
||||
ExpectedFlags: []string{"ac", "acc", "ack", "ach", "acs", "c", "config", "json", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "search", "s", "sa", "se", "sf", "t", "v", "V"},
|
||||
}
|
||||
u_compat := UsageSection{
|
||||
Name: "COMPATIBILITY OPTIONS",
|
||||
|
|
70
main.go
70
main.go
|
@ -4,17 +4,17 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/ffuf/ffuf/pkg/filter"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"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/interactive"
|
||||
"github.com/ffuf/ffuf/pkg/output"
|
||||
"github.com/ffuf/ffuf/pkg/runner"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type multiStringFlag []string
|
||||
|
@ -45,7 +45,7 @@ func (m *wordlistFlag) Set(value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
//ParseFlags parses the command line flags and (re)populates the ConfigOptions struct
|
||||
// ParseFlags parses the command line flags and (re)populates the ConfigOptions struct
|
||||
func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
|
||||
var ignored bool
|
||||
var cookies, autocalibrationstrings, headers, inputcommands multiStringFlag
|
||||
|
@ -96,6 +96,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
|
|||
flag.StringVar(&opts.Filter.Time, "ft", opts.Filter.Time, "Filter by number of milliseconds to the first response byte, either greater or less than. EG: >100 or <100")
|
||||
flag.StringVar(&opts.Filter.Words, "fw", opts.Filter.Words, "Filter by amount of words in response. Comma separated list of word counts and ranges")
|
||||
flag.StringVar(&opts.General.Delay, "p", opts.General.Delay, "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"")
|
||||
flag.StringVar(&opts.General.Searchhash, "search", opts.General.Searchhash, "Search for a FFUFHASH payload from ffuf history")
|
||||
flag.StringVar(&opts.HTTP.Data, "d", opts.HTTP.Data, "POST data")
|
||||
flag.StringVar(&opts.HTTP.Data, "data", opts.HTTP.Data, "POST data (alias of -d)")
|
||||
flag.StringVar(&opts.HTTP.Data, "data-ascii", opts.HTTP.Data, "POST data (alias of -d)")
|
||||
|
@ -142,13 +143,37 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
|
|||
func main() {
|
||||
|
||||
var err, optserr error
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// prepare the default config options from default config file
|
||||
var opts *ffuf.ConfigOptions
|
||||
opts, optserr = ffuf.ReadDefaultConfig()
|
||||
|
||||
opts = ParseFlags(opts)
|
||||
|
||||
// Handle searchhash functionality and exit
|
||||
if opts.General.Searchhash != "" {
|
||||
coptions, pos, err := ffuf.SearchHash(opts.General.Searchhash)
|
||||
if err != nil {
|
||||
fmt.Printf("[ERR] %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(coptions) > 0 {
|
||||
fmt.Printf("Request candidate(s) for hash %s\n", opts.General.Searchhash)
|
||||
}
|
||||
for _, copt := range coptions {
|
||||
conf, err := ffuf.ConfigFromOptions(&copt.ConfigOptions, ctx, cancel)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
printSearchResults(conf, pos, copt.Time, opts.General.Searchhash)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("[ERR] %s\n", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if opts.General.ShowVersion {
|
||||
fmt.Printf("ffuf version: %s\n", ffuf.Version())
|
||||
os.Exit(0)
|
||||
|
@ -157,13 +182,13 @@ func main() {
|
|||
f, err := os.OpenFile(opts.Output.DebugLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Disabling logging, encountered error(s): %s\n", err)
|
||||
log.SetOutput(ioutil.Discard)
|
||||
log.SetOutput(io.Discard)
|
||||
} else {
|
||||
log.SetOutput(f)
|
||||
defer f.Close()
|
||||
}
|
||||
} else {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
if optserr != nil {
|
||||
log.Printf("Error while opening default config file: %s", optserr)
|
||||
|
@ -183,9 +208,7 @@ func main() {
|
|||
opts = ParseFlags(opts)
|
||||
}
|
||||
|
||||
// Prepare context and set up Config struct
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Set up Config struct
|
||||
conf, err := ffuf.ConfigFromOptions(opts, ctx, cancel)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
|
||||
|
@ -193,6 +216,7 @@ func main() {
|
|||
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
job, err := prepareJob(conf)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
|
||||
|
@ -335,3 +359,23 @@ func SetupFilters(parseOpts *ffuf.ConfigOptions, conf *ffuf.Config) error {
|
|||
}
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func printSearchResults(conf *ffuf.Config, pos int, exectime time.Time, hash string) {
|
||||
inp, err := input.NewInputProvider(conf)
|
||||
if err.ErrorOrNil() != nil {
|
||||
fmt.Printf("-------------------------------------------\n")
|
||||
fmt.Println("Encountered error that prevents reproduction of the request:")
|
||||
fmt.Println(err.ErrorOrNil())
|
||||
return
|
||||
}
|
||||
inp.SetPosition(pos)
|
||||
inputdata := inp.Value()
|
||||
inputdata["FFUFHASH"] = []byte(hash)
|
||||
basereq := ffuf.BaseRequest(conf)
|
||||
dummyrunner := runner.NewRunnerByName("simple", conf, false)
|
||||
ffufreq, _ := dummyrunner.Prepare(inputdata, &basereq)
|
||||
rawreq, _ := dummyrunner.Dump(&ffufreq)
|
||||
fmt.Printf("-------------------------------------------\n")
|
||||
fmt.Printf("ffuf job started at: %s\n\n", exectime.Format(time.RFC3339))
|
||||
fmt.Printf("%s\n", string(rawreq))
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ type Config struct {
|
|||
ConfigFile string `json:"configfile"`
|
||||
Context context.Context `json:"-"`
|
||||
Data string `json:"postdata"`
|
||||
Debuglog string `json:"debuglog"`
|
||||
Delay optRange `json:"delay"`
|
||||
DirSearchCompat bool `json:"dirsearch_compatibility"`
|
||||
Extensions []string `json:"extensions"`
|
||||
|
@ -48,6 +49,8 @@ type Config struct {
|
|||
RecursionDepth int `json:"recursion_depth"`
|
||||
RecursionStrategy string `json:"recursion_strategy"`
|
||||
ReplayProxyURL string `json:"replayproxyurl"`
|
||||
RequestFile string `json:"requestfile"`
|
||||
RequestProto string `json:"requestproto"`
|
||||
SNI string `json:"sni"`
|
||||
StopOn403 bool `json:"stop_403"`
|
||||
StopOnAll bool `json:"stop_all"`
|
||||
|
@ -56,6 +59,7 @@ type Config struct {
|
|||
Timeout int `json:"timeout"`
|
||||
Url string `json:"url"`
|
||||
Verbose bool `json:"verbose"`
|
||||
Wordlists []string `json:"wordlists"`
|
||||
Http2 bool `json:"http2"`
|
||||
}
|
||||
|
||||
|
@ -75,6 +79,7 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config {
|
|||
conf.Context = ctx
|
||||
conf.Cancel = cancel
|
||||
conf.Data = ""
|
||||
conf.Debuglog = ""
|
||||
conf.Delay = optRange{0, 0, false, false}
|
||||
conf.DirSearchCompat = false
|
||||
conf.Extensions = make([]string, 0)
|
||||
|
@ -99,6 +104,8 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config {
|
|||
conf.Recursion = false
|
||||
conf.RecursionDepth = 0
|
||||
conf.RecursionStrategy = "default"
|
||||
conf.RequestFile = ""
|
||||
conf.RequestProto = "https"
|
||||
conf.SNI = ""
|
||||
conf.StopOn403 = false
|
||||
conf.StopOnAll = false
|
||||
|
@ -106,6 +113,7 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config {
|
|||
conf.Timeout = 10
|
||||
conf.Url = ""
|
||||
conf.Verbose = false
|
||||
conf.Wordlists = []string{}
|
||||
conf.Http2 = false
|
||||
return conf
|
||||
}
|
||||
|
|
127
pkg/ffuf/configmarshaller.go
Normal file
127
pkg/ffuf/configmarshaller.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package ffuf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *Config) ToOptions() ConfigOptions {
|
||||
o := ConfigOptions{}
|
||||
// HTTP options
|
||||
o.HTTP.Cookies = []string{}
|
||||
o.HTTP.Data = c.Data
|
||||
o.HTTP.FollowRedirects = c.FollowRedirects
|
||||
o.HTTP.Headers = make([]string, 0)
|
||||
for k, v := range c.Headers {
|
||||
o.HTTP.Headers = append(o.HTTP.Headers, fmt.Sprintf("%s: %s", k, v))
|
||||
}
|
||||
o.HTTP.IgnoreBody = c.IgnoreBody
|
||||
o.HTTP.Method = c.Method
|
||||
o.HTTP.ProxyURL = c.ProxyURL
|
||||
o.HTTP.Recursion = c.Recursion
|
||||
o.HTTP.RecursionDepth = c.RecursionDepth
|
||||
o.HTTP.RecursionStrategy = c.RecursionStrategy
|
||||
o.HTTP.ReplayProxyURL = c.ReplayProxyURL
|
||||
o.HTTP.SNI = c.SNI
|
||||
o.HTTP.Timeout = c.Timeout
|
||||
o.HTTP.URL = c.Url
|
||||
o.HTTP.Http2 = c.Http2
|
||||
|
||||
o.General.AutoCalibration = c.AutoCalibration
|
||||
o.General.AutoCalibrationKeyword = c.AutoCalibrationKeyword
|
||||
o.General.AutoCalibrationPerHost = c.AutoCalibrationPerHost
|
||||
o.General.AutoCalibrationStrategy = c.AutoCalibrationStrategy
|
||||
o.General.AutoCalibrationStrings = c.AutoCalibrationStrings
|
||||
o.General.Colors = c.Colors
|
||||
o.General.ConfigFile = ""
|
||||
if c.Delay.HasDelay {
|
||||
if c.Delay.IsRange {
|
||||
o.General.Delay = fmt.Sprintf("%.2f-%.2f", c.Delay.Min, c.Delay.Max)
|
||||
} else {
|
||||
o.General.Delay = fmt.Sprintf("%.2f", c.Delay.Min)
|
||||
}
|
||||
} else {
|
||||
o.General.Delay = ""
|
||||
}
|
||||
o.General.Json = c.Json
|
||||
o.General.MaxTime = c.MaxTime
|
||||
o.General.MaxTimeJob = c.MaxTimeJob
|
||||
o.General.Noninteractive = c.Noninteractive
|
||||
o.General.Quiet = c.Quiet
|
||||
o.General.Rate = int(c.Rate)
|
||||
o.General.StopOn403 = c.StopOn403
|
||||
o.General.StopOnAll = c.StopOnAll
|
||||
o.General.StopOnErrors = c.StopOnErrors
|
||||
o.General.Threads = c.Threads
|
||||
o.General.Verbose = c.Verbose
|
||||
|
||||
o.Input.DirSearchCompat = c.DirSearchCompat
|
||||
o.Input.Extensions = strings.Join(c.Extensions, ",")
|
||||
o.Input.IgnoreWordlistComments = c.IgnoreWordlistComments
|
||||
o.Input.InputMode = c.InputMode
|
||||
o.Input.InputNum = c.InputNum
|
||||
o.Input.InputShell = c.InputShell
|
||||
o.Input.Inputcommands = []string{}
|
||||
for _, v := range c.InputProviders {
|
||||
if v.Name == "command" {
|
||||
o.Input.Inputcommands = append(o.Input.Inputcommands, fmt.Sprintf("%s:%s", v.Value, v.Keyword))
|
||||
}
|
||||
}
|
||||
o.Input.Request = c.RequestFile
|
||||
o.Input.RequestProto = c.RequestProto
|
||||
o.Input.Wordlists = c.Wordlists
|
||||
|
||||
o.Output.DebugLog = c.Debuglog
|
||||
o.Output.OutputDirectory = c.OutputDirectory
|
||||
o.Output.OutputFile = c.OutputFile
|
||||
o.Output.OutputFormat = c.OutputFormat
|
||||
o.Output.OutputSkipEmptyFile = c.OutputSkipEmptyFile
|
||||
|
||||
o.Filter.Mode = c.FilterMode
|
||||
o.Filter.Lines = ""
|
||||
o.Filter.Regexp = ""
|
||||
o.Filter.Size = ""
|
||||
o.Filter.Status = ""
|
||||
o.Filter.Time = ""
|
||||
o.Filter.Words = ""
|
||||
for name, filter := range c.MatcherManager.GetFilters() {
|
||||
switch name {
|
||||
case "line":
|
||||
o.Filter.Lines = filter.Repr()
|
||||
case "regexp":
|
||||
o.Filter.Regexp = filter.Repr()
|
||||
case "size":
|
||||
o.Filter.Size = filter.Repr()
|
||||
case "status":
|
||||
o.Filter.Status = filter.Repr()
|
||||
case "time":
|
||||
o.Filter.Time = filter.Repr()
|
||||
case "words":
|
||||
o.Filter.Words = filter.Repr()
|
||||
}
|
||||
}
|
||||
o.Matcher.Mode = c.MatcherMode
|
||||
o.Matcher.Lines = ""
|
||||
o.Matcher.Regexp = ""
|
||||
o.Matcher.Size = ""
|
||||
o.Matcher.Status = ""
|
||||
o.Matcher.Time = ""
|
||||
o.Matcher.Words = ""
|
||||
for name, filter := range c.MatcherManager.GetMatchers() {
|
||||
switch name {
|
||||
case "line":
|
||||
o.Matcher.Lines = filter.Repr()
|
||||
case "regexp":
|
||||
o.Matcher.Regexp = filter.Repr()
|
||||
case "size":
|
||||
o.Matcher.Size = filter.Repr()
|
||||
case "status":
|
||||
o.Matcher.Status = filter.Repr()
|
||||
case "time":
|
||||
o.Matcher.Time = filter.Repr()
|
||||
case "words":
|
||||
o.Matcher.Words = filter.Repr()
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
|
@ -1,8 +1,15 @@
|
|||
package ffuf
|
||||
|
||||
import (
|
||||
"github.com/adrg/xdg"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
//VERSION holds the current version number
|
||||
VERSION = "1.5.0"
|
||||
//VERSION_APPENDIX holds additional version definition
|
||||
VERSION_APPENDIX = "-dev"
|
||||
CONFIGDIR = filepath.Join(xdg.ConfigHome, "ffuf")
|
||||
HISTORYDIR = filepath.Join(CONFIGDIR, "history")
|
||||
)
|
90
pkg/ffuf/history.go
Normal file
90
pkg/ffuf/history.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package ffuf
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigOptionsHistory struct {
|
||||
ConfigOptions
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
func WriteHistoryEntry(conf *Config) (string, error) {
|
||||
options := ConfigOptionsHistory{
|
||||
ConfigOptions: conf.ToOptions(),
|
||||
Time: time.Now(),
|
||||
}
|
||||
jsonoptions, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hashstr := calculateHistoryHash(jsonoptions)
|
||||
err = createConfigDir(filepath.Join(HISTORYDIR, hashstr))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(HISTORYDIR, hashstr, "options"), jsonoptions, 0640)
|
||||
return hashstr, err
|
||||
}
|
||||
|
||||
func calculateHistoryHash(options []byte) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256(options))
|
||||
}
|
||||
|
||||
func SearchHash(hash string) ([]ConfigOptionsHistory, int, error) {
|
||||
coptions := make([]ConfigOptionsHistory, 0)
|
||||
if len(hash) < 6 {
|
||||
return coptions, 0, errors.New("bad FFUFHASH value")
|
||||
}
|
||||
historypart := hash[0:5]
|
||||
position, err := strconv.ParseInt(hash[5:], 16, 32)
|
||||
if err != nil {
|
||||
return coptions, 0, errors.New("bad positional value in FFUFHASH")
|
||||
}
|
||||
all_dirs, err := os.ReadDir(HISTORYDIR)
|
||||
if err != nil {
|
||||
return coptions, 0, err
|
||||
}
|
||||
matched_dirs := make([]string, 0)
|
||||
for _, filename := range all_dirs {
|
||||
if filename.IsDir() {
|
||||
if strings.HasPrefix(strings.ToLower(filename.Name()), strings.ToLower(historypart)) {
|
||||
matched_dirs = append(matched_dirs, filename.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, dirname := range matched_dirs {
|
||||
copts, err := configFromHistory(filepath.Join(HISTORYDIR, dirname))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
coptions = append(coptions, copts)
|
||||
|
||||
}
|
||||
return coptions, int(position), err
|
||||
}
|
||||
|
||||
func configFromHistory(dirname string) (ConfigOptionsHistory, error) {
|
||||
jsonOptions, err := os.ReadFile(filepath.Join(dirname, "options"))
|
||||
if err != nil {
|
||||
return ConfigOptionsHistory{}, err
|
||||
}
|
||||
tmpOptions := ConfigOptionsHistory{}
|
||||
err = json.Unmarshal(jsonOptions, &tmpOptions)
|
||||
return tmpOptions, err
|
||||
/*
|
||||
// These are dummy values for this use case
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
conf, err := ConfigFromOptions(&tmpOptions.ConfigOptions, ctx, cancel)
|
||||
job.Input, errs = input.NewInputProvider(conf)
|
||||
return conf, tmpOptions.Time, err
|
||||
*/
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package ffuf
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//MatcherManager provides functions for managing matchers and filters
|
||||
// MatcherManager provides functions for managing matchers and filters
|
||||
type MatcherManager interface {
|
||||
SetCalibrated(calibrated bool)
|
||||
SetCalibratedForHost(host string, calibrated bool)
|
||||
|
@ -17,36 +19,39 @@ type MatcherManager interface {
|
|||
Calibrated() bool
|
||||
}
|
||||
|
||||
//FilterProvider is a generic interface for both Matchers and Filters
|
||||
// FilterProvider is a generic interface for both Matchers and Filters
|
||||
type FilterProvider interface {
|
||||
Filter(response *Response) (bool, error)
|
||||
Repr() string
|
||||
ReprVerbose() string
|
||||
}
|
||||
|
||||
//RunnerProvider is an interface for request executors
|
||||
// RunnerProvider is an interface for request executors
|
||||
type RunnerProvider interface {
|
||||
Prepare(input map[string][]byte, basereq *Request) (Request, error)
|
||||
Execute(req *Request) (Response, error)
|
||||
Dump(req *Request) ([]byte, error)
|
||||
}
|
||||
|
||||
//InputProvider interface handles the input data for RunnerProvider
|
||||
// InputProvider interface handles the input data for RunnerProvider
|
||||
type InputProvider interface {
|
||||
ActivateKeywords([]string)
|
||||
AddProvider(InputProviderConfig) error
|
||||
Keywords() []string
|
||||
Next() bool
|
||||
Position() int
|
||||
SetPosition(int)
|
||||
Reset()
|
||||
Value() map[string][]byte
|
||||
Total() int
|
||||
}
|
||||
|
||||
//InternalInputProvider interface handles providing input data to InputProvider
|
||||
// InternalInputProvider interface handles providing input data to InputProvider
|
||||
type InternalInputProvider interface {
|
||||
Keyword() string
|
||||
Next() bool
|
||||
Position() int
|
||||
SetPosition(int)
|
||||
ResetPosition()
|
||||
IncrementPosition()
|
||||
Value() []byte
|
||||
|
@ -56,7 +61,7 @@ type InternalInputProvider interface {
|
|||
Disable()
|
||||
}
|
||||
|
||||
//OutputProvider is responsible of providing output from the RunnerProvider
|
||||
// OutputProvider is responsible of providing output from the RunnerProvider
|
||||
type OutputProvider interface {
|
||||
Banner()
|
||||
Finalize() error
|
||||
|
|
|
@ -19,6 +19,7 @@ type Job struct {
|
|||
Runner RunnerProvider
|
||||
ReplayRunner RunnerProvider
|
||||
Output OutputProvider
|
||||
Jobhash string
|
||||
Counter int
|
||||
ErrorCounter int
|
||||
SpuriousErrorCounter int
|
||||
|
@ -180,6 +181,7 @@ func (j *Job) prepareQueueJob() {
|
|||
//And activate / disable inputproviders as needed
|
||||
j.Input.ActivateKeywords(found_kws)
|
||||
j.queuepos += 1
|
||||
j.Jobhash, _ = WriteHistoryEntry(j.Config)
|
||||
}
|
||||
|
||||
// SkipQueue allows to skip the current job and advance to the next queued recursion job
|
||||
|
@ -255,6 +257,8 @@ func (j *Job) startExecution() {
|
|||
<-j.Rate.RateLimiter.C
|
||||
nextInput := j.Input.Value()
|
||||
nextPosition := j.Input.Position()
|
||||
// Add FFUFHASH and its value
|
||||
nextInput["FFUFHASH"] = j.ffufHash(nextPosition)
|
||||
|
||||
wg.Add(1)
|
||||
j.Counter++
|
||||
|
@ -377,6 +381,16 @@ func (j *Job) isMatch(resp Response) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (j *Job) ffufHash(pos int) []byte {
|
||||
hashstring := ""
|
||||
r := []rune(j.Jobhash)
|
||||
if len(r) > 5 {
|
||||
hashstring = string(r[:5])
|
||||
}
|
||||
hashstring += fmt.Sprintf("%x", pos)
|
||||
return []byte(hashstring)
|
||||
}
|
||||
|
||||
func (j *Job) runTask(input map[string][]byte, position int, retried bool) {
|
||||
basereq := j.queuejobs[j.queuepos-1].req
|
||||
req, err := j.Runner.Prepare(input, &basereq)
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -17,97 +17,98 @@ import (
|
|||
)
|
||||
|
||||
type ConfigOptions struct {
|
||||
Filter FilterOptions
|
||||
General GeneralOptions
|
||||
HTTP HTTPOptions
|
||||
Input InputOptions
|
||||
Matcher MatcherOptions
|
||||
Output OutputOptions
|
||||
Filter FilterOptions `json:"filters"`
|
||||
General GeneralOptions `json:"general"`
|
||||
HTTP HTTPOptions `json:"http"`
|
||||
Input InputOptions `json:"input"`
|
||||
Matcher MatcherOptions `json:"matchers"`
|
||||
Output OutputOptions `json:"output"`
|
||||
}
|
||||
|
||||
type HTTPOptions struct {
|
||||
Cookies []string
|
||||
Data string
|
||||
FollowRedirects bool
|
||||
Headers []string
|
||||
IgnoreBody bool
|
||||
Method string
|
||||
ProxyURL string
|
||||
Recursion bool
|
||||
RecursionDepth int
|
||||
RecursionStrategy string
|
||||
ReplayProxyURL string
|
||||
SNI string
|
||||
Timeout int
|
||||
URL string
|
||||
Http2 bool
|
||||
Cookies []string `json:"-"` // this is appended in headers
|
||||
Data string `json:"data"`
|
||||
FollowRedirects bool `json:"follow_redirects"`
|
||||
Headers []string `json:"headers"`
|
||||
IgnoreBody bool `json:"ignore_body"`
|
||||
Method string `json:"method"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
Recursion bool `json:"recursion"`
|
||||
RecursionDepth int `json:"recursion_depth"`
|
||||
RecursionStrategy string `json:"recursion_strategy"`
|
||||
ReplayProxyURL string `json:"replay_proxy_url"`
|
||||
SNI string `json:"sni"`
|
||||
Timeout int `json:"timeout"`
|
||||
URL string `json:"url"`
|
||||
Http2 bool `json:"http2"`
|
||||
}
|
||||
|
||||
type GeneralOptions struct {
|
||||
AutoCalibration bool
|
||||
AutoCalibrationKeyword string
|
||||
AutoCalibrationPerHost bool
|
||||
AutoCalibrationStrategy string
|
||||
AutoCalibrationStrings []string
|
||||
Colors bool
|
||||
ConfigFile string `toml:"-"`
|
||||
Delay string
|
||||
Json bool
|
||||
MaxTime int
|
||||
MaxTimeJob int
|
||||
Noninteractive bool
|
||||
Quiet bool
|
||||
Rate int
|
||||
ShowVersion bool `toml:"-"`
|
||||
StopOn403 bool
|
||||
StopOnAll bool
|
||||
StopOnErrors bool
|
||||
Threads int
|
||||
Verbose bool
|
||||
AutoCalibration bool `json:"autocalibration"`
|
||||
AutoCalibrationKeyword string `json:"autocalibration_keyword"`
|
||||
AutoCalibrationPerHost bool `json:"autocalibration_per_host"`
|
||||
AutoCalibrationStrategy string `json:"autocalibration_strategy"`
|
||||
AutoCalibrationStrings []string `json:"autocalibration_strings"`
|
||||
Colors bool `json:"colors"`
|
||||
ConfigFile string `toml:"-" json:"config_file"`
|
||||
Delay string `json:"delay"`
|
||||
Json bool `json:"json"`
|
||||
MaxTime int `json:"maxtime"`
|
||||
MaxTimeJob int `json:"maxtime_job"`
|
||||
Noninteractive bool `json:"noninteractive"`
|
||||
Quiet bool `json:"quiet"`
|
||||
Rate int `json:"rate"`
|
||||
Searchhash string `json:"-"`
|
||||
ShowVersion bool `toml:"-" json:"-"`
|
||||
StopOn403 bool `json:"stop_on_403"`
|
||||
StopOnAll bool `json:"stop_on_all"`
|
||||
StopOnErrors bool `json:"stop_on_errors"`
|
||||
Threads int `json:"threads"`
|
||||
Verbose bool `json:"verbose"`
|
||||
}
|
||||
|
||||
type InputOptions struct {
|
||||
DirSearchCompat bool
|
||||
Extensions string
|
||||
IgnoreWordlistComments bool
|
||||
InputMode string
|
||||
InputNum int
|
||||
InputShell string
|
||||
Inputcommands []string
|
||||
Request string
|
||||
RequestProto string
|
||||
Wordlists []string
|
||||
DirSearchCompat bool `json:"dirsearch_compat"`
|
||||
Extensions string `json:"extensions"`
|
||||
IgnoreWordlistComments bool `json:"ignore_wordlist_comments"`
|
||||
InputMode string `json:"input_mode"`
|
||||
InputNum int `json:"input_num"`
|
||||
InputShell string `json:"input_shell"`
|
||||
Inputcommands []string `json:"input_commands"`
|
||||
Request string `json:"request_file"`
|
||||
RequestProto string `json:"request_proto"`
|
||||
Wordlists []string `json:"wordlists"`
|
||||
}
|
||||
|
||||
type OutputOptions struct {
|
||||
DebugLog string
|
||||
OutputDirectory string
|
||||
OutputFile string
|
||||
OutputFormat string
|
||||
OutputSkipEmptyFile bool
|
||||
DebugLog string `json:"debug_log"`
|
||||
OutputDirectory string `json:"output_directory"`
|
||||
OutputFile string `json:"output_file"`
|
||||
OutputFormat string `json:"output_format"`
|
||||
OutputSkipEmptyFile bool `json:"output_skip_empty"`
|
||||
}
|
||||
|
||||
type FilterOptions struct {
|
||||
Mode string
|
||||
Lines string
|
||||
Regexp string
|
||||
Size string
|
||||
Status string
|
||||
Time string
|
||||
Words string
|
||||
Mode string `json:"mode"`
|
||||
Lines string `json:"lines"`
|
||||
Regexp string `json:"regexp"`
|
||||
Size string `json:"size"`
|
||||
Status string `json:"status"`
|
||||
Time string `json:"time"`
|
||||
Words string `json:"words"`
|
||||
}
|
||||
|
||||
type MatcherOptions struct {
|
||||
Mode string
|
||||
Lines string
|
||||
Regexp string
|
||||
Size string
|
||||
Status string
|
||||
Time string
|
||||
Words string
|
||||
Mode string `json:"mode"`
|
||||
Lines string `json:"lines"`
|
||||
Regexp string `json:"regexp"`
|
||||
Size string `json:"size"`
|
||||
Status string `json:"status"`
|
||||
Time string `json:"time"`
|
||||
Words string `json:"words"`
|
||||
}
|
||||
|
||||
//NewConfigOptions returns a newly created ConfigOptions struct with default values
|
||||
// NewConfigOptions returns a newly created ConfigOptions struct with default values
|
||||
func NewConfigOptions() *ConfigOptions {
|
||||
c := &ConfigOptions{}
|
||||
c.Filter.Mode = "or"
|
||||
|
@ -128,6 +129,7 @@ func NewConfigOptions() *ConfigOptions {
|
|||
c.General.Noninteractive = false
|
||||
c.General.Quiet = false
|
||||
c.General.Rate = 0
|
||||
c.General.Searchhash = ""
|
||||
c.General.ShowVersion = false
|
||||
c.General.StopOn403 = false
|
||||
c.General.StopOnAll = false
|
||||
|
@ -169,7 +171,7 @@ func NewConfigOptions() *ConfigOptions {
|
|||
return c
|
||||
}
|
||||
|
||||
//ConfigFromOptions parses the values in ConfigOptions struct, ensures that the values are sane,
|
||||
// ConfigFromOptions parses the values in ConfigOptions struct, ensures that the values are sane,
|
||||
// and creates a Config struct out of them.
|
||||
func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel context.CancelFunc) (*Config, error) {
|
||||
//TODO: refactor in a proper flag library that can handle things like required flags
|
||||
|
@ -220,6 +222,7 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con
|
|||
}
|
||||
}
|
||||
|
||||
tmpWordlists := make([]string, 0)
|
||||
for _, v := range parseOpts.Input.Wordlists {
|
||||
var wl []string
|
||||
if runtime.GOOS == "windows" {
|
||||
|
@ -243,6 +246,11 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con
|
|||
} else {
|
||||
wl = strings.SplitN(v, ":", 2)
|
||||
}
|
||||
// Try to use absolute paths for wordlists
|
||||
fullpath, err := filepath.Abs(wl[0])
|
||||
if err == nil {
|
||||
wl[0] = fullpath
|
||||
}
|
||||
if len(wl) == 2 {
|
||||
if conf.InputMode == "sniper" {
|
||||
errs.Add(fmt.Errorf("sniper mode does not support wordlist keywords"))
|
||||
|
@ -261,7 +269,9 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con
|
|||
Template: template,
|
||||
})
|
||||
}
|
||||
tmpWordlists = append(tmpWordlists, strings.Join(wl, ":"))
|
||||
}
|
||||
conf.Wordlists = tmpWordlists
|
||||
|
||||
for _, v := range parseOpts.Input.Inputcommands {
|
||||
ic := strings.SplitN(v, ":", 2)
|
||||
|
@ -535,6 +545,8 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con
|
|||
}
|
||||
|
||||
func parseRawRequest(parseOpts *ConfigOptions, conf *Config) error {
|
||||
conf.RequestFile = parseOpts.Input.Request
|
||||
conf.RequestProto = parseOpts.Input.RequestProto
|
||||
file, err := os.Open(parseOpts.Input.Request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open request file: %s", err)
|
||||
|
@ -589,7 +601,7 @@ func parseRawRequest(parseOpts *ConfigOptions, conf *Config) error {
|
|||
}
|
||||
|
||||
// Set the request body
|
||||
b, err := ioutil.ReadAll(r)
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read request body: %s", err)
|
||||
}
|
||||
|
@ -669,7 +681,7 @@ func templatePresent(template string, conf *Config) bool {
|
|||
|
||||
func ReadConfig(configFile string) (*ConfigOptions, error) {
|
||||
conf := NewConfigOptions()
|
||||
configData, err := ioutil.ReadFile(configFile)
|
||||
configData, err := os.ReadFile(configFile)
|
||||
if err == nil {
|
||||
err = toml.Unmarshal(configData, conf)
|
||||
}
|
||||
|
@ -677,10 +689,14 @@ func ReadConfig(configFile string) (*ConfigOptions, error) {
|
|||
}
|
||||
|
||||
func ReadDefaultConfig() (*ConfigOptions, error) {
|
||||
userhome, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return NewConfigOptions(), err
|
||||
// Try to create configuration directory, ignore the potential error
|
||||
_ = CheckOrCreateConfigDir()
|
||||
conffile := filepath.Join(CONFIGDIR, ".ffufrc")
|
||||
if !FileExists(conffile) {
|
||||
userhome, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
conffile = filepath.Join(userhome, ".ffufrc")
|
||||
}
|
||||
}
|
||||
defaultconf := filepath.Join(userhome, ".ffufrc")
|
||||
return ReadConfig(defaultconf)
|
||||
return ReadConfig(conffile)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ffuf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
|
@ -8,10 +9,10 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
//used for random string generation in calibration function
|
||||
// used for random string generation in calibration function
|
||||
var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
//RandomString returns a random string of length of parameter n
|
||||
// RandomString returns a random string of length of parameter n
|
||||
func RandomString(n int) string {
|
||||
s := make([]rune, n)
|
||||
for i := range s {
|
||||
|
@ -20,7 +21,7 @@ func RandomString(n int) string {
|
|||
return string(s)
|
||||
}
|
||||
|
||||
//UniqStringSlice returns an unordered slice of unique strings. The duplicates are dropped
|
||||
// UniqStringSlice returns an unordered slice of unique strings. The duplicates are dropped
|
||||
func UniqStringSlice(inslice []string) []string {
|
||||
found := map[string]bool{}
|
||||
|
||||
|
@ -34,8 +35,8 @@ func UniqStringSlice(inslice []string) []string {
|
|||
return ret
|
||||
}
|
||||
|
||||
//FileExists checks if the filepath exists and is not a directory.
|
||||
//Returns false in case it's not possible to describe the named file.
|
||||
// FileExists checks if the filepath exists and is not a directory.
|
||||
// Returns false in case it's not possible to describe the named file.
|
||||
func FileExists(path string) bool {
|
||||
md, err := os.Stat(path)
|
||||
if err != nil {
|
||||
|
@ -45,7 +46,7 @@ func FileExists(path string) bool {
|
|||
return !md.IsDir()
|
||||
}
|
||||
|
||||
//RequestContainsKeyword checks if a keyword is present in any field of a request
|
||||
// RequestContainsKeyword checks if a keyword is present in any field of a request
|
||||
func RequestContainsKeyword(req Request, kw string) bool {
|
||||
if strings.Contains(req.Host, kw) {
|
||||
return true
|
||||
|
@ -67,7 +68,7 @@ func RequestContainsKeyword(req Request, kw string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
//HostURLFromRequest gets a host + path without the filename or last part of the URL path
|
||||
// HostURLFromRequest gets a host + path without the filename or last part of the URL path
|
||||
func HostURLFromRequest(req Request) string {
|
||||
u, _ := url.Parse(req.Url)
|
||||
u.Host = req.Host
|
||||
|
@ -76,7 +77,29 @@ func HostURLFromRequest(req Request) string {
|
|||
return u.Host + trimpath
|
||||
}
|
||||
|
||||
//Version returns the ffuf version string
|
||||
// Version returns the ffuf version string
|
||||
func Version() string {
|
||||
return fmt.Sprintf("%s%s", VERSION, VERSION_APPENDIX)
|
||||
}
|
||||
|
||||
func CheckOrCreateConfigDir() error {
|
||||
var err error
|
||||
err = createConfigDir(CONFIGDIR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = createConfigDir(HISTORYDIR)
|
||||
return err
|
||||
}
|
||||
|
||||
func createConfigDir(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
var pError *os.PathError
|
||||
if errors.As(err, &pError) {
|
||||
return os.MkdirAll(path, 0750)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -34,32 +34,37 @@ func NewCommandInput(keyword string, value string, conf *ffuf.Config) (*CommandI
|
|||
return &cmd, nil
|
||||
}
|
||||
|
||||
//Keyword returns the keyword assigned to this InternalInputProvider
|
||||
// Keyword returns the keyword assigned to this InternalInputProvider
|
||||
func (c *CommandInput) Keyword() string {
|
||||
return c.keyword
|
||||
}
|
||||
|
||||
//Position will return the current position in the input list
|
||||
// Position will return the current position in the input list
|
||||
func (c *CommandInput) Position() int {
|
||||
return c.count
|
||||
}
|
||||
|
||||
//ResetPosition will reset the current position of the InternalInputProvider
|
||||
// SetPosition will set the current position of the inputprovider
|
||||
func (c *CommandInput) SetPosition(pos int) {
|
||||
c.count = pos
|
||||
}
|
||||
|
||||
// ResetPosition will reset the current position of the InternalInputProvider
|
||||
func (c *CommandInput) ResetPosition() {
|
||||
c.count = 0
|
||||
}
|
||||
|
||||
//IncrementPosition increments the current position in the inputprovider
|
||||
// IncrementPosition increments the current position in the inputprovider
|
||||
func (c *CommandInput) IncrementPosition() {
|
||||
c.count += 1
|
||||
}
|
||||
|
||||
//Next will increment the cursor position, and return a boolean telling if there's iterations left
|
||||
// Next will increment the cursor position, and return a boolean telling if there's iterations left
|
||||
func (c *CommandInput) Next() bool {
|
||||
return c.count < c.config.InputNum
|
||||
}
|
||||
|
||||
//Value returns the input from command stdoutput
|
||||
// Value returns the input from command stdoutput
|
||||
func (c *CommandInput) Value() []byte {
|
||||
var stdout bytes.Buffer
|
||||
os.Setenv("FFUF_NUM", strconv.Itoa(c.count))
|
||||
|
@ -72,7 +77,7 @@ func (c *CommandInput) Value() []byte {
|
|||
return stdout.Bytes()
|
||||
}
|
||||
|
||||
//Total returns the size of wordlist
|
||||
// Total returns the size of wordlist
|
||||
func (c *CommandInput) Total() int {
|
||||
return c.config.InputNum
|
||||
}
|
||||
|
|
|
@ -62,12 +62,21 @@ func (i *MainInputProvider) ActivateKeywords(kws []string) {
|
|||
}
|
||||
}
|
||||
|
||||
//Position will return the current position of progress
|
||||
// Position will return the current position of progress
|
||||
func (i *MainInputProvider) Position() int {
|
||||
return i.position
|
||||
}
|
||||
|
||||
//Keywords returns a slice of all keywords in the inputprovider
|
||||
// SetPosition will reset the MainInputProvider to a specific position
|
||||
func (i *MainInputProvider) SetPosition(pos int) {
|
||||
if i.Config.InputMode == "clusterbomb" || i.Config.InputMode == "sniper" {
|
||||
i.setclusterbombPosition(pos)
|
||||
} else {
|
||||
i.setpitchforkPosition(pos)
|
||||
}
|
||||
}
|
||||
|
||||
// Keywords returns a slice of all keywords in the inputprovider
|
||||
func (i *MainInputProvider) Keywords() []string {
|
||||
kws := make([]string, 0)
|
||||
for _, p := range i.Providers {
|
||||
|
@ -76,7 +85,7 @@ func (i *MainInputProvider) Keywords() []string {
|
|||
return kws
|
||||
}
|
||||
|
||||
//Next will increment the cursor position, and return a boolean telling if there's inputs left
|
||||
// Next will increment the cursor position, and return a boolean telling if there's inputs left
|
||||
func (i *MainInputProvider) Next() bool {
|
||||
if i.position >= i.Total() {
|
||||
return false
|
||||
|
@ -85,7 +94,7 @@ func (i *MainInputProvider) Next() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
//Value returns a map of inputs for keywords
|
||||
// Value returns a map of inputs for keywords
|
||||
func (i *MainInputProvider) Value() map[string][]byte {
|
||||
retval := make(map[string][]byte)
|
||||
if i.Config.InputMode == "clusterbomb" || i.Config.InputMode == "sniper" {
|
||||
|
@ -97,7 +106,7 @@ func (i *MainInputProvider) Value() map[string][]byte {
|
|||
return retval
|
||||
}
|
||||
|
||||
//Reset resets all the inputproviders and counters
|
||||
// Reset resets all the inputproviders and counters
|
||||
func (i *MainInputProvider) Reset() {
|
||||
for _, p := range i.Providers {
|
||||
p.ResetPosition()
|
||||
|
@ -106,8 +115,8 @@ func (i *MainInputProvider) Reset() {
|
|||
i.msbIterator = 0
|
||||
}
|
||||
|
||||
//pitchforkValue returns a map of keyword:value pairs including all inputs.
|
||||
//This mode will iterate through wordlists in lockstep.
|
||||
// pitchforkValue returns a map of keyword:value pairs including all inputs.
|
||||
// This mode will iterate through wordlists in lockstep.
|
||||
func (i *MainInputProvider) pitchforkValue() map[string][]byte {
|
||||
values := make(map[string][]byte)
|
||||
for _, p := range i.Providers {
|
||||
|
@ -125,8 +134,14 @@ func (i *MainInputProvider) pitchforkValue() map[string][]byte {
|
|||
return values
|
||||
}
|
||||
|
||||
//clusterbombValue returns map of keyword:value pairs including all inputs.
|
||||
//this mode will iterate through all possible combinations.
|
||||
func (i *MainInputProvider) setpitchforkPosition(pos int) {
|
||||
for _, p := range i.Providers {
|
||||
p.SetPosition(pos)
|
||||
}
|
||||
}
|
||||
|
||||
// clusterbombValue returns map of keyword:value pairs including all inputs.
|
||||
// this mode will iterate through all possible combinations.
|
||||
func (i *MainInputProvider) clusterbombValue() map[string][]byte {
|
||||
values := make(map[string][]byte)
|
||||
// Should we signal the next InputProvider in the slice to increment
|
||||
|
@ -163,6 +178,18 @@ func (i *MainInputProvider) clusterbombValue() map[string][]byte {
|
|||
return values
|
||||
}
|
||||
|
||||
func (i *MainInputProvider) setclusterbombPosition(pos int) {
|
||||
i.Reset()
|
||||
if pos > i.Total() {
|
||||
// noop
|
||||
return
|
||||
}
|
||||
for i.position < pos-1 {
|
||||
i.Next()
|
||||
i.Value()
|
||||
}
|
||||
}
|
||||
|
||||
func (i *MainInputProvider) clusterbombIteratorReset() {
|
||||
index := 0
|
||||
for _, p := range i.Providers {
|
||||
|
@ -179,7 +206,7 @@ func (i *MainInputProvider) clusterbombIteratorReset() {
|
|||
}
|
||||
}
|
||||
|
||||
//Total returns the amount of input combinations available
|
||||
// Total returns the amount of input combinations available
|
||||
func (i *MainInputProvider) Total() int {
|
||||
count := 0
|
||||
if i.Config.InputMode == "pitchfork" {
|
||||
|
@ -204,7 +231,7 @@ func (i *MainInputProvider) Total() int {
|
|||
return count
|
||||
}
|
||||
|
||||
//sliceContains is a helper function that returns true if a string is included in a string slice
|
||||
// sliceContains is a helper function that returns true if a string is included in a string slice
|
||||
func sliceContains(sslice []string, str string) bool {
|
||||
for _, v := range sslice {
|
||||
if v == str {
|
||||
|
|
|
@ -42,57 +42,62 @@ func NewWordlistInput(keyword string, value string, conf *ffuf.Config) (*Wordlis
|
|||
return &wl, err
|
||||
}
|
||||
|
||||
//Position will return the current position in the input list
|
||||
// Position will return the current position in the input list
|
||||
func (w *WordlistInput) Position() int {
|
||||
return w.position
|
||||
}
|
||||
|
||||
//ResetPosition resets the position back to beginning of the wordlist.
|
||||
// SetPosition sets the current position of the inputprovider
|
||||
func (w *WordlistInput) SetPosition(pos int) {
|
||||
w.position = pos
|
||||
}
|
||||
|
||||
// ResetPosition resets the position back to beginning of the wordlist.
|
||||
func (w *WordlistInput) ResetPosition() {
|
||||
w.position = 0
|
||||
}
|
||||
|
||||
//Keyword returns the keyword assigned to this InternalInputProvider
|
||||
// Keyword returns the keyword assigned to this InternalInputProvider
|
||||
func (w *WordlistInput) Keyword() string {
|
||||
return w.keyword
|
||||
}
|
||||
|
||||
//Next will return a boolean telling if there's words left in the list
|
||||
// Next will return a boolean telling if there's words left in the list
|
||||
func (w *WordlistInput) Next() bool {
|
||||
return w.position < len(w.data)
|
||||
}
|
||||
|
||||
//IncrementPosition will increment the current position in the inputprovider data slice
|
||||
// IncrementPosition will increment the current position in the inputprovider data slice
|
||||
func (w *WordlistInput) IncrementPosition() {
|
||||
w.position += 1
|
||||
}
|
||||
|
||||
//Value returns the value from wordlist at current cursor position
|
||||
// Value returns the value from wordlist at current cursor position
|
||||
func (w *WordlistInput) Value() []byte {
|
||||
return w.data[w.position]
|
||||
}
|
||||
|
||||
//Total returns the size of wordlist
|
||||
// Total returns the size of wordlist
|
||||
func (w *WordlistInput) Total() int {
|
||||
return len(w.data)
|
||||
}
|
||||
|
||||
//Active returns boolean if the inputprovider is active
|
||||
// Active returns boolean if the inputprovider is active
|
||||
func (w *WordlistInput) Active() bool {
|
||||
return w.active
|
||||
}
|
||||
|
||||
//Enable sets the inputprovider as active
|
||||
// Enable sets the inputprovider as active
|
||||
func (w *WordlistInput) Enable() {
|
||||
w.active = true
|
||||
}
|
||||
|
||||
//Disable disables the inputprovider
|
||||
// Disable disables the inputprovider
|
||||
func (w *WordlistInput) Disable() {
|
||||
w.active = false
|
||||
}
|
||||
|
||||
//validFile checks that the wordlist file exists and can be read
|
||||
// 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 {
|
||||
|
@ -106,7 +111,7 @@ func (w *WordlistInput) validFile(path string) (bool, error) {
|
|||
return true, nil
|
||||
}
|
||||
|
||||
//readFile reads the file line by line to a byte slice
|
||||
// readFile reads the file line by line to a byte slice
|
||||
func (w *WordlistInput) readFile(path string) error {
|
||||
var file *os.File
|
||||
var err error
|
||||
|
|
|
@ -2,7 +2,7 @@ package output
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||
|
@ -49,7 +49,7 @@ func writeEJSON(filename string, config *ffuf.Config, res []ffuf.Result) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filename, outBytes, 0644)
|
||||
err = os.WriteFile(filename, outBytes, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ func writeJSON(filename string, config *ffuf.Config, res []ffuf.Result) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filename, outBytes, 0644)
|
||||
err = os.WriteFile(filename, outBytes, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -28,6 +28,7 @@ const (
|
|||
|
||||
type Stdoutput struct {
|
||||
config *ffuf.Config
|
||||
fuzzkeywords []string
|
||||
Results []ffuf.Result
|
||||
CurrentResults []ffuf.Result
|
||||
}
|
||||
|
@ -37,6 +38,11 @@ func NewStdoutput(conf *ffuf.Config) *Stdoutput {
|
|||
outp.config = conf
|
||||
outp.Results = make([]ffuf.Result, 0)
|
||||
outp.CurrentResults = make([]ffuf.Result, 0)
|
||||
outp.fuzzkeywords = make([]string, 0)
|
||||
for _, ip := range conf.InputProviders {
|
||||
outp.fuzzkeywords = append(outp.fuzzkeywords, ip.Keyword)
|
||||
}
|
||||
sort.Strings(outp.fuzzkeywords)
|
||||
return &outp
|
||||
}
|
||||
|
||||
|
@ -352,7 +358,7 @@ func (s *Stdoutput) writeResultToFile(resp ffuf.Response) string {
|
|||
fileName = fmt.Sprintf("%x", md5.Sum([]byte(fileContent)))
|
||||
|
||||
filePath = path.Join(s.config.OutputDirectory, fileName)
|
||||
err := ioutil.WriteFile(filePath, []byte(fileContent), 0640)
|
||||
err := os.WriteFile(filePath, []byte(fileContent), 0640)
|
||||
if err != nil {
|
||||
s.Error(err.Error())
|
||||
}
|
||||
|
@ -416,13 +422,13 @@ func (s *Stdoutput) resultMultiline(res ffuf.Result) {
|
|||
if res.ResultFile != "" {
|
||||
reslines = fmt.Sprintf("%s%s| RES | %s\n", reslines, TERMINAL_CLEAR_LINE, res.ResultFile)
|
||||
}
|
||||
for k, v := range res.Input {
|
||||
for _, k := range s.fuzzkeywords {
|
||||
if inSlice(k, s.config.CommandKeywords) {
|
||||
// If we're using external command for input, display the position instead of input
|
||||
reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, strconv.Itoa(res.Position))
|
||||
} else {
|
||||
// Wordlist input
|
||||
reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, v)
|
||||
reslines = fmt.Sprintf(res_str, reslines, TERMINAL_CLEAR_LINE, k, res.Input[k])
|
||||
}
|
||||
}
|
||||
fmt.Printf("%s\n%s\n", res_hdr, reslines)
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||
)
|
||||
|
||||
//Download results < 5MB
|
||||
// Download results < 5MB
|
||||
const MAX_DOWNLOAD_SIZE = 5242880
|
||||
|
||||
type SimpleRunner struct {
|
||||
|
@ -47,7 +47,7 @@ func NewSimpleRunner(conf *ffuf.Config, replay bool) ffuf.RunnerProvider {
|
|||
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
||||
Timeout: time.Duration(time.Duration(conf.Timeout) * time.Second),
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: conf.Http2,
|
||||
ForceAttemptHTTP2: conf.Http2,
|
||||
Proxy: proxyURL,
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 500,
|
||||
|
@ -131,7 +131,6 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
|
|||
if len(r.config.OutputDirectory) > 0 {
|
||||
rawreq, _ = httputil.DumpRequestOut(httpreq, true)
|
||||
}
|
||||
|
||||
httpresp, err := r.client.Do(httpreq)
|
||||
if err != nil {
|
||||
return ffuf.Response{}, err
|
||||
|
@ -156,7 +155,7 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
|
|||
resp.Raw = string(rawresp)
|
||||
}
|
||||
|
||||
if respbody, err := ioutil.ReadAll(httpresp.Body); err == nil {
|
||||
if respbody, err := io.ReadAll(httpresp.Body); err == nil {
|
||||
resp.ContentLength = int64(len(string(respbody)))
|
||||
resp.Data = respbody
|
||||
}
|
||||
|
@ -169,3 +168,29 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
|
|||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (r *SimpleRunner) Dump(req *ffuf.Request) ([]byte, error) {
|
||||
var httpreq *http.Request
|
||||
var err error
|
||||
data := bytes.NewReader(req.Data)
|
||||
httpreq, err = http.NewRequestWithContext(r.config.Context, req.Method, req.Url, data)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
// set default User-Agent header if not present
|
||||
if _, ok := req.Headers["User-Agent"]; !ok {
|
||||
req.Headers["User-Agent"] = fmt.Sprintf("%s v%s", "Fuzz Faster U Fool", ffuf.Version())
|
||||
}
|
||||
|
||||
// Handle Go http.Request special cases
|
||||
if _, ok := req.Headers["Host"]; ok {
|
||||
httpreq.Host = req.Headers["Host"]
|
||||
}
|
||||
|
||||
req.Host = httpreq.Host
|
||||
for k, v := range req.Headers {
|
||||
httpreq.Header.Set(k, v)
|
||||
}
|
||||
return httputil.DumpRequestOut(httpreq, true)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue