github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/http/build_request.go (about) 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/corpix/uarand" 13 "github.com/pkg/errors" 14 15 "github.com/projectdiscovery/gologger" 16 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs" 17 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions" 18 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" 19 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump" 20 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/race" 21 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/raw" 22 protocolutils "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils" 23 httputil "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils/http" 24 "github.com/projectdiscovery/nuclei/v2/pkg/types" 25 "github.com/projectdiscovery/nuclei/v2/pkg/types/scanstrategy" 26 "github.com/projectdiscovery/rawhttp" 27 "github.com/projectdiscovery/retryablehttp-go" 28 errorutil "github.com/projectdiscovery/utils/errors" 29 readerutil "github.com/projectdiscovery/utils/reader" 30 stringsutil "github.com/projectdiscovery/utils/strings" 31 urlutil "github.com/projectdiscovery/utils/url" 32 ) 33 34 // ErrEvalExpression 35 var ( 36 ErrEvalExpression = errorutil.NewWithTag("expr", "could not evaluate helper expressions") 37 ErrUnresolvedVars = errorutil.NewWithFmt("unresolved variables `%v` found in request") 38 ) 39 40 // generatedRequest is a single generated request wrapped for a template request 41 type generatedRequest struct { 42 original *Request 43 rawRequest *raw.Request 44 meta map[string]interface{} 45 pipelinedClient *rawhttp.PipelineClient 46 request *retryablehttp.Request 47 dynamicValues map[string]interface{} 48 interactshURLs []string 49 customCancelFunction context.CancelFunc 50 } 51 52 func (g *generatedRequest) URL() string { 53 if g.request != nil { 54 return g.request.URL.String() 55 } 56 if g.rawRequest != nil { 57 return g.rawRequest.FullURL 58 } 59 return "" 60 } 61 62 // Total returns the total number of requests for the generator 63 func (r *requestGenerator) Total() int { 64 if r.payloadIterator != nil { 65 return len(r.request.Raw) * r.payloadIterator.Remaining() 66 } 67 return len(r.request.Path) 68 } 69 70 // Make creates a http request for the provided input. 71 // It returns ErrNoMoreRequests as error when all the requests have been exhausted. 72 func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context, reqData string, payloads, dynamicValues map[string]interface{}) (*generatedRequest, error) { 73 // value of `reqData` depends on the type of request specified in template 74 // 1. If request is raw request = reqData contains raw request (i.e http request dump) 75 // 2. If request is Normal ( simply put not a raw request) (Ex: with placeholders `path`) = reqData contains relative path 76 if r.request.SelfContained { 77 return r.makeSelfContainedRequest(ctx, reqData, payloads, dynamicValues) 78 } 79 isRawRequest := len(r.request.Raw) > 0 80 // replace interactsh variables with actual interactsh urls 81 if r.options.Interactsh != nil { 82 reqData, r.interactshURLs = r.options.Interactsh.Replace(reqData, []string{}) 83 for payloadName, payloadValue := range payloads { 84 payloads[payloadName], r.interactshURLs = r.options.Interactsh.Replace(types.ToString(payloadValue), r.interactshURLs) 85 } 86 } else { 87 for payloadName, payloadValue := range payloads { 88 payloads[payloadName] = types.ToString(payloadValue) 89 } 90 } 91 92 // Parse target url 93 parsed, err := urlutil.Parse(input.MetaInput.Input) 94 if err != nil { 95 return nil, err 96 } 97 98 // Non-Raw Requests ex `{{BaseURL}}/somepath` may or maynot have slash after variable and the same is the case for 99 // target url to avoid inconsistencies extra slash if exists has to removed from default variables 100 hasTrailingSlash := false 101 if !isRawRequest { 102 // if path contains port ex: {{BaseURL}}:8080 use port specified in reqData 103 parsed, reqData = httputil.UpdateURLPortFromPayload(parsed, reqData) 104 hasTrailingSlash = httputil.HasTrailingSlash(reqData) 105 } 106 107 // defaultreqvars are vars generated from request/input ex: {{baseURL}}, {{Host}} etc 108 // contextargs generate extra vars that may/may not be available always (ex: "ip") 109 defaultReqVars := protocolutils.GenerateVariables(parsed, hasTrailingSlash, contextargs.GenerateVariables(input)) 110 // optionvars are vars passed from CLI or env variables 111 optionVars := generators.BuildPayloadFromOptions(r.request.options.Options) 112 113 variablesMap, interactURLs := r.options.Variables.EvaluateWithInteractsh(generators.MergeMaps(defaultReqVars, optionVars), r.options.Interactsh) 114 if len(interactURLs) > 0 { 115 r.interactshURLs = append(r.interactshURLs, interactURLs...) 116 } 117 // allVars contains all variables from all sources 118 allVars := generators.MergeMaps(dynamicValues, defaultReqVars, optionVars, variablesMap, r.options.Constants) 119 120 // Evaluate payload variables 121 // eg: payload variables can be username: jon.doe@{{Hostname}} 122 for payloadName, payloadValue := range payloads { 123 payloads[payloadName], err = expressions.Evaluate(types.ToString(payloadValue), allVars) 124 if err != nil { 125 return nil, ErrEvalExpression.Wrap(err).WithTag("http") 126 } 127 } 128 // finalVars contains allVars and any generator/fuzzing specific payloads 129 // payloads used in generator should be given the most preference 130 finalVars := generators.MergeMaps(allVars, payloads) 131 132 if vardump.EnableVarDump { 133 gologger.Debug().Msgf("Final Protocol request variables: \n%s\n", vardump.DumpVariables(finalVars)) 134 } 135 136 // Note: If possible any changes to current logic (i.e evaluate -> then parse URL) 137 // should be avoided since it is dependent on `urlutil` core logic 138 139 // Evaluate (replace) variable with final values 140 reqData, err = expressions.Evaluate(reqData, finalVars) 141 if err != nil { 142 return nil, ErrEvalExpression.Wrap(err).WithTag("http") 143 } 144 145 if isRawRequest { 146 return r.generateRawRequest(ctx, reqData, parsed, finalVars, payloads) 147 } 148 149 reqURL, err := urlutil.ParseURL(reqData, true) 150 if err != nil { 151 return nil, errorutil.NewWithTag("http", "failed to parse url %v while creating http request", reqData) 152 } 153 // while merging parameters first preference is given to target params 154 finalparams := parsed.Params 155 finalparams.Merge(reqURL.Params.Encode()) 156 reqURL.Params = finalparams 157 return r.generateHttpRequest(ctx, reqURL, finalVars, payloads) 158 } 159 160 // selfContained templates do not need/use target data and all values i.e {{Hostname}} , {{BaseURL}} etc are already available 161 // in template . makeSelfContainedRequest parses and creates variables map and then creates corresponding http request or raw request 162 func (r *requestGenerator) makeSelfContainedRequest(ctx context.Context, data string, payloads, dynamicValues map[string]interface{}) (*generatedRequest, error) { 163 isRawRequest := r.request.isRaw() 164 165 values := generators.MergeMaps( 166 generators.BuildPayloadFromOptions(r.request.options.Options), 167 dynamicValues, 168 payloads, // payloads should override other variables in case of duplicate vars 169 ) 170 // adds all variables from `variables` section in template 171 variablesMap := r.request.options.Variables.Evaluate(values) 172 values = generators.MergeMaps(variablesMap, values) 173 174 signerVars := GetDefaultSignerVars(r.request.Signature.Value) 175 // this will ensure that default signer variables are overwritten by other variables 176 values = generators.MergeMaps(signerVars, values, r.options.Constants) 177 178 // priority of variables is as follows (from low to high) for self contained templates 179 // default signer vars < variables < cli vars < payload < dynamic values < constants 180 181 // evaluate request 182 data, err := expressions.Evaluate(data, values) 183 if err != nil { 184 return nil, ErrEvalExpression.Wrap(err).WithTag("self-contained") 185 } 186 // If the request is a raw request, get the URL from the request 187 // header and use it to make the request. 188 if isRawRequest { 189 // Get the hostname from the URL section to build the request. 190 reader := bufio.NewReader(strings.NewReader(data)) 191 read_line: 192 s, err := reader.ReadString('\n') 193 if err != nil { 194 return nil, fmt.Errorf("could not read request: %w", err) 195 } 196 // ignore all annotations 197 if stringsutil.HasPrefixAny(s, "@") { 198 goto read_line 199 } 200 201 parts := strings.Split(s, " ") 202 if len(parts) < 3 { 203 return nil, fmt.Errorf("malformed request supplied") 204 } 205 206 if err := expressions.ContainsUnresolvedVariables(parts[1]); err != nil { 207 return nil, ErrUnresolvedVars.Msgf(parts[1]) 208 } 209 210 parsed, err := urlutil.ParseURL(parts[1], true) 211 if err != nil { 212 return nil, fmt.Errorf("could not parse request URL: %w", err) 213 } 214 values = generators.MergeMaps( 215 generators.MergeMaps(dynamicValues, protocolutils.GenerateVariables(parsed, false, nil)), 216 values, 217 ) 218 // Evaluate (replace) variable with final values 219 data, err = expressions.Evaluate(data, values) 220 if err != nil { 221 return nil, ErrEvalExpression.Wrap(err).WithTag("self-contained", "raw") 222 } 223 return r.generateRawRequest(ctx, data, parsed, values, payloads) 224 } 225 if err := expressions.ContainsUnresolvedVariables(data); err != nil { 226 // early exit: if there are any unresolved variables in `path` after evaluation 227 // then return early since this will definitely fail 228 return nil, ErrUnresolvedVars.Msgf(data) 229 } 230 231 urlx, err := urlutil.ParseURL(data, true) 232 if err != nil { 233 return nil, errorutil.NewWithErr(err).Msgf("failed to parse %v in self contained request", data).WithTag("self-contained") 234 } 235 return r.generateHttpRequest(ctx, urlx, values, payloads) 236 } 237 238 // generateHttpRequest generates http request from request data from template and variables 239 // finalVars = contains all variables including generator and protocol specific variables 240 // generatorValues = contains variables used in fuzzing or other generator specific values 241 func (r *requestGenerator) generateHttpRequest(ctx context.Context, urlx *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) { 242 method, err := expressions.Evaluate(r.request.Method.String(), finalVars) 243 if err != nil { 244 return nil, ErrEvalExpression.Wrap(err).Msgf("failed to evaluate while generating http request") 245 } 246 // Build a request on the specified URL 247 req, err := retryablehttp.NewRequestFromURLWithContext(ctx, method, urlx, nil) 248 if err != nil { 249 return nil, err 250 } 251 252 request, err := r.fillRequest(req, finalVars) 253 if err != nil { 254 return nil, err 255 } 256 return &generatedRequest{request: request, meta: generatorValues, original: r.request, dynamicValues: finalVars, interactshURLs: r.interactshURLs}, nil 257 } 258 259 // generateRawRequest generates Raw Request from request data from template and variables 260 // finalVars = contains all variables including generator and protocol specific variables 261 // generatorValues = contains variables used in fuzzing or other generator specific values 262 func (r *requestGenerator) generateRawRequest(ctx context.Context, rawRequest string, baseURL *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) { 263 264 var rawRequestData *raw.Request 265 var err error 266 if r.request.SelfContained { 267 // in self contained requests baseURL is extracted from raw request itself 268 rawRequestData, err = raw.ParseRawRequest(rawRequest, r.request.Unsafe) 269 } else { 270 rawRequestData, err = raw.Parse(rawRequest, baseURL, r.request.Unsafe, r.request.DisablePathAutomerge) 271 } 272 if err != nil { 273 return nil, errorutil.NewWithErr(err).Msgf("failed to parse raw request") 274 } 275 276 // Unsafe option uses rawhttp library 277 if r.request.Unsafe { 278 if len(r.options.Options.CustomHeaders) > 0 { 279 _ = rawRequestData.TryFillCustomHeaders(r.options.Options.CustomHeaders) 280 } 281 if rawRequestData.Data != "" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodHead, http.MethodGet) && rawRequestData.Headers["Transfer-Encoding"] != "chunked" { 282 rawRequestData.Headers["Content-Length"] = strconv.Itoa(len(rawRequestData.Data)) 283 } 284 unsafeReq := &generatedRequest{rawRequest: rawRequestData, meta: generatorValues, original: r.request, interactshURLs: r.interactshURLs} 285 return unsafeReq, nil 286 } 287 288 urlx, err := urlutil.ParseURL(rawRequestData.FullURL, true) 289 if err != nil { 290 return nil, errorutil.NewWithErr(err).Msgf("failed to create request with url %v got %v", rawRequestData.FullURL, err).WithTag("raw") 291 } 292 req, err := retryablehttp.NewRequestFromURLWithContext(ctx, rawRequestData.Method, urlx, rawRequestData.Data) 293 if err != nil { 294 return nil, err 295 } 296 297 // force transfer encoding if conditions are met 298 if len(rawRequestData.Data) > 0 && req.Header.Get("Transfer-Encoding") != "chunked" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodGet, http.MethodHead) { 299 req.ContentLength = int64(len(rawRequestData.Data)) 300 } 301 302 // override the body with a new one that will be used to read the request body in parallel threads 303 // for race condition testing 304 if r.request.Threads > 0 && r.request.Race { 305 req.Body = race.NewOpenGateWithTimeout(req.Body, time.Duration(2)*time.Second) 306 } 307 for key, value := range rawRequestData.Headers { 308 if key == "" { 309 continue 310 } 311 req.Header[key] = []string{value} 312 if key == "Host" { 313 req.Host = value 314 } 315 } 316 request, err := r.fillRequest(req, finalVars) 317 if err != nil { 318 return nil, err 319 } 320 321 generatedRequest := &generatedRequest{ 322 request: request, 323 meta: generatorValues, 324 original: r.request, 325 dynamicValues: finalVars, 326 interactshURLs: r.interactshURLs, 327 } 328 329 if reqWithOverrides, hasAnnotations := r.request.parseAnnotations(rawRequest, req); hasAnnotations { 330 generatedRequest.request = reqWithOverrides.request 331 generatedRequest.customCancelFunction = reqWithOverrides.cancelFunc 332 generatedRequest.interactshURLs = append(generatedRequest.interactshURLs, reqWithOverrides.interactshURLs...) 333 } 334 335 return generatedRequest, nil 336 } 337 338 // fillRequest fills various headers in the request with values 339 func (r *requestGenerator) fillRequest(req *retryablehttp.Request, values map[string]interface{}) (*retryablehttp.Request, error) { 340 // Set the header values requested 341 for header, value := range r.request.Headers { 342 if r.options.Interactsh != nil { 343 value, r.interactshURLs = r.options.Interactsh.Replace(value, r.interactshURLs) 344 } 345 value, err := expressions.Evaluate(value, values) 346 if err != nil { 347 return nil, ErrEvalExpression.Wrap(err).Msgf("failed to evaluate while adding headers to request") 348 } 349 req.Header[header] = []string{value} 350 if header == "Host" { 351 req.Host = value 352 } 353 } 354 355 // In case of multiple threads the underlying connection should remain open to allow reuse 356 if r.request.Threads <= 0 && req.Header.Get("Connection") == "" && r.options.Options.ScanStrategy != scanstrategy.HostSpray.String() { 357 req.Close = true 358 } 359 360 // Check if the user requested a request body 361 if r.request.Body != "" { 362 body := r.request.Body 363 if r.options.Interactsh != nil { 364 body, r.interactshURLs = r.options.Interactsh.Replace(r.request.Body, r.interactshURLs) 365 } 366 body, err := expressions.Evaluate(body, values) 367 if err != nil { 368 return nil, ErrEvalExpression.Wrap(err) 369 } 370 bodyReader, err := readerutil.NewReusableReadCloser([]byte(body)) 371 if err != nil { 372 return nil, errors.Wrap(err, "failed to create reusable reader for request body") 373 } 374 req.Body = bodyReader 375 } 376 if !r.request.Unsafe { 377 httputil.SetHeader(req, "User-Agent", uarand.GetRandom()) 378 } 379 380 // Only set these headers on non-raw requests 381 if len(r.request.Raw) == 0 && !r.request.Unsafe { 382 httputil.SetHeader(req, "Accept", "*/*") 383 httputil.SetHeader(req, "Accept-Language", "en") 384 } 385 386 if !LeaveDefaultPorts { 387 switch { 388 case req.URL.Scheme == "http" && strings.HasSuffix(req.Host, ":80"): 389 req.Host = strings.TrimSuffix(req.Host, ":80") 390 case req.URL.Scheme == "https" && strings.HasSuffix(req.Host, ":443"): 391 req.Host = strings.TrimSuffix(req.Host, ":443") 392 } 393 } 394 395 if r.request.DigestAuthUsername != "" { 396 req.Auth = &retryablehttp.Auth{ 397 Type: retryablehttp.DigestAuth, 398 Username: r.request.DigestAuthUsername, 399 Password: r.request.DigestAuthPassword, 400 } 401 } 402 403 return req, nil 404 }