Path: blob/dev/pkg/protocols/http/build_request.go
2845 views
package http12import (3"bufio"4"context"5"fmt"6"net/http"7"strconv"8"strings"9"time"1011"github.com/pkg/errors"12"github.com/projectdiscovery/useragent"1314"github.com/projectdiscovery/gologger"15"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"16"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"17"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"18"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"19"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"20"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"21"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/race"22"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/raw"23protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"24httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"25"github.com/projectdiscovery/nuclei/v3/pkg/types"26"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"27"github.com/projectdiscovery/rawhttp"28"github.com/projectdiscovery/retryablehttp-go"29"github.com/projectdiscovery/utils/errkit"30stringsutil "github.com/projectdiscovery/utils/strings"31urlutil "github.com/projectdiscovery/utils/url"32)3334const (35ReqURLPatternKey = "req_url_pattern"36)3738// ErrEvalExpression39type errorTemplate struct {40format string41}4243func (e errorTemplate) Wrap(err error) wrapperError {44return wrapperError{template: e, err: err}45}4647func (e errorTemplate) Msgf(args ...interface{}) error {48return errkit.Newf(e.format, args...)49}5051type wrapperError struct {52template errorTemplate53err error54}5556func (w wrapperError) WithTag(tag string) error {57return errkit.Wrap(w.err, w.template.format)58}5960func (w wrapperError) Msgf(format string, args ...interface{}) error {61return errkit.Wrapf(w.err, format, args...)62}6364func (w wrapperError) Error() string {65return errkit.Wrap(w.err, w.template.format).Error()66}6768// ErrEvalExpression69var (70ErrEvalExpression = errorTemplate{"could not evaluate helper expressions"}71ErrUnresolvedVars = errorTemplate{"unresolved variables `%v` found in request"}72)7374// generatedRequest is a single generated request wrapped for a template request75type generatedRequest struct {76original *Request77rawRequest *raw.Request78meta map[string]interface{}79pipelinedClient *rawhttp.PipelineClient80request *retryablehttp.Request81dynamicValues map[string]interface{}82interactshURLs []string83customCancelFunction context.CancelFunc84// requestURLPattern tracks unmodified request url pattern without values ( it is used for constant vuln_hash)85// ex: {{BaseURL}}/api/exp?param={{randstr}}86requestURLPattern string8788fuzzGeneratedRequest fuzz.GeneratedRequest89}9091// setReqURLPattern sets the url request pattern for the generated request92func (gr *generatedRequest) setReqURLPattern(reqURLPattern string) {93if idx := strings.IndexByte(reqURLPattern, '\n'); idx >= 0 {94reqURLPattern = strings.TrimSpace(reqURLPattern[:idx])95// this is raw request (if it has 3 parts after strings.Fields then its valid only use 2nd part)96parts := strings.Fields(reqURLPattern)97if len(parts) >= 3 {98// remove first and last and use all in between99parts = parts[1 : len(parts)-1]100reqURLPattern = strings.Join(parts, " ")101}102} else {103reqURLPattern = strings.TrimSpace(reqURLPattern)104}105106// now urlRequestPattern is generated replace preprocessor values with actual placeholders107// that were used (these are called generated 'constants' and contains {{}} in var name)108for k, v := range gr.original.options.Constants {109if strings.HasPrefix(k, "{{") && strings.HasSuffix(k, "}}") {110// this takes care of all preprocessors ( currently we have randstr and its variations)111reqURLPattern = strings.ReplaceAll(reqURLPattern, fmt.Sprint(v), k)112}113}114gr.requestURLPattern = reqURLPattern115}116117// ApplyAuth applies the auth provider to the generated request118func (g *generatedRequest) ApplyAuth(provider authprovider.AuthProvider) {119if provider == nil {120return121}122if g.request != nil {123authStrategies := provider.LookupURLX(g.request.URL)124for _, strategy := range authStrategies {125strategy.ApplyOnRR(g.request)126}127}128if g.rawRequest != nil {129parsed, err := urlutil.ParseAbsoluteURL(g.rawRequest.FullURL, true)130if err != nil {131gologger.Warning().Msgf("[authprovider] Could not parse URL %s: %s\n", g.rawRequest.FullURL, err)132return133}134authStrategies := provider.LookupURLX(parsed)135// here we need to apply it custom because we don't have a standard/official136// rawhttp request format ( which we probably should have )137for _, strategy := range authStrategies {138g.rawRequest.ApplyAuthStrategy(strategy)139}140}141}142143func (g *generatedRequest) URL() string {144if g.request != nil {145return g.request.String()146}147if g.rawRequest != nil {148return g.rawRequest.FullURL149}150return ""151}152153// Make creates a http request for the provided input.154// It returns ErrNoMoreRequests as error when all the requests have been exhausted.155func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context, reqData string, payloads, dynamicValues map[string]interface{}) (gr *generatedRequest, err error) {156origReqData := reqData157defer func() {158if gr != nil {159gr.setReqURLPattern(origReqData)160}161}()162// value of `reqData` depends on the type of request specified in template163// 1. If request is raw request = reqData contains raw request (i.e http request dump)164// 2. If request is Normal ( simply put not a raw request) (Ex: with placeholders `path`) = reqData contains relative path165166// add template context values to dynamicValues (this takes care of self-contained and other types of requests)167// Note: `iterate-all` and flow are mutually exclusive. flow uses templateCtx and iterate-all uses dynamicValues168if r.request.options.HasTemplateCtx(input.MetaInput) {169// skip creating template context if not available170dynamicValues = generators.MergeMaps(dynamicValues, r.request.options.GetTemplateCtx(input.MetaInput).GetAll())171}172173isRawRequest := len(r.request.Raw) > 0174// replace interactsh variables with actual interactsh urls175if r.options.Interactsh != nil {176reqData, r.interactshURLs = r.options.Interactsh.Replace(reqData, []string{})177for payloadName, payloadValue := range payloads {178payloads[payloadName], r.interactshURLs = r.options.Interactsh.Replace(types.ToString(payloadValue), r.interactshURLs)179}180} else {181for payloadName, payloadValue := range payloads {182payloads[payloadName] = types.ToStringNSlice(payloadValue)183}184}185186if r.request.SelfContained {187return r.makeSelfContainedRequest(ctx, reqData, payloads, dynamicValues)188}189190// Parse target url191parsed, err := urlutil.ParseAbsoluteURL(input.MetaInput.Input, false)192if err != nil {193return nil, err194}195196// Non-Raw Requests ex `{{BaseURL}}/somepath` may or maynot have slash after variable and the same is the case for197// target url to avoid inconsistencies extra slash if exists has to removed from default variables198hasTrailingSlash := false199if !isRawRequest {200// if path contains port ex: {{BaseURL}}:8080 use port specified in reqData201parsed, reqData = httputil.UpdateURLPortFromPayload(parsed, reqData)202hasTrailingSlash = httputil.HasTrailingSlash(reqData)203}204205// defaultreqvars are vars generated from request/input ex: {{baseURL}}, {{Host}} etc206// contextargs generate extra vars that may/may not be available always (ex: "ip")207defaultReqVars := protocolutils.GenerateVariables(parsed, hasTrailingSlash, contextargs.GenerateVariables(input))208// optionvars are vars passed from CLI or env variables209optionVars := generators.BuildPayloadFromOptions(r.request.options.Options)210211variablesMap, interactURLs := r.options.Variables.EvaluateWithInteractsh(generators.MergeMaps(dynamicValues, defaultReqVars, optionVars), r.options.Interactsh)212if len(interactURLs) > 0 {213r.interactshURLs = append(r.interactshURLs, interactURLs...)214}215// allVars contains all variables from all sources216allVars := generators.MergeMaps(dynamicValues, defaultReqVars, optionVars, variablesMap, r.options.Constants)217218// Evaluate payload variables219// eg: payload variables can be username: jon.doe@{{Hostname}}220for payloadName, payloadValue := range payloads {221payloads[payloadName], err = expressions.Evaluate(types.ToString(payloadValue), allVars)222if err != nil {223return nil, errkit.Wrap(err, "could not evaluate helper expressions")224}225}226// finalVars contains allVars and any generator/fuzzing specific payloads227// payloads used in generator should be given the most preference228finalVars := generators.MergeMaps(allVars, payloads)229230if vardump.EnableVarDump {231gologger.Debug().Msgf("HTTP Protocol request variables: %s\n", vardump.DumpVariables(finalVars))232}233234// Note: If possible any changes to current logic (i.e evaluate -> then parse URL)235// should be avoided since it is dependent on `urlutil` core logic236237// Evaluate (replace) variable with final values238reqData, err = expressions.Evaluate(reqData, finalVars)239if err != nil {240return nil, errkit.Wrap(err, "could not evaluate helper expressions")241}242243if isRawRequest {244return r.generateRawRequest(ctx, reqData, parsed, finalVars, payloads)245}246247reqURL, err := urlutil.ParseAbsoluteURL(reqData, true)248if err != nil {249return nil, errkit.Newf("failed to parse url %v while creating http request", reqData)250}251// while merging parameters first preference is given to target params252finalparams := parsed.Params253finalparams.Merge(reqURL.Params.Encode())254reqURL.Params = finalparams255return r.generateHttpRequest(ctx, reqURL, finalVars, payloads)256}257258// selfContained templates do not need/use target data and all values i.e {{Hostname}} , {{BaseURL}} etc are already available259// in template . makeSelfContainedRequest parses and creates variables map and then creates corresponding http request or raw request260func (r *requestGenerator) makeSelfContainedRequest(ctx context.Context, data string, payloads, dynamicValues map[string]interface{}) (*generatedRequest, error) {261isRawRequest := r.request.isRaw()262263values := generators.MergeMaps(264generators.BuildPayloadFromOptions(r.request.options.Options),265dynamicValues,266payloads, // payloads should override other variables in case of duplicate vars267)268// adds all variables from `variables` section in template269variablesMap := r.request.options.Variables.Evaluate(values)270values = generators.MergeMaps(variablesMap, values)271272signerVars := GetDefaultSignerVars(r.request.Signature.Value)273// this will ensure that default signer variables are overwritten by other variables274values = generators.MergeMaps(signerVars, values, r.options.Constants)275276// priority of variables is as follows (from low to high) for self contained templates277// default signer vars < variables < cli vars < payload < dynamic values < constants278279// evaluate request280data, err := expressions.Evaluate(data, values)281if err != nil {282return nil, errkit.Wrap(err, "could not evaluate helper expressions")283}284// If the request is a raw request, get the URL from the request285// header and use it to make the request.286if isRawRequest {287// Get the hostname from the URL section to build the request.288reader := bufio.NewReader(strings.NewReader(data))289read_line:290s, err := reader.ReadString('\n')291if err != nil {292return nil, fmt.Errorf("could not read request: %w", err)293}294// ignore all annotations295if stringsutil.HasPrefixAny(s, "@") {296goto read_line297}298299parts := strings.Split(s, " ")300if len(parts) < 3 {301return nil, fmt.Errorf("malformed request supplied")302}303304if err := expressions.ContainsUnresolvedVariables(parts[1]); err != nil && !r.request.SkipVariablesCheck {305return nil, errkit.Newf("unresolved variables `%v` found in request", parts[1])306}307308parsed, err := urlutil.ParseURL(parts[1], true)309if err != nil {310return nil, fmt.Errorf("could not parse request URL: %w", err)311}312values = generators.MergeMaps(313generators.MergeMaps(dynamicValues, protocolutils.GenerateVariables(parsed, false, nil)),314values,315)316// Evaluate (replace) variable with final values317data, err = expressions.Evaluate(data, values)318if err != nil {319return nil, errkit.Wrap(err, "could not evaluate helper expressions")320}321return r.generateRawRequest(ctx, data, parsed, values, payloads)322}323if err := expressions.ContainsUnresolvedVariables(data); err != nil && !r.request.SkipVariablesCheck {324// early exit: if there are any unresolved variables in `path` after evaluation325// then return early since this will definitely fail326return nil, errkit.Newf("unresolved variables `%v` found in request", data)327}328329urlx, err := urlutil.ParseURL(data, true)330if err != nil {331return nil, errkit.Wrapf(err, "failed to parse %v in self contained request", data)332}333return r.generateHttpRequest(ctx, urlx, values, payloads)334}335336// generateHttpRequest generates http request from request data from template and variables337// finalVars = contains all variables including generator and protocol specific variables338// generatorValues = contains variables used in fuzzing or other generator specific values339func (r *requestGenerator) generateHttpRequest(ctx context.Context, urlx *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) {340method, err := expressions.Evaluate(r.request.Method.String(), finalVars)341if err != nil {342return nil, errkit.Wrap(err, "failed to evaluate while generating http request")343}344// Build a request on the specified URL345req, err := retryablehttp.NewRequestFromURLWithContext(ctx, method, urlx, nil)346if err != nil {347return nil, err348}349350request, err := r.fillRequest(req, finalVars)351if err != nil {352return nil, err353}354return &generatedRequest{request: request, meta: generatorValues, original: r.request, dynamicValues: finalVars, interactshURLs: r.interactshURLs}, nil355}356357// generateRawRequest generates Raw Request from request data from template and variables358// finalVars = contains all variables including generator and protocol specific variables359// generatorValues = contains variables used in fuzzing or other generator specific values360func (r *requestGenerator) generateRawRequest(ctx context.Context, rawRequest string, baseURL *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) {361362var rawRequestData *raw.Request363var err error364if r.request.SelfContained {365// in self contained requests baseURL is extracted from raw request itself366rawRequestData, err = raw.ParseRawRequest(rawRequest, r.request.Unsafe)367} else {368rawRequestData, err = raw.Parse(rawRequest, baseURL, r.request.Unsafe, r.request.DisablePathAutomerge)369}370if err != nil {371return nil, errkit.Wrap(err, "failed to parse raw request")372}373374// Unsafe option uses rawhttp library375if r.request.Unsafe {376if len(r.options.Options.CustomHeaders) > 0 {377_ = rawRequestData.TryFillCustomHeaders(r.options.Options.CustomHeaders)378}379if rawRequestData.Data != "" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodHead, http.MethodGet) && rawRequestData.Headers["Transfer-Encoding"] != "chunked" {380rawRequestData.Headers["Content-Length"] = strconv.Itoa(len(rawRequestData.Data))381}382unsafeReq := &generatedRequest{rawRequest: rawRequestData, meta: generatorValues, original: r.request, interactshURLs: r.interactshURLs}383return unsafeReq, nil384}385urlx, err := urlutil.ParseAbsoluteURL(rawRequestData.FullURL, true)386if err != nil {387return nil, errkit.Wrapf(err, "failed to create request with url %v got %v", rawRequestData.FullURL, err)388}389req, err := retryablehttp.NewRequestFromURLWithContext(ctx, rawRequestData.Method, urlx, rawRequestData.Data)390if err != nil {391return nil, err392}393394// force transfer encoding if conditions are met395if len(rawRequestData.Data) > 0 && req.Header.Get("Transfer-Encoding") != "chunked" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodGet, http.MethodHead) {396req.ContentLength = int64(len(rawRequestData.Data))397}398399// override the body with a new one that will be used to read the request body in parallel threads400// for race condition testing401if r.request.Threads > 0 && r.request.Race {402req.Body = race.NewOpenGateWithTimeout(req.Body, time.Duration(2)*time.Second)403}404for key, value := range rawRequestData.Headers {405if key == "" {406continue407}408req.Header[key] = []string{value}409if key == "Host" {410req.Host = value411}412}413request, err := r.fillRequest(req, finalVars)414if err != nil {415return nil, err416}417418generatedRequest := &generatedRequest{419request: request,420meta: generatorValues,421original: r.request,422dynamicValues: finalVars,423interactshURLs: r.interactshURLs,424}425426if reqWithOverrides, hasAnnotations := r.request.parseAnnotations(rawRequest, req); hasAnnotations {427generatedRequest.request = reqWithOverrides.request428generatedRequest.customCancelFunction = reqWithOverrides.cancelFunc429generatedRequest.interactshURLs = append(generatedRequest.interactshURLs, reqWithOverrides.interactshURLs...)430}431432return generatedRequest, nil433}434435// fillRequest fills various headers in the request with values436func (r *requestGenerator) fillRequest(req *retryablehttp.Request, values map[string]interface{}) (*retryablehttp.Request, error) {437// Set the header values requested438for header, value := range r.request.Headers {439if r.options.Interactsh != nil {440value, r.interactshURLs = r.options.Interactsh.Replace(value, r.interactshURLs)441}442value, err := expressions.Evaluate(value, values)443if err != nil {444return nil, errkit.Wrap(err, "failed to evaluate while adding headers to request")445}446req.Header[header] = []string{value}447if header == "Host" {448req.Host = value449}450}451452// In case of multiple threads the underlying connection should remain open to allow reuse453if r.request.Threads <= 0 && req.Header.Get("Connection") == "" && r.options.Options.ScanStrategy != scanstrategy.HostSpray.String() {454req.Close = true455}456457// Check if the user requested a request body458if r.request.Body != "" {459body := r.request.Body460if r.options.Interactsh != nil {461body, r.interactshURLs = r.options.Interactsh.Replace(r.request.Body, r.interactshURLs)462}463body, err := expressions.Evaluate(body, values)464if err != nil {465return nil, errkit.Wrap(err, "could not evaluate helper expressions")466}467if err := req.SetBodyString(body); err != nil {468return nil, errors.Wrap(err, "failed to set request body")469}470}471if !r.request.Unsafe {472userAgent := useragent.PickRandom()473httputil.SetHeader(req, "User-Agent", userAgent.Raw)474}475476// Only set these headers on non-raw requests477if len(r.request.Raw) == 0 && !r.request.Unsafe {478httputil.SetHeader(req, "Accept", "*/*")479httputil.SetHeader(req, "Accept-Language", "en")480}481482if !LeaveDefaultPorts {483switch {484case req.Scheme == "http" && strings.HasSuffix(req.Host, ":80"):485req.Host = strings.TrimSuffix(req.Host, ":80")486case req.Scheme == "https" && strings.HasSuffix(req.Host, ":443"):487req.Host = strings.TrimSuffix(req.Host, ":443")488}489}490491if r.request.DigestAuthUsername != "" {492req.Auth = &retryablehttp.Auth{493Type: retryablehttp.DigestAuth,494Username: r.request.DigestAuthUsername,495Password: r.request.DigestAuthPassword,496}497}498499return req, nil500}501502503