nuclei/v2/internal/runner/runner.go

587 lines
17 KiB
Go
Raw Normal View History

package runner
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
2021-04-16 16:56:41 +05:30
"time"
2020-08-24 00:16:18 +05:30
"github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"github.com/remeh/sizedwaitgroup"
"github.com/rs/xid"
"go.uber.org/atomic"
"go.uber.org/ratelimit"
"gopkg.in/yaml.v2"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/hmap/store/hybrid"
"github.com/projectdiscovery/nuclei/v2/internal/colorizer"
2021-02-26 13:13:11 +05:30
"github.com/projectdiscovery/nuclei/v2/pkg/catalog"
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v2/pkg/output"
"github.com/projectdiscovery/nuclei/v2/pkg/parsers"
"github.com/projectdiscovery/nuclei/v2/pkg/progress"
2020-10-18 03:09:24 +02:00
"github.com/projectdiscovery/nuclei/v2/pkg/projectfile"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/clusterer"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/hosterrorscache"
2021-04-16 16:56:41 +05:30
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolinit"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/headless/engine"
"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/disk"
2021-06-05 18:01:08 +05:30
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/sarif"
2020-07-01 16:17:24 +05:30
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
"github.com/projectdiscovery/nuclei/v2/pkg/types"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
"github.com/projectdiscovery/nuclei/v2/pkg/utils/stats"
)
// Runner is a client for running the enumeration process.
type Runner struct {
hostMap *hybrid.HybridMap
output output.Writer
2021-04-16 16:56:41 +05:30
interactsh *interactsh.Client
inputCount int64
templatesConfig *config.Config
options *types.Options
projectFile *projectfile.ProjectFile
2021-02-26 13:13:11 +05:30
catalog *catalog.Catalog
progress progress.Progress
colorizer aurora.Aurora
issuesClient *reporting.Client
addColor func(severity.Severity) string
browser *engine.Browser
ratelimiter ratelimit.Limiter
hostErrors *hosterrorscache.Cache
}
// New creates a new client for running enumeration process.
func New(options *types.Options) (*Runner, error) {
runner := &Runner{
options: options,
}
if options.UpdateNuclei {
2021-07-25 03:13:46 +05:30
if err := updateNucleiVersionToLatest(runner.options.Verbose); err != nil {
return nil, err
}
return nil, nil
}
if options.Validate {
parsers.ShouldValidate = true
}
2020-06-25 03:53:37 +05:30
if err := runner.updateTemplates(); err != nil {
gologger.Warning().Msgf("Could not update templates: %s\n", err)
2020-06-25 03:53:37 +05:30
}
2021-07-25 03:13:46 +05:30
if options.Headless {
browser, err := engine.New(options)
if err != nil {
return nil, err
}
runner.browser = browser
}
2021-06-11 14:44:37 +05:30
runner.catalog = catalog.New(runner.options.TemplatesDirectory)
reportingOptions, err := createReportingOptions(options)
if err != nil {
return nil, err
2021-06-05 18:01:08 +05:30
}
if reportingOptions != nil {
2021-07-07 19:23:25 +05:30
client, err := reporting.New(reportingOptions, options.ReportingDB)
if err != nil {
return nil, errors.Wrap(err, "could not create issue reporting client")
}
2021-07-07 19:23:25 +05:30
runner.issuesClient = client
}
// output coloring
useColor := !options.NoColor
runner.colorizer = aurora.NewAurora(useColor)
runner.addColor = colorizer.New(runner.colorizer)
if options.TemplateList {
runner.listAvailableTemplates()
os.Exit(0)
}
if (len(options.Templates) == 0 || !options.NewTemplates || (options.TargetsFilePath == "" && !options.Stdin && len(options.Targets) == 0)) && options.UpdateTemplates {
2020-06-25 03:53:37 +05:30
os.Exit(0)
}
2021-07-07 19:23:25 +05:30
hm, err := hybrid.New(hybrid.DefaultDiskOptions)
if err != nil {
return nil, errors.Wrap(err, "could not create temporary input file")
}
2021-07-07 19:23:25 +05:30
runner.hostMap = hm
runner.inputCount = 0
dupeCount := 0
// Handle multiple targets
if len(options.Targets) != 0 {
for _, target := range options.Targets {
url := strings.TrimSpace(target)
if url == "" {
continue
}
if _, ok := runner.hostMap.Get(url); ok {
dupeCount++
continue
}
runner.inputCount++
// nolint:errcheck // ignoring error
runner.hostMap.Set(url, nil)
}
}
// Handle stdin
if options.Stdin {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
url := strings.TrimSpace(scanner.Text())
if url == "" {
continue
}
if _, ok := runner.hostMap.Get(url); ok {
dupeCount++
continue
}
runner.inputCount++
2020-11-20 11:12:06 +01:00
// nolint:errcheck // ignoring error
runner.hostMap.Set(url, nil)
}
2020-07-23 20:19:19 +02:00
}
2021-08-04 10:28:56 -07:00
// Handle target file
if options.TargetsFilePath != "" {
input, inputErr := os.Open(options.TargetsFilePath)
2021-07-07 19:23:25 +05:30
if inputErr != nil {
return nil, errors.Wrap(inputErr, "could not open targets file")
}
scanner := bufio.NewScanner(input)
for scanner.Scan() {
url := strings.TrimSpace(scanner.Text())
if url == "" {
continue
}
if _, ok := runner.hostMap.Get(url); ok {
dupeCount++
continue
}
runner.inputCount++
2020-11-20 11:12:06 +01:00
// nolint:errcheck // ignoring error
runner.hostMap.Set(url, nil)
}
input.Close()
}
if dupeCount > 0 {
gologger.Info().Msgf("Supplied input was automatically deduplicated (%d removed).", dupeCount)
2020-07-23 20:19:19 +02:00
}
2020-04-04 18:21:05 +05:30
// Create the output file if asked
outputWriter, err := output.NewStandardWriter(!options.NoColor, options.NoMeta, options.NoTimestamp, options.JSON, options.JSONRequests, options.Output, options.TraceLogFile)
2020-12-29 18:15:27 +05:30
if err != nil {
return nil, errors.Wrap(err, "could not create output file")
2020-04-04 18:21:05 +05:30
}
2021-02-26 13:13:11 +05:30
runner.output = outputWriter
2020-07-23 20:19:19 +02:00
if options.JSON && options.EnableProgressBar {
options.StatsJSON = true
}
if options.StatsJSON {
options.EnableProgressBar = true
}
// Creates the progress tracking object
var progressErr error
runner.progress, progressErr = progress.NewStatsTicker(options.StatsInterval, options.EnableProgressBar, options.StatsJSON, options.Metrics, options.MetricsPort)
if progressErr != nil {
return nil, progressErr
}
2020-07-23 20:19:19 +02:00
2020-10-17 02:10:47 +02:00
// create project file if requested or load existing one
if options.Project {
2020-10-30 13:06:05 +01:00
var projectFileErr error
runner.projectFile, projectFileErr = projectfile.New(&projectfile.Options{Path: options.ProjectPath, Cleanup: utils.IsBlank(options.ProjectPath)})
2020-10-30 13:06:05 +01:00
if projectFileErr != nil {
return nil, projectFileErr
2020-10-15 23:39:00 +02:00
}
}
2021-04-18 17:29:01 +05:30
if !options.NoInteractsh {
2021-04-16 16:56:41 +05:30
interactshClient, err := interactsh.New(&interactsh.Options{
ServerURL: options.InteractshURL,
2021-09-06 19:10:32 +05:30
Authorization: options.InteractshToken,
2021-04-16 16:56:41 +05:30
CacheSize: int64(options.InteractionsCacheSize),
Eviction: time.Duration(options.InteractionsEviction) * time.Second,
ColldownPeriod: time.Duration(options.InteractionsColldownPeriod) * time.Second,
PollDuration: time.Duration(options.InteractionsPollDuration) * time.Second,
Output: runner.output,
IssuesClient: runner.issuesClient,
2021-04-16 16:56:41 +05:30
Progress: runner.progress,
Debug: runner.options.Debug,
2021-04-16 16:56:41 +05:30
})
if err != nil {
gologger.Error().Msgf("Could not create interactsh client: %s", err)
} else {
runner.interactsh = interactshClient
2021-04-16 16:56:41 +05:30
}
}
if options.RateLimitMinute > 0 {
runner.ratelimiter = ratelimit.New(options.RateLimitMinute, ratelimit.Per(60*time.Second))
} else if options.RateLimit > 0 {
runner.ratelimiter = ratelimit.New(options.RateLimit)
} else {
runner.ratelimiter = ratelimit.NewUnlimited()
}
return runner, nil
}
func createReportingOptions(options *types.Options) (*reporting.Options, error) {
var reportingOptions *reporting.Options
if options.ReportingConfig != "" {
file, err := os.Open(options.ReportingConfig)
if err != nil {
return nil, errors.Wrap(err, "could not open reporting config file")
}
reportingOptions = &reporting.Options{}
if parseErr := yaml.NewDecoder(file).Decode(reportingOptions); parseErr != nil {
file.Close()
return nil, errors.Wrap(parseErr, "could not parse reporting config file")
}
file.Close()
}
if options.DiskExportDirectory != "" {
if reportingOptions != nil {
reportingOptions.DiskExporter = &disk.Options{Directory: options.DiskExportDirectory}
} else {
reportingOptions = &reporting.Options{}
reportingOptions.DiskExporter = &disk.Options{Directory: options.DiskExportDirectory}
}
}
if options.SarifExport != "" {
if reportingOptions != nil {
reportingOptions.SarifExporter = &sarif.Options{File: options.SarifExport}
} else {
reportingOptions = &reporting.Options{}
reportingOptions.SarifExporter = &sarif.Options{File: options.SarifExport}
}
}
return reportingOptions, nil
}
// Close releases all the resources and cleans up
2020-04-04 18:21:05 +05:30
func (r *Runner) Close() {
2020-09-11 21:35:29 +05:30
if r.output != nil {
r.output.Close()
}
r.hostMap.Close()
if r.projectFile != nil {
r.projectFile.Close()
2020-10-15 23:39:00 +02:00
}
protocolinit.Close()
2020-04-04 18:21:05 +05:30
}
// RunEnumeration sets up the input layer for giving input nuclei.
// binary and runs the actual enumeration
func (r *Runner) RunEnumeration() error {
2021-03-22 15:00:26 +05:30
defer r.Close()
// If user asked for new templates to be executed, collect the list from template directory.
if r.options.NewTemplates {
2021-03-09 15:00:22 +05:30
templatesLoaded, err := r.readNewTemplatesFile()
if err != nil {
return errors.Wrap(err, "could not get newly added templates")
}
2021-03-09 15:00:22 +05:30
r.options.Templates = append(r.options.Templates, templatesLoaded...)
}
ignoreFile := config.ReadIgnoreFile()
r.options.ExcludeTags = append(r.options.ExcludeTags, ignoreFile.Tags...)
r.options.ExcludedTemplates = append(r.options.ExcludedTemplates, ignoreFile.Files...)
var cache *hosterrorscache.Cache
2021-09-01 01:09:16 +05:30
if r.options.MaxHostError > 0 {
cache = hosterrorscache.New(r.options.MaxHostError, hosterrorscache.DefaultMaxHostsCount).SetVerbose(r.options.Verbose)
}
r.hostErrors = cache
2021-07-01 14:36:40 +05:30
executerOpts := protocols.ExecuterOptions{
Output: r.output,
Options: r.options,
Progress: r.progress,
Catalog: r.catalog,
IssuesClient: r.issuesClient,
RateLimiter: r.ratelimiter,
Interactsh: r.interactsh,
ProjectFile: r.projectFile,
Browser: r.browser,
HostErrorsCache: cache,
2021-07-01 14:36:40 +05:30
}
workflowLoader, err := parsers.NewLoader(&executerOpts)
if err != nil {
return errors.Wrap(err, "Could not create loader.")
}
executerOpts.WorkflowLoader = workflowLoader
loaderConfig := loader.Config{
Templates: r.options.Templates,
Workflows: r.options.Workflows,
ExcludeTemplates: r.options.ExcludedTemplates,
Tags: r.options.Tags,
ExcludeTags: r.options.ExcludeTags,
IncludeTemplates: r.options.IncludeTemplates,
Authors: r.options.Author,
Severities: r.options.Severities,
IncludeTags: r.options.IncludeTags,
TemplatesDirectory: r.options.TemplatesDirectory,
Catalog: r.catalog,
2021-07-01 14:36:40 +05:30
ExecutorOptions: executerOpts,
}
store, err := loader.New(&loaderConfig)
if err != nil {
return errors.Wrap(err, "could not load templates from config")
2020-08-02 15:48:10 +02:00
}
store.Load()
if r.options.Validate {
2021-08-27 17:06:06 +03:00
if err := store.ValidateTemplates(r.options.Templates, r.options.Workflows); err != nil {
return err
}
2021-08-31 21:39:20 +05:30
if stats.GetValue(parsers.SyntaxErrorStats) == 0 && stats.GetValue(parsers.SyntaxWarningStats) == 0 {
gologger.Info().Msgf("All templates validated successfully\n")
} else {
return errors.New("encountered errors while performing template validation")
}
2021-07-07 19:23:25 +05:30
return nil // exit
}
// Display stats for any loaded templates syntax warnings or errors
2021-08-31 21:39:20 +05:30
stats.Display(parsers.SyntaxWarningStats)
stats.Display(parsers.SyntaxErrorStats)
2021-07-01 14:36:40 +05:30
builder := &strings.Builder{}
if r.templatesConfig != nil && r.templatesConfig.NucleiLatestVersion != "" {
builder.WriteString(" (")
if strings.Contains(config.Version, "-dev") {
builder.WriteString(r.colorizer.Blue("development").String())
} else if config.Version == r.templatesConfig.NucleiLatestVersion {
2021-07-01 14:36:40 +05:30
builder.WriteString(r.colorizer.Green("latest").String())
} else {
builder.WriteString(r.colorizer.Red("outdated").String())
}
builder.WriteString(")")
}
messageStr := builder.String()
builder.Reset()
gologger.Info().Msgf("Using Nuclei Engine %s%s", config.Version, messageStr)
if r.templatesConfig != nil && r.templatesConfig.NucleiTemplatesLatestVersion != "" { // TODO extract duplicated logic
2021-07-01 14:36:40 +05:30
builder.WriteString(" (")
if r.templatesConfig.CurrentVersion == r.templatesConfig.NucleiTemplatesLatestVersion {
builder.WriteString(r.colorizer.Green("latest").String())
} else {
builder.WriteString(r.colorizer.Red("outdated").String())
}
builder.WriteString(")")
}
messageStr = builder.String()
builder.Reset()
2021-08-27 23:23:01 +05:30
if r.templatesConfig != nil {
gologger.Info().Msgf("Using Nuclei Templates %s%s", r.templatesConfig.CurrentVersion, messageStr)
}
2021-07-01 14:36:40 +05:30
if r.interactsh != nil {
gologger.Info().Msgf("Using Interactsh Server %s", r.options.InteractshURL)
}
if len(store.Templates()) > 0 {
2021-09-01 02:01:55 +05:30
gologger.Info().Msgf("Templates added in last update: %d", r.countNewTemplates())
gologger.Info().Msgf("Templates loaded for scan: %d", len(store.Templates()))
2021-07-01 14:36:40 +05:30
}
if len(store.Workflows()) > 0 {
2021-09-01 02:01:55 +05:30
gologger.Info().Msgf("Workflows loaded for scan: %d", len(store.Workflows()))
2021-07-01 14:36:40 +05:30
}
2020-08-02 18:33:55 +02:00
// pre-parse all the templates, apply filters
finalTemplates := []*templates.Template{}
2021-07-05 17:29:45 +05:30
var unclusteredRequests int64
2021-07-01 14:36:40 +05:30
for _, template := range store.Templates() {
2021-01-15 14:17:34 +05:30
// workflows will dynamically adjust the totals while running, as
// it can't be known in advance which requests will be called
2021-01-15 14:17:34 +05:30
if len(template.Workflows) > 0 {
continue
}
unclusteredRequests += int64(template.TotalRequests) * r.inputCount
}
if r.options.VerboseVerbose {
2021-07-01 14:36:40 +05:30
for _, template := range store.Templates() {
r.logAvailableTemplate(template.Path)
}
for _, template := range store.Workflows() {
r.logAvailableTemplate(template.Path)
}
}
templatesMap := make(map[string]*templates.Template)
for _, v := range store.Templates() {
templatesMap[v.Path] = v
2021-07-01 14:36:40 +05:30
}
originalTemplatesCount := len(store.Templates())
2021-01-16 12:26:38 +05:30
clusterCount := 0
2021-07-01 14:36:40 +05:30
clusters := clusterer.Cluster(templatesMap)
for _, cluster := range clusters {
2021-02-07 15:12:38 +05:30
if len(cluster) > 1 && !r.options.OfflineHTTP {
executerOpts := protocols.ExecuterOptions{
Output: r.output,
Options: r.options,
Progress: r.progress,
Catalog: r.catalog,
RateLimiter: r.ratelimiter,
IssuesClient: r.issuesClient,
Browser: r.browser,
ProjectFile: r.projectFile,
Interactsh: r.interactsh,
HostErrorsCache: cache,
2021-02-07 15:12:38 +05:30
}
clusterID := fmt.Sprintf("cluster-%s", xid.New().String())
finalTemplates = append(finalTemplates, &templates.Template{
ID: clusterID,
RequestsHTTP: cluster[0].RequestsHTTP,
2021-02-07 15:12:38 +05:30
Executer: clusterer.NewExecuter(cluster, &executerOpts),
TotalRequests: len(cluster[0].RequestsHTTP),
})
2021-02-25 12:37:47 +05:30
clusterCount += len(cluster)
} else {
2021-02-26 13:13:11 +05:30
finalTemplates = append(finalTemplates, cluster...)
}
}
2021-07-05 17:29:45 +05:30
finalTemplates = append(finalTemplates, store.Workflows()...)
2021-01-15 14:17:34 +05:30
2021-06-14 17:14:16 +05:30
var totalRequests int64
2021-01-15 14:17:34 +05:30
for _, t := range finalTemplates {
if len(t.Workflows) > 0 {
continue
}
totalRequests += int64(t.TotalRequests) * r.inputCount
}
if totalRequests < unclusteredRequests {
2021-07-08 00:41:22 +05:30
gologger.Info().Msgf("Templates clustered: %d (Reduced %d HTTP Requests)", clusterCount, unclusteredRequests-totalRequests)
2021-01-15 14:17:34 +05:30
}
2021-07-01 14:36:40 +05:30
workflowCount := len(store.Workflows())
templateCount := originalTemplatesCount + workflowCount
2020-08-02 18:33:55 +02:00
// 0 matches means no templates were found in directory
2020-08-02 18:33:55 +02:00
if templateCount == 0 {
return errors.New("no templates were found")
}
/*
TODO does it make sense to run the logic below if there are no targets specified?
Can we safely assume the user is just experimenting with the template/workflow filters before running them?
*/
results := &atomic.Bool{}
2020-10-17 02:10:47 +02:00
wgtemplates := sizedwaitgroup.New(r.options.TemplateThreads)
2020-07-24 18:12:16 +02:00
// tracks global progress and captures stdout/stderr until p.Wait finishes
r.progress.Init(r.inputCount, templateCount, totalRequests)
for _, t := range finalTemplates {
wgtemplates.Add()
go func(template *templates.Template) {
defer wgtemplates.Done()
if len(template.Workflows) > 0 {
results.CAS(false, r.processWorkflowWithList(template))
2021-03-01 12:42:13 +05:30
} else {
results.CAS(false, r.processTemplateWithList(template))
}
}(t)
}
wgtemplates.Wait()
2021-04-16 16:56:41 +05:30
if r.interactsh != nil {
2021-04-18 16:10:10 +05:30
matched := r.interactsh.Close()
if matched {
results.CAS(false, true)
}
2021-04-16 16:56:41 +05:30
}
r.progress.Stop()
2020-07-23 20:19:19 +02:00
if r.issuesClient != nil {
r.issuesClient.Close()
}
if !results.Load() {
2021-01-11 20:19:16 +05:30
gologger.Info().Msgf("No results found. Better luck next time!")
}
if r.browser != nil {
r.browser.Close()
}
return nil
}
// readNewTemplatesFile reads newly added templates from directory if it exists
func (r *Runner) readNewTemplatesFile() ([]string, error) {
2021-08-27 23:23:01 +05:30
if r.templatesConfig == nil {
return nil, nil
}
additionsFile := filepath.Join(r.templatesConfig.TemplatesDirectory, ".new-additions")
file, err := os.Open(additionsFile)
if err != nil {
return nil, err
}
defer file.Close()
2021-03-09 15:00:22 +05:30
templatesList := []string{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if text == "" {
continue
}
2021-03-09 15:00:22 +05:30
templatesList = append(templatesList, text)
}
2021-03-09 15:00:22 +05:30
return templatesList, nil
}
2021-07-08 15:15:26 +05:30
// readNewTemplatesFile reads newly added templates from directory if it exists
func (r *Runner) countNewTemplates() int {
2021-08-27 23:23:01 +05:30
if r.templatesConfig == nil {
return 0
}
additionsFile := filepath.Join(r.templatesConfig.TemplatesDirectory, ".new-additions")
2021-07-08 15:15:26 +05:30
file, err := os.Open(additionsFile)
if err != nil {
return 0
}
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if text == "" {
continue
}
count++
}
return count
}