Compare commits

...

10 Commits

Author SHA1 Message Date
albertvala
1cd61646ff fix(nix): temporary fix - change name to ffuf
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
golangci-lint / lint (push) Has been cancelled
2025-02-04 19:00:44 +01:00
albertvala
75b2c01f88 fix: fix the flake 2025-02-04 18:35:14 +01:00
albertvala
7dcecc3f85 update flake lock 2025-02-04 18:22:39 +01:00
albertvala
9087fe7fd2 add nix stuff 2025-02-04 17:56:27 +01:00
7cdf8d3d77 feat: multiyear project, log matched response body to stdout 2025-02-04 16:30:50 +01:00
Joona Hoikkala
de9ac86677
Fixed setting unlimited rate in interactive console (#748)
* Fixed setting unlimited rate in interactive console

* Add changelog entry
2023-10-22 17:34:24 +03:00
Joona Hoikkala
0e024f4208
Fix autocalibration-strategy merging, add tests (#732) 2023-09-20 13:22:05 +03:00
Joona Hoikkala
6487328cd8
Fix csv test (#731) 2023-09-20 10:44:52 +03:00
Joona Hoikkala
7a2756a8f3
Prepare for v2.1.0 release (#724) 2023-09-16 15:18:12 +03:00
Joona Hoikkala
36124a1afe
Default to match 2XX (#723)
* Change the status matcher defaults to accept any 2XX response code

* Add changelog entry
2023-09-15 19:11:48 +03:00
16 changed files with 364 additions and 38 deletions

View File

@ -1,16 +1,26 @@
## Changelog
- master
- New
- Changed
- Fix a bug in autocalibration strategy merging, when two files have the same strategy key
- Fix panic when setting rate to 0 in the interactive console
- v2.1.0
- New
- autocalibration-strategy refactored to support extensible strategy configuration
- New cli flag `-raw` to omit urlencoding for URIs
- New cli flags `-ck` and `-cc` to enable the use of client side certificate authentication
- Integration with `github.com/ffuf/pencode` library, added `-enc` cli flag to do various in-fly encodings for input data
- Changed
- Fix multiline output
- Explicitly allow TLS1.0
- Fix markdown output file format
- Fix csv output file format
- Fixed divide by 0 error when setting rate limit to 0 manually.
- Automatic brotli and deflate decompression
- Report if request times out when a time based matcher or filter is active
- All 2XX status codes are now matched
- Allow adding "unused" wordlists in config file
- v2.0.0
- New

View File

@ -150,16 +150,19 @@ parameter.
To define the test case for ffuf, use the keyword `FUZZ` anywhere in the URL (`-u`), headers (`-H`), or POST data (`-d`).
```
Fuzz Faster U Fool - v2.0.0
Fuzz Faster U Fool - v2.1.0
HTTP OPTIONS:
-H Header `"Name: Value"`, separated by colon. Multiple -H flags are accepted.
-X HTTP method to use
-b Cookie data `"NAME1=VALUE1; NAME2=VALUE2"` for copy as curl functionality.
-cc Client cert for authentication. Client key needs to be defined as well for this to work
-ck Client key for authentication. Client certificate needs to be defined as well for this to work
-d POST data
-http2 Use HTTP2 protocol (default: false)
-ignore-body Do not fetch the response content. (default: false)
-r Follow redirects (default: false)
-raw Do not encode URI (default: false)
-recursion Scan recursively. Only FUZZ keyword is supported, and URL (-u) has to end in it. (default: false)
-recursion-depth Maximum recursion depth. (default: 0)
-recursion-strategy Recursion strategy: "default" for a redirect based, and "greedy" to recurse on all matches (default: default)
@ -175,7 +178,7 @@ GENERAL OPTIONS:
-acc Custom auto-calibration string. Can be used multiple times. Implies -ac
-ach Per host autocalibration (default: false)
-ack Autocalibration keyword (default: FUZZ)
-acs Autocalibration strategy: "basic" or "advanced" (default: basic)
-acs Custom auto-calibration strategies. Can be used multiple times. Implies -ac
-c Colorize output. (default: false)
-config Load configuration from a file
-json JSON output, printing newline-delimited JSON records (default: false)
@ -195,7 +198,7 @@ GENERAL OPTIONS:
-v Verbose output, printing full URL and redirect location (if any) with the results. (default: false)
MATCHER OPTIONS:
-mc Match HTTP status codes, or "all" for everything. (default: 200,204,301,302,307,401,403,405,500)
-mc Match HTTP status codes, or "all" for everything. (default: 200-299,301,302,307,401,403,405,500)
-ml Match amount of lines in response
-mmode Matcher set operator. Either of: and, or (default: or)
-mr Match regexp
@ -215,6 +218,7 @@ FILTER OPTIONS:
INPUT OPTIONS:
-D DirSearch wordlist compatibility mode. Used in conjunction with -e flag. (default: false)
-e Comma separated list of extensions. Extends FUZZ keyword.
-enc Encoders for keywords, eg. 'FUZZ:urlencode b64encode'
-ic Ignore wordlist comments (default: false)
-input-cmd Command producing the input. --input-num is required when using this input method. Overrides -w.
-input-num Number of inputs to test. Used in conjunction with --input-cmd. (default: 100)

22
default.nix Normal file
View File

@ -0,0 +1,22 @@
{ pkgs ? (
let
inherit (builtins) fetchTree fromJSON readFile;
inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix;
in
import (fetchTree nixpkgs.locked) {
overlays = [
(import "${fetchTree gomod2nix.locked}/overlay.nix")
];
}
)
, buildGoApplication ? pkgs.buildGoApplication
}:
buildGoApplication {
# TODO: Change name to ffuff to avoid conflicts
pname = "ffuf";
version = "0.1";
pwd = ./.;
src = ./.;
modules = ./gomod2nix.toml;
}

85
flake.lock Normal file
View File

@ -0,0 +1,85 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1733668782,
"narHash": "sha256-tPsqU00FhgdFr0JiQUiBMgPVbl1jbPCY5gbFiJycL3I=",
"owner": "nix-community",
"repo": "gomod2nix",
"rev": "514283ec89c39ad0079ff2f3b1437404e4cba608",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1738546358,
"narHash": "sha256-nLivjIygCiqLp5QcL7l56Tca/elVqM9FG1hGd9ZSsrg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c6e957d81b96751a3d5967a0fd73694f303cc914",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

29
flake.nix Normal file
View File

@ -0,0 +1,29 @@
{
description = "A basic gomod2nix flake";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.gomod2nix.url = "github:nix-community/gomod2nix";
inputs.gomod2nix.inputs.nixpkgs.follows = "nixpkgs";
inputs.gomod2nix.inputs.flake-utils.follows = "flake-utils";
outputs = { self, nixpkgs, flake-utils, gomod2nix }:
(flake-utils.lib.eachDefaultSystem
(system:
let
pkgs = nixpkgs.legacyPackages.${system};
# The current default sdk for macOS fails to compile go projects, so we use a newer one for now.
# This has no effect on other platforms.
callPackage = pkgs.darwin.apple_sdk_11_0.callPackage or pkgs.callPackage;
in
{
packages.default = callPackage ./. {
inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
};
devShells.default = callPackage ./shell.nix {
inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix;
};
})
);
}

30
gomod2nix.toml Normal file
View File

@ -0,0 +1,30 @@
schema = 3
[mod]
[mod."github.com/PuerkitoBio/goquery"]
version = "v1.8.0"
hash = "sha256-I3QaPWATvBOL/F26fIiYWKS13yBUYo+9o3tcsGIu8tY="
[mod."github.com/adrg/xdg"]
version = "v0.4.0"
hash = "sha256-zGjkdUQmrVqD6rMO9oDY+TeJCpuqnHyvkPCaXDlac/U="
[mod."github.com/andybalholm/brotli"]
version = "v1.0.5"
hash = "sha256-/qS8wU8yZQJ+uTOg66rEl9s7spxq9VIXF5L1BcaEClc="
[mod."github.com/andybalholm/cascadia"]
version = "v1.3.1"
hash = "sha256-M0u22DXSeXUaYtl1KoW1qWL46niFpycFkraCEQ/luYA="
[mod."github.com/davecgh/go-spew"]
version = "v1.1.1"
hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI="
[mod."github.com/ffuf/pencode"]
version = "v0.0.0-20230421231718-2cea7e60a693"
hash = "sha256-/ysCCAHXmBBQC8MojiRbmbZSWo5gUE3u9AuCV24ul18="
[mod."github.com/pelletier/go-toml"]
version = "v1.9.5"
hash = "sha256-RJ9K1BTId0Mled7S66iGgxHkZ5JKEIsrrNaEfM8aImc="
[mod."golang.org/x/net"]
version = "v0.7.0"
hash = "sha256-LgZYZRwtMqm+soNh+esxDSeRuIDxRGb9OEfYaFJHCDI="
[mod."golang.org/x/sys"]
version = "v0.5.0"
hash = "sha256-0LTr3KeJ1OMQAwYUQo1513dXJtQAJn5Dq8sFkc8ps1U="

View File

@ -51,7 +51,7 @@ func (m *wordlistFlag) Set(value string) error {
func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
var ignored bool
var cookies, autocalibrationstrings, autocalibrationstrategies, headers, inputcommands multiStringFlag
var cookies, autocalibrationstrings, autocalibrationstrategies, headers, inputcommands multiStringFlag
var wordlists, encoders wordlistFlag
cookies = opts.HTTP.Cookies
@ -144,7 +144,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
opts.General.AutoCalibrationStrings = autocalibrationstrings
if len(autocalibrationstrategies) > 0 {
opts.General.AutoCalibrationStrategies = []string {}
opts.General.AutoCalibrationStrategies = []string{}
for _, strategy := range autocalibrationstrategies {
opts.General.AutoCalibrationStrategies = append(opts.General.AutoCalibrationStrategies, strings.Split(strategy, ",")...)
}

View File

@ -1,14 +1,14 @@
package ffuf
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"strconv"
"time"
"encoding/json"
"path/filepath"
"os"
)
type AutocalibrationStrategy map[string][]string
@ -20,9 +20,9 @@ func (j *Job) autoCalibrationStrings() map[string][]string {
if len(j.Config.AutoCalibrationStrings) > 0 {
cInputs["custom"] = append(cInputs["custom"], j.Config.AutoCalibrationStrings...)
return cInputs
}
for _, strategy := range j.Config.AutoCalibrationStrategies {
jsonStrategy, err := os.ReadFile(filepath.Join(AUTOCALIBDIR, strategy+".json"))
if err != nil {
@ -36,36 +36,36 @@ func (j *Job) autoCalibrationStrings() map[string][]string {
j.Output.Warning(fmt.Sprintf("Skipping strategy \"%s\" because of error: %s\n", strategy, err))
continue
}
cInputs = mergeMaps(cInputs, tmpStrategy)
}
return cInputs
}
func setupDefaultAutocalibrationStrategies() error {
basic_strategy := AutocalibrationStrategy {
"basic_admin": []string{"admin"+RandomString(16), "admin"+RandomString(8)},
"htaccess": []string{".htaccess"+RandomString(16), ".htaccess"+RandomString(8)},
basic_strategy := AutocalibrationStrategy{
"basic_admin": []string{"admin" + RandomString(16), "admin" + RandomString(8)},
"htaccess": []string{".htaccess" + RandomString(16), ".htaccess" + RandomString(8)},
"basic_random": []string{RandomString(16), RandomString(8)},
}
basic_strategy_json, err := json.Marshal(basic_strategy)
if err != nil {
return err
}
advanced_strategy := AutocalibrationStrategy {
"basic_admin": []string{"admin"+RandomString(16), "admin"+RandomString(8)},
"htaccess": []string{".htaccess"+RandomString(16), ".htaccess"+RandomString(8)},
advanced_strategy := AutocalibrationStrategy{
"basic_admin": []string{"admin" + RandomString(16), "admin" + RandomString(8)},
"htaccess": []string{".htaccess" + RandomString(16), ".htaccess" + RandomString(8)},
"basic_random": []string{RandomString(16), RandomString(8)},
"admin_dir": []string{"admin"+RandomString(16)+"/", "admin"+RandomString(8)+"/"},
"random_dir": []string{RandomString(16)+"/", RandomString(8)+"/"},
"admin_dir": []string{"admin" + RandomString(16) + "/", "admin" + RandomString(8) + "/"},
"random_dir": []string{RandomString(16) + "/", RandomString(8) + "/"},
}
advanced_strategy_json, err := json.Marshal(advanced_strategy)
if err != nil {
return err
}
basic_strategy_file := filepath.Join(AUTOCALIBDIR, "basic.json")
if !FileExists(basic_strategy_file) {
err = os.WriteFile(filepath.Join(AUTOCALIBDIR, "basic.json"), basic_strategy_json, 0640)
@ -76,7 +76,7 @@ func setupDefaultAutocalibrationStrategies() error {
err = os.WriteFile(filepath.Join(AUTOCALIBDIR, "advanced.json"), advanced_strategy_json, 0640)
return err
}
return nil
}

View File

@ -0,0 +1,108 @@
package ffuf
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
// NullOutput is a dummy output provider that does nothing
type NullOutput struct {
Results []Result
}
func NewNullOutput() *NullOutput { return &NullOutput{} }
func (o *NullOutput) Banner() {}
func (o *NullOutput) Finalize() error { return nil }
func (o *NullOutput) Progress(status Progress) {}
func (o *NullOutput) Info(infostring string) {}
func (o *NullOutput) Error(errstring string) {}
func (o *NullOutput) Raw(output string) {}
func (o *NullOutput) Warning(warnstring string) {}
func (o *NullOutput) Result(resp Response) {}
func (o *NullOutput) PrintResult(res Result) {}
func (o *NullOutput) SaveFile(filename, format string) error { return nil }
func (o *NullOutput) GetCurrentResults() []Result { return o.Results }
func (o *NullOutput) SetCurrentResults(results []Result) { o.Results = results }
func (o *NullOutput) Reset() {}
func (o *NullOutput) Cycle() {}
func TestAutoCalibrationStrings(t *testing.T) {
// Create a temporary directory for the test
tmpDir, err := os.MkdirTemp("", "ffuf-test")
AUTOCALIBDIR = tmpDir
if err != nil {
t.Fatalf("Failed to create temporary directory: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create a test strategy file
strategy := AutocalibrationStrategy{
"test": {"foo", "bar"},
}
strategyJSON, err := json.Marshal(strategy)
if err != nil {
t.Fatalf("Failed to marshal strategy to JSON: %v", err)
}
strategyFile := filepath.Join(tmpDir, "test.json")
err = os.WriteFile(strategyFile, strategyJSON, 0644)
if err != nil {
t.Fatalf("Failed to write strategy file: %v", err)
}
// Create a test job with the strategy
job := &Job{
Config: &Config{
AutoCalibrationStrategies: []string{"test"},
},
Output: NewNullOutput(),
}
cInputs := job.autoCalibrationStrings()
// Verify that the custom strategy was added
if len(cInputs["custom"]) != 0 {
t.Errorf("Expected custom strategy to be empty, but got %v", cInputs["custom"])
}
// Verify that the test strategy was added
expected := []string{"foo", "bar"}
if len(cInputs["test"]) != len(expected) {
t.Errorf("Expected test strategy to have %d inputs, but got %d", len(expected), len(cInputs["test"]))
}
for i, input := range cInputs["test"] {
if input != expected[i] {
t.Errorf("Expected test strategy input %d to be %q, but got %q", i, expected[i], input)
}
}
// Verify that a missing strategy is skipped
job = &Job{
Config: &Config{
AutoCalibrationStrategies: []string{"missing"},
},
Output: NewNullOutput(),
}
cInputs = job.autoCalibrationStrings()
if len(cInputs) != 0 {
t.Errorf("Expected missing strategy to be skipped, but got %v", cInputs)
}
// Verify that a malformed strategy is skipped
malformedStrategy := []byte(`{"test": "foo"}`)
malformedFile := filepath.Join(tmpDir, "malformed.json")
err = os.WriteFile(malformedFile, malformedStrategy, 0644)
if err != nil {
t.Fatalf("Failed to write malformed strategy file: %v", err)
}
job = &Job{
Config: &Config{
AutoCalibrationStrategies: []string{"malformed"},
},
Output: NewNullOutput(),
}
cInputs = job.autoCalibrationStrings()
if len(cInputs) != 0 {
t.Errorf("Expected malformed strategy to be skipped, but got %v", cInputs)
}
}

View File

@ -7,7 +7,7 @@ import (
var (
//VERSION holds the current version number
VERSION = "2.0.0"
VERSION = "2.1.0"
//VERSION_APPENDIX holds additional version definition
VERSION_APPENDIX = "-dev"
CONFIGDIR = filepath.Join(xdg.ConfigHome, "ffuf")

View File

@ -347,6 +347,7 @@ func (j *Job) isMatch(resp Response) bool {
}
if match {
matched = true
fmt.Printf("%s\n", resp.Data)
} else if j.Config.MatcherMode == "and" {
// we already know this isn't "and" match
return false

View File

@ -170,7 +170,7 @@ func NewConfigOptions() *ConfigOptions {
c.Matcher.Lines = ""
c.Matcher.Regexp = ""
c.Matcher.Size = ""
c.Matcher.Status = "200,204,301,302,307,401,403,405,500"
c.Matcher.Status = "200-299,301,302,307,401,403,405,500"
c.Matcher.Time = ""
c.Matcher.Words = ""
c.Output.DebugLog = ""
@ -373,7 +373,6 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con
conf.ClientKey = parseOpts.HTTP.ClientKey
}
//Prepare headers and make canonical
for _, v := range parseOpts.HTTP.Headers {
hs := strings.SplitN(v, ":", 2)

View File

@ -19,15 +19,13 @@ func NewRateThrottle(conf *Config) *RateThrottle {
Config: conf,
lastAdjustment: time.Now(),
}
if conf.Rate > 0 {
r.rateCounter = ring.New(int(conf.Rate * 5))
} else {
r.rateCounter = ring.New(conf.Threads * 5)
}
if conf.Rate > 0 {
ratemicros := 1000000 / conf.Rate
r.RateLimiter = time.NewTicker(time.Microsecond * time.Duration(ratemicros))
} else {
r.rateCounter = ring.New(conf.Threads * 5)
//Million rps is probably a decent hardcoded upper speedlimit
r.RateLimiter = time.NewTicker(time.Microsecond * 1)
}
@ -72,10 +70,17 @@ func (r *RateThrottle) ChangeRate(rate int) {
}
r.RateLimiter.Stop()
r.RateLimiter = time.NewTicker(time.Microsecond * time.Duration(ratemicros))
if rate > 0 {
r.RateLimiter = time.NewTicker(time.Microsecond * time.Duration(ratemicros))
// reset the rate counter
r.rateCounter = ring.New(rate * 5)
} else {
r.RateLimiter = time.NewTicker(time.Microsecond * 1)
// reset the rate counter
r.rateCounter = ring.New(r.Config.Threads * 5)
}
r.Config.Rate = int64(rate)
// reset the rate counter
r.rateCounter = ring.New(rate * 5)
}
// rateTick adds a new duration measurement tick to rate counter

View File

@ -131,7 +131,16 @@ func mergeMaps(m1 map[string][]string, m2 map[string][]string) map[string][]stri
merged[k] = v
}
for key, value := range m2 {
merged[key] = value
if _, ok := merged[key]; !ok {
// Key not found, add it
merged[key] = value
continue
}
for _, entry := range value {
if !StrInSlice(entry, merged[key]) {
merged[key] = append(merged[key], entry)
}
}
}
return merged
}
}

View File

@ -10,7 +10,7 @@ import (
func TestToCSV(t *testing.T) {
result := ffuf.Result{
Input: map[string][]byte{"x": {66}},
Input: map[string][]byte{"x": {66}, "FFUFHASH": {65}},
Position: 1,
StatusCode: 200,
ContentLength: 3,
@ -37,8 +37,8 @@ func TestToCSV(t *testing.T) {
"5",
"application/json",
"123ns",
"resultfile"}) {
"resultfile",
"A"}) {
t.Errorf("CSV was not generated in expected format")
}
}

24
shell.nix Normal file
View File

@ -0,0 +1,24 @@
{ pkgs ? (
let
inherit (builtins) fetchTree fromJSON readFile;
inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix;
in
import (fetchTree nixpkgs.locked) {
overlays = [
(import "${fetchTree gomod2nix.locked}/overlay.nix")
];
}
)
, mkGoEnv ? pkgs.mkGoEnv
, gomod2nix ? pkgs.gomod2nix
}:
let
goEnv = mkGoEnv { pwd = ./.; };
in
pkgs.mkShell {
packages = [
goEnv
gomod2nix
];
}