nuclei/pkg/fuzz/analyzers/time/time_delay.go
HD Moore f26996cb89
Remove singletons from Nuclei engine (continuation of #6210) (#6296)
* introducing execution id

* wip

* .

* adding separate execution context id

* lint

* vet

* fixing pg dialers

* test ignore

* fixing loader FD limit

* test

* fd fix

* wip: remove CloseProcesses() from dev merge

* wip: fix merge issue

* protocolstate: stop memguarding on last dialer delete

* avoid data race in dialers.RawHTTPClient

* use shared logger and avoid race conditions

* use shared logger and avoid race conditions

* go mod

* patch executionId into compiled template cache

* clean up comment in Parse

* go mod update

* bump echarts

* address merge issues

* fix use of gologger

* switch cmd/nuclei to options.Logger

* address merge issues with go.mod

* go vet: address copy of lock with new Copy function

* fixing tests

* disable speed control

* fix nil ExecuterOptions

* removing deprecated code

* fixing result print

* default logger

* cli default logger

* filter warning from results

* fix performance test

* hardcoding path

* disable upload

* refactor(runner): uses `Warning` instead of `Print` for `pdcpUploadErrMsg`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* Revert "disable upload"

This reverts commit 114fbe6663361bf41cf8b2645fd2d57083d53682.

* Revert "hardcoding path"

This reverts commit cf12ca800e0a0e974bd9fd4826a24e51547f7c00.

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
Co-authored-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com>
2025-07-10 01:17:26 +05:30

223 lines
6.5 KiB
Go

// Package time implements a time delay analyzer using linear
// regression heuristics inspired from ZAP to discover time
// based issues.
//
// The approach is the one used in ZAP for timing based checks.
// Advantages of this approach are many compared to the old approach of
// heuristics of sleep time.
//
// NOTE: This algorithm has been heavily modified after being introduced
// in nuclei. Now the logic has sever bug fixes and improvements and
// has been evolving to be more stable.
//
// As we are building a statistical model, we can predict if the delay
// is random or not very quickly. Also, the payloads are alternated to send
// a very high sleep and a very low sleep. This way the comparison is
// faster to eliminate negative cases. Only legitimate cases are sent for
// more verification.
//
// For more details on the algorithm, follow the links below:
// - https://groups.google.com/g/zaproxy-develop/c/KGSkNHlLtqk
// - https://github.com/zaproxy/zap-extensions/pull/5053
//
// This file has been implemented from its original version. It was originally licensed under the Apache License 2.0 (see LICENSE file for details).
// The original algorithm is implemented in ZAP Active Scanner.
package time
import (
"errors"
"fmt"
"math"
"strings"
)
type timeDelayRequestSender func(delay int) (float64, error)
// requestsSentMetadata is used to store the delay requested
// and delay received for each request
type requestsSentMetadata struct {
delay int
delayReceived float64
}
// checkTimingDependency checks the timing dependency for a given request
//
// It alternates and sends first a high request, then a low request. Each time
// it checks if the delay of the application can be predictably controlled.
func checkTimingDependency(
requestsLimit int,
highSleepTimeSeconds int,
correlationErrorRange float64,
slopeErrorRange float64,
baselineDelay float64,
requestSender timeDelayRequestSender,
) (bool, string, error) {
if requestsLimit < 2 {
return false, "", errors.New("requests limit should be at least 2")
}
regression := newSimpleLinearRegression()
requestsLeft := requestsLimit
var requestsSent []requestsSentMetadata
for requestsLeft > 0 {
isCorrelationPossible, delayRecieved, err := sendRequestAndTestConfidence(regression, highSleepTimeSeconds, requestSender, baselineDelay)
if err != nil {
return false, "", err
}
if !isCorrelationPossible {
return false, "", nil
}
// Check the delay is greater than baseline by seconds requested
if delayRecieved < baselineDelay+float64(highSleepTimeSeconds)*0.8 {
return false, "", nil
}
requestsSent = append(requestsSent, requestsSentMetadata{
delay: highSleepTimeSeconds,
delayReceived: delayRecieved,
})
isCorrelationPossibleSecond, delayRecievedSecond, err := sendRequestAndTestConfidence(regression, int(DefaultLowSleepTimeSeconds), requestSender, baselineDelay)
if err != nil {
return false, "", err
}
if !isCorrelationPossibleSecond {
return false, "", nil
}
if delayRecievedSecond < baselineDelay+float64(DefaultLowSleepTimeSeconds)*0.8 {
return false, "", nil
}
requestsLeft = requestsLeft - 2
requestsSent = append(requestsSent, requestsSentMetadata{
delay: int(DefaultLowSleepTimeSeconds),
delayReceived: delayRecievedSecond,
})
}
result := regression.IsWithinConfidence(correlationErrorRange, 1.0, slopeErrorRange)
if result {
var resultReason strings.Builder
resultReason.WriteString(fmt.Sprintf(
"[time_delay] made %d requests (baseline: %.2fs) successfully, with a regression slope of %.2f and correlation %.2f",
requestsLimit,
baselineDelay,
regression.slope,
regression.correlation,
))
for _, request := range requestsSent {
resultReason.WriteString(fmt.Sprintf("\n - delay: %ds, delayReceived: %fs", request.delay, request.delayReceived))
}
return result, resultReason.String(), nil
}
return result, "", nil
}
// sendRequestAndTestConfidence sends a request and tests the confidence of delay
func sendRequestAndTestConfidence(
regression *simpleLinearRegression,
delay int,
requestSender timeDelayRequestSender,
baselineDelay float64,
) (bool, float64, error) {
delayReceived, err := requestSender(delay)
if err != nil {
return false, 0, err
}
if delayReceived < float64(delay) {
return false, 0, nil
}
regression.AddPoint(float64(delay), delayReceived-baselineDelay)
if !regression.IsWithinConfidence(0.3, 1.0, 0.5) {
return false, delayReceived, nil
}
return true, delayReceived, nil
}
type simpleLinearRegression struct {
count float64
sumX float64
sumY float64
sumXX float64
sumYY float64
sumXY float64
slope float64
intercept float64
correlation float64
}
func newSimpleLinearRegression() *simpleLinearRegression {
return &simpleLinearRegression{
// Start everything at zero until we have data
slope: 0.0,
intercept: 0.0,
correlation: 0.0,
}
}
func (o *simpleLinearRegression) AddPoint(x, y float64) {
o.count += 1
o.sumX += x
o.sumY += y
o.sumXX += x * x
o.sumYY += y * y
o.sumXY += x * y
// Need at least two points for meaningful calculation
if o.count < 2 {
return
}
n := o.count
meanX := o.sumX / n
meanY := o.sumY / n
// Compute sample variances and covariance
varX := (o.sumXX - n*meanX*meanX) / (n - 1)
varY := (o.sumYY - n*meanY*meanY) / (n - 1)
covXY := (o.sumXY - n*meanX*meanY) / (n - 1)
// If varX is zero, slope cannot be computed meaningfully.
// This would mean all X are the same, so handle that edge case.
if varX == 0 {
o.slope = 0.0
o.intercept = meanY // Just the mean
o.correlation = 0.0 // No correlation since all X are identical
return
}
o.slope = covXY / varX
o.intercept = meanY - o.slope*meanX
// If varX or varY are zero, we cannot compute correlation properly.
if varX > 0 && varY > 0 {
o.correlation = covXY / (math.Sqrt(varX) * math.Sqrt(varY))
} else {
o.correlation = 0.0
}
}
func (o *simpleLinearRegression) Predict(x float64) float64 {
return o.slope*x + o.intercept
}
func (o *simpleLinearRegression) IsWithinConfidence(correlationErrorRange float64, expectedSlope float64, slopeErrorRange float64) bool {
if o.count < 2 {
return true
}
// Check if slope is within error range of expected slope
// Also consider cases where slope is approximately 2x of expected slope
// as this can happen with time-based responses
slopeDiff := math.Abs(expectedSlope - o.slope)
slope2xDiff := math.Abs(expectedSlope*2 - o.slope)
if slopeDiff > slopeErrorRange && slope2xDiff > slopeErrorRange {
return false
}
return o.correlation > 1.0-correlationErrorRange
}