2021-02-21 16:31:34 +05:30
|
|
|
package engine
|
|
|
|
|
|
|
|
|
|
import (
|
2023-06-26 19:25:51 +02:00
|
|
|
"bufio"
|
2023-06-09 12:33:03 +03:00
|
|
|
"fmt"
|
2023-06-26 19:25:51 +02:00
|
|
|
"net/http"
|
2021-02-21 16:31:34 +05:30
|
|
|
"net/url"
|
2021-12-29 09:48:46 +01:00
|
|
|
"strings"
|
2021-12-30 12:59:42 +01:00
|
|
|
"sync"
|
2021-03-01 14:20:56 +05:30
|
|
|
"time"
|
2021-02-21 16:31:34 +05:30
|
|
|
|
|
|
|
|
"github.com/go-rod/rod"
|
|
|
|
|
"github.com/go-rod/rod/lib/proto"
|
2025-02-10 21:46:35 +07:00
|
|
|
"github.com/projectdiscovery/gologger"
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
2025-02-10 21:46:35 +07:00
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
|
2025-02-10 21:46:35 +07:00
|
|
|
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
2025-02-10 21:46:35 +07:00
|
|
|
errorutil "github.com/projectdiscovery/utils/errors"
|
|
|
|
|
urlutil "github.com/projectdiscovery/utils/url"
|
2021-02-21 16:31:34 +05:30
|
|
|
)
|
|
|
|
|
|
2021-10-18 09:32:38 +02:00
|
|
|
// Page is a single page in an isolated browser instance
|
2021-02-21 16:31:34 +05:30
|
|
|
type Page struct {
|
2025-06-24 07:02:18 +07:00
|
|
|
ctx *contextargs.Context
|
|
|
|
|
inputURL *urlutil.URL
|
|
|
|
|
options *Options
|
|
|
|
|
page *rod.Page
|
|
|
|
|
rules []rule
|
|
|
|
|
instance *Instance
|
|
|
|
|
hijackRouter *rod.HijackRouter
|
|
|
|
|
hijackNative *Hijack
|
|
|
|
|
mutex *sync.RWMutex
|
|
|
|
|
History []HistoryData
|
|
|
|
|
InteractshURLs []string
|
|
|
|
|
payloads map[string]interface{}
|
|
|
|
|
variables map[string]interface{}
|
|
|
|
|
lastActionNavigate *Action
|
2021-12-29 09:48:46 +01:00
|
|
|
}
|
|
|
|
|
|
2021-12-29 09:51:50 +01:00
|
|
|
// HistoryData contains the page request/response pairs
|
2021-12-29 09:48:46 +01:00
|
|
|
type HistoryData struct {
|
|
|
|
|
RawRequest string
|
|
|
|
|
RawResponse string
|
2021-02-21 16:31:34 +05:30
|
|
|
}
|
|
|
|
|
|
2023-06-26 19:25:51 +02:00
|
|
|
// Options contains additional configuration options for the browser instance
|
|
|
|
|
type Options struct {
|
2023-11-18 10:32:10 +03:00
|
|
|
Timeout time.Duration
|
|
|
|
|
DisableCookie bool
|
|
|
|
|
Options *types.Options
|
2023-06-26 19:25:51 +02:00
|
|
|
}
|
|
|
|
|
|
2021-02-21 16:31:34 +05:30
|
|
|
// Run runs a list of actions by creating a new page in the browser.
|
2025-02-10 21:46:35 +07:00
|
|
|
func (i *Instance) Run(ctx *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (ActionData, *Page, error) {
|
2021-02-21 16:31:34 +05:30
|
|
|
page, err := i.engine.Page(proto.TargetCreateTarget{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
2023-06-26 19:25:51 +02:00
|
|
|
page = page.Timeout(options.Timeout)
|
2021-03-01 14:20:56 +05:30
|
|
|
|
2021-02-21 16:31:34 +05:30
|
|
|
if i.browser.customAgent != "" {
|
2021-02-26 13:13:11 +05:30
|
|
|
if userAgentErr := page.SetUserAgent(&proto.NetworkSetUserAgentOverride{UserAgent: i.browser.customAgent}); userAgentErr != nil {
|
|
|
|
|
return nil, nil, userAgentErr
|
2021-02-21 16:31:34 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 21:46:35 +07:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-26 19:25:51 +02:00
|
|
|
createdPage := &Page{
|
2025-02-10 21:46:35 +07:00
|
|
|
options: options,
|
|
|
|
|
page: page,
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
instance: i,
|
|
|
|
|
mutex: &sync.RWMutex{},
|
|
|
|
|
payloads: payloads,
|
|
|
|
|
variables: variables,
|
|
|
|
|
inputURL: input,
|
2023-06-26 19:25:51 +02:00
|
|
|
}
|
2023-01-05 12:56:18 +01:00
|
|
|
|
2024-12-17 11:08:42 +01:00
|
|
|
httpclient, err := i.browser.getHTTPClient()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-05 12:56:18 +01:00
|
|
|
// in case the page has request/response modification rules - enable global hijacking
|
|
|
|
|
if createdPage.hasModificationRules() || containsModificationActions(actions...) {
|
|
|
|
|
hijackRouter := page.HijackRequests()
|
2024-12-17 11:08:42 +01:00
|
|
|
if err := hijackRouter.Add("*", "", createdPage.routingRuleHandler(httpclient)); err != nil {
|
2023-01-05 12:56:18 +01:00
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
createdPage.hijackRouter = hijackRouter
|
|
|
|
|
go hijackRouter.Run()
|
|
|
|
|
} else {
|
|
|
|
|
hijackRouter := NewHijack(page)
|
|
|
|
|
hijackRouter.SetPattern(&proto.FetchRequestPattern{
|
|
|
|
|
URLPattern: "*",
|
|
|
|
|
RequestStage: proto.FetchRequestStageResponse,
|
|
|
|
|
})
|
|
|
|
|
createdPage.hijackNative = hijackRouter
|
|
|
|
|
hijackRouterHandler := hijackRouter.Start(createdPage.routingRuleHandlerNative)
|
|
|
|
|
go func() {
|
|
|
|
|
_ = hijackRouterHandler()
|
|
|
|
|
}()
|
2021-02-21 16:31:34 +05:30
|
|
|
}
|
|
|
|
|
|
2021-08-31 12:55:52 +03:00
|
|
|
if err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{Viewport: &proto.PageViewport{
|
2021-02-21 16:31:34 +05:30
|
|
|
Scale: 1,
|
|
|
|
|
Width: float64(1920),
|
|
|
|
|
Height: float64(1080),
|
2021-08-31 12:55:52 +03:00
|
|
|
}}); err != nil {
|
2021-02-21 16:31:34 +05:30
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
2021-08-31 12:55:52 +03:00
|
|
|
|
|
|
|
|
if _, err := page.SetExtraHeaders([]string{"Accept-Language", "en, en-GB, en-us;"}); err != nil {
|
2021-02-21 16:31:34 +05:30
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
2021-08-31 12:55:52 +03:00
|
|
|
|
2023-06-26 19:25:51 +02:00
|
|
|
// inject cookies
|
|
|
|
|
// each http request is performed via the native go http client
|
|
|
|
|
// we first inject the shared cookies
|
2025-02-10 21:46:35 +07:00
|
|
|
URL, err := url.Parse(ctx.MetaInput.Input)
|
2023-06-26 19:25:51 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
2023-06-09 12:33:03 +03:00
|
|
|
|
2023-11-18 10:32:10 +03:00
|
|
|
if !options.DisableCookie {
|
2025-02-10 21:46:35 +07:00
|
|
|
if cookies := ctx.CookieJar.Cookies(URL); len(cookies) > 0 {
|
2023-06-26 19:25:51 +02:00
|
|
|
var NetworkCookies []*proto.NetworkCookie
|
|
|
|
|
for _, cookie := range cookies {
|
|
|
|
|
networkCookie := &proto.NetworkCookie{
|
|
|
|
|
Name: cookie.Name,
|
|
|
|
|
Value: cookie.Value,
|
|
|
|
|
Domain: cookie.Domain,
|
|
|
|
|
Path: cookie.Path,
|
|
|
|
|
HTTPOnly: cookie.HttpOnly,
|
|
|
|
|
Secure: cookie.Secure,
|
|
|
|
|
Expires: proto.TimeSinceEpoch(cookie.Expires.Unix()),
|
|
|
|
|
SameSite: proto.NetworkCookieSameSite(GetSameSite(cookie)),
|
|
|
|
|
Priority: proto.NetworkCookiePriorityLow,
|
|
|
|
|
}
|
|
|
|
|
NetworkCookies = append(NetworkCookies, networkCookie)
|
|
|
|
|
}
|
|
|
|
|
params := proto.CookiesToParams(NetworkCookies)
|
|
|
|
|
for _, param := range params {
|
2025-02-10 21:46:35 +07:00
|
|
|
param.URL = ctx.MetaInput.Input
|
2023-06-26 19:25:51 +02:00
|
|
|
}
|
|
|
|
|
err := page.SetCookies(params)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 21:46:35 +07:00
|
|
|
data, err := createdPage.ExecuteActions(ctx, actions)
|
2021-02-21 16:31:34 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
2023-06-09 12:33:03 +03:00
|
|
|
|
2023-11-18 10:32:10 +03:00
|
|
|
if !options.DisableCookie {
|
2023-06-26 19:25:51 +02:00
|
|
|
// at the end of actions pull out updated cookies from the browser and inject them into the shared cookie jar
|
2023-11-18 10:32:10 +03:00
|
|
|
if cookies, err := page.Cookies([]string{URL.String()}); !options.DisableCookie && err == nil && len(cookies) > 0 {
|
2023-06-26 19:25:51 +02:00
|
|
|
var httpCookies []*http.Cookie
|
|
|
|
|
for _, cookie := range cookies {
|
|
|
|
|
httpCookie := &http.Cookie{
|
|
|
|
|
Name: cookie.Name,
|
|
|
|
|
Value: cookie.Value,
|
|
|
|
|
Domain: cookie.Domain,
|
|
|
|
|
Path: cookie.Path,
|
|
|
|
|
HttpOnly: cookie.HTTPOnly,
|
|
|
|
|
Secure: cookie.Secure,
|
|
|
|
|
}
|
|
|
|
|
httpCookies = append(httpCookies, httpCookie)
|
|
|
|
|
}
|
2025-02-10 21:46:35 +07:00
|
|
|
ctx.CookieJar.SetCookies(URL, httpCookies)
|
2023-06-26 19:25:51 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The first item of history data will contain the very first request from the browser
|
|
|
|
|
// we assume it's the one matching the initial URL
|
|
|
|
|
if len(createdPage.History) > 0 {
|
|
|
|
|
firstItem := createdPage.History[0]
|
|
|
|
|
if resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(firstItem.RawResponse)), nil); err == nil {
|
|
|
|
|
data["header"] = utils.HeadersToString(resp.Header)
|
|
|
|
|
data["status_code"] = fmt.Sprint(resp.StatusCode)
|
2025-07-09 14:47:26 -05:00
|
|
|
defer func() {
|
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
|
}()
|
2023-06-26 19:25:51 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-06-09 12:33:03 +03:00
|
|
|
|
2021-02-21 16:31:34 +05:30
|
|
|
return data, createdPage, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close closes a browser page
|
|
|
|
|
func (p *Page) Close() {
|
2023-01-05 12:56:18 +01:00
|
|
|
if p.hijackRouter != nil {
|
|
|
|
|
_ = p.hijackRouter.Stop()
|
|
|
|
|
}
|
|
|
|
|
if p.hijackNative != nil {
|
|
|
|
|
_ = p.hijackNative.Stop()
|
|
|
|
|
}
|
2025-07-01 00:40:44 +07:00
|
|
|
_ = p.page.Close()
|
2021-02-21 16:31:34 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Page returns the current page for the actions
|
|
|
|
|
func (p *Page) Page() *rod.Page {
|
|
|
|
|
return p.page
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Browser returns the browser that created the current page
|
|
|
|
|
func (p *Page) Browser() *rod.Browser {
|
|
|
|
|
return p.instance.engine
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// URL returns the URL for the current page.
|
|
|
|
|
func (p *Page) URL() string {
|
|
|
|
|
info, err := p.page.Info()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return info.URL
|
|
|
|
|
}
|
2021-12-29 09:48:46 +01:00
|
|
|
|
2021-12-29 09:51:50 +01:00
|
|
|
// DumpHistory returns the full page navigation history
|
2021-12-29 09:48:46 +01:00
|
|
|
func (p *Page) DumpHistory() string {
|
2022-01-31 08:52:36 +01:00
|
|
|
p.mutex.RLock()
|
|
|
|
|
defer p.mutex.RUnlock()
|
2021-12-30 12:59:42 +01:00
|
|
|
|
2021-12-29 09:48:46 +01:00
|
|
|
var historyDump strings.Builder
|
|
|
|
|
for _, historyData := range p.History {
|
|
|
|
|
historyDump.WriteString(historyData.RawRequest)
|
|
|
|
|
historyDump.WriteString(historyData.RawResponse)
|
|
|
|
|
}
|
|
|
|
|
return historyDump.String()
|
|
|
|
|
}
|
2021-12-30 12:59:42 +01:00
|
|
|
|
|
|
|
|
// addToHistory adds a request/response pair to the page history
|
2022-01-31 08:52:36 +01:00
|
|
|
func (p *Page) addToHistory(historyData ...HistoryData) {
|
|
|
|
|
p.mutex.Lock()
|
|
|
|
|
defer p.mutex.Unlock()
|
2021-12-30 12:59:42 +01:00
|
|
|
|
2022-01-31 08:52:36 +01:00
|
|
|
p.History = append(p.History, historyData...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Page) addInteractshURL(URLs ...string) {
|
|
|
|
|
p.mutex.Lock()
|
|
|
|
|
defer p.mutex.Unlock()
|
|
|
|
|
|
|
|
|
|
p.InteractshURLs = append(p.InteractshURLs, URLs...)
|
2021-12-30 12:59:42 +01:00
|
|
|
}
|
2023-01-05 12:56:18 +01:00
|
|
|
|
|
|
|
|
func (p *Page) hasModificationRules() bool {
|
|
|
|
|
for _, rule := range p.rules {
|
|
|
|
|
if containsAnyModificationActionType(rule.Action) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-24 07:02:18 +07:00
|
|
|
// updateLastNavigatedURL updates the last navigated URL in the instance's
|
|
|
|
|
// request log.
|
|
|
|
|
func (p *Page) updateLastNavigatedURL() {
|
|
|
|
|
if p.lastActionNavigate == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
templateURL := p.lastActionNavigate.GetArg("url")
|
|
|
|
|
p.instance.requestLog[templateURL] = p.URL()
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-05 12:56:18 +01:00
|
|
|
func containsModificationActions(actions ...*Action) bool {
|
|
|
|
|
for _, action := range actions {
|
|
|
|
|
if containsAnyModificationActionType(action.ActionType.ActionType) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containsAnyModificationActionType(actionTypes ...ActionType) bool {
|
|
|
|
|
for _, actionType := range actionTypes {
|
|
|
|
|
switch actionType {
|
|
|
|
|
case ActionSetMethod:
|
|
|
|
|
return true
|
|
|
|
|
case ActionAddHeader:
|
|
|
|
|
return true
|
|
|
|
|
case ActionSetHeader:
|
|
|
|
|
return true
|
|
|
|
|
case ActionDeleteHeader:
|
|
|
|
|
return true
|
|
|
|
|
case ActionSetBody:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2023-06-09 12:33:03 +03:00
|
|
|
|
2023-06-26 19:25:51 +02:00
|
|
|
func GetSameSite(cookie *http.Cookie) string {
|
|
|
|
|
switch cookie.SameSite {
|
|
|
|
|
case http.SameSiteNoneMode:
|
|
|
|
|
return "none"
|
|
|
|
|
case http.SameSiteLaxMode:
|
|
|
|
|
return "lax"
|
|
|
|
|
case http.SameSiteStrictMode:
|
|
|
|
|
return "strict"
|
|
|
|
|
case http.SameSiteDefaultMode:
|
|
|
|
|
fallthrough
|
|
|
|
|
default:
|
|
|
|
|
return ""
|
2023-06-09 12:33:03 +03:00
|
|
|
}
|
|
|
|
|
}
|