Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/operators/matchers/match.go
2858 views
1
package matchers
2
3
import (
4
"os"
5
"strings"
6
7
"github.com/Knetic/govaluate"
8
"github.com/antchfx/htmlquery"
9
"github.com/antchfx/xmlquery"
10
11
"github.com/projectdiscovery/gologger"
12
"github.com/projectdiscovery/nuclei/v3/pkg/operators/common/dsl"
13
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"
14
stringsutil "github.com/projectdiscovery/utils/strings"
15
)
16
17
var (
18
// showDSLErr controls whether to show hidden DSL errors or not
19
showDSLErr = strings.EqualFold(os.Getenv("SHOW_DSL_ERRORS"), "true")
20
)
21
22
// MatchStatusCode matches a status code check against a corpus
23
func (matcher *Matcher) MatchStatusCode(statusCode int) bool {
24
// Iterate over all the status codes accepted as valid
25
//
26
// Status codes don't support AND conditions.
27
for _, status := range matcher.Status {
28
// Continue if the status codes don't match
29
if statusCode != status {
30
continue
31
}
32
// Return on the first match.
33
return true
34
}
35
return false
36
}
37
38
// MatchSize matches a size check against a corpus
39
func (matcher *Matcher) MatchSize(length int) bool {
40
// Iterate over all the sizes accepted as valid
41
//
42
// Sizes codes don't support AND conditions.
43
for _, size := range matcher.Size {
44
// Continue if the size doesn't match
45
if length != size {
46
continue
47
}
48
// Return on the first match.
49
return true
50
}
51
return false
52
}
53
54
// MatchWords matches a word check against a corpus.
55
func (matcher *Matcher) MatchWords(corpus string, data map[string]interface{}) (bool, []string) {
56
if matcher.CaseInsensitive {
57
corpus = strings.ToLower(corpus)
58
}
59
60
var matchedWords []string
61
// Iterate over all the words accepted as valid
62
for i, word := range matcher.Words {
63
if data == nil {
64
data = make(map[string]interface{})
65
}
66
67
var err error
68
word, err = expressions.Evaluate(word, data)
69
if err != nil {
70
gologger.Warning().Msgf("Error while evaluating word matcher: %q", word)
71
if matcher.condition == ANDCondition {
72
return false, []string{}
73
}
74
}
75
// Continue if the word doesn't match
76
if !strings.Contains(corpus, word) {
77
// If we are in an AND request and a match failed,
78
// return false as the AND condition fails on any single mismatch.
79
switch matcher.condition {
80
case ANDCondition:
81
return false, []string{}
82
case ORCondition:
83
continue
84
}
85
}
86
87
// If the condition was an OR, return on the first match.
88
if matcher.condition == ORCondition && !matcher.MatchAll {
89
return true, []string{word}
90
}
91
matchedWords = append(matchedWords, word)
92
93
// If we are at the end of the words, return with true
94
if len(matcher.Words)-1 == i && !matcher.MatchAll {
95
return true, matchedWords
96
}
97
}
98
if len(matchedWords) > 0 && matcher.MatchAll {
99
return true, matchedWords
100
}
101
return false, []string{}
102
}
103
104
// MatchRegex matches a regex check against a corpus
105
func (matcher *Matcher) MatchRegex(corpus string) (bool, []string) {
106
var matchedRegexes []string
107
// Iterate over all the regexes accepted as valid
108
for i, regex := range matcher.regexCompiled {
109
// Literal prefix short-circuit
110
rstr := regex.String()
111
if !strings.Contains(rstr, "(?i") { // covers (?i) and (?i:
112
if prefix, ok := regex.LiteralPrefix(); ok && prefix != "" {
113
if !strings.Contains(corpus, prefix) {
114
switch matcher.condition {
115
case ANDCondition:
116
return false, []string{}
117
case ORCondition:
118
continue
119
}
120
}
121
}
122
}
123
124
// Fast OR-path: return first match without full scan
125
if matcher.condition == ORCondition && !matcher.MatchAll {
126
m := regex.FindAllString(corpus, 1)
127
if len(m) == 0 {
128
continue
129
}
130
return true, m
131
}
132
133
// Single scan: get all matches directly
134
currentMatches := regex.FindAllString(corpus, -1)
135
if len(currentMatches) == 0 {
136
switch matcher.condition {
137
case ANDCondition:
138
return false, []string{}
139
case ORCondition:
140
continue
141
}
142
}
143
144
// If the condition was an OR (and MatchAll true), we still need to gather all
145
matchedRegexes = append(matchedRegexes, currentMatches...)
146
147
// If we are at the end of the regex, return with true
148
if len(matcher.regexCompiled)-1 == i && !matcher.MatchAll {
149
return true, matchedRegexes
150
}
151
}
152
if len(matchedRegexes) > 0 && matcher.MatchAll {
153
return true, matchedRegexes
154
}
155
return false, []string{}
156
}
157
158
// MatchBinary matches a binary check against a corpus
159
func (matcher *Matcher) MatchBinary(corpus string) (bool, []string) {
160
var matchedBinary []string
161
// Iterate over all the words accepted as valid
162
for i, binary := range matcher.binaryDecoded {
163
if !strings.Contains(corpus, binary) {
164
// If we are in an AND request and a match failed,
165
// return false as the AND condition fails on any single mismatch.
166
switch matcher.condition {
167
case ANDCondition:
168
return false, []string{}
169
case ORCondition:
170
continue
171
}
172
}
173
174
// If the condition was an OR, return on the first match.
175
if matcher.condition == ORCondition {
176
return true, []string{binary}
177
}
178
179
matchedBinary = append(matchedBinary, binary)
180
181
// If we are at the end of the words, return with true
182
if len(matcher.Binary)-1 == i {
183
return true, matchedBinary
184
}
185
}
186
return false, []string{}
187
}
188
189
// MatchDSL matches on a generic map result
190
func (matcher *Matcher) MatchDSL(data map[string]interface{}) bool {
191
logExpressionEvaluationFailure := func(matcherName string, err error) {
192
gologger.Warning().Msgf("Could not evaluate expression: %s, error: %s", matcherName, err.Error())
193
}
194
195
// Iterate over all the expressions accepted as valid
196
for i, expression := range matcher.dslCompiled {
197
if varErr := expressions.ContainsUnresolvedVariables(expression.String()); varErr != nil {
198
resolvedExpression, err := expressions.Evaluate(expression.String(), data)
199
if err != nil {
200
logExpressionEvaluationFailure(matcher.Name, err)
201
return false
202
}
203
expression, err = govaluate.NewEvaluableExpressionWithFunctions(resolvedExpression, dsl.HelperFunctions)
204
if err != nil {
205
logExpressionEvaluationFailure(matcher.Name, err)
206
return false
207
}
208
}
209
210
result, err := expression.Evaluate(data)
211
if err != nil {
212
if matcher.condition == ANDCondition {
213
return false
214
}
215
if !matcher.ignoreErr(err) {
216
gologger.Warning().Msgf("[%s] %s", data["template-id"], err.Error())
217
}
218
continue
219
}
220
221
if boolResult, ok := result.(bool); !ok {
222
gologger.Error().Label("WRN").Msgf("[%s] The return value of a DSL statement must return a boolean value.", data["template-id"])
223
continue
224
} else if !boolResult {
225
// If we are in an AND request and a match failed,
226
// return false as the AND condition fails on any single mismatch.
227
switch matcher.condition {
228
case ANDCondition:
229
return false
230
case ORCondition:
231
continue
232
}
233
}
234
235
// If the condition was an OR, return on the first match.
236
if matcher.condition == ORCondition {
237
return true
238
}
239
240
// If we are at the end of the dsl, return with true
241
if len(matcher.dslCompiled)-1 == i {
242
return true
243
}
244
}
245
return false
246
}
247
248
// MatchXPath matches on a generic map result
249
func (matcher *Matcher) MatchXPath(corpus string) bool {
250
if strings.HasPrefix(corpus, "<?xml") {
251
return matcher.MatchXML(corpus)
252
}
253
return matcher.MatchHTML(corpus)
254
}
255
256
// MatchHTML matches items from HTML using XPath selectors
257
func (matcher *Matcher) MatchHTML(corpus string) bool {
258
doc, err := htmlquery.Parse(strings.NewReader(corpus))
259
if err != nil {
260
return false
261
}
262
263
matches := 0
264
265
for _, k := range matcher.XPath {
266
nodes, err := htmlquery.QueryAll(doc, k)
267
if err != nil {
268
continue
269
}
270
271
// Continue if the xpath doesn't return any nodes
272
if len(nodes) == 0 {
273
// If we are in an AND request and a match failed,
274
// return false as the AND condition fails on any single mismatch.
275
switch matcher.condition {
276
case ANDCondition:
277
return false
278
case ORCondition:
279
continue
280
}
281
}
282
283
// If the condition was an OR, return on the first match.
284
if matcher.condition == ORCondition && !matcher.MatchAll {
285
return true
286
}
287
288
matches = matches + len(nodes)
289
}
290
return matches > 0
291
}
292
293
// MatchXML matches items from XML using XPath selectors
294
func (matcher *Matcher) MatchXML(corpus string) bool {
295
doc, err := xmlquery.Parse(strings.NewReader(corpus))
296
if err != nil {
297
return false
298
}
299
300
matches := 0
301
302
for _, k := range matcher.XPath {
303
nodes, err := xmlquery.QueryAll(doc, k)
304
if err != nil {
305
continue
306
}
307
308
// Continue if the xpath doesn't return any nodes
309
if len(nodes) == 0 {
310
// If we are in an AND request and a match failed,
311
// return false as the AND condition fails on any single mismatch.
312
switch matcher.condition {
313
case ANDCondition:
314
return false
315
case ORCondition:
316
continue
317
}
318
}
319
320
// If the condition was an OR, return on the first match.
321
if matcher.condition == ORCondition && !matcher.MatchAll {
322
return true
323
}
324
matches = matches + len(nodes)
325
}
326
327
return matches > 0
328
}
329
330
// ignoreErr checks if the error is to be ignored or not
331
// Reference: https://github.com/projectdiscovery/nuclei/issues/3950
332
func (m *Matcher) ignoreErr(err error) bool {
333
if showDSLErr {
334
return false
335
}
336
if stringsutil.ContainsAny(err.Error(), "No parameter", "error parsing argument value") {
337
return true
338
}
339
return false
340
}
341
342