nuclei/v2/pkg/requests/bulk-http-request.go

452 lines
13 KiB
Go
Raw Normal View History

package requests
import (
2020-04-28 23:02:07 +02:00
"bufio"
"context"
2020-04-29 02:57:18 +02:00
"fmt"
2020-04-04 02:50:32 +05:30
"io/ioutil"
"net"
2020-04-04 02:50:32 +05:30
"net/http"
2020-04-04 03:26:11 +05:30
"net/url"
"regexp"
2020-04-04 02:50:32 +05:30
"strings"
"github.com/Knetic/govaluate"
2020-07-01 16:17:24 +05:30
"github.com/projectdiscovery/nuclei/v2/pkg/extractors"
"github.com/projectdiscovery/nuclei/v2/pkg/generators"
"github.com/projectdiscovery/nuclei/v2/pkg/matchers"
2020-04-04 17:12:29 +05:30
retryablehttp "github.com/projectdiscovery/retryablehttp-go"
)
const (
two = 2
three = 3
)
var urlWithPortRgx = regexp.MustCompile(`{{BaseURL}}:(\d+)`)
2020-09-05 18:27:02 +02:00
var urlWithPathRgx = regexp.MustCompile(`{{BaseURL}}.*/`)
2020-07-18 21:42:23 +02:00
// BulkHTTPRequest contains a request to be made from a template
type BulkHTTPRequest struct {
// CookieReuse is an optional setting that makes cookies shared within requests
CookieReuse bool `yaml:"cookie-reuse,omitempty"`
// Redirects specifies whether redirects should be followed.
Redirects bool `yaml:"redirects,omitempty"`
Name string `yaml:"Name,omitempty"`
// AttackType is the attack type
// Sniper, PitchFork and ClusterBomb. Default is Sniper
AttackType string `yaml:"attack,omitempty"`
// attackType is internal attack type
attackType generators.Type
// Path contains the path/s for the request variables
2020-07-13 03:30:07 +02:00
Payloads map[string]interface{} `yaml:"payloads,omitempty"`
// Method is the request method, whether GET, POST, PUT, etc
Method string `yaml:"method"`
// Path contains the path/s for the request
Path []string `yaml:"path"`
// Headers contains headers to send with the request
Headers map[string]string `yaml:"headers,omitempty"`
// Body is an optional parameter which contains the request body for POST methods, etc
Body string `yaml:"body,omitempty"`
// Matchers contains the detection mechanism for the request to identify
// whether the request was successful
Matchers []*matchers.Matcher `yaml:"matchers,omitempty"`
2020-04-26 05:50:33 +05:30
// MatchersCondition is the condition of the matchers
// whether to use AND or OR. Default is OR.
MatchersCondition string `yaml:"matchers-condition,omitempty"`
// matchersCondition is internal condition for the matchers.
matchersCondition matchers.ConditionType
// Extractors contains the extraction mechanism for the request to identify
// and extract parts of the response.
Extractors []*extractors.Extractor `yaml:"extractors,omitempty"`
// MaxRedirects is the maximum number of redirects that should be followed.
MaxRedirects int `yaml:"max-redirects,omitempty"`
// Raw contains raw requests
Raw []string `yaml:"raw,omitempty"`
// Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining (race conditions/billions requests)
// All requests must be indempotent (GET/POST)
2020-10-08 16:34:47 +02:00
Pipeline bool `yaml:"pipeline,omitempty"`
PipelineMaxConnections int `yaml:"pipeline-max-connections,omitempty"`
PipelineMaxWorkers int `yaml:"pipeline-max-workers,omitempty"`
// Specify in order to skip request RFC normalization
Unsafe bool `yaml:"unsafe,omitempty"`
// DisableAutoHostname Enable/Disable Host header for unsafe raw requests
DisableAutoHostname bool `yaml:"disable-automatic-host-header,omitempty"`
// DisableAutoContentLength Enable/Disable Content-Length header for unsafe raw requests
DisableAutoContentLength bool `yaml:"disable-automatic-content-length-header,omitempty"`
// Internal Finite State Machine keeping track of scan process
2020-07-24 13:37:01 +02:00
gsfm *GeneratorFSM
}
2020-04-04 02:50:32 +05:30
// GetMatchersCondition returns the condition for the matcher
2020-07-18 21:42:23 +02:00
func (r *BulkHTTPRequest) GetMatchersCondition() matchers.ConditionType {
return r.matchersCondition
}
// SetMatchersCondition sets the condition for the matcher
2020-07-18 21:42:23 +02:00
func (r *BulkHTTPRequest) SetMatchersCondition(condition matchers.ConditionType) {
r.matchersCondition = condition
}
// GetAttackType returns the attack
2020-07-18 21:42:23 +02:00
func (r *BulkHTTPRequest) GetAttackType() generators.Type {
return r.attackType
}
// SetAttackType sets the attack
2020-07-18 21:42:23 +02:00
func (r *BulkHTTPRequest) SetAttackType(attack generators.Type) {
r.attackType = attack
}
// GetRequestCount returns the total number of requests the YAML rule will perform
2020-07-23 20:19:19 +02:00
func (r *BulkHTTPRequest) GetRequestCount() int64 {
return int64(len(r.Raw) | len(r.Path))
}
// MakeHTTPRequest makes the HTTP request
func (r *BulkHTTPRequest) MakeHTTPRequest(ctx context.Context, baseURL string, dynamicValues map[string]interface{}, data string) (*HTTPRequest, error) {
2020-04-04 03:26:11 +05:30
parsed, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
2020-07-20 00:41:31 +02:00
hostname := parsed.Host
2020-04-04 02:50:32 +05:30
2020-07-19 03:14:19 +02:00
values := generators.MergeMaps(dynamicValues, map[string]interface{}{
"BaseURL": baseURLWithTemplatePrefs(data, parsed),
2020-04-29 02:57:18 +02:00
"Hostname": hostname,
2020-07-19 03:14:19 +02:00
})
2020-04-29 02:57:18 +02:00
2020-07-18 21:42:23 +02:00
// if data contains \n it's a raw request
if strings.Contains(data, "\n") {
return r.makeHTTPRequestFromRaw(ctx, baseURL, data, values)
2020-04-29 02:57:18 +02:00
}
return r.makeHTTPRequestFromModel(ctx, data, values)
2020-04-29 02:57:18 +02:00
}
// MakeHTTPRequestFromModel creates a *http.Request from a request template
func (r *BulkHTTPRequest) makeHTTPRequestFromModel(ctx context.Context, data string, values map[string]interface{}) (*HTTPRequest, error) {
2020-07-18 21:42:23 +02:00
replacer := newReplacer(values)
URL := replacer.Replace(data)
2020-07-18 21:42:23 +02:00
// Build a request on the specified URL
req, err := http.NewRequestWithContext(ctx, r.Method, URL, nil)
2020-07-18 21:42:23 +02:00
if err != nil {
return nil, err
}
2020-07-18 21:42:23 +02:00
request, err := r.fillRequest(req, values)
if err != nil {
return nil, err
}
2020-04-04 02:50:32 +05:30
return &HTTPRequest{Request: request}, nil
2020-04-04 02:50:32 +05:30
}
2020-04-28 23:02:07 +02:00
// InitGenerator initializes the generator
2020-07-24 18:12:16 +02:00
func (r *BulkHTTPRequest) InitGenerator() {
r.gsfm = NewGeneratorFSM(r.attackType, r.Payloads, r.Path, r.Raw)
}
// CreateGenerator creates the generator
func (r *BulkHTTPRequest) CreateGenerator(reqURL string) {
r.gsfm.Add(reqURL)
2020-07-18 21:42:23 +02:00
}
2020-04-04 02:50:32 +05:30
// HasGenerator check if an URL has a generator
func (r *BulkHTTPRequest) HasGenerator(reqURL string) bool {
return r.gsfm.Has(reqURL)
2020-07-25 22:25:21 +02:00
}
// ReadOne reads and return a generator by URL
func (r *BulkHTTPRequest) ReadOne(reqURL string) {
r.gsfm.ReadOne(reqURL)
2020-04-04 02:50:32 +05:30
}
2020-04-28 23:02:07 +02:00
// makeHTTPRequestFromRaw creates a *http.Request from a raw request
func (r *BulkHTTPRequest) makeHTTPRequestFromRaw(ctx context.Context, baseURL, data string, values map[string]interface{}) (*HTTPRequest, error) {
2020-07-18 21:42:23 +02:00
// Add trailing line
data += "\n"
2020-07-18 21:42:23 +02:00
if len(r.Payloads) > 0 {
2020-07-24 13:37:01 +02:00
r.gsfm.InitOrSkip(baseURL)
r.ReadOne(baseURL)
return r.handleRawWithPaylods(ctx, data, baseURL, values, r.gsfm.Value(baseURL))
}
2020-07-18 21:42:23 +02:00
// otherwise continue with normal flow
return r.handleRawWithPaylods(ctx, data, baseURL, values, nil)
}
func (r *BulkHTTPRequest) handleRawWithPaylods(ctx context.Context, raw, baseURL string, values, genValues map[string]interface{}) (*HTTPRequest, error) {
baseValues := generators.CopyMap(values)
finValues := generators.MergeMaps(baseValues, genValues)
replacer := newReplacer(finValues)
// Replace the dynamic variables in the URL if any
raw = replacer.Replace(raw)
dynamicValues := make(map[string]interface{})
// find all potentials tokens between {{}}
var re = regexp.MustCompile(`(?m)\{\{.+}}`)
for _, match := range re.FindAllString(raw, -1) {
// check if the match contains a dynamic variable
2020-07-20 01:37:07 +02:00
expr := generators.TrimDelimiters(match)
compiled, err := govaluate.NewEvaluableExpressionWithFunctions(expr, generators.HelperFunctions())
2020-07-20 01:37:07 +02:00
if err != nil {
return nil, err
}
2020-07-20 01:37:07 +02:00
result, err := compiled.Evaluate(finValues)
if err != nil {
return nil, err
}
2020-07-20 01:37:07 +02:00
dynamicValues[expr] = result
}
2020-04-28 23:02:07 +02:00
// replace dynamic values
dynamicReplacer := newReplacer(dynamicValues)
raw = dynamicReplacer.Replace(raw)
rawRequest, err := r.parseRawRequest(raw, baseURL)
if err != nil {
2020-07-18 21:42:23 +02:00
return nil, err
}
// rawhttp
if r.Unsafe {
rawRequest.AutomaticContentLength = !r.DisableAutoContentLength
rawRequest.AutomaticHostHeader = !r.DisableAutoHostname
return &HTTPRequest{RawRequest: rawRequest, Meta: genValues}, nil
}
// retryablehttp
req, err := http.NewRequestWithContext(ctx, rawRequest.Method, rawRequest.FullURL, strings.NewReader(rawRequest.Data))
if err != nil {
2020-07-18 21:42:23 +02:00
return nil, err
}
// copy headers
for key, value := range rawRequest.Headers {
2020-07-17 16:04:13 +02:00
req.Header[key] = []string{value}
2020-06-29 19:50:11 +05:30
}
request, err := r.fillRequest(req, values)
if err != nil {
2020-07-18 21:42:23 +02:00
return nil, err
}
return &HTTPRequest{Request: request, Meta: genValues}, nil
2020-04-29 02:57:18 +02:00
}
2020-04-28 23:02:07 +02:00
2020-07-18 21:42:23 +02:00
func (r *BulkHTTPRequest) fillRequest(req *http.Request, values map[string]interface{}) (*retryablehttp.Request, error) {
setHeader(req, "Connection", "close")
req.Close = true
2020-04-30 17:39:33 +02:00
replacer := newReplacer(values)
2020-07-04 14:34:41 +07:00
2020-04-29 02:57:18 +02:00
// Check if the user requested a request body
if r.Body != "" {
req.Body = ioutil.NopCloser(strings.NewReader(r.Body))
}
2020-04-28 23:02:07 +02:00
2020-04-29 02:57:18 +02:00
// Set the header values requested
for header, value := range r.Headers {
2020-07-17 16:04:13 +02:00
req.Header[header] = []string{replacer.Replace(value)}
2020-04-29 02:57:18 +02:00
}
2020-04-28 23:02:07 +02:00
2020-08-29 16:07:05 +02:00
setHeader(req, "User-Agent", "Nuclei - Open-source project (github.com/projectdiscovery/nuclei)")
2020-04-29 23:07:19 +02:00
2020-07-04 14:34:41 +07:00
// raw requests are left untouched
if len(r.Raw) > 0 {
return retryablehttp.FromRequest(req)
}
2020-08-29 16:07:05 +02:00
setHeader(req, "Accept", "*/*")
setHeader(req, "Accept-Language", "en")
2020-04-28 23:02:07 +02:00
2020-04-29 02:57:18 +02:00
return retryablehttp.FromRequest(req)
2020-04-28 23:02:07 +02:00
}
// HTTPRequest is the basic HTTP request
type HTTPRequest struct {
Request *retryablehttp.Request
RawRequest *RawRequest
Meta map[string]interface{}
}
2020-05-22 00:23:38 +02:00
func setHeader(req *http.Request, name, value string) {
// Set some headers only if the header wasn't supplied by the user
if _, ok := req.Header[name]; !ok {
req.Header.Set(name, value)
}
}
// baseURLWithTemplatePrefs returns the url for BaseURL keeping
// the template port and path preference
2020-09-05 18:27:02 +02:00
func baseURLWithTemplatePrefs(data string, parsedURL *url.URL) string {
// template port preference over input URL port
hasPort := len(urlWithPortRgx.FindStringSubmatch(data)) > 0
if hasPort {
hostname, _, _ := net.SplitHostPort(parsedURL.Host)
parsedURL.Host = hostname
}
// template path preference over input URL path
hasPath := len(urlWithPathRgx.FindStringSubmatch(data)) > 0
if hasPath {
parsedURL.Path = ""
}
return parsedURL.String()
}
2020-05-22 00:23:38 +02:00
// CustomHeaders valid for all requests
type CustomHeaders []string
// String returns just a label
func (c *CustomHeaders) String() string {
return "Custom Global Headers"
}
// Set a new global header
func (c *CustomHeaders) Set(value string) error {
*c = append(*c, value)
return nil
}
2020-06-29 19:50:11 +05:30
// RawRequest defines a basic HTTP raw request
2020-07-18 21:42:23 +02:00
type RawRequest struct {
FullURL string
Method string
Path string
Data string
Headers map[string]string
AutomaticHostHeader bool
AutomaticContentLength bool
2020-06-29 19:50:11 +05:30
}
// parseRawRequest parses the raw request as supplied by the user
func (r *BulkHTTPRequest) parseRawRequest(request, baseURL string) (*RawRequest, error) {
2020-06-29 19:50:11 +05:30
reader := bufio.NewReader(strings.NewReader(request))
2020-07-18 21:42:23 +02:00
rawRequest := RawRequest{
2020-06-29 19:50:11 +05:30
Headers: make(map[string]string),
}
s, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("could not read request: %s", err)
}
2020-06-29 19:50:11 +05:30
parts := strings.Split(s, " ")
if len(parts) < three {
2020-06-29 19:50:11 +05:30
return nil, fmt.Errorf("malformed request supplied")
}
// Set the request Method
rawRequest.Method = parts[0]
// Accepts all malformed headers
var key, value string
2020-06-29 19:50:11 +05:30
for {
line, readErr := reader.ReadString('\n')
2020-06-29 19:50:11 +05:30
line = strings.TrimSpace(line)
if readErr != nil || line == "" {
2020-06-29 19:50:11 +05:30
break
}
p := strings.SplitN(line, ":", two)
key = p[0]
if len(p) > 1 {
value = p[1]
2020-06-29 19:50:11 +05:30
}
rawRequest.Headers[key] = value
2020-06-29 19:50:11 +05:30
}
// Handle case with the full http url in path. In that case,
// ignore any host header that we encounter and use the path as request URL
if strings.HasPrefix(parts[1], "http") {
parsed, parseErr := url.Parse(parts[1])
if parseErr != nil {
return nil, fmt.Errorf("could not parse request URL: %s", parseErr)
2020-06-29 19:50:11 +05:30
}
2020-06-29 19:50:11 +05:30
rawRequest.Path = parts[1]
rawRequest.Headers["Host"] = parsed.Host
} else {
rawRequest.Path = parts[1]
}
2020-07-04 14:34:41 +07:00
// If raw request doesn't have a Host header and/ path,
// this will be generated from the parsed baseURL
parsedURL, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("could not parse request URL: %s", err)
}
var hostURL string
if rawRequest.Headers["Host"] == "" {
2020-07-04 14:34:41 +07:00
hostURL = parsedURL.Host
} else {
hostURL = rawRequest.Headers["Host"]
}
if rawRequest.Path == "" {
2020-07-04 14:34:41 +07:00
rawRequest.Path = parsedURL.Path
} else if strings.HasPrefix(rawRequest.Path, "?") {
2020-07-04 14:34:41 +07:00
// requests generated from http.ReadRequest have incorrect RequestURI, so they
// cannot be used to perform another request directly, we need to generate a new one
// with the new target url
rawRequest.Path = fmt.Sprintf("%s%s", parsedURL.Path, rawRequest.Path)
2020-07-04 14:34:41 +07:00
}
rawRequest.FullURL = fmt.Sprintf("%s://%s%s", parsedURL.Scheme, strings.TrimSpace(hostURL), rawRequest.Path)
2020-07-04 14:34:41 +07:00
2020-06-29 19:50:11 +05:30
// Set the request body
b, err := ioutil.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("could not read request body: %s", err)
}
2020-06-29 19:50:11 +05:30
rawRequest.Data = string(b)
2020-06-29 19:50:11 +05:30
return &rawRequest, nil
}
2020-07-18 21:42:23 +02:00
// Next returns the next generator by URL
func (r *BulkHTTPRequest) Next(reqURL string) bool {
return r.gsfm.Next(reqURL)
2020-07-18 21:42:23 +02:00
}
2020-09-19 22:26:59 +02:00
// Position returns the current generator's position by URL
func (r *BulkHTTPRequest) Position(reqURL string) int {
return r.gsfm.Position(reqURL)
2020-07-18 21:42:23 +02:00
}
2020-07-23 20:19:19 +02:00
// Reset resets the generator by URL
func (r *BulkHTTPRequest) Reset(reqURL string) {
r.gsfm.Reset(reqURL)
2020-07-18 21:42:23 +02:00
}
// Current returns the current generator by URL
func (r *BulkHTTPRequest) Current(reqURL string) string {
return r.gsfm.Current(reqURL)
2020-07-18 21:42:23 +02:00
}
2020-07-23 23:28:34 +02:00
// Total is the total number of requests
2020-07-18 21:42:23 +02:00
func (r *BulkHTTPRequest) Total() int {
return len(r.Path) + len(r.Raw)
}
// Increment increments the processed request
func (r *BulkHTTPRequest) Increment(reqURL string) {
r.gsfm.Increment(reqURL)
2020-07-18 21:42:23 +02:00
}