mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 20:25:27 +00:00
headless: automerge and other improvements (#3958)
* headless: automerge and other improvements * fix typo in function signature
This commit is contained in:
parent
16894cf0e0
commit
beb1bf6d2c
@ -17,6 +17,7 @@ type Instance struct {
|
|||||||
|
|
||||||
// redundant due to dependency cycle
|
// redundant due to dependency cycle
|
||||||
interactsh *interactsh.Client
|
interactsh *interactsh.Client
|
||||||
|
requestLog map[string]string // contains actual request that was sent
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInstance creates a new instance for the current browser.
|
// NewInstance creates a new instance for the current browser.
|
||||||
@ -35,7 +36,14 @@ func (b *Browser) NewInstance() (*Instance, error) {
|
|||||||
// We use a custom sleeper that sleeps from 100ms to 500 ms waiting
|
// We use a custom sleeper that sleeps from 100ms to 500 ms waiting
|
||||||
// for an interaction. Used throughout rod for clicking, etc.
|
// for an interaction. Used throughout rod for clicking, etc.
|
||||||
browser = browser.Sleeper(func() utils.Sleeper { return maxBackoffSleeper(10) })
|
browser = browser.Sleeper(func() utils.Sleeper { return maxBackoffSleeper(10) })
|
||||||
return &Instance{browser: b, engine: browser}, nil
|
return &Instance{browser: b, engine: browser, requestLog: map[string]string{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a map of [template-defined-urls] -> [actual-request-sent]
|
||||||
|
// Note: this does not include CORS or other requests while rendering that were not explicitly
|
||||||
|
// specified in template
|
||||||
|
func (i *Instance) GetRequestLog() map[string]string {
|
||||||
|
return i.requestLog
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes all the tabs and pages for a browser instance
|
// Close closes all the tabs and pages for a browser instance
|
||||||
|
|||||||
@ -134,7 +134,7 @@ func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := createdPage.ExecuteActions(input, actions)
|
data, err := createdPage.ExecuteActions(input, actions, payloads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,8 @@ package engine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -19,17 +16,21 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/projectdiscovery/gologger"
|
"github.com/projectdiscovery/gologger"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators"
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump"
|
||||||
|
protocolutils "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
|
||||||
|
httputil "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils/http"
|
||||||
errorutil "github.com/projectdiscovery/utils/errors"
|
errorutil "github.com/projectdiscovery/utils/errors"
|
||||||
fileutil "github.com/projectdiscovery/utils/file"
|
fileutil "github.com/projectdiscovery/utils/file"
|
||||||
folderutil "github.com/projectdiscovery/utils/folder"
|
folderutil "github.com/projectdiscovery/utils/folder"
|
||||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||||
|
urlutil "github.com/projectdiscovery/utils/url"
|
||||||
"github.com/segmentio/ksuid"
|
"github.com/segmentio/ksuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errinvalidArguments = errors.New("invalid arguments provided")
|
errinvalidArguments = errors.New("invalid arguments provided")
|
||||||
reUrlWithPort = regexp.MustCompile(`{{BaseURL}}:(\d+)`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -39,17 +40,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ExecuteActions executes a list of actions on a page.
|
// ExecuteActions executes a list of actions on a page.
|
||||||
func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (map[string]string, error) {
|
func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, variables map[string]interface{}) (map[string]string, error) {
|
||||||
baseURL, err := url.Parse(input.MetaInput.Input)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
outData := make(map[string]string)
|
outData := make(map[string]string)
|
||||||
|
var err error
|
||||||
for _, act := range actions {
|
for _, act := range actions {
|
||||||
switch act.ActionType.ActionType {
|
switch act.ActionType.ActionType {
|
||||||
case ActionNavigate:
|
case ActionNavigate:
|
||||||
err = p.NavigateURL(act, outData, baseURL)
|
err = p.NavigateURL(act, outData, variables)
|
||||||
case ActionScript:
|
case ActionScript:
|
||||||
err = p.RunScript(act, outData)
|
err = p.RunScript(act, outData)
|
||||||
case ActionClick:
|
case ActionClick:
|
||||||
@ -237,25 +234,57 @@ func (p *Page) ActionSetMethod(act *Action, out map[string]string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NavigateURL executes an ActionLoadURL actions loading a URL for the page.
|
// NavigateURL executes an ActionLoadURL actions loading a URL for the page.
|
||||||
func (p *Page) NavigateURL(action *Action, out map[string]string, parsed *url.URL) error {
|
func (p *Page) NavigateURL(action *Action, out map[string]string, allvars map[string]interface{}) error {
|
||||||
URL := p.getActionArgWithDefaultValues(action, "url")
|
// input <- is input url from cli
|
||||||
if URL == "" {
|
// target <- is the url from template (ex: {{BaseURL}}/test)
|
||||||
|
input, err := urlutil.Parse(p.input.MetaInput.Input)
|
||||||
|
if err != nil {
|
||||||
|
return errorutil.NewWithErr(err).Msgf("could not parse url %s", p.input.MetaInput.Input)
|
||||||
|
}
|
||||||
|
target := p.getActionArgWithDefaultValues(action, "url")
|
||||||
|
if target == "" {
|
||||||
return errinvalidArguments
|
return errinvalidArguments
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the dynamic value substitution here.
|
// if target contains port ex: {{BaseURL}}:8080 use port specified in input
|
||||||
URL, parsed = baseURLWithTemplatePrefs(URL, parsed)
|
input, target = httputil.UpdateURLPortFromPayload(input, target)
|
||||||
if strings.HasSuffix(parsed.Path, "/") && strings.Contains(URL, "{{BaseURL}}/") {
|
hasTrailingSlash := httputil.HasTrailingSlash(target)
|
||||||
parsed.Path = strings.TrimSuffix(parsed.Path, "/")
|
|
||||||
}
|
|
||||||
parsedString := parsed.String()
|
|
||||||
final := replaceWithValues(URL, map[string]interface{}{
|
|
||||||
"Hostname": parsed.Hostname(),
|
|
||||||
"BaseURL": parsedString,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := p.page.Navigate(final); err != nil {
|
// create vars from input url
|
||||||
return errors.Wrap(err, "could not navigate")
|
defaultReqVars := protocolutils.GenerateVariables(input, hasTrailingSlash, contextargs.GenerateVariables(p.input))
|
||||||
|
// merge all variables
|
||||||
|
// Note: ideally we should evaluate all available variables with reqvars
|
||||||
|
// but due to cyclic dependency between packages `engine` and `protocols`
|
||||||
|
// allvars are evaluated,merged and passed from headless package itself
|
||||||
|
// TODO: remove cyclic dependency between packages `engine` and `protocols`
|
||||||
|
allvars = generators.MergeMaps(allvars, defaultReqVars)
|
||||||
|
|
||||||
|
if vardump.EnableVarDump {
|
||||||
|
gologger.Debug().Msgf("Final Protocol request variables: \n%s\n", vardump.DumpVariables(allvars))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the target url with all variables
|
||||||
|
target, err = expressions.Evaluate(target, allvars)
|
||||||
|
if err != nil {
|
||||||
|
return errorutil.NewWithErr(err).Msgf("could not evaluate url %s", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL, err := urlutil.ParseURL(target, true)
|
||||||
|
if err != nil {
|
||||||
|
return errorutil.NewWithTag("http", "failed to parse url %v while creating http request", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== parameter automerge =====
|
||||||
|
// while merging parameters first preference is given to target params
|
||||||
|
finalparams := input.Params.Clone()
|
||||||
|
finalparams.Merge(reqURL.Params.Encode())
|
||||||
|
reqURL.Params = finalparams
|
||||||
|
|
||||||
|
// log all navigated requests
|
||||||
|
p.instance.requestLog[action.GetArg("url")] = reqURL.String()
|
||||||
|
|
||||||
|
if err := p.page.Navigate(reqURL.String()); err != nil {
|
||||||
|
return errorutil.NewWithErr(err).Msgf("could not navigate to url %s", reqURL.String())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -609,23 +638,6 @@ func selectorBy(selector string) rod.SelectorType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// baseURLWithTemplatePrefs returns the url for BaseURL keeping
|
|
||||||
// the template port and path preference over the user provided one.
|
|
||||||
func baseURLWithTemplatePrefs(data string, parsed *url.URL) (string, *url.URL) {
|
|
||||||
// template port preference over input URL port if template has a port
|
|
||||||
matches := reUrlWithPort.FindAllStringSubmatch(data, -1)
|
|
||||||
if len(matches) == 0 {
|
|
||||||
return data, parsed
|
|
||||||
}
|
|
||||||
port := matches[0][1]
|
|
||||||
parsed.Host = net.JoinHostPort(parsed.Hostname(), port)
|
|
||||||
data = strings.ReplaceAll(data, ":"+port, "")
|
|
||||||
if parsed.Path == "" {
|
|
||||||
parsed.Path = "/"
|
|
||||||
}
|
|
||||||
return data, parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Page) getActionArg(action *Action, arg string) string {
|
func (p *Page) getActionArg(action *Action, arg string) string {
|
||||||
return p.getActionArgWithValues(action, arg, nil)
|
return p.getActionArgWithValues(action, arg, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package headless
|
package headless
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -119,21 +120,31 @@ func (request *Request) executeRequestWithPayloads(input *contextargs.Context, p
|
|||||||
}
|
}
|
||||||
defer page.Close()
|
defer page.Close()
|
||||||
|
|
||||||
|
reqLog := instance.GetRequestLog()
|
||||||
|
navigatedURL := request.getLastNavigationURLWithLog(reqLog) // also known as matchedURL if there is a match
|
||||||
|
|
||||||
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), nil)
|
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), nil)
|
||||||
request.options.Progress.IncrementRequests()
|
request.options.Progress.IncrementRequests()
|
||||||
gologger.Verbose().Msgf("Sent Headless request to %s", input.MetaInput.Input)
|
gologger.Verbose().Msgf("Sent Headless request to %s", navigatedURL)
|
||||||
|
|
||||||
reqBuilder := &strings.Builder{}
|
reqBuilder := &strings.Builder{}
|
||||||
if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.DebugResponse {
|
if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.DebugResponse {
|
||||||
gologger.Info().Msgf("[%s] Dumped Headless request for %s", request.options.TemplateID, input.MetaInput.Input)
|
gologger.Info().Msgf("[%s] Dumped Headless request for %s", request.options.TemplateID, navigatedURL)
|
||||||
|
|
||||||
for _, act := range request.Steps {
|
for _, act := range request.Steps {
|
||||||
actStepStr := act.String()
|
if act.ActionType.ActionType == engine.ActionNavigate {
|
||||||
actStepStr = strings.ReplaceAll(actStepStr, "{{BaseURL}}", input.MetaInput.Input)
|
value := act.GetArg("url")
|
||||||
reqBuilder.WriteString("\t" + actStepStr + "\n")
|
if reqLog[value] != "" {
|
||||||
|
reqBuilder.WriteString(fmt.Sprintf("\tnavigate => %v\n", reqLog[value]))
|
||||||
|
} else {
|
||||||
|
reqBuilder.WriteString(fmt.Sprintf("%v not found in %v\n", value, reqLog))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actStepStr := act.String()
|
||||||
|
reqBuilder.WriteString("\t" + actStepStr + "\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
gologger.Debug().Msgf(reqBuilder.String())
|
gologger.Debug().Msgf(reqBuilder.String())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseBody string
|
var responseBody string
|
||||||
@ -142,7 +153,7 @@ func (request *Request) executeRequestWithPayloads(input *contextargs.Context, p
|
|||||||
responseBody, _ = html.HTML()
|
responseBody, _ = html.HTML()
|
||||||
}
|
}
|
||||||
|
|
||||||
outputEvent := request.responseToDSLMap(responseBody, out["header"], out["status_code"], reqBuilder.String(), input.MetaInput.Input, input.MetaInput.Input, page.DumpHistory())
|
outputEvent := request.responseToDSLMap(responseBody, out["header"], out["status_code"], reqBuilder.String(), input.MetaInput.Input, navigatedURL, page.DumpHistory())
|
||||||
for k, v := range out {
|
for k, v := range out {
|
||||||
outputEvent[k] = v
|
outputEvent[k] = v
|
||||||
}
|
}
|
||||||
@ -215,3 +226,16 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, payloads
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getLastNaviationURL returns last successfully navigated URL
|
||||||
|
func (request *Request) getLastNavigationURLWithLog(reqLog map[string]string) string {
|
||||||
|
for i := len(request.Steps) - 1; i >= 0; i-- {
|
||||||
|
if request.Steps[i].ActionType.ActionType == engine.ActionNavigate {
|
||||||
|
templateURL := request.Steps[i].GetArg("url")
|
||||||
|
if reqLog[templateURL] != "" {
|
||||||
|
return reqLog[templateURL]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import (
|
|||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump"
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/race"
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/race"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/raw"
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/raw"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/utils"
|
|
||||||
protocolutils "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
|
protocolutils "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
|
||||||
|
httputil "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils/http"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||||
"github.com/projectdiscovery/rawhttp"
|
"github.com/projectdiscovery/rawhttp"
|
||||||
"github.com/projectdiscovery/retryablehttp-go"
|
"github.com/projectdiscovery/retryablehttp-go"
|
||||||
@ -97,8 +97,8 @@ func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context,
|
|||||||
hasTrailingSlash := false
|
hasTrailingSlash := false
|
||||||
if !isRawRequest {
|
if !isRawRequest {
|
||||||
// if path contains port ex: {{BaseURL}}:8080 use port specified in reqData
|
// if path contains port ex: {{BaseURL}}:8080 use port specified in reqData
|
||||||
parsed, reqData = utils.UpdateURLPortFromPayload(parsed, reqData)
|
parsed, reqData = httputil.UpdateURLPortFromPayload(parsed, reqData)
|
||||||
hasTrailingSlash = utils.HasTrailingSlash(reqData)
|
hasTrailingSlash = httputil.HasTrailingSlash(reqData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultreqvars are vars generated from request/input ex: {{baseURL}}, {{Host}} etc
|
// defaultreqvars are vars generated from request/input ex: {{baseURL}}, {{Host}} etc
|
||||||
@ -362,13 +362,13 @@ func (r *requestGenerator) fillRequest(req *retryablehttp.Request, values map[st
|
|||||||
req.Body = bodyReader
|
req.Body = bodyReader
|
||||||
}
|
}
|
||||||
if !r.request.Unsafe {
|
if !r.request.Unsafe {
|
||||||
utils.SetHeader(req, "User-Agent", uarand.GetRandom())
|
httputil.SetHeader(req, "User-Agent", uarand.GetRandom())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only set these headers on non-raw requests
|
// Only set these headers on non-raw requests
|
||||||
if len(r.request.Raw) == 0 && !r.request.Unsafe {
|
if len(r.request.Raw) == 0 && !r.request.Unsafe {
|
||||||
utils.SetHeader(req, "Accept", "*/*")
|
httputil.SetHeader(req, "Accept", "*/*")
|
||||||
utils.SetHeader(req, "Accept-Language", "en")
|
httputil.SetHeader(req, "Accept-Language", "en")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !LeaveDefaultPorts {
|
if !LeaveDefaultPorts {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package utils
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package utils
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
Loading…
x
Reference in New Issue
Block a user