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