Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/fuzz/analyzers/time/time_delay.go
2070 views
1
// Package time implements a time delay analyzer using linear
2
// regression heuristics inspired from ZAP to discover time
3
// based issues.
4
//
5
// The approach is the one used in ZAP for timing based checks.
6
// Advantages of this approach are many compared to the old approach of
7
// heuristics of sleep time.
8
//
9
// NOTE: This algorithm has been heavily modified after being introduced
10
// in nuclei. Now the logic has sever bug fixes and improvements and
11
// has been evolving to be more stable.
12
//
13
// As we are building a statistical model, we can predict if the delay
14
// is random or not very quickly. Also, the payloads are alternated to send
15
// a very high sleep and a very low sleep. This way the comparison is
16
// faster to eliminate negative cases. Only legitimate cases are sent for
17
// more verification.
18
//
19
// For more details on the algorithm, follow the links below:
20
// - https://groups.google.com/g/zaproxy-develop/c/KGSkNHlLtqk
21
// - https://github.com/zaproxy/zap-extensions/pull/5053
22
//
23
// This file has been implemented from its original version. It was originally licensed under the Apache License 2.0 (see LICENSE file for details).
24
// The original algorithm is implemented in ZAP Active Scanner.
25
package time
26
27
import (
28
"errors"
29
"fmt"
30
"math"
31
"strings"
32
)
33
34
type timeDelayRequestSender func(delay int) (float64, error)
35
36
// requestsSentMetadata is used to store the delay requested
37
// and delay received for each request
38
type requestsSentMetadata struct {
39
delay int
40
delayReceived float64
41
}
42
43
// checkTimingDependency checks the timing dependency for a given request
44
//
45
// It alternates and sends first a high request, then a low request. Each time
46
// it checks if the delay of the application can be predictably controlled.
47
func checkTimingDependency(
48
requestsLimit int,
49
highSleepTimeSeconds int,
50
correlationErrorRange float64,
51
slopeErrorRange float64,
52
baselineDelay float64,
53
requestSender timeDelayRequestSender,
54
) (bool, string, error) {
55
if requestsLimit < 2 {
56
return false, "", errors.New("requests limit should be at least 2")
57
}
58
59
regression := newSimpleLinearRegression()
60
requestsLeft := requestsLimit
61
62
var requestsSent []requestsSentMetadata
63
for requestsLeft > 0 {
64
isCorrelationPossible, delayRecieved, err := sendRequestAndTestConfidence(regression, highSleepTimeSeconds, requestSender, baselineDelay)
65
if err != nil {
66
return false, "", err
67
}
68
if !isCorrelationPossible {
69
return false, "", nil
70
}
71
// Check the delay is greater than baseline by seconds requested
72
if delayRecieved < baselineDelay+float64(highSleepTimeSeconds)*0.8 {
73
return false, "", nil
74
}
75
requestsSent = append(requestsSent, requestsSentMetadata{
76
delay: highSleepTimeSeconds,
77
delayReceived: delayRecieved,
78
})
79
80
isCorrelationPossibleSecond, delayRecievedSecond, err := sendRequestAndTestConfidence(regression, int(DefaultLowSleepTimeSeconds), requestSender, baselineDelay)
81
if err != nil {
82
return false, "", err
83
}
84
if !isCorrelationPossibleSecond {
85
return false, "", nil
86
}
87
if delayRecievedSecond < baselineDelay+float64(DefaultLowSleepTimeSeconds)*0.8 {
88
return false, "", nil
89
}
90
requestsLeft = requestsLeft - 2
91
92
requestsSent = append(requestsSent, requestsSentMetadata{
93
delay: int(DefaultLowSleepTimeSeconds),
94
delayReceived: delayRecievedSecond,
95
})
96
}
97
98
result := regression.IsWithinConfidence(correlationErrorRange, 1.0, slopeErrorRange)
99
if result {
100
var resultReason strings.Builder
101
resultReason.WriteString(fmt.Sprintf(
102
"[time_delay] made %d requests (baseline: %.2fs) successfully, with a regression slope of %.2f and correlation %.2f",
103
requestsLimit,
104
baselineDelay,
105
regression.slope,
106
regression.correlation,
107
))
108
for _, request := range requestsSent {
109
resultReason.WriteString(fmt.Sprintf("\n - delay: %ds, delayReceived: %fs", request.delay, request.delayReceived))
110
}
111
return result, resultReason.String(), nil
112
}
113
return result, "", nil
114
}
115
116
// sendRequestAndTestConfidence sends a request and tests the confidence of delay
117
func sendRequestAndTestConfidence(
118
regression *simpleLinearRegression,
119
delay int,
120
requestSender timeDelayRequestSender,
121
baselineDelay float64,
122
) (bool, float64, error) {
123
delayReceived, err := requestSender(delay)
124
if err != nil {
125
return false, 0, err
126
}
127
128
if delayReceived < float64(delay) {
129
return false, 0, nil
130
}
131
132
regression.AddPoint(float64(delay), delayReceived-baselineDelay)
133
134
if !regression.IsWithinConfidence(0.3, 1.0, 0.5) {
135
return false, delayReceived, nil
136
}
137
return true, delayReceived, nil
138
}
139
140
type simpleLinearRegression struct {
141
count float64
142
143
sumX float64
144
sumY float64
145
sumXX float64
146
sumYY float64
147
sumXY float64
148
149
slope float64
150
intercept float64
151
correlation float64
152
}
153
154
func newSimpleLinearRegression() *simpleLinearRegression {
155
return &simpleLinearRegression{
156
// Start everything at zero until we have data
157
slope: 0.0,
158
intercept: 0.0,
159
correlation: 0.0,
160
}
161
}
162
163
func (o *simpleLinearRegression) AddPoint(x, y float64) {
164
o.count += 1
165
o.sumX += x
166
o.sumY += y
167
o.sumXX += x * x
168
o.sumYY += y * y
169
o.sumXY += x * y
170
171
// Need at least two points for meaningful calculation
172
if o.count < 2 {
173
return
174
}
175
176
n := o.count
177
meanX := o.sumX / n
178
meanY := o.sumY / n
179
180
// Compute sample variances and covariance
181
varX := (o.sumXX - n*meanX*meanX) / (n - 1)
182
varY := (o.sumYY - n*meanY*meanY) / (n - 1)
183
covXY := (o.sumXY - n*meanX*meanY) / (n - 1)
184
185
// If varX is zero, slope cannot be computed meaningfully.
186
// This would mean all X are the same, so handle that edge case.
187
if varX == 0 {
188
o.slope = 0.0
189
o.intercept = meanY // Just the mean
190
o.correlation = 0.0 // No correlation since all X are identical
191
return
192
}
193
194
o.slope = covXY / varX
195
o.intercept = meanY - o.slope*meanX
196
197
// If varX or varY are zero, we cannot compute correlation properly.
198
if varX > 0 && varY > 0 {
199
o.correlation = covXY / (math.Sqrt(varX) * math.Sqrt(varY))
200
} else {
201
o.correlation = 0.0
202
}
203
}
204
205
func (o *simpleLinearRegression) Predict(x float64) float64 {
206
return o.slope*x + o.intercept
207
}
208
209
func (o *simpleLinearRegression) IsWithinConfidence(correlationErrorRange float64, expectedSlope float64, slopeErrorRange float64) bool {
210
if o.count < 2 {
211
return true
212
}
213
// Check if slope is within error range of expected slope
214
// Also consider cases where slope is approximately 2x of expected slope
215
// as this can happen with time-based responses
216
slopeDiff := math.Abs(expectedSlope - o.slope)
217
slope2xDiff := math.Abs(expectedSlope*2 - o.slope)
218
if slopeDiff > slopeErrorRange && slope2xDiff > slopeErrorRange {
219
return false
220
}
221
return o.correlation > 1.0-correlationErrorRange
222
}
223
224