feat(headless): eval DSL exprs in args (#6017)

* refactor(headless): mv `input` -> `ctx` field name

Signed-off-by: Dwi Siswanto <git@dw1.io>

* feat(headless): eval DSL exprs in args

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(headless): rm duplicate imports

Signed-off-by: Dwi Siswanto <git@dw1.io>

* feat(headless): rm duplicate dumped req vars

* refactor(headless): unify `getTimeParameter` retrieval

Now, `getTimeParameter` tries to get the parameter
as an integer, then as a `time.Duration`, and
finally falls back to the default value
(multiplied by the unit).

Signed-off-by: Dwi Siswanto <git@dw1.io>

* feat(headless): adjust default timeout value to 5s

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(headless): use `getTimeParameter`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(headless): add nolint directive - `replaceWithValues`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* feat(headless): revert parameter automerge & adds `inputURL` field

Signed-off-by: Dwi Siswanto <git@dw1.io>

* test(headless): add headless-dsl integration test

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
This commit is contained in:
Dwi Siswanto 2025-02-10 21:46:35 +07:00 committed by GitHub
parent d2d5ee9d48
commit d2636b9ca2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 349 additions and 161 deletions

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -13,6 +14,7 @@ import (
var headlessTestcases = []TestCaseInfo{ var headlessTestcases = []TestCaseInfo{
{Path: "protocols/headless/headless-basic.yaml", TestCase: &headlessBasic{}}, {Path: "protocols/headless/headless-basic.yaml", TestCase: &headlessBasic{}},
{Path: "protocols/headless/headless-waitevent.yaml", TestCase: &headlessBasic{}}, {Path: "protocols/headless/headless-waitevent.yaml", TestCase: &headlessBasic{}},
{Path: "protocols/headless/headless-dsl.yaml", TestCase: &headlessBasic{}},
{Path: "protocols/headless/headless-self-contained.yaml", TestCase: &headlessSelfContained{}}, {Path: "protocols/headless/headless-self-contained.yaml", TestCase: &headlessSelfContained{}},
{Path: "protocols/headless/headless-header-action.yaml", TestCase: &headlessHeaderActions{}}, {Path: "protocols/headless/headless-header-action.yaml", TestCase: &headlessHeaderActions{}},
{Path: "protocols/headless/headless-extract-values.yaml", TestCase: &headlessExtractValues{}}, {Path: "protocols/headless/headless-extract-values.yaml", TestCase: &headlessExtractValues{}},
@ -30,7 +32,7 @@ type headlessBasic struct{}
func (h *headlessBasic) Execute(filePath string) error { func (h *headlessBasic) Execute(filePath string) error {
router := httprouter.New() router := httprouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
_, _ = w.Write([]byte("<html><body></body></html>")) _, _ = fmt.Fprintf(w, "<html><body>%s</body></html>", r.URL.Query().Get("_"))
}) })
ts := httptest.NewServer(router) ts := httptest.NewServer(router)
defer ts.Close() defer ts.Close()

View File

@ -0,0 +1,18 @@
id: headless-dsl
info:
name: Headless DSL
author: dwisiswant0
severity: info
tags: headless
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}/?_={{urlencode(concat('foo', '-', 'bar'))}}"
- action: waitload
matchers:
- type: word
words:
- "foo-bar"

View File

@ -11,14 +11,21 @@ import (
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/proto"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
"github.com/projectdiscovery/nuclei/v3/pkg/types" "github.com/projectdiscovery/nuclei/v3/pkg/types"
errorutil "github.com/projectdiscovery/utils/errors"
urlutil "github.com/projectdiscovery/utils/url"
) )
// Page is a single page in an isolated browser instance // Page is a single page in an isolated browser instance
type Page struct { type Page struct {
input *contextargs.Context ctx *contextargs.Context
inputURL *urlutil.URL
options *Options options *Options
page *rod.Page page *rod.Page
rules []rule rules []rule
@ -29,6 +36,7 @@ type Page struct {
History []HistoryData History []HistoryData
InteractshURLs []string InteractshURLs []string
payloads map[string]interface{} payloads map[string]interface{}
variables map[string]interface{}
} }
// HistoryData contains the page request/response pairs // HistoryData contains the page request/response pairs
@ -45,7 +53,7 @@ type Options struct {
} }
// Run runs a list of actions by creating a new page in the browser. // Run runs a list of actions by creating a new page in the browser.
func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (ActionData, *Page, error) { func (i *Instance) Run(ctx *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (ActionData, *Page, error) {
page, err := i.engine.Page(proto.TargetCreateTarget{}) page, err := i.engine.Page(proto.TargetCreateTarget{})
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -58,13 +66,33 @@ func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads m
} }
} }
payloads = generators.MergeMaps(payloads,
generators.BuildPayloadFromOptions(i.browser.options),
)
target := ctx.MetaInput.Input
input, err := urlutil.Parse(target)
if err != nil {
return nil, nil, errorutil.NewWithErr(err).Msgf("could not parse URL %s", target)
}
hasTrailingSlash := httputil.HasTrailingSlash(target)
variables := utils.GenerateVariables(input, hasTrailingSlash, contextargs.GenerateVariables(ctx))
variables = generators.MergeMaps(variables, payloads)
if vardump.EnableVarDump {
gologger.Debug().Msgf("Headless Protocol request variables: %s\n", vardump.DumpVariables(variables))
}
createdPage := &Page{ createdPage := &Page{
options: options, options: options,
page: page, page: page,
input: input, ctx: ctx,
instance: i, instance: i,
mutex: &sync.RWMutex{}, mutex: &sync.RWMutex{},
payloads: payloads, payloads: payloads,
variables: variables,
inputURL: input,
} }
httpclient, err := i.browser.getHTTPClient() httpclient, err := i.browser.getHTTPClient()
@ -108,13 +136,13 @@ func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads m
// inject cookies // inject cookies
// each http request is performed via the native go http client // each http request is performed via the native go http client
// we first inject the shared cookies // we first inject the shared cookies
URL, err := url.Parse(input.MetaInput.Input) URL, err := url.Parse(ctx.MetaInput.Input)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if !options.DisableCookie { if !options.DisableCookie {
if cookies := input.CookieJar.Cookies(URL); len(cookies) > 0 { if cookies := ctx.CookieJar.Cookies(URL); len(cookies) > 0 {
var NetworkCookies []*proto.NetworkCookie var NetworkCookies []*proto.NetworkCookie
for _, cookie := range cookies { for _, cookie := range cookies {
networkCookie := &proto.NetworkCookie{ networkCookie := &proto.NetworkCookie{
@ -132,7 +160,7 @@ func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads m
} }
params := proto.CookiesToParams(NetworkCookies) params := proto.CookiesToParams(NetworkCookies)
for _, param := range params { for _, param := range params {
param.URL = input.MetaInput.Input param.URL = ctx.MetaInput.Input
} }
err := page.SetCookies(params) err := page.SetCookies(params)
if err != nil { if err != nil {
@ -141,7 +169,7 @@ func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads m
} }
} }
data, err := createdPage.ExecuteActions(input, actions, payloads) data, err := createdPage.ExecuteActions(ctx, actions)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -161,7 +189,7 @@ func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads m
} }
httpCookies = append(httpCookies, httpCookie) httpCookies = append(httpCookies, httpCookie)
} }
input.CookieJar.SetCookies(URL, httpCookies) ctx.CookieJar.SetCookies(URL, httpCookies)
} }
} }

View File

@ -2,6 +2,7 @@ package engine
import ( import (
"context" "context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -19,11 +20,7 @@ import (
"github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
contextutil "github.com/projectdiscovery/utils/context" contextutil "github.com/projectdiscovery/utils/context"
"github.com/projectdiscovery/utils/errkit" "github.com/projectdiscovery/utils/errkit"
errorutil "github.com/projectdiscovery/utils/errors" errorutil "github.com/projectdiscovery/utils/errors"
@ -48,7 +45,7 @@ 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, variables map[string]interface{}) (outData ActionData, err error) { func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (outData ActionData, err error) {
outData = make(ActionData) outData = make(ActionData)
// waitFuncs are function that needs to be executed after navigation // waitFuncs are function that needs to be executed after navigation
// typically used for waitEvent // typically used for waitEvent
@ -69,7 +66,7 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, var
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, variables) err = p.NavigateURL(act, outData)
if err == nil { if err == nil {
// if navigation successful trigger all waitFuncs (if any) // if navigation successful trigger all waitFuncs (if any)
for _, waitFunc := range waitFuncs { for _, waitFunc := range waitFuncs {
@ -176,7 +173,7 @@ func (p *Page) WaitVisible(act *Action, out ActionData) error {
return errors.Wrap(err, "Wrong timeout given") return errors.Wrap(err, "Wrong timeout given")
} }
pollTime, err := getPollTime(p, act) pollTime, err := getTimeParameter(p, act, "pollTime", 100, time.Millisecond)
if err != nil { if err != nil {
return errors.Wrap(err, "Wrong polling time given") return errors.Wrap(err, "Wrong polling time given")
} }
@ -236,151 +233,220 @@ func getNavigationFunc(p *Page, act *Action, event proto.PageLifecycleEventName)
} }
func getTimeout(p *Page, act *Action) (time.Duration, error) { func getTimeout(p *Page, act *Action) (time.Duration, error) {
return geTimeParameter(p, act, "timeout", 3, time.Second) return getTimeParameter(p, act, "timeout", 5, time.Second)
} }
func getPollTime(p *Page, act *Action) (time.Duration, error) { // getTimeParameter returns a time parameter from an action. It first tries to
return geTimeParameter(p, act, "pollTime", 100, time.Millisecond) // get the parameter as an integer, then as a time.Duration, and finally falls
} // back to the default value (multiplied by the unit).
func getTimeParameter(p *Page, act *Action, argName string, defaultValue, unit time.Duration) (time.Duration, error) {
func geTimeParameter(p *Page, act *Action, parameterName string, defaultValue time.Duration, duration time.Duration) (time.Duration, error) { argValue, err := p.getActionArg(act, argName)
pollTimeString := p.getActionArgWithDefaultValues(act, parameterName)
if pollTimeString == "" {
return defaultValue * duration, nil
}
timeout, err := strconv.Atoi(pollTimeString)
if err != nil { if err != nil {
return time.Duration(0), err return time.Duration(0), err
} }
return time.Duration(timeout) * duration, nil
convertedValue, err := strconv.Atoi(argValue)
if err == nil {
return time.Duration(convertedValue) * unit, nil
}
// fallback to time.ParseDuration
parsedTimeValue, err := time.ParseDuration(argValue)
if err == nil {
return parsedTimeValue, nil
}
return defaultValue * unit, nil
} }
// ActionAddHeader executes a AddHeader action. // ActionAddHeader executes a AddHeader action.
func (p *Page) ActionAddHeader(act *Action, out ActionData) error { func (p *Page) ActionAddHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
args["key"] = p.getActionArgWithDefaultValues(act, "key")
args["value"] = p.getActionArgWithDefaultValues(act, "value") part, err := p.getActionArg(act, "part")
p.rules = append(p.rules, rule{Action: ActionAddHeader, Part: in, Args: args}) if err != nil {
return err
}
args["key"], err = p.getActionArg(act, "key")
if err != nil {
return err
}
args["value"], err = p.getActionArg(act, "value")
if err != nil {
return err
}
p.rules = append(p.rules, rule{
Action: ActionAddHeader,
Part: part,
Args: args,
})
return nil return nil
} }
// ActionSetHeader executes a SetHeader action. // ActionSetHeader executes a SetHeader action.
func (p *Page) ActionSetHeader(act *Action, out ActionData) error { func (p *Page) ActionSetHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
args["key"] = p.getActionArgWithDefaultValues(act, "key")
args["value"] = p.getActionArgWithDefaultValues(act, "value") part, err := p.getActionArg(act, "part")
p.rules = append(p.rules, rule{Action: ActionSetHeader, Part: in, Args: args}) if err != nil {
return err
}
args["key"], err = p.getActionArg(act, "key")
if err != nil {
return err
}
args["value"], err = p.getActionArg(act, "value")
if err != nil {
return err
}
p.rules = append(p.rules, rule{
Action: ActionSetHeader,
Part: part,
Args: args,
})
return nil return nil
} }
// ActionDeleteHeader executes a DeleteHeader action. // ActionDeleteHeader executes a DeleteHeader action.
func (p *Page) ActionDeleteHeader(act *Action, out ActionData) error { func (p *Page) ActionDeleteHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
args["key"] = p.getActionArgWithDefaultValues(act, "key")
p.rules = append(p.rules, rule{Action: ActionDeleteHeader, Part: in, Args: args}) part, err := p.getActionArg(act, "part")
if err != nil {
return err
}
args["key"], err = p.getActionArg(act, "key")
if err != nil {
return err
}
p.rules = append(p.rules, rule{
Action: ActionDeleteHeader,
Part: part,
Args: args,
})
return nil return nil
} }
// ActionSetBody executes a SetBody action. // ActionSetBody executes a SetBody action.
func (p *Page) ActionSetBody(act *Action, out ActionData) error { func (p *Page) ActionSetBody(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
args["body"] = p.getActionArgWithDefaultValues(act, "body")
p.rules = append(p.rules, rule{Action: ActionSetBody, Part: in, Args: args}) part, err := p.getActionArg(act, "part")
if err != nil {
return err
}
args["body"], err = p.getActionArg(act, "body")
if err != nil {
return err
}
p.rules = append(p.rules, rule{
Action: ActionSetBody,
Part: part,
Args: args,
})
return nil return nil
} }
// ActionSetMethod executes an SetMethod action. // ActionSetMethod executes an SetMethod action.
func (p *Page) ActionSetMethod(act *Action, out ActionData) error { func (p *Page) ActionSetMethod(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
args["method"] = p.getActionArgWithDefaultValues(act, "method")
p.rules = append(p.rules, rule{Action: ActionSetMethod, Part: in, Args: args, Once: &sync.Once{}}) part, err := p.getActionArg(act, "part")
if err != nil {
return err
}
args["method"], err = p.getActionArg(act, "method")
if err != nil {
return err
}
p.rules = append(p.rules, rule{
Action: ActionSetMethod,
Part: part,
Args: args,
Once: &sync.Once{},
})
return nil return nil
} }
// 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 ActionData, allvars map[string]interface{}) error { func (p *Page) NavigateURL(action *Action, out ActionData) error {
// input <- is input url from cli url, err := p.getActionArg(action, "url")
// target <- is the url from template (ex: {{BaseURL}}/test)
input, err := urlutil.Parse(p.input.MetaInput.Input)
if err != nil { if err != nil {
return errorutil.NewWithErr(err).Msgf("could not parse url %s", p.input.MetaInput.Input) return err
} }
target := p.getActionArgWithDefaultValues(action, "url")
if target == "" { if url == "" {
return errinvalidArguments return errinvalidArguments
} }
// if target contains port ex: {{BaseURL}}:8080 use port specified in input parsedURL, err := urlutil.ParseURL(url, true)
input, target = httputil.UpdateURLPortFromPayload(input, target)
hasTrailingSlash := httputil.HasTrailingSlash(target)
// create vars from input url
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("Headless Protocol request variables: %s\n", vardump.DumpVariables(allvars))
}
// Evaluate the target url with all variables
target, err = expressions.Evaluate(target, allvars)
if err != nil { if err != nil {
return errorutil.NewWithErr(err).Msgf("could not evaluate url %s", target) return errorutil.NewWithTag("headless", "failed to parse url %v while creating http request", url)
}
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 ===== // ===== parameter automerge =====
// while merging parameters first preference is given to target params // while merging parameters first preference is given to target params
finalparams := input.Params.Clone() finalparams := parsedURL.Params.Clone()
finalparams.Merge(reqURL.Params.Encode()) finalparams.Merge(p.inputURL.Params.Encode())
reqURL.Params = finalparams parsedURL.Params = finalparams
// log all navigated requests // log all navigated requests
p.instance.requestLog[action.GetArg("url")] = reqURL.String() p.instance.requestLog[action.GetArg("url")] = parsedURL.String()
if err := p.page.Navigate(reqURL.String()); err != nil { if err := p.page.Navigate(parsedURL.String()); err != nil {
return errorutil.NewWithErr(err).Msgf("could not navigate to url %s", reqURL.String()) return errorutil.NewWithErr(err).Msgf("could not navigate to url %s", parsedURL.String())
} }
return nil return nil
} }
// RunScript runs a script on the loaded page // RunScript runs a script on the loaded page
func (p *Page) RunScript(action *Action, out ActionData) error { func (p *Page) RunScript(act *Action, out ActionData) error {
code := p.getActionArgWithDefaultValues(action, "code") code, err := p.getActionArg(act, "code")
if err != nil {
return err
}
if code == "" { if code == "" {
return errinvalidArguments return errinvalidArguments
} }
if p.getActionArgWithDefaultValues(action, "hook") == "true" {
hook, err := p.getActionArg(act, "hook")
if err != nil {
return err
}
if hook == "true" {
if _, err := p.page.EvalOnNewDocument(code); err != nil { if _, err := p.page.EvalOnNewDocument(code); err != nil {
return err return err
} }
} }
data, err := p.page.Eval(code) data, err := p.page.Eval(code)
if err != nil { if err != nil {
return err return err
} }
if data != nil && action.Name != "" {
out[action.Name] = data.Value.String() if data != nil && act.Name != "" {
out[act.Name] = data.Value.String()
} }
return nil return nil
} }
@ -401,7 +467,12 @@ func (p *Page) ClickElement(act *Action, out ActionData) error {
// KeyboardAction executes a keyboard action on the page. // KeyboardAction executes a keyboard action on the page.
func (p *Page) KeyboardAction(act *Action, out ActionData) error { func (p *Page) KeyboardAction(act *Action, out ActionData) error {
return p.page.Keyboard.Type([]input.Key(p.getActionArgWithDefaultValues(act, "keys"))...) keys, err := p.getActionArg(act, "keys")
if err != nil {
return err
}
return p.page.Keyboard.Type([]input.Key(keys)...)
} }
// RightClickElement executes right click actions for an element. // RightClickElement executes right click actions for an element.
@ -421,16 +492,26 @@ func (p *Page) RightClickElement(act *Action, out ActionData) error {
// Screenshot executes screenshot action on a page // Screenshot executes screenshot action on a page
func (p *Page) Screenshot(act *Action, out ActionData) error { func (p *Page) Screenshot(act *Action, out ActionData) error {
to := p.getActionArgWithDefaultValues(act, "to") to, err := p.getActionArg(act, "to")
if err != nil {
return err
}
if to == "" { if to == "" {
to = ksuid.New().String() to = ksuid.New().String()
if act.Name != "" { if act.Name != "" {
out[act.Name] = to out[act.Name] = to
} }
} }
var data []byte var data []byte
var err error
if p.getActionArgWithDefaultValues(act, "fullpage") == "true" { fullpage, err := p.getActionArg(act, "fullpage")
if err != nil {
return err
}
if fullpage == "true" {
data, err = p.page.Screenshot(true, &proto.PageCaptureScreenshot{}) data, err = p.page.Screenshot(true, &proto.PageCaptureScreenshot{})
} else { } else {
data, err = p.page.Screenshot(false, &proto.PageCaptureScreenshot{}) data, err = p.page.Screenshot(false, &proto.PageCaptureScreenshot{})
@ -438,25 +519,32 @@ func (p *Page) Screenshot(act *Action, out ActionData) error {
if err != nil { if err != nil {
return errors.Wrap(err, "could not take screenshot") return errors.Wrap(err, "could not take screenshot")
} }
targetPath := p.getActionArgWithDefaultValues(act, "to")
targetPath, err = fileutil.CleanPath(targetPath) to, err = fileutil.CleanPath(to)
if err != nil { if err != nil {
return errorutil.New("could not clean output screenshot path %s", targetPath) return errorutil.New("could not clean output screenshot path %s", to)
} }
// allow if targetPath is child of current working directory // allow if targetPath is child of current working directory
if !protocolstate.IsLFAAllowed() { if !protocolstate.IsLFAAllowed() {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return errorutil.NewWithErr(err).Msgf("could not get current working directory") return errorutil.NewWithErr(err).Msgf("could not get current working directory")
} }
if !strings.HasPrefix(targetPath, cwd) {
if !strings.HasPrefix(to, cwd) {
// writing outside of cwd requires -lfa flag // writing outside of cwd requires -lfa flag
return ErrLFAccessDenied return ErrLFAccessDenied
} }
} }
mkdir, err := p.getActionArg(act, "mkdir")
if err != nil {
return err
}
// edgecase create directory if mkdir=true and path contains directory // edgecase create directory if mkdir=true and path contains directory
if p.getActionArgWithDefaultValues(act, "mkdir") == "true" && stringsutil.ContainsAny(to, folderutil.UnixPathSeparator, folderutil.WindowsPathSeparator) { if mkdir == "true" && stringsutil.ContainsAny(to, folderutil.UnixPathSeparator, folderutil.WindowsPathSeparator) {
// creates new directory if needed based on path `to` // creates new directory if needed based on path `to`
// TODO: replace all permission bits with fileutil constants (https://github.com/projectdiscovery/utils/issues/113) // TODO: replace all permission bits with fileutil constants (https://github.com/projectdiscovery/utils/issues/113)
if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil { if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil {
@ -465,7 +553,7 @@ func (p *Page) Screenshot(act *Action, out ActionData) error {
} }
// actual file path to write // actual file path to write
filePath := targetPath filePath := to
if !strings.HasSuffix(filePath, ".png") { if !strings.HasSuffix(filePath, ".png") {
filePath += ".png" filePath += ".png"
} }
@ -484,7 +572,10 @@ func (p *Page) Screenshot(act *Action, out ActionData) error {
// InputElement executes input element actions for an element. // InputElement executes input element actions for an element.
func (p *Page) InputElement(act *Action, out ActionData) error { func (p *Page) InputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value") value, err := p.getActionArg(act, "value")
if err != nil {
return err
}
if value == "" { if value == "" {
return errinvalidArguments return errinvalidArguments
} }
@ -503,7 +594,10 @@ func (p *Page) InputElement(act *Action, out ActionData) error {
// TimeInputElement executes time input on an element // TimeInputElement executes time input on an element
func (p *Page) TimeInputElement(act *Action, out ActionData) error { func (p *Page) TimeInputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value") value, err := p.getActionArg(act, "value")
if err != nil {
return err
}
if value == "" { if value == "" {
return errinvalidArguments return errinvalidArguments
} }
@ -526,7 +620,10 @@ func (p *Page) TimeInputElement(act *Action, out ActionData) error {
// SelectInputElement executes select input statement action on a element // SelectInputElement executes select input statement action on a element
func (p *Page) SelectInputElement(act *Action, out ActionData) error { func (p *Page) SelectInputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value") value, err := p.getActionArg(act, "value")
if err != nil {
return err
}
if value == "" { if value == "" {
return errinvalidArguments return errinvalidArguments
} }
@ -538,14 +635,26 @@ func (p *Page) SelectInputElement(act *Action, out ActionData) error {
return errors.Wrap(err, errCouldNotScroll) return errors.Wrap(err, errCouldNotScroll)
} }
selectedBool := false var selectedBool bool
if p.getActionArgWithDefaultValues(act, "selected") == "true" {
selected, err := p.getActionArg(act, "selected")
if err != nil {
return err
}
if selected == "true" {
selectedBool = true selectedBool = true
} }
by := p.getActionArgWithDefaultValues(act, "selector")
if err := element.Select([]string{value}, selectedBool, selectorBy(by)); err != nil { selector, err := p.getActionArg(act, "selector")
if err != nil {
return err
}
if err := element.Select([]string{value}, selectedBool, selectorBy(selector)); err != nil {
return errors.Wrap(err, "could not select input") return errors.Wrap(err, "could not select input")
} }
return nil return nil
} }
@ -603,14 +712,21 @@ func (p *Page) FilesInput(act *Action, out ActionData) error {
if err != nil { if err != nil {
return errors.Wrap(err, errCouldNotGetElement) return errors.Wrap(err, errCouldNotGetElement)
} }
if err = element.ScrollIntoView(); err != nil { if err = element.ScrollIntoView(); err != nil {
return errors.Wrap(err, errCouldNotScroll) return errors.Wrap(err, errCouldNotScroll)
} }
value := p.getActionArgWithDefaultValues(act, "value")
value, err := p.getActionArg(act, "value")
if err != nil {
return err
}
filesPaths := strings.Split(value, ",") filesPaths := strings.Split(value, ",")
if err := element.SetFiles(filesPaths); err != nil { if err := element.SetFiles(filesPaths); err != nil {
return errors.Wrap(err, "could not set files") return errors.Wrap(err, "could not set files")
} }
return nil return nil
} }
@ -620,19 +736,32 @@ func (p *Page) ExtractElement(act *Action, out ActionData) error {
if err != nil { if err != nil {
return errors.Wrap(err, errCouldNotGetElement) return errors.Wrap(err, errCouldNotGetElement)
} }
if err = element.ScrollIntoView(); err != nil { if err = element.ScrollIntoView(); err != nil {
return errors.Wrap(err, errCouldNotScroll) return errors.Wrap(err, errCouldNotScroll)
} }
switch p.getActionArgWithDefaultValues(act, "target") {
target, err := p.getActionArg(act, "target")
if err != nil {
return err
}
switch target {
case "attribute": case "attribute":
attrName := p.getActionArgWithDefaultValues(act, "attribute") attribute, err := p.getActionArg(act, "attribute")
if attrName == "" { if err != nil {
return err
}
if attribute == "" {
return errors.New("attribute can't be empty") return errors.New("attribute can't be empty")
} }
attrValue, err := element.Attribute(attrName)
attrValue, err := element.Attribute(attribute)
if err != nil { if err != nil {
return errors.Wrap(err, "could not get attribute") return errors.Wrap(err, "could not get attribute")
} }
if act.Name != "" { if act.Name != "" {
out[act.Name] = *attrValue out[act.Name] = *attrValue
} }
@ -641,6 +770,7 @@ func (p *Page) ExtractElement(act *Action, out ActionData) error {
if err != nil { if err != nil {
return errors.Wrap(err, "could not get element text node") return errors.Wrap(err, "could not get element text node")
} }
if act.Name != "" { if act.Name != "" {
out[act.Name] = text out[act.Name] = text
} }
@ -650,30 +780,33 @@ func (p *Page) ExtractElement(act *Action, out ActionData) error {
// WaitEvent waits for an event to happen on the page. // WaitEvent waits for an event to happen on the page.
func (p *Page) WaitEvent(act *Action, out ActionData) (func() error, error) { func (p *Page) WaitEvent(act *Action, out ActionData) (func() error, error) {
event := p.getActionArgWithDefaultValues(act, "event") event, err := p.getActionArg(act, "event")
if err != nil {
return nil, err
}
if event == "" { if event == "" {
return nil, errors.New("event not recognized") return nil, errors.New("event not recognized")
} }
var waitEvent proto.Event var waitEvent proto.Event
gotType := proto.GetType(event) gotType := proto.GetType(event)
if gotType == nil { if gotType == nil {
return nil, errorutil.New("event %v does not exist", event) return nil, errorutil.New("event %q does not exist", event)
} }
tmp, ok := reflect.New(gotType).Interface().(proto.Event) tmp, ok := reflect.New(gotType).Interface().(proto.Event)
if !ok { if !ok {
return nil, errorutil.New("event %v is not a page event", event) return nil, errorutil.New("event %q is not a page event", event)
} }
waitEvent = tmp waitEvent = tmp
maxDuration := 10 * time.Second // 10 sec is max wait duration for any event
// allow user to specify max-duration for wait-event // allow user to specify max-duration for wait-event
if value := p.getActionArgWithDefaultValues(act, "max-duration"); value != "" { maxDuration, err := getTimeParameter(p, act, "max-duration", 5, time.Second)
var err error if err != nil {
maxDuration, err = time.ParseDuration(value) return nil, err
if err != nil {
return nil, errorutil.NewWithErr(err).Msgf("could not parse max-duration")
}
} }
// Just wait the event to happen // Just wait the event to happen
@ -681,23 +814,20 @@ func (p *Page) WaitEvent(act *Action, out ActionData) (func() error, error) {
// execute actual wait event // execute actual wait event
ctx, cancel := context.WithTimeoutCause(context.Background(), maxDuration, ErrActionExecDealine) ctx, cancel := context.WithTimeoutCause(context.Background(), maxDuration, ErrActionExecDealine)
defer cancel() defer cancel()
err = contextutil.ExecFunc(ctx, p.page.WaitEvent(waitEvent)) err = contextutil.ExecFunc(ctx, p.page.WaitEvent(waitEvent))
return return
} }
return waitFunc, nil return waitFunc, nil
} }
// HandleDialog handles JavaScript dialog (alert, confirm, prompt, or onbeforeunload). // HandleDialog handles JavaScript dialog (alert, confirm, prompt, or onbeforeunload).
func (p *Page) HandleDialog(act *Action, out ActionData) error { func (p *Page) HandleDialog(act *Action, out ActionData) error {
maxDuration := 10 * time.Second maxDuration, err := getTimeParameter(p, act, "max-duration", 10, time.Second)
if err != nil {
if dur := p.getActionArgWithDefaultValues(act, "max-duration"); dur != "" { return err
var err error
maxDuration, err = time.ParseDuration(dur)
if err != nil {
return errorutil.NewWithErr(err).Msgf("could not parse max-duration")
}
} }
ctx, cancel := context.WithTimeout(context.Background(), maxDuration) ctx, cancel := context.WithTimeout(context.Background(), maxDuration)
@ -766,15 +896,13 @@ func (p *Page) DebugAction(act *Action, out ActionData) error {
// SleepAction sleeps on the page for a specified duration // SleepAction sleeps on the page for a specified duration
func (p *Page) SleepAction(act *Action, out ActionData) error { func (p *Page) SleepAction(act *Action, out ActionData) error {
seconds := act.Data["duration"] duration, err := getTimeParameter(p, act, "duration", 5, time.Second)
if seconds == "" {
seconds = "5"
}
parsed, err := strconv.Atoi(seconds)
if err != nil { if err != nil {
return err return err
} }
time.Sleep(time.Duration(parsed) * time.Second)
time.Sleep(duration)
return nil return nil
} }
@ -792,20 +920,28 @@ func selectorBy(selector string) rod.SelectorType {
} }
} }
func (p *Page) getActionArgWithDefaultValues(action *Action, arg string) string { func (p *Page) getActionArg(action *Action, arg string) (string, error) {
return p.getActionArgWithValues(action, arg, generators.MergeMaps( var err error
generators.BuildPayloadFromOptions(p.instance.browser.options),
p.payloads,
))
}
func (p *Page) getActionArgWithValues(action *Action, arg string, values map[string]interface{}) string {
argValue := action.GetArg(arg) argValue := action.GetArg(arg)
argValue = replaceWithValues(argValue, values)
if p.instance.interactsh != nil { if p.instance.interactsh != nil {
var interactshURLs []string var interactshURLs []string
argValue, interactshURLs = p.instance.interactsh.Replace(argValue, p.InteractshURLs) argValue, interactshURLs = p.instance.interactsh.Replace(argValue, p.InteractshURLs)
p.addInteractshURL(interactshURLs...) p.addInteractshURL(interactshURLs...)
} }
return argValue
exprs := getExpressions(argValue, p.variables)
err = expressions.ContainsUnresolvedVariables(exprs...)
if err != nil {
return "", errorutil.NewWithErr(err).Msgf("argument %q, value: %q", arg, argValue)
}
argValue, err = expressions.Evaluate(argValue, p.variables)
if err != nil {
return "", fmt.Errorf("could not get value for argument %q: %s", arg, err)
}
return argValue, nil
} }

View File

@ -42,7 +42,7 @@ func (p *Page) routingRuleHandler(httpClient *http.Client) func(ctx *rod.Hijack)
// each http request is performed via the native go http client // each http request is performed via the native go http client
// we first inject the shared cookies // we first inject the shared cookies
if !p.options.DisableCookie { if !p.options.DisableCookie {
if cookies := p.input.CookieJar.Cookies(ctx.Request.URL()); len(cookies) > 0 { if cookies := p.ctx.CookieJar.Cookies(ctx.Request.URL()); len(cookies) > 0 {
httpClient.Jar.SetCookies(ctx.Request.URL(), cookies) httpClient.Jar.SetCookies(ctx.Request.URL(), cookies)
} }
} }
@ -54,7 +54,7 @@ func (p *Page) routingRuleHandler(httpClient *http.Client) func(ctx *rod.Hijack)
// retrieve the updated cookies from the native http client and inject them into the shared cookie jar // retrieve the updated cookies from the native http client and inject them into the shared cookie jar
// keeps existing one if not present // keeps existing one if not present
if cookies := httpClient.Jar.Cookies(ctx.Request.URL()); len(cookies) > 0 { if cookies := httpClient.Jar.Cookies(ctx.Request.URL()); len(cookies) > 0 {
p.input.CookieJar.SetCookies(ctx.Request.URL(), cookies) p.ctx.CookieJar.SetCookies(ctx.Request.URL(), cookies)
} }
} }

View File

@ -1,10 +1,19 @@
package engine package engine
import ( import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/marker" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/marker"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
// replaceWithValues replaces the template markers with the values
//
// Deprecated: Not used anymore.
// nolint: unused
func replaceWithValues(data string, values map[string]interface{}) string { func replaceWithValues(data string, values map[string]interface{}) string {
return fasttemplate.ExecuteStringStd(data, marker.ParenthesisOpen, marker.ParenthesisClose, values) return fasttemplate.ExecuteStringStd(data, marker.ParenthesisOpen, marker.ParenthesisClose, values)
} }
func getExpressions(data string, values map[string]interface{}) []string {
return expressions.FindExpressions(data, marker.ParenthesisOpen, marker.ParenthesisClose, values)
}

View File

@ -20,7 +20,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils" protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types" templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
@ -120,10 +119,6 @@ func (request *Request) executeRequestWithPayloads(input *contextargs.Context, p
} }
defer instance.Close() defer instance.Close()
if vardump.EnableVarDump {
gologger.Debug().Msgf("Headless Protocol request variables: %s\n", vardump.DumpVariables(payloads))
}
instance.SetInteractsh(request.options.Interactsh) instance.SetInteractsh(request.options.Interactsh)
if _, err := url.Parse(input.MetaInput.Input); err != nil { if _, err := url.Parse(input.MetaInput.Input); err != nil {