diff --git a/cmd/nuclei/main.go b/cmd/nuclei/main.go index 5e95a8b1b2..d264f88e98 100644 --- a/cmd/nuclei/main.go +++ b/cmd/nuclei/main.go @@ -407,6 +407,7 @@ on extensive configurability, massive extensibility and ease of use.`) flagSet.BoolVarP(&options.ShowBrowser, "show-browser", "sb", false, "show the browser on the screen when running templates with headless mode"), flagSet.StringSliceVarP(&options.HeadlessOptionalArguments, "headless-options", "ho", nil, "start headless chrome with additional options", goflags.FileCommaSeparatedStringSliceOptions), flagSet.BoolVarP(&options.UseInstalledChrome, "system-chrome", "sc", false, "use local installed Chrome browser instead of nuclei installed"), + flagSet.StringVarP(&options.CDPEndpoint, "cdp-endpoint", "cdpe", "", "use remote browser via Chrome DevTools Protocol (CDP) endpoint"), flagSet.BoolVarP(&options.ShowActions, "list-headless-action", "lha", false, "list available headless actions"), ) diff --git a/pkg/protocols/headless/engine/engine.go b/pkg/protocols/headless/engine/engine.go index 04d6b2a655..4b70a1f012 100644 --- a/pkg/protocols/headless/engine/engine.go +++ b/pkg/protocols/headless/engine/engine.go @@ -29,61 +29,72 @@ type Browser struct { // New creates a new nuclei headless browser module func New(options *types.Options) (*Browser, error) { - dataStore, err := os.MkdirTemp("", "nuclei-*") - if err != nil { - return nil, errors.Wrap(err, "could not create temporary directory") - } - previousPIDs := processutil.FindProcesses(processutil.IsChromeProcess) - - chromeLauncher := launcher.New(). - Leakless(false). - Set("disable-gpu", "true"). - Set("ignore-certificate-errors", "true"). - Set("ignore-certificate-errors", "1"). - Set("disable-crash-reporter", "true"). - Set("disable-notifications", "true"). - Set("hide-scrollbars", "true"). - Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)). - Set("mute-audio", "true"). - Set("incognito", "true"). - Delete("use-mock-keychain"). - UserDataDir(dataStore) - - if MustDisableSandbox() { - chromeLauncher = chromeLauncher.NoSandbox(true) - } + var launcherURL, dataStore string + var previousPIDs map[int32]struct{} + var err error - executablePath, err := os.Executable() - if err != nil { - return nil, err - } + chromeLauncher := launcher.New() - // if musl is used, most likely we are on alpine linux which is not supported by go-rod, so we fallback to default chrome - useMusl, _ := fileutil.UseMusl(executablePath) - if options.UseInstalledChrome || useMusl { - if chromePath, hasChrome := launcher.LookPath(); hasChrome { - chromeLauncher.Bin(chromePath) - } else { - return nil, errors.New("the chrome browser is not installed") + if options.CDPEndpoint == "" { + previousPIDs = processutil.FindProcesses(processutil.IsChromeProcess) + + dataStore, err = os.MkdirTemp("", "nuclei-*") + if err != nil { + return nil, errors.Wrap(err, "could not create temporary directory") } - } - if options.ShowBrowser { - chromeLauncher = chromeLauncher.Headless(false) - } else { - chromeLauncher = chromeLauncher.Headless(true) - } - if types.ProxyURL != "" { - chromeLauncher = chromeLauncher.Proxy(types.ProxyURL) - } + chromeLauncher = chromeLauncher. + Leakless(false). + Set("disable-crash-reporter"). + Set("disable-gpu"). + Set("disable-notifications"). + Set("hide-scrollbars"). + Set("ignore-certificate-errors"). + Set("ignore-ssl-errors"). + Set("incognito"). + Set("mute-audio"). + Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)). + Delete("use-mock-keychain"). + UserDataDir(dataStore) + + if MustDisableSandbox() { + chromeLauncher = chromeLauncher.NoSandbox(true) + } - for k, v := range options.ParseHeadlessOptionalArguments() { - chromeLauncher.Set(flags.Flag(k), v) - } + executablePath, err := os.Executable() + if err != nil { + return nil, err + } - launcherURL, err := chromeLauncher.Launch() - if err != nil { - return nil, err + // if musl is used, most likely we are on alpine linux which is not supported by go-rod, so we fallback to default chrome + useMusl, _ := fileutil.UseMusl(executablePath) + if options.UseInstalledChrome || useMusl { + if chromePath, hasChrome := launcher.LookPath(); hasChrome { + chromeLauncher.Bin(chromePath) + } else { + return nil, errors.New("the chrome browser is not installed") + } + } + + if options.ShowBrowser { + chromeLauncher = chromeLauncher.Headless(false) + } else { + chromeLauncher = chromeLauncher.Headless(true) + } + if types.ProxyURL != "" { + chromeLauncher = chromeLauncher.Proxy(types.ProxyURL) + } + + for k, v := range options.ParseHeadlessOptionalArguments() { + chromeLauncher.Set(flags.Flag(k), v) + } + + launcherURL, err = chromeLauncher.Launch() + if err != nil { + return nil, err + } + } else { + launcherURL = options.CDPEndpoint } browser := rod.New().ControlURL(launcherURL) @@ -135,7 +146,13 @@ func (b *Browser) UserAgent() string { } // Close closes the browser engine +// +// When connected over CDP, it does NOT close the browsers. func (b *Browser) Close() { + if b.options.CDPEndpoint != "" { + return + } + b.engine.Close() os.RemoveAll(b.tempDir) processutil.CloseProcesses(processutil.IsChromeProcess, b.previousPIDs) diff --git a/pkg/types/types.go b/pkg/types/types.go index f6e7ab4470..632d51fe56 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -167,6 +167,8 @@ type Options struct { ForceAttemptHTTP2 bool // StatsJSON writes stats output in JSON format StatsJSON bool + // CDPEndpoint specifies the endpoint for Chrome DevTools Protocol (CDP) + CDPEndpoint string // Headless specifies whether to allow headless mode templates Headless bool // ShowBrowser specifies whether the show the browser in headless mode