forked from projectdiscovery/nuclei
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrequest_fuzz.go
304 lines (282 loc) · 10.8 KB
/
request_fuzz.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package http
// === Fuzzing Documentation (Scoped to this File) =====
// -> request.executeFuzzingRule [iterates over payloads(+requests) and executes]
// -> request.executePayloadUsingRules [executes single payload on all rules (if more than 1)]
// -> request.executeGeneratedFuzzingRequest [execute final generated fuzzing request and get result]
import (
"fmt"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/projectdiscovery/useragent"
urlutil "github.com/projectdiscovery/utils/url"
)
// executeFuzzingRule executes fuzzing request for a URL
// TODO:
// 1. use SPMHandler and rewrite stop at first match logic here
// 2. use scanContext instead of contextargs.Context
func (request *Request) executeFuzzingRule(input *contextargs.Context, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
// methdology:
// to check applicablity of rule, we first try to execute it with one value
// if it is applicable, we execute all requests
// if it is not applicable, we log and fail silently
// check if target should be fuzzed or not
if !request.ShouldFuzzTarget(input) {
urlx, _ := input.MetaInput.URL()
if urlx != nil {
gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, urlx.String())
} else {
gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, input.MetaInput.Input)
}
return nil
}
if input.MetaInput.Input == "" && input.MetaInput.ReqResp == nil {
return errors.New("empty input provided for fuzzing")
}
// ==== fuzzing when full HTTP request is provided =====
if input.MetaInput.ReqResp != nil {
baseRequest, err := input.MetaInput.ReqResp.BuildRequest()
if err != nil {
return errors.Wrap(err, "fuzz: could not build request obtained from target file")
}
input.MetaInput.Input = baseRequest.URL.String()
// execute with one value first to checks its applicability
err = request.executeAllFuzzingRules(input, previous, baseRequest, callback)
if err != nil {
// in case of any error, return it
if fuzz.IsErrRuleNotApplicable(err) {
// log and fail silently
gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err)
return nil
}
if errors.Is(err, ErrMissingVars) {
return err
}
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed : %s\n", request.options.TemplateID, err)
}
return nil
}
// ==== fuzzing when only URL is provided =====
// we need to use this url instead of input
inputx := input.Clone()
parsed, err := urlutil.ParseAbsoluteURL(input.MetaInput.Input, true)
if err != nil {
return errors.Wrap(err, "fuzz: could not parse input url")
}
baseRequest, err := retryablehttp.NewRequestFromURL(http.MethodGet, parsed, nil)
if err != nil {
return errors.Wrap(err, "fuzz: could not build request from url")
}
userAgent := useragent.PickRandom()
baseRequest.Header.Set("User-Agent", userAgent.Raw)
// execute with one value first to checks its applicability
err = request.executeAllFuzzingRules(inputx, previous, baseRequest, callback)
if err != nil {
// in case of any error, return it
if fuzz.IsErrRuleNotApplicable(err) {
// log and fail silently
gologger.Verbose().Msgf("[%s] fuzz: rule not applicable : %s\n", request.options.TemplateID, err)
return nil
}
if errors.Is(err, ErrMissingVars) {
return err
}
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed : %s\n", request.options.TemplateID, err)
}
return nil
}
// executeAllFuzzingRules executes all fuzzing rules defined in template for a given base request
func (request *Request) executeAllFuzzingRules(input *contextargs.Context, values map[string]interface{}, baseRequest *retryablehttp.Request, callback protocols.OutputEventCallback) error {
applicable := false
for _, rule := range request.Fuzzing {
select {
case <-input.Context().Done():
return input.Context().Err()
default:
}
err := rule.Execute(&fuzz.ExecuteRuleInput{
Input: input,
DisplayFuzzPoints: request.options.Options.DisplayFuzzPoints,
Callback: func(gr fuzz.GeneratedRequest) bool {
select {
case <-input.Context().Done():
return false
default:
}
// TODO: replace this after scanContext Refactor
return request.executeGeneratedFuzzingRequest(gr, input, callback)
},
Values: values,
BaseRequest: baseRequest.Clone(input.Context()),
})
if err == nil {
applicable = true
continue
}
if fuzz.IsErrRuleNotApplicable(err) {
gologger.Verbose().Msgf("[%s] fuzz: rule not applicable : %s\n", request.options.TemplateID, err)
continue
}
if err == types.ErrNoMoreRequests {
return nil
}
return errors.Wrap(err, "could not execute rule")
}
if !applicable {
return fuzz.ErrRuleNotApplicable.Msgf(fmt.Sprintf("no rule was applicable for this request: %v", input.MetaInput.Input))
}
return nil
}
// executeGeneratedFuzzingRequest executes a generated fuzzing request after building it using rules and payloads
func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest, input *contextargs.Context, callback protocols.OutputEventCallback) bool {
hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)
hasInteractMarkers := len(gr.InteractURLs) > 0
if request.options.HostErrorsCache != nil && request.options.HostErrorsCache.Check(input) {
return false
}
request.options.RateLimitTake()
req := &generatedRequest{
request: gr.Request,
dynamicValues: gr.DynamicValues,
interactshURLs: gr.InteractURLs,
original: request,
}
var gotMatches bool
requestErr := request.executeRequest(input, req, gr.DynamicValues, hasInteractMatchers, func(event *output.InternalWrappedEvent) {
for _, result := range event.Results {
result.IsFuzzingResult = true
result.FuzzingMethod = gr.Request.Method
result.FuzzingParameter = gr.Parameter
result.FuzzingPosition = gr.Component.Name()
}
setInteractshCallback := false
if hasInteractMarkers && hasInteractMatchers && request.options.Interactsh != nil {
requestData := &interactsh.RequestData{
MakeResultFunc: request.MakeResultEvent,
Event: event,
Operators: request.CompiledOperators,
MatchFunc: request.Match,
ExtractFunc: request.Extract,
Parameter: gr.Parameter,
Request: gr.Request,
}
setInteractshCallback = true
request.options.Interactsh.RequestEvent(gr.InteractURLs, requestData)
gotMatches = request.options.Interactsh.AlreadyMatched(requestData)
} else {
callback(event)
}
// Add the extracts to the dynamic values if any.
if event.OperatorsResult != nil {
gotMatches = event.OperatorsResult.Matched
}
if request.options.FuzzParamsFrequency != nil && !setInteractshCallback {
if !gotMatches {
request.options.FuzzParamsFrequency.MarkParameter(gr.Parameter, gr.Request.URL.String(), request.options.TemplateID)
} else {
request.options.FuzzParamsFrequency.UnmarkParameter(gr.Parameter, gr.Request.URL.String(), request.options.TemplateID)
}
}
}, 0)
// If a variable is unresolved, skip all further requests
if errors.Is(requestErr, ErrMissingVars) {
return false
}
if requestErr != nil {
if request.options.HostErrorsCache != nil {
request.options.HostErrorsCache.MarkFailed(input, requestErr)
}
gologger.Verbose().Msgf("[%s] Error occurred in request: %s\n", request.options.TemplateID, requestErr)
}
request.options.Progress.IncrementRequests()
// If this was a match, and we want to stop at first match, skip all further requests.
shouldStopAtFirstMatch := request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch
if shouldStopAtFirstMatch && gotMatches {
return false
}
return true
}
// ShouldFuzzTarget checks if given target should be fuzzed or not using `filter` field in template
func (request *Request) ShouldFuzzTarget(input *contextargs.Context) bool {
if len(request.FuzzPreCondition) == 0 {
return true
}
status := []bool{}
for index, filter := range request.FuzzPreCondition {
isMatch, _ := request.Match(request.filterDataMap(input), filter)
status = append(status, isMatch)
if request.options.Options.MatcherStatus {
gologger.Debug().Msgf("[%s] [%s] Filter => %s : %v", input.MetaInput.Target(), request.options.TemplateID, operators.GetMatcherName(filter, index), isMatch)
}
}
if len(status) == 0 {
return true
}
var matched bool
if request.fuzzPreConditionOperator == matchers.ANDCondition {
matched = operators.EvalBoolSlice(status, true)
} else {
matched = operators.EvalBoolSlice(status, false)
}
if request.options.Options.MatcherStatus {
gologger.Debug().Msgf("[%s] [%s] Final Filter Status => %v", input.MetaInput.Target(), request.options.TemplateID, matched)
}
return matched
}
// input data map returns map[string]interface{} from input
func (request *Request) filterDataMap(input *contextargs.Context) map[string]interface{} {
m := make(map[string]interface{})
parsed, err := input.MetaInput.URL()
if err != nil {
m["host"] = input.MetaInput.Input
return m
}
m = protocolutils.GenerateVariables(parsed, true, m)
for k, v := range m {
m[strings.ToLower(k)] = v
}
m["path"] = parsed.Path // override existing
m["query"] = parsed.RawQuery
// add request data like headers, body etc
if input.MetaInput.ReqResp != nil && input.MetaInput.ReqResp.Request != nil {
req := input.MetaInput.ReqResp.Request
m["method"] = req.Method
m["body"] = req.Body
sb := &strings.Builder{}
req.Headers.Iterate(func(k, v string) bool {
k = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(k), "-", "_"))
if strings.EqualFold(k, "Cookie") {
m["cookie"] = v
}
if strings.EqualFold(k, "User_Agent") {
m["user_agent"] = v
}
if strings.EqualFold(k, "content_type") {
m["content_type"] = v
}
sb.WriteString(fmt.Sprintf("%s: %s\n", k, v))
return true
})
m["header"] = sb.String()
} else {
// add default method value
m["method"] = http.MethodGet
}
// dump if svd is enabled
if request.options.Options.ShowVarDump {
gologger.Debug().Msgf("Fuzz Filter Variables: \n%s\n", vardump.DumpVariables(m))
}
return m
}