Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/operators/matchers/fuzz_harness.go
4538 views
1
package matchers
2
3
import (
4
"encoding/hex"
5
"strconv"
6
"strings"
7
)
8
9
const (
10
fuzzMaxInputSize = 16 << 10
11
fuzzMaxItems = 8
12
fuzzMaxValueBytes = 256
13
)
14
15
var (
16
fuzzMatcherTypes = []MatcherType{WordsMatcher, RegexMatcher, BinaryMatcher, StatusMatcher, SizeMatcher, DSLMatcher, XPathMatcher}
17
fuzzConditions = []string{"", "and", "or"}
18
fuzzEncodings = []string{"", "hex"}
19
fuzzParts = []string{"", "body", "raw", "all_headers", "header", "response"}
20
)
21
22
type fuzzMatcherCandidate struct {
23
matcherType MatcherType
24
condition string
25
part string
26
encoding string
27
name string
28
negative bool
29
caseInsensitive bool
30
matchAll bool
31
values []string
32
status []int
33
size []int
34
}
35
36
func matcherFromFuzzData(data []byte) (*Matcher, bool) {
37
if len(data) == 0 || len(data) > fuzzMaxInputSize {
38
return nil, false
39
}
40
41
payload := data
42
43
candidate := newFuzzMatcherCandidate(data)
44
candidate.applyLines(splitFuzzLines(payload))
45
candidate.addFallbackValues(payload)
46
candidate.addFallbackIntegers(payload)
47
48
return candidate.build()
49
}
50
51
func newFuzzMatcherCandidate(data []byte) *fuzzMatcherCandidate {
52
flags := fuzzByteAt(data, 1)
53
return &fuzzMatcherCandidate{
54
matcherType: fuzzMatcherTypes[int(fuzzByteAt(data, 0))%len(fuzzMatcherTypes)],
55
condition: fuzzConditions[int(fuzzByteAt(data, 1))%len(fuzzConditions)],
56
part: fuzzParts[int(fuzzByteAt(data, 2))%len(fuzzParts)],
57
encoding: fuzzEncodings[int(fuzzByteAt(data, 3))%len(fuzzEncodings)],
58
name: fuzzName(data),
59
negative: flags&0x01 != 0,
60
caseInsensitive: flags&0x02 != 0,
61
matchAll: flags&0x04 != 0,
62
}
63
}
64
65
func (candidate *fuzzMatcherCandidate) applyLines(lines []string) {
66
for _, line := range lines {
67
key, rawValue, ok := cutFuzzKV(line)
68
if !ok {
69
candidate.addValue(line)
70
continue
71
}
72
73
switch key {
74
case "type":
75
matcherType, err := toMatcherTypes(rawValue)
76
if err != nil {
77
candidate.matcherType = MatcherType(0)
78
} else {
79
candidate.matcherType = matcherType
80
}
81
case "condition":
82
candidate.condition = trimFuzzValue(rawValue)
83
case "part":
84
candidate.part = trimFuzzValue(rawValue)
85
case "encoding":
86
candidate.encoding = trimFuzzValue(rawValue)
87
case "name":
88
candidate.name = fuzzNameFromText(rawValue)
89
case "negative":
90
candidate.negative = parseFuzzBool(rawValue, candidate.negative)
91
case "case-insensitive":
92
candidate.caseInsensitive = parseFuzzBool(rawValue, candidate.caseInsensitive)
93
case "match-all":
94
candidate.matchAll = parseFuzzBool(rawValue, candidate.matchAll)
95
case "value":
96
candidate.addValue(rawValue)
97
case "word":
98
candidate.matcherType = WordsMatcher
99
candidate.addValue(rawValue)
100
case "regex":
101
candidate.matcherType = RegexMatcher
102
candidate.addValue(rawValue)
103
case "binary":
104
candidate.matcherType = BinaryMatcher
105
candidate.addValue(rawValue)
106
case "dsl":
107
candidate.matcherType = DSLMatcher
108
candidate.addValue(rawValue)
109
case "xpath":
110
candidate.matcherType = XPathMatcher
111
candidate.addValue(rawValue)
112
case "status":
113
candidate.matcherType = StatusMatcher
114
candidate.status = append(candidate.status, fuzzParseStatuses(rawValue)...)
115
case "size":
116
candidate.matcherType = SizeMatcher
117
candidate.size = append(candidate.size, fuzzParseSizes(rawValue)...)
118
}
119
}
120
}
121
122
func (candidate *fuzzMatcherCandidate) addFallbackValues(payload []byte) {
123
if len(candidate.values) > 0 || len(candidate.values) >= fuzzMaxItems {
124
return
125
}
126
127
for _, value := range splitFuzzFields(payload) {
128
candidate.addValue(value)
129
if len(candidate.values) >= fuzzMaxItems {
130
return
131
}
132
}
133
}
134
135
func (candidate *fuzzMatcherCandidate) addFallbackIntegers(payload []byte) {
136
if len(candidate.status) == 0 {
137
candidate.status = append(candidate.status, fuzzStatusValue(int(fuzzByteAt(payload, 0))))
138
candidate.status = append(candidate.status, fuzzStatusValue(int(fuzzByteAt(payload, 1))))
139
}
140
if len(candidate.size) == 0 {
141
candidate.size = append(candidate.size, fuzzSizeValue(int(fuzzByteAt(payload, 2))|(int(fuzzByteAt(payload, 3))<<8)))
142
candidate.size = append(candidate.size, fuzzSizeValue(int(fuzzByteAt(payload, 4))|(int(fuzzByteAt(payload, 5))<<8)))
143
}
144
}
145
146
func (candidate *fuzzMatcherCandidate) addValue(value string) {
147
value = trimFuzzValue(value)
148
if value == "" || len(candidate.values) >= fuzzMaxItems {
149
return
150
}
151
candidate.values = append(candidate.values, value)
152
}
153
154
func (candidate *fuzzMatcherCandidate) build() (*Matcher, bool) {
155
matcher := &Matcher{
156
Type: MatcherTypeHolder{MatcherType: candidate.matcherType},
157
Condition: candidate.condition,
158
Part: candidate.part,
159
Negative: candidate.negative,
160
Name: candidate.name,
161
Encoding: candidate.encoding,
162
CaseInsensitive: candidate.caseInsensitive,
163
MatchAll: candidate.matchAll,
164
}
165
166
switch candidate.matcherType {
167
case DSLMatcher:
168
matcher.Part = ""
169
matcher.Encoding = ""
170
matcher.CaseInsensitive = false
171
case StatusMatcher, SizeMatcher:
172
matcher.Encoding = ""
173
matcher.CaseInsensitive = false
174
case XPathMatcher:
175
matcher.Encoding = ""
176
matcher.CaseInsensitive = false
177
case RegexMatcher, BinaryMatcher:
178
matcher.CaseInsensitive = false
179
}
180
181
switch candidate.matcherType {
182
case WordsMatcher:
183
matcher.Words = append([]string(nil), candidate.values...)
184
case RegexMatcher:
185
matcher.Regex = append([]string(nil), candidate.values...)
186
case BinaryMatcher:
187
matcher.Binary = append([]string(nil), candidate.values...)
188
case DSLMatcher:
189
matcher.DSL = append([]string(nil), candidate.values...)
190
case XPathMatcher:
191
matcher.XPath = append([]string(nil), candidate.values...)
192
case StatusMatcher:
193
matcher.Status = append([]int(nil), candidate.status...)
194
case SizeMatcher:
195
matcher.Size = append([]int(nil), candidate.size...)
196
default:
197
matcher.Words = append([]string(nil), candidate.values...)
198
}
199
200
if matcher.GetType() == StatusMatcher || matcher.GetType() == SizeMatcher {
201
return matcher, len(matcher.Status) > 0 || len(matcher.Size) > 0
202
}
203
return matcher, len(candidate.values) > 0
204
}
205
206
func splitFuzzLines(data []byte) []string {
207
fields := strings.FieldsFunc(string(data), func(r rune) bool {
208
return r == '\n' || r == '\r' || r == ';'
209
})
210
if len(fields) > fuzzMaxItems*4 {
211
fields = fields[:fuzzMaxItems*4]
212
}
213
214
lines := make([]string, 0, len(fields))
215
for _, field := range fields {
216
field = trimFuzzValue(field)
217
if field != "" {
218
lines = append(lines, field)
219
}
220
}
221
return lines
222
}
223
224
func splitFuzzFields(data []byte) []string {
225
fields := strings.FieldsFunc(string(data), func(r rune) bool {
226
return r == '\n' || r == '\r' || r == '|' || r == ','
227
})
228
if len(fields) > fuzzMaxItems {
229
fields = fields[:fuzzMaxItems]
230
}
231
232
values := make([]string, 0, len(fields))
233
for _, field := range fields {
234
field = trimFuzzValue(field)
235
if field != "" {
236
values = append(values, field)
237
}
238
}
239
return values
240
}
241
242
func cutFuzzKV(line string) (string, string, bool) {
243
key, value, ok := strings.Cut(line, "=")
244
if !ok {
245
key, value, ok = strings.Cut(line, ":")
246
}
247
if !ok {
248
return "", "", false
249
}
250
return strings.ToLower(strings.TrimSpace(key)), trimFuzzValue(value), true
251
}
252
253
func trimFuzzValue(value string) string {
254
value = strings.TrimSpace(strings.ReplaceAll(value, "\x00", ""))
255
if len(value) > fuzzMaxValueBytes {
256
value = value[:fuzzMaxValueBytes]
257
}
258
return value
259
}
260
261
func fuzzName(data []byte) string {
262
if len(data) == 0 {
263
return ""
264
}
265
if len(data) > 8 {
266
data = data[:8]
267
}
268
return "fuzz-" + hex.EncodeToString(data)
269
}
270
271
func fuzzNameFromText(value string) string {
272
value = strings.ToLower(trimFuzzValue(value))
273
if value == "" {
274
return ""
275
}
276
var builder strings.Builder
277
for _, r := range value {
278
switch {
279
case r >= 'a' && r <= 'z':
280
builder.WriteRune(r)
281
case r >= '0' && r <= '9':
282
builder.WriteRune(r)
283
case r == '-':
284
builder.WriteRune(r)
285
}
286
if builder.Len() >= 32 {
287
break
288
}
289
}
290
if builder.Len() == 0 {
291
return ""
292
}
293
return builder.String()
294
}
295
296
func parseFuzzBool(value string, fallback bool) bool {
297
switch strings.ToLower(trimFuzzValue(value)) {
298
case "1", "true", "yes", "on":
299
return true
300
case "0", "false", "no", "off":
301
return false
302
default:
303
return fallback
304
}
305
}
306
307
func fuzzParseStatuses(value string) []int {
308
parsed := fuzzParseNumbers(value)
309
statuses := make([]int, 0, len(parsed))
310
for _, number := range parsed {
311
statuses = append(statuses, fuzzStatusValue(number))
312
}
313
return statuses
314
}
315
316
func fuzzParseSizes(value string) []int {
317
parsed := fuzzParseNumbers(value)
318
sizes := make([]int, 0, len(parsed))
319
for _, number := range parsed {
320
sizes = append(sizes, fuzzSizeValue(number))
321
}
322
return sizes
323
}
324
325
func fuzzParseNumbers(value string) []int {
326
tokens := strings.FieldsFunc(value, func(r rune) bool {
327
return r == ',' || r == '|' || r == ' ' || r == '\t'
328
})
329
if len(tokens) > fuzzMaxItems {
330
tokens = tokens[:fuzzMaxItems]
331
}
332
333
parsed := make([]int, 0, len(tokens))
334
for _, token := range tokens {
335
token = trimFuzzValue(token)
336
if token == "" {
337
continue
338
}
339
number, err := strconv.Atoi(token)
340
if err != nil {
341
number = 0
342
for i := 0; i < len(token); i++ {
343
number += int(token[i])
344
}
345
}
346
parsed = append(parsed, number)
347
}
348
return parsed
349
}
350
351
func fuzzStatusValue(number int) int {
352
if number >= 100 && number <= 599 {
353
return number
354
}
355
if number < 0 {
356
number = -number
357
}
358
return 100 + (number % 500)
359
}
360
361
func fuzzSizeValue(number int) int {
362
if number > 0 && number <= 1<<20 {
363
return number
364
}
365
if number < 0 {
366
number = -number
367
}
368
return 1 + (number % (1 << 16))
369
}
370
371
func fuzzByteAt(data []byte, index int) byte {
372
if index < 0 || index >= len(data) {
373
return 0
374
}
375
return data[index]
376
}
377
378