nuclei/pkg/fuzz/analyzers/time/analyzer.go
Dwi Siswanto 87ed0b2bb9
build: bump all direct modules (#6290)
* 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>
2025-07-01 00:40:44 +07:00

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
}