diff --git a/v2/cmd/nuclei/main.go b/v2/cmd/nuclei/main.go index 2b0c0bbf..c76e722d 100644 --- a/v2/cmd/nuclei/main.go +++ b/v2/cmd/nuclei/main.go @@ -84,6 +84,7 @@ based on templates offering massive extensibility and ease of use.`) set.BoolVarP(&options.Workflows, "workflows", "w", false, "Only run workflow templates with nuclei") set.IntVarP(&options.StatsInterval, "stats-interval", "si", 5, "Number of seconds between each stats line") set.BoolVar(&options.SystemResolvers, "system-resolvers", false, "Use system dns resolving as error fallback") + set.IntVar(&options.PageTimeout, "page-timeout", 15, "Seconds to wait for each page in headless") _ = set.Parse() if cfgFile != "" { diff --git a/v2/pkg/protocols/headless/engine/action.go b/v2/pkg/protocols/headless/engine/action.go index 6d78f3a1..3c5296ad 100644 --- a/v2/pkg/protocols/headless/engine/action.go +++ b/v2/pkg/protocols/headless/engine/action.go @@ -46,6 +46,10 @@ const ( ActionWaitEvent // ActionKeyboard performs a keyboard action event on a page. ActionKeyboard + // Action debug slows down headless and adds a sleep to each page. + ActionDebug + // ActionSleep executes a sleep for a specified duration + ActionSleep ) // ActionStringToAction converts an action from string to internal representation @@ -69,6 +73,8 @@ var ActionStringToAction = map[string]ActionType{ "setbody": ActionSetBody, "waitevent": ActionWaitEvent, "keyboard": ActionKeyboard, + "debug": ActionDebug, + "sleep": ActionSleep, } // ActionToActionString converts an action from internal representation to string @@ -92,6 +98,8 @@ var ActionToActionString = map[ActionType]string{ ActionSetBody: "setbody", ActionWaitEvent: "waitevent", ActionKeyboard: "keyboard", + ActionDebug: "debug", + ActionSleep: "sleep", } // Action is an action taken by the browser to reach a navigation diff --git a/v2/pkg/protocols/headless/engine/page.go b/v2/pkg/protocols/headless/engine/page.go index 25e49807..185eb33c 100644 --- a/v2/pkg/protocols/headless/engine/page.go +++ b/v2/pkg/protocols/headless/engine/page.go @@ -2,6 +2,7 @@ package engine import ( "net/url" + "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" @@ -16,11 +17,13 @@ type Page struct { } // Run runs a list of actions by creating a new page in the browser. -func (i *Instance) Run(baseURL *url.URL, actions []*Action) (map[string]string, *Page, error) { +func (i *Instance) Run(baseURL *url.URL, actions []*Action, timeout time.Duration) (map[string]string, *Page, error) { page, err := i.engine.Page(proto.TargetCreateTarget{}) if err != nil { return nil, nil, err } + page = page.Timeout(timeout) + if i.browser.customAgent != "" { if userAgentErr := page.SetUserAgent(&proto.NetworkSetUserAgentOverride{UserAgent: i.browser.customAgent}); userAgentErr != nil { return nil, nil, userAgentErr diff --git a/v2/pkg/protocols/headless/engine/page_actions.go b/v2/pkg/protocols/headless/engine/page_actions.go index 215cdce8..92752399 100644 --- a/v2/pkg/protocols/headless/engine/page_actions.go +++ b/v2/pkg/protocols/headless/engine/page_actions.go @@ -63,6 +63,10 @@ func (p *Page) ExecuteActions(baseURL *url.URL, actions []*Action) (map[string]s err = p.ActionSetMethod(act, outData) case ActionKeyboard: err = p.KeyboardAction(act, outData) + case ActionDebug: + err = p.DebugAction(act, outData) + case ActionSleep: + err = p.SleepAction(act, outData) default: continue } @@ -467,6 +471,27 @@ func (p *Page) pageElementBy(data map[string]string) (*rod.Element, error) { } } +// DebugAction enables debug action on a page. +func (p *Page) DebugAction(act *Action, out map[string]string) error { + p.instance.browser.engine.SlowMotion(5 * time.Second) + p.instance.browser.engine.Trace(true) + return nil +} + +// SleepAction sleeps on the page for a specified duration +func (p *Page) SleepAction(act *Action, out map[string]string) error { + seconds := act.Data["duration"] + if seconds == "" { + seconds = "5" + } + parsed, err := strconv.Atoi(seconds) + if err != nil { + return err + } + time.Sleep(time.Duration(parsed) * time.Second) + return nil +} + // selectorBy returns a selector from a representation. func selectorBy(selector string) rod.SelectorType { switch selector { diff --git a/v2/pkg/protocols/headless/engine/page_actions_test.go b/v2/pkg/protocols/headless/engine/page_actions_test.go index e5993336..8a5b6e0b 100644 --- a/v2/pkg/protocols/headless/engine/page_actions_test.go +++ b/v2/pkg/protocols/headless/engine/page_actions_test.go @@ -7,6 +7,7 @@ import ( "net/url" "strings" "testing" + "time" "github.com/projectdiscovery/nuclei/v2/pkg/types" "github.com/stretchr/testify/require" @@ -37,7 +38,7 @@ func TestActionNavigate(t *testing.T) { require.Nil(t, err, "could not parse URL") actions := []*Action{{ActionType: "navigate", Data: map[string]string{"url": "{{BaseURL}}"}}, {ActionType: "waitload"}} - _, page, err := instance.Run(parsed, actions) + _, page, err := instance.Run(parsed, actions, 20*time.Second) require.Nil(t, err, "could not run page actions") defer page.Close() @@ -73,7 +74,7 @@ func TestActionScript(t *testing.T) { {ActionType: "waitload"}, {ActionType: "script", Name: "test", Data: map[string]string{"code": "window.test"}}, } - out, page, err := instance.Run(parsed, actions) + out, page, err := instance.Run(parsed, actions, 20*time.Second) require.Nil(t, err, "could not run page actions") defer page.Close() @@ -102,7 +103,7 @@ func TestActionScript(t *testing.T) { {ActionType: "waitload"}, {ActionType: "script", Name: "test", Data: map[string]string{"code": "window.test"}}, } - out, page, err := instance.Run(parsed, actions) + out, page, err := instance.Run(parsed, actions, 20*time.Second) require.Nil(t, err, "could not run page actions") defer page.Close() @@ -139,7 +140,7 @@ func TestActionClick(t *testing.T) { {ActionType: "waitload"}, {ActionType: "click", Data: map[string]string{"selector": "button"}}, // Use css selector for clicking } - _, page, err := instance.Run(parsed, actions) + _, page, err := instance.Run(parsed, actions, 20*time.Second) require.Nil(t, err, "could not run page actions") defer page.Close() @@ -185,7 +186,7 @@ func TestActionRightClick(t *testing.T) { {ActionType: "waitload"}, {ActionType: "rightclick", Data: map[string]string{"selector": "button"}}, // Use css selector for clicking } - _, page, err := instance.Run(parsed, actions) + _, page, err := instance.Run(parsed, actions, 20*time.Second) require.Nil(t, err, "could not run page actions") defer page.Close() @@ -223,7 +224,7 @@ func TestActionTextInput(t *testing.T) { {ActionType: "waitload"}, {ActionType: "text", Data: map[string]string{"selector": "input", "value": "test"}}, } - _, page, err := instance.Run(parsed, actions) + _, page, err := instance.Run(parsed, actions, 20*time.Second) require.Nil(t, err, "could not run page actions") defer page.Close() @@ -257,7 +258,7 @@ func TestActionHeadersChange(t *testing.T) { {ActionType: "navigate", Data: map[string]string{"url": "{{BaseURL}}"}}, {ActionType: "waitload"}, } - _, page, err := instance.Run(parsed, actions) + _, page, err := instance.Run(parsed, actions, 20*time.Second) require.Nil(t, err, "could not run page actions") defer page.Close() diff --git a/v2/pkg/protocols/headless/request.go b/v2/pkg/protocols/headless/request.go index 5a00962a..6fa38394 100644 --- a/v2/pkg/protocols/headless/request.go +++ b/v2/pkg/protocols/headless/request.go @@ -3,6 +3,7 @@ package headless import ( "net/url" "strings" + "time" "github.com/pkg/errors" "github.com/projectdiscovery/gologger" @@ -28,7 +29,7 @@ func (r *Request) ExecuteWithResults(input string, metadata, previous output.Int r.options.Progress.DecrementRequests(1) return errors.Wrap(err, "could get html element") } - out, page, err := instance.Run(parsed, r.Steps) + out, page, err := instance.Run(parsed, r.Steps, time.Duration(r.options.Options.PageTimeout)*time.Second) if err != nil { r.options.Output.Request(r.options.TemplateID, input, "headless", err) r.options.Progress.DecrementRequests(1) diff --git a/v2/pkg/types/types.go b/v2/pkg/types/types.go index 1fe962d6..9260f688 100644 --- a/v2/pkg/types/types.go +++ b/v2/pkg/types/types.go @@ -55,6 +55,8 @@ type Options struct { Retries int // Rate-Limit is the maximum number of requests per specified target RateLimit int + //`PageTimeout is the maximum time to wait for a page in seconds + PageTimeout int // OfflineHTTP is a flag that specific offline processing of http response // using same matchers/extractors from http protocol without the need // to send a new request, reading responses from a file.