nuclei/pkg/fuzz/analyzers/time/time_delay.go
Ice3man 5f0b7eb19b
feat: added initial live DAST server implementation (#5772)
* feat: added initial live DAST server implementation

* feat: more logging + misc additions

* feat: auth file support enhancements for more complex scenarios + misc

* feat: added io.Reader support to input providers for http

* feat: added stats db to fuzzing + use sdk for dast server + misc

* feat: more additions and enhancements

* misc changes to live server

* misc

* use utils pprof server

* feat: added simpler stats tracking system

* feat: fixed analyzer timeout issue + missing case fix

* misc changes fix

* feat: changed the logics a bit + misc changes and additions

* feat: re-added slope checks + misc

* feat: added baseline measurements for time based checks

* chore(server): fix typos

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(templates): potential DOM XSS

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(authx): potential NIL deref

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat: misc review changes

* removed debug logging

* feat: remove existing cookies only

* feat: lint fixes

* misc

* misc text update

* request endpoint update

* feat: added tracking for status code, waf-detection & grouped errors (#6028)

* feat: added tracking for status code, waf-detection & grouped errors

* lint error fixes

* feat: review changes + moving to package + misc

---------

Co-authored-by: sandeep <8293321+ehsandeep@users.noreply.github.com>

* fix var dump (#5921)

* fix var dump

* fix dump test

* Added filename length restriction for debug mode (-srd flag) (#5931)

Co-authored-by: Andrey Matveenko <an.matveenko@vkteam.ru>

* more updates

* Update pkg/output/stats/waf/waf.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: sandeep <8293321+ehsandeep@users.noreply.github.com>
Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Dogan Can Bakir <65292895+dogancanbakir@users.noreply.github.com>
Co-authored-by: 9flowers <51699499+Lercas@users.noreply.github.com>
Co-authored-by: Andrey Matveenko <an.matveenko@vkteam.ru>
Co-authored-by: Sandeep Singh <sandeep@projectdiscovery.io>
2025-02-13 18:46:28 +05:30

227 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 {
if requestsLeft <= 0 {
break
}
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
}