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  }