mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-18 19:25:27 +00:00
* chore: fix non-constant fmt string in call Signed-off-by: Dwi Siswanto <git@dw1.io> * build: bump all direct modules Signed-off-by: Dwi Siswanto <git@dw1.io> * chore(hosterrorscache): update import path Signed-off-by: Dwi Siswanto <git@dw1.io> * fix(charts): break changes Signed-off-by: Dwi Siswanto <git@dw1.io> * build: pinned `github.com/zmap/zcrypto` to v0.0.0-20240512203510-0fef58d9a9db Signed-off-by: Dwi Siswanto <git@dw1.io> * chore: golangci-lint auto fixes Signed-off-by: Dwi Siswanto <git@dw1.io> * chore: satisfy lints Signed-off-by: Dwi Siswanto <git@dw1.io> * build: migrate `github.com/xanzy/go-gitlab` => `gitlab.com/gitlab-org/api/client-go` Signed-off-by: Dwi Siswanto <git@dw1.io> * feat(json): update build constraints Signed-off-by: Dwi Siswanto <git@dw1.io> * chore: dont panicking on close err Signed-off-by: Dwi Siswanto <git@dw1.io> --------- Signed-off-by: Dwi Siswanto <git@dw1.io>
202 lines
5.7 KiB
Go
202 lines
5.7 KiB
Go
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(7)
|
|
DefaultRequestsLimit = int(4)
|
|
DefaultTimeCorrelationErrorRange = float64(0.15)
|
|
DefaultTimeSlopeErrorRange = float64(0.30)
|
|
DefaultLowSleepTimeSeconds = float64(3)
|
|
|
|
defaultSleepTimeDuration = 7 * 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.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
|
|
}
|
|
|
|
// Check the baseline delay of the request by doing two requests
|
|
baselineDelay, err := getBaselineDelay(reqSender)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
matched, matchReason, err := checkTimingDependency(
|
|
requestsLimit,
|
|
sleepDuration,
|
|
timeCorrelationErrorRange,
|
|
timeSlopeErrorRange,
|
|
baselineDelay,
|
|
reqSender,
|
|
)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
if matched {
|
|
return true, matchReason, nil
|
|
}
|
|
return false, "", nil
|
|
}
|
|
|
|
func getBaselineDelay(reqSender timeDelayRequestSender) (float64, error) {
|
|
var delays []float64
|
|
// Use zero or a very small delay to measure baseline
|
|
for i := 0; i < 3; i++ {
|
|
delay, err := reqSender(0)
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "could not get baseline delay")
|
|
}
|
|
delays = append(delays, delay)
|
|
}
|
|
|
|
var total float64
|
|
for _, d := range delays {
|
|
total += d
|
|
}
|
|
avg := total / float64(len(delays))
|
|
return avg, nil
|
|
}
|
|
|
|
// doHTTPRequestWithTimeTracing does a http request with time tracing
|
|
func doHTTPRequestWithTimeTracing(req *retryablehttp.Request, httpclient *retryablehttp.Client) (float64, error) {
|
|
var serverTime time.Duration
|
|
var wroteRequest time.Time
|
|
|
|
trace := &httptrace.ClientTrace{
|
|
WroteHeaders: func() {
|
|
wroteRequest = time.Now()
|
|
},
|
|
GotFirstResponseByte: func() {
|
|
serverTime = time.Since(wroteRequest)
|
|
},
|
|
}
|
|
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
|
|
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 serverTime.Seconds(), nil
|
|
}
|