-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added time based delay analyzer to fuzzing implementation (#5781)
* feat: added fuzzing output enhancements * changes as requested * misc * feat: added dfp flag to display fuzz points + misc additions * feat: added support for fuzzing nested path segments * feat: added parts to fuzzing requests * feat: added tracking for parameter occurence frequency in fuzzing * added cli flag for fuzz frequency * fixed broken tests * fixed path based sqli integration test * feat: added configurable fuzzing aggression level for payloads * fixed failing test * feat: added analyzers implementation for fuzzing * feat: misc changes to analyzer * feat: misc additions of units + tests fix * misc changes to implementation
- Loading branch information
1 parent
13af7cc
commit b046f76
Showing
16 changed files
with
723 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package analyzers | ||
|
||
import ( | ||
"math/rand" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz" | ||
"github.com/projectdiscovery/retryablehttp-go" | ||
) | ||
|
||
// Analyzer is an interface for all the analyzers | ||
// that can be used for the fuzzer | ||
type Analyzer interface { | ||
// Name returns the name of the analyzer | ||
Name() string | ||
// ApplyTransformation applies the transformation to the initial payload. | ||
ApplyInitialTransformation(data string, params map[string]interface{}) string | ||
// Analyze is the main function for the analyzer | ||
Analyze(options *Options) (bool, string, error) | ||
} | ||
|
||
// AnalyzerTemplate is the template for the analyzer | ||
type AnalyzerTemplate struct { | ||
// description: | | ||
// Name is the name of the analyzer to use | ||
// values: | ||
// - time_delay | ||
Name string `json:"name" yaml:"name"` | ||
// description: | | ||
// Parameters is the parameters for the analyzer | ||
// | ||
// Parameters are different for each analyzer. For example, you can customize | ||
// time_delay analyzer with sleep_duration, time_slope_error_range, etc. Refer | ||
// to the docs for each analyzer to get an idea about parameters. | ||
Parameters map[string]interface{} `json:"parameters" yaml:"parameters"` | ||
} | ||
|
||
var ( | ||
analyzers map[string]Analyzer | ||
) | ||
|
||
// RegisterAnalyzer registers a new analyzer | ||
func RegisterAnalyzer(name string, analyzer Analyzer) { | ||
analyzers[name] = analyzer | ||
} | ||
|
||
// GetAnalyzer returns the analyzer for a given name | ||
func GetAnalyzer(name string) Analyzer { | ||
return analyzers[name] | ||
} | ||
|
||
func init() { | ||
analyzers = make(map[string]Analyzer) | ||
} | ||
|
||
// Options contains the options for the analyzer | ||
type Options struct { | ||
FuzzGenerated fuzz.GeneratedRequest | ||
HttpClient *retryablehttp.Client | ||
ResponseTimeDelay time.Duration | ||
AnalyzerParameters map[string]interface{} | ||
} | ||
|
||
var ( | ||
random = rand.New(rand.NewSource(time.Now().UnixNano())) | ||
) | ||
|
||
// ApplyPayloadTransformations applies the payload transformations to the payload | ||
// It supports the below payloads - | ||
// - [RANDNUM] => random number between 1000 and 9999 | ||
// - [RANDSTR] => random string of 4 characters | ||
func ApplyPayloadTransformations(value string) string { | ||
randomInt := GetRandomInteger() | ||
randomStr := randStringBytesMask(4) | ||
|
||
value = strings.ReplaceAll(value, "[RANDNUM]", strconv.Itoa(randomInt)) | ||
value = strings.ReplaceAll(value, "[RANDSTR]", randomStr) | ||
return value | ||
} | ||
|
||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
const ( | ||
letterIdxBits = 6 // 6 bits to represent a letter index | ||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits | ||
) | ||
|
||
func randStringBytesMask(n int) string { | ||
b := make([]byte, n) | ||
for i := 0; i < n; { | ||
if idx := int(random.Int63() & letterIdxMask); idx < len(letterBytes) { | ||
b[i] = letterBytes[idx] | ||
i++ | ||
} | ||
} | ||
return string(b) | ||
} | ||
|
||
// GetRandomInteger returns a random integer between 1000 and 9999 | ||
func GetRandomInteger() int { | ||
return random.Intn(9000) + 1000 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package time | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"net/http/httptrace" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/projectdiscovery/gologger" | ||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers" | ||
"github.com/projectdiscovery/retryablehttp-go" | ||
) | ||
|
||
// Analyzer is a time delay analyzer for the fuzzer | ||
type Analyzer struct{} | ||
|
||
const ( | ||
DefaultSleepDuration = int(5) | ||
DefaultRequestsLimit = int(4) | ||
DefaultTimeCorrelationErrorRange = float64(0.15) | ||
DefaultTimeSlopeErrorRange = float64(0.30) | ||
|
||
defaultSleepTimeDuration = 5 * time.Second | ||
) | ||
|
||
var _ analyzers.Analyzer = &Analyzer{} | ||
|
||
func init() { | ||
analyzers.RegisterAnalyzer("time_delay", &Analyzer{}) | ||
} | ||
|
||
// Name is the name of the analyzer | ||
func (a *Analyzer) Name() string { | ||
return "time_delay" | ||
} | ||
|
||
// ApplyInitialTransformation applies the transformation to the initial payload. | ||
// | ||
// It supports the below payloads - | ||
// - [SLEEPTIME] => sleep_duration | ||
// - [INFERENCE] => Inference payload for time delay analyzer | ||
// | ||
// It also applies the payload transformations to the payload | ||
// which includes [RANDNUM] and [RANDSTR] | ||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string { | ||
duration := DefaultSleepDuration | ||
if len(params) > 0 { | ||
if v, ok := params["sleep_duration"]; ok { | ||
duration, ok = v.(int) | ||
if !ok { | ||
duration = DefaultSleepDuration | ||
gologger.Warning().Msgf("Invalid sleep_duration parameter type, using default value: %d", duration) | ||
} | ||
} | ||
} | ||
data = strings.ReplaceAll(data, "[SLEEPTIME]", strconv.Itoa(duration)) | ||
data = analyzers.ApplyPayloadTransformations(data) | ||
|
||
// Also support [INFERENCE] for the time delay analyzer | ||
if strings.Contains(data, "[INFERENCE]") { | ||
randInt := analyzers.GetRandomInteger() | ||
data = strings.ReplaceAll(data, "[INFERENCE]", fmt.Sprintf("%d=%d", randInt, randInt)) | ||
} | ||
return data | ||
} | ||
|
||
func (a *Analyzer) parseAnalyzerParameters(params map[string]interface{}) (int, int, float64, float64, error) { | ||
requestsLimit := DefaultRequestsLimit | ||
sleepDuration := DefaultSleepDuration | ||
timeCorrelationErrorRange := DefaultTimeCorrelationErrorRange | ||
timeSlopeErrorRange := DefaultTimeSlopeErrorRange | ||
|
||
if len(params) == 0 { | ||
return requestsLimit, sleepDuration, timeCorrelationErrorRange, timeSlopeErrorRange, nil | ||
} | ||
var ok bool | ||
for k, v := range params { | ||
switch k { | ||
case "sleep_duration": | ||
sleepDuration, ok = v.(int) | ||
case "requests_limit": | ||
requestsLimit, ok = v.(int) | ||
case "time_correlation_error_range": | ||
timeCorrelationErrorRange, ok = v.(float64) | ||
case "time_slope_error_range": | ||
timeSlopeErrorRange, ok = v.(float64) | ||
} | ||
if !ok { | ||
return 0, 0, 0, 0, errors.Errorf("invalid parameter type for %s", k) | ||
} | ||
} | ||
return requestsLimit, sleepDuration, timeCorrelationErrorRange, timeSlopeErrorRange, nil | ||
} | ||
|
||
// Analyze is the main function for the analyzer | ||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) { | ||
if options.ResponseTimeDelay < defaultSleepTimeDuration { | ||
return false, "", nil | ||
} | ||
|
||
// Parse parameters for this analyzer if any or use default values | ||
requestsLimit, sleepDuration, timeCorrelationErrorRange, timeSlopeErrorRange, err := | ||
a.parseAnalyzerParameters(options.AnalyzerParameters) | ||
if err != nil { | ||
return false, "", err | ||
} | ||
|
||
reqSender := func(delay int) (float64, error) { | ||
gr := options.FuzzGenerated | ||
replaced := strings.ReplaceAll(gr.OriginalPayload, "[SLEEPTIME]", strconv.Itoa(delay)) | ||
replaced = a.ApplyInitialTransformation(replaced, options.AnalyzerParameters) | ||
|
||
if err := gr.Component.SetValue(gr.Key, replaced); err != nil { | ||
return 0, errors.Wrap(err, "could not set value in component") | ||
} | ||
|
||
rebuilt, err := gr.Component.Rebuild() | ||
if err != nil { | ||
return 0, errors.Wrap(err, "could not rebuild request") | ||
} | ||
gologger.Verbose().Msgf("[%s] Sending request with %d delay for: %s", a.Name(), delay, rebuilt.URL.String()) | ||
|
||
timeTaken, err := doHTTPRequestWithTimeTracing(rebuilt, options.HttpClient) | ||
if err != nil { | ||
return 0, errors.Wrap(err, "could not do request with time tracing") | ||
} | ||
return timeTaken, nil | ||
} | ||
matched, matchReason, err := checkTimingDependency( | ||
requestsLimit, | ||
sleepDuration, | ||
timeCorrelationErrorRange, | ||
timeSlopeErrorRange, | ||
reqSender, | ||
) | ||
if err != nil { | ||
return false, "", err | ||
} | ||
if matched { | ||
return true, matchReason, nil | ||
} | ||
return false, "", nil | ||
} | ||
|
||
// doHTTPRequestWithTimeTracing does a http request with time tracing | ||
func doHTTPRequestWithTimeTracing(req *retryablehttp.Request, httpclient *retryablehttp.Client) (float64, error) { | ||
var ttfb time.Duration | ||
var start time.Time | ||
|
||
trace := &httptrace.ClientTrace{ | ||
GotFirstResponseByte: func() { ttfb = time.Since(start) }, | ||
} | ||
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) | ||
start = time.Now() | ||
resp, err := httpclient.Do(req) | ||
if err != nil { | ||
return 0, errors.Wrap(err, "could not do request") | ||
} | ||
|
||
_, err = io.ReadAll(resp.Body) | ||
if err != nil { | ||
return 0, errors.Wrap(err, "could not read response body") | ||
} | ||
return ttfb.Seconds(), nil | ||
} |
Oops, something went wrong.