Path: blob/dev/pkg/protocols/http/build_request.go
2070 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"30readerutil "github.com/projectdiscovery/utils/reader"31stringsutil "github.com/projectdiscovery/utils/strings"32urlutil "github.com/projectdiscovery/utils/url"33)3435const (36ReqURLPatternKey = "req_url_pattern"37)3839// ErrEvalExpression40type errorTemplate struct {41format string42}4344func (e errorTemplate) Wrap(err error) wrapperError {45return wrapperError{template: e, err: err}46}4748func (e errorTemplate) Msgf(args ...interface{}) error {49return errkit.Newf(e.format, args...)50}5152type wrapperError struct {53template errorTemplate54err error55}5657func (w wrapperError) WithTag(tag string) error {58return errkit.Wrap(w.err, w.template.format)59}6061func (w wrapperError) Msgf(format string, args ...interface{}) error {62return errkit.Wrapf(w.err, format, args...)63}6465func (w wrapperError) Error() string {66return errkit.Wrap(w.err, w.template.format).Error()67}6869// ErrEvalExpression70var (71ErrEvalExpression = errorTemplate{"could not evaluate helper expressions"}72ErrUnresolvedVars = errorTemplate{"unresolved variables `%v` found in request"}73)7475// generatedRequest is a single generated request wrapped for a template request76type generatedRequest struct {77original *Request78rawRequest *raw.Request79meta map[string]interface{}80pipelinedClient *rawhttp.PipelineClient81request *retryablehttp.Request82dynamicValues map[string]interface{}83interactshURLs []string84customCancelFunction context.CancelFunc85// requestURLPattern tracks unmodified request url pattern without values ( it is used for constant vuln_hash)86// ex: {{BaseURL}}/api/exp?param={{randstr}}87requestURLPattern string8889fuzzGeneratedRequest fuzz.GeneratedRequest90}9192// setReqURLPattern sets the url request pattern for the generated request93func (gr *generatedRequest) setReqURLPattern(reqURLPattern string) {94data := strings.Split(reqURLPattern, "\n")95if len(data) > 1 {96reqURLPattern = strings.TrimSpace(data[0])97// this is raw request (if it has 3 parts after strings.Fields then its valid only use 2nd part)98parts := strings.Fields(reqURLPattern)99if len(parts) >= 3 {100// remove first and last and use all in between101parts = parts[1 : len(parts)-1]102reqURLPattern = strings.Join(parts, " ")103}104} else {105reqURLPattern = strings.TrimSpace(reqURLPattern)106}107108// now urlRequestPattern is generated replace preprocessor values with actual placeholders109// that were used (these are called generated 'constants' and contains {{}} in var name)110for k, v := range gr.original.options.Constants {111if strings.HasPrefix(k, "{{") && strings.HasSuffix(k, "}}") {112// this takes care of all preprocessors ( currently we have randstr and its variations)113reqURLPattern = strings.ReplaceAll(reqURLPattern, fmt.Sprint(v), k)114}115}116gr.requestURLPattern = reqURLPattern117}118119// ApplyAuth applies the auth provider to the generated request120func (g *generatedRequest) ApplyAuth(provider authprovider.AuthProvider) {121if provider == nil {122return123}124if g.request != nil {125authStrategies := provider.LookupURLX(g.request.URL)126for _, strategy := range authStrategies {127strategy.ApplyOnRR(g.request)128}129}130if g.rawRequest != nil {131parsed, err := urlutil.ParseAbsoluteURL(g.rawRequest.FullURL, true)132if err != nil {133gologger.Warning().Msgf("[authprovider] Could not parse URL %s: %s\n", g.rawRequest.FullURL, err)134return135}136authStrategies := provider.LookupURLX(parsed)137// here we need to apply it custom because we don't have a standard/official138// rawhttp request format ( which we probably should have )139for _, strategy := range authStrategies {140g.rawRequest.ApplyAuthStrategy(strategy)141}142}143}144145func (g *generatedRequest) URL() string {146if g.request != nil {147return g.request.String()148}149if g.rawRequest != nil {150return g.rawRequest.FullURL151}152return ""153}154155// Make creates a http request for the provided input.156// It returns ErrNoMoreRequests as error when all the requests have been exhausted.157func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context, reqData string, payloads, dynamicValues map[string]interface{}) (gr *generatedRequest, err error) {158origReqData := reqData159defer func() {160if gr != nil {161gr.setReqURLPattern(origReqData)162}163}()164// value of `reqData` depends on the type of request specified in template165// 1. If request is raw request = reqData contains raw request (i.e http request dump)166// 2. If request is Normal ( simply put not a raw request) (Ex: with placeholders `path`) = reqData contains relative path167168// add template context values to dynamicValues (this takes care of self-contained and other types of requests)169// Note: `iterate-all` and flow are mutually exclusive. flow uses templateCtx and iterate-all uses dynamicValues170if r.request.options.HasTemplateCtx(input.MetaInput) {171// skip creating template context if not available172dynamicValues = generators.MergeMaps(dynamicValues, r.request.options.GetTemplateCtx(input.MetaInput).GetAll())173}174175isRawRequest := len(r.request.Raw) > 0176// replace interactsh variables with actual interactsh urls177if r.options.Interactsh != nil {178reqData, r.interactshURLs = r.options.Interactsh.Replace(reqData, []string{})179for payloadName, payloadValue := range payloads {180payloads[payloadName], r.interactshURLs = r.options.Interactsh.Replace(types.ToString(payloadValue), r.interactshURLs)181}182} else {183for payloadName, payloadValue := range payloads {184payloads[payloadName] = types.ToStringNSlice(payloadValue)185}186}187188if r.request.SelfContained {189return r.makeSelfContainedRequest(ctx, reqData, payloads, dynamicValues)190}191192// Parse target url193parsed, err := urlutil.ParseAbsoluteURL(input.MetaInput.Input, false)194if err != nil {195return nil, err196}197198// Non-Raw Requests ex `{{BaseURL}}/somepath` may or maynot have slash after variable and the same is the case for199// target url to avoid inconsistencies extra slash if exists has to removed from default variables200hasTrailingSlash := false201if !isRawRequest {202// if path contains port ex: {{BaseURL}}:8080 use port specified in reqData203parsed, reqData = httputil.UpdateURLPortFromPayload(parsed, reqData)204hasTrailingSlash = httputil.HasTrailingSlash(reqData)205}206207// defaultreqvars are vars generated from request/input ex: {{baseURL}}, {{Host}} etc208// contextargs generate extra vars that may/may not be available always (ex: "ip")209defaultReqVars := protocolutils.GenerateVariables(parsed, hasTrailingSlash, contextargs.GenerateVariables(input))210// optionvars are vars passed from CLI or env variables211optionVars := generators.BuildPayloadFromOptions(r.request.options.Options)212213variablesMap, interactURLs := r.options.Variables.EvaluateWithInteractsh(generators.MergeMaps(defaultReqVars, optionVars), r.options.Interactsh)214if len(interactURLs) > 0 {215r.interactshURLs = append(r.interactshURLs, interactURLs...)216}217// allVars contains all variables from all sources218allVars := generators.MergeMaps(dynamicValues, defaultReqVars, optionVars, variablesMap, r.options.Constants)219220// Evaluate payload variables221// eg: payload variables can be username: jon.doe@{{Hostname}}222for payloadName, payloadValue := range payloads {223payloads[payloadName], err = expressions.Evaluate(types.ToString(payloadValue), allVars)224if err != nil {225return nil, errkit.Wrap(err, "could not evaluate helper expressions")226}227}228// finalVars contains allVars and any generator/fuzzing specific payloads229// payloads used in generator should be given the most preference230finalVars := generators.MergeMaps(allVars, payloads)231232if vardump.EnableVarDump {233gologger.Debug().Msgf("HTTP Protocol request variables: %s\n", vardump.DumpVariables(finalVars))234}235236// Note: If possible any changes to current logic (i.e evaluate -> then parse URL)237// should be avoided since it is dependent on `urlutil` core logic238239// Evaluate (replace) variable with final values240reqData, err = expressions.Evaluate(reqData, finalVars)241if err != nil {242return nil, errkit.Wrap(err, "could not evaluate helper expressions")243}244245if isRawRequest {246return r.generateRawRequest(ctx, reqData, parsed, finalVars, payloads)247}248249reqURL, err := urlutil.ParseAbsoluteURL(reqData, true)250if err != nil {251return nil, errkit.Newf("failed to parse url %v while creating http request", reqData)252}253// while merging parameters first preference is given to target params254finalparams := parsed.Params255finalparams.Merge(reqURL.Params.Encode())256reqURL.Params = finalparams257return r.generateHttpRequest(ctx, reqURL, finalVars, payloads)258}259260// selfContained templates do not need/use target data and all values i.e {{Hostname}} , {{BaseURL}} etc are already available261// in template . makeSelfContainedRequest parses and creates variables map and then creates corresponding http request or raw request262func (r *requestGenerator) makeSelfContainedRequest(ctx context.Context, data string, payloads, dynamicValues map[string]interface{}) (*generatedRequest, error) {263isRawRequest := r.request.isRaw()264265values := generators.MergeMaps(266generators.BuildPayloadFromOptions(r.request.options.Options),267dynamicValues,268payloads, // payloads should override other variables in case of duplicate vars269)270// adds all variables from `variables` section in template271variablesMap := r.request.options.Variables.Evaluate(values)272values = generators.MergeMaps(variablesMap, values)273274signerVars := GetDefaultSignerVars(r.request.Signature.Value)275// this will ensure that default signer variables are overwritten by other variables276values = generators.MergeMaps(signerVars, values, r.options.Constants)277278// priority of variables is as follows (from low to high) for self contained templates279// default signer vars < variables < cli vars < payload < dynamic values < constants280281// evaluate request282data, err := expressions.Evaluate(data, values)283if err != nil {284return nil, errkit.Wrap(err, "could not evaluate helper expressions")285}286// If the request is a raw request, get the URL from the request287// header and use it to make the request.288if isRawRequest {289// Get the hostname from the URL section to build the request.290reader := bufio.NewReader(strings.NewReader(data))291read_line:292s, err := reader.ReadString('\n')293if err != nil {294return nil, fmt.Errorf("could not read request: %w", err)295}296// ignore all annotations297if stringsutil.HasPrefixAny(s, "@") {298goto read_line299}300301parts := strings.Split(s, " ")302if len(parts) < 3 {303return nil, fmt.Errorf("malformed request supplied")304}305306if err := expressions.ContainsUnresolvedVariables(parts[1]); err != nil && !r.request.SkipVariablesCheck {307return nil, errkit.Newf("unresolved variables `%v` found in request", parts[1])308}309310parsed, err := urlutil.ParseURL(parts[1], true)311if err != nil {312return nil, fmt.Errorf("could not parse request URL: %w", err)313}314values = generators.MergeMaps(315generators.MergeMaps(dynamicValues, protocolutils.GenerateVariables(parsed, false, nil)),316values,317)318// Evaluate (replace) variable with final values319data, err = expressions.Evaluate(data, values)320if err != nil {321return nil, errkit.Wrap(err, "could not evaluate helper expressions")322}323return r.generateRawRequest(ctx, data, parsed, values, payloads)324}325if err := expressions.ContainsUnresolvedVariables(data); err != nil && !r.request.SkipVariablesCheck {326// early exit: if there are any unresolved variables in `path` after evaluation327// then return early since this will definitely fail328return nil, errkit.Newf("unresolved variables `%v` found in request", data)329}330331urlx, err := urlutil.ParseURL(data, true)332if err != nil {333return nil, errkit.Wrapf(err, "failed to parse %v in self contained request", data)334}335return r.generateHttpRequest(ctx, urlx, values, payloads)336}337338// generateHttpRequest generates http request from request data from template and variables339// finalVars = contains all variables including generator and protocol specific variables340// generatorValues = contains variables used in fuzzing or other generator specific values341func (r *requestGenerator) generateHttpRequest(ctx context.Context, urlx *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) {342method, err := expressions.Evaluate(r.request.Method.String(), finalVars)343if err != nil {344return nil, errkit.Wrap(err, "failed to evaluate while generating http request")345}346// Build a request on the specified URL347req, err := retryablehttp.NewRequestFromURLWithContext(ctx, method, urlx, nil)348if err != nil {349return nil, err350}351352request, err := r.fillRequest(req, finalVars)353if err != nil {354return nil, err355}356return &generatedRequest{request: request, meta: generatorValues, original: r.request, dynamicValues: finalVars, interactshURLs: r.interactshURLs}, nil357}358359// generateRawRequest generates Raw Request from request data from template and variables360// finalVars = contains all variables including generator and protocol specific variables361// generatorValues = contains variables used in fuzzing or other generator specific values362func (r *requestGenerator) generateRawRequest(ctx context.Context, rawRequest string, baseURL *urlutil.URL, finalVars, generatorValues map[string]interface{}) (*generatedRequest, error) {363364var rawRequestData *raw.Request365var err error366if r.request.SelfContained {367// in self contained requests baseURL is extracted from raw request itself368rawRequestData, err = raw.ParseRawRequest(rawRequest, r.request.Unsafe)369} else {370rawRequestData, err = raw.Parse(rawRequest, baseURL, r.request.Unsafe, r.request.DisablePathAutomerge)371}372if err != nil {373return nil, errkit.Wrap(err, "failed to parse raw request")374}375376// Unsafe option uses rawhttp library377if r.request.Unsafe {378if len(r.options.Options.CustomHeaders) > 0 {379_ = rawRequestData.TryFillCustomHeaders(r.options.Options.CustomHeaders)380}381if rawRequestData.Data != "" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodHead, http.MethodGet) && rawRequestData.Headers["Transfer-Encoding"] != "chunked" {382rawRequestData.Headers["Content-Length"] = strconv.Itoa(len(rawRequestData.Data))383}384unsafeReq := &generatedRequest{rawRequest: rawRequestData, meta: generatorValues, original: r.request, interactshURLs: r.interactshURLs}385return unsafeReq, nil386}387urlx, err := urlutil.ParseAbsoluteURL(rawRequestData.FullURL, true)388if err != nil {389return nil, errkit.Wrapf(err, "failed to create request with url %v got %v", rawRequestData.FullURL, err)390}391req, err := retryablehttp.NewRequestFromURLWithContext(ctx, rawRequestData.Method, urlx, rawRequestData.Data)392if err != nil {393return nil, err394}395396// force transfer encoding if conditions are met397if len(rawRequestData.Data) > 0 && req.Header.Get("Transfer-Encoding") != "chunked" && !stringsutil.EqualFoldAny(rawRequestData.Method, http.MethodGet, http.MethodHead) {398req.ContentLength = int64(len(rawRequestData.Data))399}400401// override the body with a new one that will be used to read the request body in parallel threads402// for race condition testing403if r.request.Threads > 0 && r.request.Race {404req.Body = race.NewOpenGateWithTimeout(req.Body, time.Duration(2)*time.Second)405}406for key, value := range rawRequestData.Headers {407if key == "" {408continue409}410req.Header[key] = []string{value}411if key == "Host" {412req.Host = value413}414}415request, err := r.fillRequest(req, finalVars)416if err != nil {417return nil, err418}419420generatedRequest := &generatedRequest{421request: request,422meta: generatorValues,423original: r.request,424dynamicValues: finalVars,425interactshURLs: r.interactshURLs,426}427428if reqWithOverrides, hasAnnotations := r.request.parseAnnotations(rawRequest, req); hasAnnotations {429generatedRequest.request = reqWithOverrides.request430generatedRequest.customCancelFunction = reqWithOverrides.cancelFunc431generatedRequest.interactshURLs = append(generatedRequest.interactshURLs, reqWithOverrides.interactshURLs...)432}433434return generatedRequest, nil435}436437// fillRequest fills various headers in the request with values438func (r *requestGenerator) fillRequest(req *retryablehttp.Request, values map[string]interface{}) (*retryablehttp.Request, error) {439// Set the header values requested440for header, value := range r.request.Headers {441if r.options.Interactsh != nil {442value, r.interactshURLs = r.options.Interactsh.Replace(value, r.interactshURLs)443}444value, err := expressions.Evaluate(value, values)445if err != nil {446return nil, errkit.Wrap(err, "failed to evaluate while adding headers to request")447}448req.Header[header] = []string{value}449if header == "Host" {450req.Host = value451}452}453454// In case of multiple threads the underlying connection should remain open to allow reuse455if r.request.Threads <= 0 && req.Header.Get("Connection") == "" && r.options.Options.ScanStrategy != scanstrategy.HostSpray.String() {456req.Close = true457}458459// Check if the user requested a request body460if r.request.Body != "" {461body := r.request.Body462if r.options.Interactsh != nil {463body, r.interactshURLs = r.options.Interactsh.Replace(r.request.Body, r.interactshURLs)464}465body, err := expressions.Evaluate(body, values)466if err != nil {467return nil, errkit.Wrap(err, "could not evaluate helper expressions")468}469bodyReader, err := readerutil.NewReusableReadCloser([]byte(body))470if err != nil {471return nil, errors.Wrap(err, "failed to create reusable reader for request body")472}473req.Body = bodyReader474}475if !r.request.Unsafe {476userAgent := useragent.PickRandom()477httputil.SetHeader(req, "User-Agent", userAgent.Raw)478}479480// Only set these headers on non-raw requests481if len(r.request.Raw) == 0 && !r.request.Unsafe {482httputil.SetHeader(req, "Accept", "*/*")483httputil.SetHeader(req, "Accept-Language", "en")484}485486if !LeaveDefaultPorts {487switch {488case req.Scheme == "http" && strings.HasSuffix(req.Host, ":80"):489req.Host = strings.TrimSuffix(req.Host, ":80")490case req.Scheme == "https" && strings.HasSuffix(req.Host, ":443"):491req.Host = strings.TrimSuffix(req.Host, ":443")492}493}494495if r.request.DigestAuthUsername != "" {496req.Auth = &retryablehttp.Auth{497Type: retryablehttp.DigestAuth,498Username: r.request.DigestAuthUsername,499Password: r.request.DigestAuthPassword,500}501}502503return req, nil504}505506507