Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/protocols/http/build_request.go
2845 views
1
package http
2
3
import (
4
"bufio"
5
"context"
6
"fmt"
7
"net/http"
8
"strconv"
9
"strings"
10
"time"
11
12
"github.com/pkg/errors"
13
"github.com/projectdiscovery/useragent"
14
15
"github.com/projectdiscovery/gologger"
16
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
17
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
18
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
19
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"
20
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/race"
23
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/raw"
24
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
25
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
26
"github.com/projectdiscovery/nuclei/v3/pkg/types"
27
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
28
"github.com/projectdiscovery/rawhttp"
29
"github.com/projectdiscovery/retryablehttp-go"
30
"github.com/projectdiscovery/utils/errkit"
31
stringsutil "github.com/projectdiscovery/utils/strings"
32
urlutil "github.com/projectdiscovery/utils/url"
33
)
34
35
const (
36
ReqURLPatternKey = "req_url_pattern"
37
)
38
39
// ErrEvalExpression
40
type errorTemplate struct {
41
format string
42
}
43
44
func (e errorTemplate) Wrap(err error) wrapperError {
45
return wrapperError{template: e, err: err}
46
}
47
48
func (e errorTemplate) Msgf(args ...interface{}) error {
49
return errkit.Newf(e.format, args...)
50
}
51
52
type wrapperError struct {
53
template errorTemplate
54
err error
55
}
56
57
func (w wrapperError) WithTag(tag string) error {
58
return errkit.Wrap(w.err, w.template.format)
59
}
60
61
func (w wrapperError) Msgf(format string, args ...interface{}) error {
62
return errkit.Wrapf(w.err, format, args...)
63
}
64
65
func (w wrapperError) Error() string {
66
return errkit.Wrap(w.err, w.template.format).Error()
67
}
68
69
// ErrEvalExpression
70
var (
71
ErrEvalExpression = errorTemplate{"could not evaluate helper expressions"}
72
ErrUnresolvedVars = errorTemplate{"unresolved variables `%v` found in request"}
73
)
74
75
// generatedRequest is a single generated request wrapped for a template request
76
type generatedRequest struct {
77
original *Request
78
rawRequest *raw.Request
79
meta map[string]interface{}
80
pipelinedClient *rawhttp.PipelineClient
81
request *retryablehttp.Request
82
dynamicValues map[string]interface{}
83
interactshURLs []string
84
customCancelFunction context.CancelFunc
85
// requestURLPattern tracks unmodified request url pattern without values ( it is used for constant vuln_hash)
86
// ex: {{BaseURL}}/api/exp?param={{randstr}}
87
requestURLPattern string
88
89
fuzzGeneratedRequest fuzz.GeneratedRequest
90
}
91
92
// setReqURLPattern sets the url request pattern for the generated request
93
func (gr *generatedRequest) setReqURLPattern(reqURLPattern string) {
94
if idx := strings.IndexByte(reqURLPattern, '\n'); idx >= 0 {
95
reqURLPattern = strings.TrimSpace(reqURLPattern[:idx])
96
// this is raw request (if it has 3 parts after strings.Fields then its valid only use 2nd part)
97
parts := strings.Fields(reqURLPattern)
98
if len(parts) >= 3 {
99
// remove first and last and use all in between
100
parts = parts[1 : len(parts)-1]
101
reqURLPattern = strings.Join(parts, " ")
102
}
103
} else {
104
reqURLPattern = strings.TrimSpace(reqURLPattern)
105
}
106
107
// now urlRequestPattern is generated replace preprocessor values with actual placeholders
108
// that were used (these are called generated 'constants' and contains {{}} in var name)
109
for k, v := range gr.original.options.Constants {
110
if strings.HasPrefix(k, "{{") && strings.HasSuffix(k, "}}") {
111
// this takes care of all preprocessors ( currently we have randstr and its variations)
112
reqURLPattern = strings.ReplaceAll(reqURLPattern, fmt.Sprint(v), k)
113
}
114
}
115
gr.requestURLPattern = reqURLPattern
116
}
117
118
// ApplyAuth applies the auth provider to the generated request
119
func (g *generatedRequest) ApplyAuth(provider authprovider.AuthProvider) {
120
if provider == nil {
121
return
122
}
123
if g.request != nil {
124
authStrategies := provider.LookupURLX(g.request.URL)
125
for _, strategy := range authStrategies {
126
strategy.ApplyOnRR(g.request)
127
}
128
}
129
if g.rawRequest != nil {
130
parsed, err := urlutil.ParseAbsoluteURL(g.rawRequest.FullURL, true)
131
if err != nil {
132
gologger.Warning().Msgf("[authprovider] Could not parse URL %s: %s\n", g.rawRequest.FullURL, err)
133
return
134
}
135
authStrategies := provider.LookupURLX(parsed)
136
// here we need to apply it custom because we don't have a standard/official
137
// rawhttp request format ( which we probably should have )
138
for _, strategy := range authStrategies {
139
g.rawRequest.ApplyAuthStrategy(strategy)
140
}
141
}
142
}
143
144
func (g *generatedRequest) URL() string {
145
if g.request != nil {
146
return g.request.String()
147
}
148
if g.rawRequest != nil {
149
return g.rawRequest.FullURL
150
}
151
return ""
152
}
153
154
// Make creates a http request for the provided input.
155
// It returns ErrNoMoreRequests as error when all the requests have been exhausted.
156
func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context, reqData string, payloads, dynamicValues map[string]interface{}) (gr *generatedRequest, err error) {
157
origReqData := reqData
158
defer func() {
159
if gr != nil {
160
gr.setReqURLPattern(origReqData)
161
}
162
}()
163
// value of `reqData` depends on the type of request specified in template
164
// 1. If request is raw request = reqData contains raw request (i.e http request dump)
165
// 2. If request is Normal ( simply put not a raw request) (Ex: with placeholders `path`) = reqData contains relative path
166
167
// add template context values to dynamicValues (this takes care of self-contained and other types of requests)
168
// Note: `iterate-all` and flow are mutually exclusive. flow uses templateCtx and iterate-all uses dynamicValues
169
if r.request.options.HasTemplateCtx(input.MetaInput) {
170
// skip creating template context if not available
171
dynamicValues = generators.MergeMaps(dynamicValues, r.request.options.GetTemplateCtx(input.MetaInput).GetAll())
172
}
173
174
isRawRequest := len(r.request.Raw) > 0
175
// replace interactsh variables with actual interactsh urls
176
if r.options.Interactsh != nil {
177
reqData, r.interactshURLs = r.options.Interactsh.Replace(reqData, []string{})
178
for payloadName, payloadValue := range payloads {
179
payloads[payloadName], r.interactshURLs = r.options.Interactsh.Replace(types.ToString(payloadValue), r.interactshURLs)
180
}
181
} else {
182
for payloadName, payloadValue := range payloads {
183
payloads[payloadName] = types.ToStringNSlice(payloadValue)
184
}
185
}
186
187
if r.request.SelfContained {
188
return r.makeSelfContainedRequest(ctx, reqData, payloads, dynamicValues)
189
}
190
191
// Parse target url
192
parsed, err := urlutil.ParseAbsoluteURL(input.MetaInput.Input, false)
193
if err != nil {
194
return nil, err
195
}
196
197
// Non-Raw Requests ex `{{BaseURL}}/somepath` may or maynot have slash after variable and the same is the case for
198
// target url to avoid inconsistencies extra slash if exists has to removed from default variables
199
hasTrailingSlash := false
200
if !isRawRequest {
201
// if path contains port ex: {{BaseURL}}:8080 use port specified in reqData
202
parsed, reqData = httputil.UpdateURLPortFromPayload(parsed, reqData)
203
hasTrailingSlash = httputil.HasTrailingSlash(reqData)
204
}
205
206
// defaultreqvars are vars generated from request/input ex: {{baseURL}}, {{Host}} etc
207
// contextargs generate extra vars that may/may not be available always (ex: "ip")
208
defaultReqVars := protocolutils.GenerateVariables(parsed, hasTrailingSlash, contextargs.GenerateVariables(input))
209
// optionvars are vars passed from CLI or env variables
210
optionVars := generators.BuildPayloadFromOptions(r.request.options.Options)
211
212
variablesMap, interactURLs := r.options.Variables.EvaluateWithInteractsh(generators.MergeMaps(dynamicValues, defaultReqVars, optionVars), r.options.Interactsh)
213
if len(interactURLs) > 0 {
214
r.interactshURLs = append(r.interactshURLs, interactURLs...)
215
}
216
// allVars contains all variables from all sources
217
allVars := generators.MergeMaps(dynamicValues, defaultReqVars, optionVars, variablesMap, r.options.Constants)
218
219
// Evaluate payload variables
220
// eg: payload variables can be username: jon.doe@{{Hostname}}
221
for payloadName, payloadValue := range payloads {
222
payloads[payloadName], err = expressions.Evaluate(types.ToString(payloadValue), allVars)
223
if err != nil {
224
return nil, errkit.Wrap(err, "could not evaluate helper expressions")
225
}
226
}
227
// finalVars contains allVars and any generator/fuzzing specific payloads
228
// payloads used in generator should be given the most preference
229
finalVars := generators.MergeMaps(allVars, payloads)
230
231
if vardump.EnableVarDump {
232
gologger.Debug().Msgf("HTTP Protocol request variables: %s\n", vardump.DumpVariables(finalVars))
233
}
234
235
// Note: If possible any changes to current logic (i.e evaluate -> then parse URL)
236
// should be avoided since it is dependent on `urlutil` core logic
237
238
// Evaluate (replace) variable with final values
239
reqData, err = expressions.Evaluate(reqData, finalVars)
240
if err != nil {
241
return nil, errkit.Wrap(err, "could not evaluate helper expressions")
242
}
243
244
if isRawRequest {
245
return r.generateRawRequest(ctx, reqData, parsed, finalVars, payloads)
246
}
247
248
reqURL, err := urlutil.ParseAbsoluteURL(reqData, true)
249
if err != nil {
250
return nil, errkit.Newf("failed to parse url %v while creating http request", reqData)
251
}
252
// while merging parameters first preference is given to target params
253
finalparams := parsed.Params
254
finalparams.Merge(reqURL.Params.Encode())
255
reqURL.Params = finalparams
256
return r.generateHttpRequest(ctx, reqURL, finalVars, payloads)
257
}
258
259
// selfContained templates do not need/use target data and all values i.e {{Hostname}} , {{BaseURL}} etc are already available
260
// in template . makeSelfContainedRequest parses and creates variables map and then creates corresponding http request or raw request
261
func (r *requestGenerator) makeSelfContainedRequest(ctx context.Context, data string, payloads, dynamicValues map[string]interface{}) (*generatedRequest, error) {
262
isRawRequest := r.request.isRaw()
263
264
values := generators.MergeMaps(
265
generators.BuildPayloadFromOptions(r.request.options.Options),
266
dynamicValues,
267
payloads, // payloads should override other variables in case of duplicate vars
268
)
269
// adds all variables from `variables` section in template
270
variablesMap := r.request.options.Variables.Evaluate(values)
271
values = generators.MergeMaps(variablesMap, values)
272
273
signerVars := GetDefaultSignerVars(r.request.Signature.Value)
274
// this will ensure that default signer variables are overwritten by other variables
275
values = generators.MergeMaps(signerVars, values, r.options.Constants)
276
277
// priority of variables is as follows (from low to high) for self contained templates
278
// default signer vars < variables < cli vars < payload < dynamic values < constants
279
280
// evaluate request
281
data, err := expressions.Evaluate(data, values)
282
if err != nil {
283
return nil, errkit.Wrap(err, "could not evaluate helper expressions")
284
}
285
// If the request is a raw request, get the URL from the request
286
// header and use it to make the request.
287
if isRawRequest {
288
// Get the hostname from the URL section to build the request.
289
reader := bufio.NewReader(strings.NewReader(data))
290
read_line:
291
s, err := reader.ReadString('\n')
292
if err != nil {
293
return nil, fmt.Errorf("could not read request: %w", err)
294
}
295
// ignore all annotations
296
if stringsutil.HasPrefixAny(s, "@") {
297
goto read_line
298
}
299
300
parts := strings.Split(s, " ")
301
if len(parts) < 3 {
302
return nil, fmt.Errorf("malformed request supplied")
303
}
304
305
if err := expressions.ContainsUnresolvedVariables(parts[1]); err != nil && !r.request.SkipVariablesCheck {
306
return nil, errkit.Newf("unresolved variables `%v` found in request", parts[1])
307
}
308
309
parsed, err := urlutil.ParseURL(parts[1], true)
310
if err != nil {
311
return nil, fmt.Errorf("could not parse request URL: %w", err)
312
}
313
values = generators.MergeMaps(
314
generators.MergeMaps(dynamicValues, protocolutils.GenerateVariables(parsed, false, nil)),
315
values,
316
)
317
// Evaluate (replace) variable with final values
318
data, err = expressions.Evaluate(data, values)
319
if err != nil {
320
return nil, errkit.Wrap(err, "could not evaluate helper expressions")
321
}
322
return r.generateRawRequest(ctx, data, parsed, values, payloads)
323
}
324
if err := expressions.ContainsUnresolvedVariables(data); err != nil && !r.request.SkipVariablesCheck {
325
// early exit: if there are any unresolved variables in `path` after evaluation
326
// then return early since this will definitely fail
327
return nil, errkit.Newf("unresolved variables `%v` found in request", data)
328
}
329
330
urlx, err := urlutil.ParseURL(data, true)
331
if err != nil {
332
return nil, errkit.Wrapf(err, "failed to parse %v in self contained request", data)
333
}
334
return r.generateHttpRequest(ctx, urlx, values, payloads)
335
}
336
337
// generateHttpRequest generates http request from request data from template and variables
338
// finalVars = contains all variables including generator and protocol specific variables
339
// generatorValues = contains variables used in fuzzing or other generator specific values
340
func (r *requestGenerator) generateHttpRequest(ctx context.Context, urlx *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) {
341
method, err := expressions.Evaluate(r.request.Method.String(), finalVars)
342
if err != nil {
343
return nil, errkit.Wrap(err, "failed to evaluate while generating http request")
344
}
345
// Build a request on the specified URL
346
req, err := retryablehttp.NewRequestFromURLWithContext(ctx, method, urlx, nil)
347
if err != nil {
348
return nil, err
349
}
350
351
request, err := r.fillRequest(req, finalVars)
352
if err != nil {
353
return nil, err
354
}
355
return &generatedRequest{request: request, meta: generatorValues, original: r.request, dynamicValues: finalVars, interactshURLs: r.interactshURLs}, nil
356
}
357
358
// generateRawRequest generates Raw Request from request data from template and variables
359
// finalVars = contains all variables including generator and protocol specific variables
360
// generatorValues = contains variables used in fuzzing or other generator specific values
361
func (r *requestGenerator) generateRawRequest(ctx context.Context, rawRequest string, baseURL *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) {
362
363
var rawRequestData *raw.Request
364
var err error
365
if r.request.SelfContained {
366
// in self contained requests baseURL is extracted from raw request itself
367
rawRequestData, err = raw.ParseRawRequest(rawRequest, r.request.Unsafe)
368
} else {
369
rawRequestData, err = raw.Parse(rawRequest, baseURL, r.request.Unsafe, r.request.DisablePathAutomerge)
370
}
371
if err != nil {
372
return nil, errkit.Wrap(err, "failed to parse raw request")
373
}
374
375
// Unsafe option uses rawhttp library
376
if r.request.Unsafe {
377
if len(r.options.Options.CustomHeaders) > 0 {
378
_ = rawRequestData.TryFillCustomHeaders(r.options.Options.CustomHeaders)
379
}
380
if rawRequestData.Data != "" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodHead, http.MethodGet) && rawRequestData.Headers["Transfer-Encoding"] != "chunked" {
381
rawRequestData.Headers["Content-Length"] = strconv.Itoa(len(rawRequestData.Data))
382
}
383
unsafeReq := &generatedRequest{rawRequest: rawRequestData, meta: generatorValues, original: r.request, interactshURLs: r.interactshURLs}
384
return unsafeReq, nil
385
}
386
urlx, err := urlutil.ParseAbsoluteURL(rawRequestData.FullURL, true)
387
if err != nil {
388
return nil, errkit.Wrapf(err, "failed to create request with url %v got %v", rawRequestData.FullURL, err)
389
}
390
req, err := retryablehttp.NewRequestFromURLWithContext(ctx, rawRequestData.Method, urlx, rawRequestData.Data)
391
if err != nil {
392
return nil, err
393
}
394
395
// force transfer encoding if conditions are met
396
if len(rawRequestData.Data) > 0 && req.Header.Get("Transfer-Encoding") != "chunked" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodGet, http.MethodHead) {
397
req.ContentLength = int64(len(rawRequestData.Data))
398
}
399
400
// override the body with a new one that will be used to read the request body in parallel threads
401
// for race condition testing
402
if r.request.Threads > 0 && r.request.Race {
403
req.Body = race.NewOpenGateWithTimeout(req.Body, time.Duration(2)*time.Second)
404
}
405
for key, value := range rawRequestData.Headers {
406
if key == "" {
407
continue
408
}
409
req.Header[key] = []string{value}
410
if key == "Host" {
411
req.Host = value
412
}
413
}
414
request, err := r.fillRequest(req, finalVars)
415
if err != nil {
416
return nil, err
417
}
418
419
generatedRequest := &generatedRequest{
420
request: request,
421
meta: generatorValues,
422
original: r.request,
423
dynamicValues: finalVars,
424
interactshURLs: r.interactshURLs,
425
}
426
427
if reqWithOverrides, hasAnnotations := r.request.parseAnnotations(rawRequest, req); hasAnnotations {
428
generatedRequest.request = reqWithOverrides.request
429
generatedRequest.customCancelFunction = reqWithOverrides.cancelFunc
430
generatedRequest.interactshURLs = append(generatedRequest.interactshURLs, reqWithOverrides.interactshURLs...)
431
}
432
433
return generatedRequest, nil
434
}
435
436
// fillRequest fills various headers in the request with values
437
func (r *requestGenerator) fillRequest(req *retryablehttp.Request, values map[string]interface{}) (*retryablehttp.Request, error) {
438
// Set the header values requested
439
for header, value := range r.request.Headers {
440
if r.options.Interactsh != nil {
441
value, r.interactshURLs = r.options.Interactsh.Replace(value, r.interactshURLs)
442
}
443
value, err := expressions.Evaluate(value, values)
444
if err != nil {
445
return nil, errkit.Wrap(err, "failed to evaluate while adding headers to request")
446
}
447
req.Header[header] = []string{value}
448
if header == "Host" {
449
req.Host = value
450
}
451
}
452
453
// In case of multiple threads the underlying connection should remain open to allow reuse
454
if r.request.Threads <= 0 && req.Header.Get("Connection") == "" && r.options.Options.ScanStrategy != scanstrategy.HostSpray.String() {
455
req.Close = true
456
}
457
458
// Check if the user requested a request body
459
if r.request.Body != "" {
460
body := r.request.Body
461
if r.options.Interactsh != nil {
462
body, r.interactshURLs = r.options.Interactsh.Replace(r.request.Body, r.interactshURLs)
463
}
464
body, err := expressions.Evaluate(body, values)
465
if err != nil {
466
return nil, errkit.Wrap(err, "could not evaluate helper expressions")
467
}
468
if err := req.SetBodyString(body); err != nil {
469
return nil, errors.Wrap(err, "failed to set request body")
470
}
471
}
472
if !r.request.Unsafe {
473
userAgent := useragent.PickRandom()
474
httputil.SetHeader(req, "User-Agent", userAgent.Raw)
475
}
476
477
// Only set these headers on non-raw requests
478
if len(r.request.Raw) == 0 && !r.request.Unsafe {
479
httputil.SetHeader(req, "Accept", "*/*")
480
httputil.SetHeader(req, "Accept-Language", "en")
481
}
482
483
if !LeaveDefaultPorts {
484
switch {
485
case req.Scheme == "http" && strings.HasSuffix(req.Host, ":80"):
486
req.Host = strings.TrimSuffix(req.Host, ":80")
487
case req.Scheme == "https" && strings.HasSuffix(req.Host, ":443"):
488
req.Host = strings.TrimSuffix(req.Host, ":443")
489
}
490
}
491
492
if r.request.DigestAuthUsername != "" {
493
req.Auth = &retryablehttp.Auth{
494
Type: retryablehttp.DigestAuth,
495
Username: r.request.DigestAuthUsername,
496
Password: r.request.DigestAuthPassword,
497
}
498
}
499
500
return req, nil
501
}
502
503