nuclei/v2/pkg/protocols/http/build_request.go

330 lines
11 KiB
Go
Raw Normal View History

2020-12-25 20:39:16 +00:00
package http
import (
2020-12-27 20:03:50 +00:00
"context"
"fmt"
2020-12-27 20:03:50 +00:00
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
2020-12-25 20:39:16 +00:00
"regexp"
2020-12-27 20:03:50 +00:00
"strings"
"time"
2020-12-27 20:03:50 +00:00
"github.com/Knetic/govaluate"
"github.com/projectdiscovery/nuclei/v2/pkg/operators/common/dsl"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators"
2020-12-27 20:03:50 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/replacer"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/race"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/raw"
"github.com/projectdiscovery/rawhttp"
2020-12-27 20:03:50 +00:00
"github.com/projectdiscovery/retryablehttp-go"
2020-12-25 20:39:16 +00:00
)
2020-12-27 20:03:50 +00:00
var (
urlWithPortRegex = regexp.MustCompile(`{{BaseURL}}:(\d+)`)
templateExpressionRegex = regexp.MustCompile(`(?m)\{\{[^}]+\}\}`)
)
// requestGenerator generates requests sequentially based on various
// configurations for a http request template.
//
// If payload values are present, an iterator is created for the payload
// values. Paths and Raw requests are supported as base input, so
// it will automatically select between them based on the template.
type requestGenerator struct {
currentIndex int
request *Request
payloadIterator *generators.Iterator
}
// newGenerator creates a new request generator instance
func (r *Request) newGenerator() *requestGenerator {
generator := &requestGenerator{request: r}
if len(r.Payloads) > 0 {
generator.payloadIterator = r.generator.NewIterator()
}
return generator
}
// nextValue returns the next path or the next raw request depending on user input
// It returns false if all the inputs have been exhausted by the generator instance.
func (r *requestGenerator) nextValue() (string, map[string]interface{}, bool) {
// If we have paths, return the next path.
if len(r.request.Path) > 0 && r.currentIndex < len(r.request.Path) {
if item := r.request.Path[r.currentIndex]; item != "" {
r.currentIndex++
return item, nil, true
}
}
// If we have raw requests, start with the request at current index.
// If we are not at the start, then check if the iterator for payloads
// has finished if there are any.
//
// If the iterator has finished for the current raw request
// then reset it and move on to the next value, otherwise use the last request.
if len(r.request.Raw) > 0 && r.currentIndex < len(r.request.Raw) {
if r.payloadIterator != nil {
payload, ok := r.payloadIterator.Value()
if !ok {
r.currentIndex++
r.payloadIterator.Reset()
// No more payloads request for us now.
if len(r.request.Raw) == r.currentIndex {
return "", nil, false
}
if item := r.request.Raw[r.currentIndex]; item != "" {
newPayload, ok := r.payloadIterator.Value()
return item, newPayload, ok
}
return "", nil, false
}
return r.request.Raw[r.currentIndex], payload, true
}
if item := r.request.Raw[r.currentIndex]; item != "" {
r.currentIndex++
return item, nil, true
}
}
return "", nil, false
}
2020-12-27 20:03:50 +00:00
// generatedRequest is a single wrapped generated request for a template request
type generatedRequest struct {
original *Request
rawRequest *raw.Request
meta map[string]interface{}
pipelinedClient *rawhttp.PipelineClient
request *retryablehttp.Request
2020-12-27 20:03:50 +00:00
}
// Make creates a http request for the provided input.
// It returns io.EOF as error when all the requests have been exhausted.
2020-12-27 20:03:50 +00:00
func (r *requestGenerator) Make(baseURL string, dynamicValues map[string]interface{}) (*generatedRequest, error) {
data, payloads, ok := r.nextValue()
if !ok {
return nil, io.EOF
}
2020-12-25 20:39:16 +00:00
ctx := context.Background()
parsed, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
hostname := parsed.Host
values := generators.MergeMaps(dynamicValues, map[string]interface{}{
"BaseURL": baseURLWithTemplatePrefs(data, parsed),
"Hostname": hostname,
})
// If data contains \n it's a raw request, process it like that. Else
// continue with the template based request flow.
2020-12-25 20:39:16 +00:00
if strings.Contains(data, "\n") {
2020-12-27 20:03:50 +00:00
return r.makeHTTPRequestFromRaw(ctx, baseURL, data, values, payloads)
2020-12-25 20:39:16 +00:00
}
return r.makeHTTPRequestFromModel(ctx, data, values)
}
// Remaining returns the remaining number of requests for the generator
func (r *requestGenerator) Remaining() int {
if r.payloadIterator != nil {
payloadRemaining := r.payloadIterator.Remaining()
return (len(r.request.Raw) - r.currentIndex + 1) * payloadRemaining
}
return len(r.request.Path) - r.currentIndex + 1
}
// baseURLWithTemplatePrefs returns the url for BaseURL keeping
// the template port and path preference
func baseURLWithTemplatePrefs(data string, parsedURL *url.URL) string {
// template port preference over input URL port
// template has port
hasPort := len(urlWithPortRegex.FindStringSubmatch(data)) > 0
if hasPort {
// check if also the input contains port, in this case extracts the url
if hostname, _, err := net.SplitHostPort(parsedURL.Host); err == nil {
parsedURL.Host = hostname
}
}
return parsedURL.String()
}
2020-12-25 20:39:16 +00:00
// MakeHTTPRequestFromModel creates a *http.Request from a request template
2020-12-27 20:03:50 +00:00
func (r *requestGenerator) makeHTTPRequestFromModel(ctx context.Context, data string, values map[string]interface{}) (*generatedRequest, error) {
URL := replacer.New(values).Replace(data)
2020-12-25 20:39:16 +00:00
// Build a request on the specified URL
2020-12-27 20:03:50 +00:00
req, err := http.NewRequestWithContext(ctx, r.request.Method, URL, nil)
2020-12-25 20:39:16 +00:00
if err != nil {
return nil, err
}
request, err := r.fillRequest(req, values)
if err != nil {
return nil, err
}
2020-12-27 20:03:50 +00:00
return &generatedRequest{request: request}, nil
2020-12-25 20:39:16 +00:00
}
// makeHTTPRequestFromRaw creates a *http.Request from a raw request
2020-12-27 20:03:50 +00:00
func (r *requestGenerator) makeHTTPRequestFromRaw(ctx context.Context, baseURL, data string, values, payloads map[string]interface{}) (*generatedRequest, error) {
2020-12-25 20:39:16 +00:00
// Add trailing line
data += "\n"
2020-12-27 20:03:50 +00:00
// If we have payloads, handle them by evaluating them at runtime.
if len(r.request.Payloads) > 0 {
finalPayloads, err := r.getPayloadValues(baseURL, payloads)
2020-12-25 20:39:16 +00:00
if err != nil {
return nil, err
}
2020-12-27 20:03:50 +00:00
return r.handleRawWithPaylods(ctx, data, baseURL, values, finalPayloads)
2020-12-25 20:39:16 +00:00
}
return r.handleRawWithPaylods(ctx, data, baseURL, values, nil)
}
2020-12-27 20:03:50 +00:00
// handleRawWithPaylods handles raw requests along with paylaods
func (r *requestGenerator) handleRawWithPaylods(ctx context.Context, rawRequest, baseURL string, values, genValues map[string]interface{}) (*generatedRequest, error) {
2020-12-25 20:39:16 +00:00
baseValues := generators.CopyMap(values)
finValues := generators.MergeMaps(baseValues, genValues)
// Replace the dynamic variables in the URL if any
rawRequest = replacer.New(finValues).Replace(rawRequest)
2020-12-25 20:39:16 +00:00
dynamicValues := make(map[string]interface{})
2020-12-27 20:03:50 +00:00
for _, match := range templateExpressionRegex.FindAllString(rawRequest, -1) {
2020-12-25 20:39:16 +00:00
// check if the match contains a dynamic variable
expr := generators.TrimDelimiters(match)
2020-12-27 20:03:50 +00:00
compiled, err := govaluate.NewEvaluableExpressionWithFunctions(expr, dsl.HelperFunctions())
2020-12-25 20:39:16 +00:00
if err != nil {
return nil, err
}
result, err := compiled.Evaluate(finValues)
if err != nil {
return nil, err
}
dynamicValues[expr] = result
}
// Replacer dynamic values if any in raw request and parse it
rawRequest = replacer.New(dynamicValues).Replace(rawRequest)
2020-12-27 20:03:50 +00:00
rawRequestData, err := raw.Parse(rawRequest, baseURL, r.request.Unsafe)
2020-12-25 20:39:16 +00:00
if err != nil {
return nil, err
}
// rawhttp
2020-12-27 20:03:50 +00:00
if r.request.Unsafe {
unsafeReq := &generatedRequest{
rawRequest: rawRequestData,
meta: genValues,
original: r.request,
2020-12-25 20:39:16 +00:00
}
return unsafeReq, nil
}
// retryablehttp
var body io.ReadCloser
2020-12-27 20:03:50 +00:00
body = ioutil.NopCloser(strings.NewReader(rawRequestData.Data))
if r.request.Race {
2020-12-25 20:39:16 +00:00
// More or less this ensures that all requests hit the endpoint at the same approximated time
// Todo: sync internally upon writing latest request byte
body = race.NewOpenGateWithTimeout(body, time.Duration(2)*time.Second)
2020-12-25 20:39:16 +00:00
}
2020-12-27 20:03:50 +00:00
req, err := http.NewRequestWithContext(ctx, rawRequestData.Method, rawRequestData.FullURL, body)
2020-12-25 20:39:16 +00:00
if err != nil {
return nil, err
}
// copy headers
2020-12-27 20:03:50 +00:00
for key, value := range rawRequestData.Headers {
2020-12-25 20:39:16 +00:00
req.Header[key] = []string{value}
}
request, err := r.fillRequest(req, values)
if err != nil {
return nil, err
}
2020-12-27 20:03:50 +00:00
return &generatedRequest{request: request, meta: genValues}, nil
2020-12-25 20:39:16 +00:00
}
2020-12-27 20:03:50 +00:00
// fillRequest fills various headers in the request with values
func (r *requestGenerator) fillRequest(req *http.Request, values map[string]interface{}) (*retryablehttp.Request, error) {
2020-12-25 20:39:16 +00:00
// Set the header values requested
2020-12-27 20:03:50 +00:00
replacer := replacer.New(values)
for header, value := range r.request.Headers {
2020-12-25 20:39:16 +00:00
req.Header[header] = []string{replacer.Replace(value)}
}
// In case of multiple threads the underlying connection should remain open to allow reuse
2020-12-27 20:03:50 +00:00
if r.request.Threads <= 0 && req.Header.Get("Connection") == "" {
2020-12-25 20:39:16 +00:00
req.Close = true
}
// Check if the user requested a request body
2020-12-27 20:03:50 +00:00
if r.request.Body != "" {
req.Body = ioutil.NopCloser(strings.NewReader(r.request.Body))
2020-12-25 20:39:16 +00:00
}
setHeader(req, "User-Agent", "Nuclei - Open-source project (github.com/projectdiscovery/nuclei)")
// raw requests are left untouched
2020-12-27 20:03:50 +00:00
if len(r.request.Raw) > 0 {
2020-12-25 20:39:16 +00:00
return retryablehttp.FromRequest(req)
}
2020-12-27 20:03:50 +00:00
setHeader(req, "Accept", "*/*")
2020-12-25 20:39:16 +00:00
setHeader(req, "Accept-Language", "en")
return retryablehttp.FromRequest(req)
}
// setHeader sets some headers only if the header wasn't supplied by the user
func setHeader(req *http.Request, name, value string) {
if _, ok := req.Header[name]; !ok {
req.Header.Set(name, value)
}
}
// getPayloadValues returns current payload values for a request
2020-12-27 20:03:50 +00:00
func (r *requestGenerator) getPayloadValues(reqURL string, templatePayloads map[string]interface{}) (map[string]interface{}, error) {
payloadProcessedValues := make(map[string]interface{})
2020-12-27 20:03:50 +00:00
for k, v := range templatePayloads {
kexp := v.(string)
// if it doesn't containing markups, we just continue
2020-12-27 20:03:50 +00:00
if !strings.Contains(kexp, replacer.MarkerParenthesisOpen) || strings.Contains(kexp, replacer.MarkerParenthesisClose) || strings.Contains(kexp, replacer.MarkerGeneral) {
payloadProcessedValues[k] = v
continue
}
// attempts to expand expressions
2020-12-27 20:03:50 +00:00
compiled, err := govaluate.NewEvaluableExpressionWithFunctions(kexp, dsl.HelperFunctions())
if err != nil {
// it is a simple literal payload => proceed with literal value
payloadProcessedValues[k] = v
continue
}
// it is an expression - try to solve it
2020-12-27 20:03:50 +00:00
expValue, err := compiled.Evaluate(templatePayloads)
if err != nil {
// an error occurred => proceed with literal value
payloadProcessedValues[k] = v
continue
}
payloadProcessedValues[k] = fmt.Sprint(expValue)
}
var err error
if len(payloadProcessedValues) == 0 {
err = ErrNoPayload
}
return payloadProcessedValues, err
}
// ErrNoPayload error to avoid the additional base null request
var ErrNoPayload = fmt.Errorf("no payload found")