nuclei/v2/internal/runner/runner.go

420 lines
13 KiB
Go
Raw Normal View History

package runner
import (
"bufio"
"fmt"
"os"
"path"
"strings"
2021-04-16 11:26:41 +00:00
"time"
2020-08-23 18:46:18 +00:00
"github.com/logrusorgru/aurora"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/hmap/store/hybrid"
"github.com/projectdiscovery/nuclei/v2/internal/collaborator"
"github.com/projectdiscovery/nuclei/v2/internal/colorizer"
2021-02-26 07:43:11 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/catalog"
"github.com/projectdiscovery/nuclei/v2/pkg/output"
"github.com/projectdiscovery/nuclei/v2/pkg/progress"
2020-10-18 01:09:24 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/projectfile"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/clusterer"
2021-04-16 11:26:41 +00:00
"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"
2020-07-01 10:47:24 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
"github.com/projectdiscovery/nuclei/v2/pkg/types"
2020-10-18 01:09:24 +00:00
"github.com/remeh/sizedwaitgroup"
"github.com/rs/xid"
"go.uber.org/atomic"
"go.uber.org/ratelimit"
"gopkg.in/yaml.v2"
)
// Runner is a client for running the enumeration process.
type Runner struct {
hostMap *hybrid.HybridMap
output output.Writer
2021-04-16 11:26:41 +00:00
interactsh *interactsh.Client
inputCount int64
2020-06-24 22:23:37 +00:00
templatesConfig *nucleiConfig
options *types.Options
projectFile *projectfile.ProjectFile
2021-02-26 07:43:11 +00:00
catalog *catalog.Catalog
progress progress.Progress
colorizer aurora.Aurora
issuesClient *reporting.Client
severityColors *colorizer.Colorizer
browser *engine.Browser
ratelimiter ratelimit.Limiter
}
// New creates a new client for running enumeration process.
func New(options *types.Options) (*Runner, error) {
runner := &Runner{
options: options,
}
if options.Headless {
browser, err := engine.New(options)
if err != nil {
return nil, err
}
runner.browser = browser
}
2020-06-24 22:23:37 +00:00
if err := runner.updateTemplates(); err != nil {
gologger.Warning().Msgf("Could not update templates: %s\n", err)
2020-06-24 22:23:37 +00:00
}
2021-01-14 07:54:50 +00:00
// Read nucleiignore file if given a templateconfig
if runner.templatesConfig != nil {
runner.readNucleiIgnoreFile()
}
2021-02-26 07:43:11 +00:00
runner.catalog = catalog.New(runner.options.TemplatesDirectory)
runner.catalog.AppendIgnore(runner.templatesConfig.IgnorePaths)
var reportingOptions *reporting.Options
if options.ReportingConfig != "" {
file, err := os.Open(options.ReportingConfig)
if err != nil {
gologger.Fatal().Msgf("Could not open reporting config file: %s\n", err)
}
reportingOptions = &reporting.Options{}
2021-03-22 09:18:11 +00:00
if parseErr := yaml.NewDecoder(file).Decode(reportingOptions); parseErr != nil {
file.Close()
gologger.Fatal().Msgf("Could not parse reporting config file: %s\n", parseErr)
}
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 reportingOptions != nil {
if client, err := reporting.New(reportingOptions, options.ReportingDB); err != nil {
gologger.Fatal().Msgf("Could not create issue reporting client: %s\n", err)
} else {
runner.issuesClient = client
}
}
// output coloring
useColor := !options.NoColor
runner.colorizer = aurora.NewAurora(useColor)
runner.severityColors = colorizer.New(runner.colorizer)
if options.TemplateList {
runner.listAvailableTemplates()
os.Exit(0)
}
if (len(options.Templates) == 0 || !options.NewTemplates || (options.Targets == "" && !options.Stdin && options.Target == "")) && options.UpdateTemplates {
2020-06-24 22:23:37 +00:00
os.Exit(0)
}
if hm, err := hybrid.New(hybrid.DefaultDiskOptions); err != nil {
gologger.Fatal().Msgf("Could not create temporary input file: %s\n", err)
} else {
runner.hostMap = hm
}
runner.inputCount = 0
dupeCount := 0
// Handle single target
if options.Target != "" {
runner.inputCount++
2020-11-20 10:12:06 +00:00
// nolint:errcheck // ignoring error
runner.hostMap.Set(options.Target, 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 10:12:06 +00:00
// nolint:errcheck // ignoring error
runner.hostMap.Set(url, nil)
}
2020-07-23 18:19:19 +00:00
}
// Handle taget file
if options.Targets != "" {
input, err := os.Open(options.Targets)
if err != nil {
gologger.Fatal().Msgf("Could not open targets file '%s': %s\n", options.Targets, err)
}
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 10:12:06 +00: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 18:19:19 +00:00
}
2020-04-04 12:51:05 +00:00
// Create the output file if asked
2021-02-26 07:43:11 +00:00
outputWriter, err := output.NewStandardWriter(!options.NoColor, options.NoMeta, options.JSON, options.Output, options.TraceLogFile)
2020-12-29 12:45:27 +00:00
if err != nil {
gologger.Fatal().Msgf("Could not create output file '%s': %s\n", options.Output, err)
2020-04-04 12:51:05 +00:00
}
2021-02-26 07:43:11 +00:00
runner.output = outputWriter
2020-07-23 18:19:19 +00:00
// Creates the progress tracking object
var progressErr error
runner.progress, progressErr = progress.NewStatsTicker(options.StatsInterval, options.EnableProgressBar, options.Metrics, options.MetricsPort)
if progressErr != nil {
return nil, progressErr
}
2020-07-23 18:19:19 +00:00
2020-10-17 00:10:47 +00:00
// create project file if requested or load existing one
if options.Project {
2020-10-30 12:06:05 +00:00
var projectFileErr error
runner.projectFile, projectFileErr = projectfile.New(&projectfile.Options{Path: options.ProjectPath, Cleanup: options.ProjectPath == ""})
2020-10-30 12:06:05 +00:00
if projectFileErr != nil {
return nil, projectFileErr
2020-10-15 21:39:00 +00:00
}
}
2021-04-18 11:59:01 +00:00
if !options.NoInteractsh {
2021-04-16 11:26:41 +00:00
interactshClient, err := interactsh.New(&interactsh.Options{
ServerURL: options.InteractshURL,
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,
Progress: runner.progress,
})
if err != nil {
return nil, err
}
runner.interactsh = interactshClient
}
2020-10-23 08:13:34 +00:00
// Enable Polling
if options.BurpCollaboratorBiid != "" {
collaborator.DefaultCollaborator.Collab.AddBIID(options.BurpCollaboratorBiid)
}
if options.RateLimit > 0 {
runner.ratelimiter = ratelimit.New(options.RateLimit)
} else {
runner.ratelimiter = ratelimit.NewUnlimited()
}
return runner, nil
}
// Close releases all the resources and cleans up
2020-04-04 12:51:05 +00:00
func (r *Runner) Close() {
2020-09-11 16:05:29 +00:00
if r.output != nil {
r.output.Close()
}
r.hostMap.Close()
if r.projectFile != nil {
r.projectFile.Close()
2020-10-15 21:39:00 +00:00
}
protocolinit.Close()
2020-04-04 12:51:05 +00:00
}
// RunEnumeration sets up the input layer for giving input nuclei.
// binary and runs the actual enumeration
func (r *Runner) RunEnumeration() {
2021-03-22 09:30:26 +00:00
defer r.Close()
2021-03-13 19:45:33 +00:00
// If we have no templates, run on whole template directory with provided tags
if len(r.options.Templates) == 0 && len(r.options.Workflows) == 0 && !r.options.NewTemplates && (len(r.options.Tags) > 0 || len(r.options.ExcludeTags) > 0) {
r.options.Templates = append(r.options.Templates, r.options.TemplatesDirectory)
}
if r.options.NewTemplates {
2021-03-09 09:30:22 +00:00
templatesLoaded, err := r.readNewTemplatesFile()
if err != nil {
gologger.Warning().Msgf("Could not get newly added templates: %s\n", err)
}
2021-03-09 09:30:22 +00:00
r.options.Templates = append(r.options.Templates, templatesLoaded...)
}
2021-03-31 20:09:25 +00:00
includedTemplates := r.catalog.GetTemplatesPath(r.options.Templates, false)
excludedTemplates := r.catalog.GetTemplatesPath(r.options.ExcludedTemplates, true)
2020-08-02 13:48:10 +00:00
// defaults to all templates
allTemplates := includedTemplates
2020-08-02 13:48:10 +00:00
if len(excludedTemplates) > 0 {
excludedMap := make(map[string]struct{}, len(excludedTemplates))
for _, excl := range excludedTemplates {
excludedMap[excl] = struct{}{}
}
// rebuild list with only non-excluded templates
allTemplates = []string{}
2020-08-02 13:48:10 +00:00
for _, incl := range includedTemplates {
if _, found := excludedMap[incl]; !found {
allTemplates = append(allTemplates, incl)
} else {
gologger.Warning().Msgf("Excluding '%s'", incl)
2020-08-02 13:48:10 +00:00
}
}
}
2020-08-02 16:33:55 +00:00
// pre-parse all the templates, apply filters
finalTemplates := []*templates.Template{}
2021-03-31 20:09:25 +00:00
workflowPaths := r.catalog.GetTemplatesPath(r.options.Workflows, false)
availableTemplates, _ := r.getParsedTemplatesFor(allTemplates, r.options.Severity, false)
availableWorkflows, workflowCount := r.getParsedTemplatesFor(workflowPaths, r.options.Severity, true)
2021-01-15 08:47:34 +00:00
var unclusteredRequests int64 = 0
for _, template := range availableTemplates {
// workflows will dynamically adjust the totals while running, as
// it can't be know in advance which requests will be called
if len(template.Workflows) > 0 {
continue
}
unclusteredRequests += int64(template.TotalRequests) * r.inputCount
}
originalTemplatesCount := len(availableTemplates)
2021-01-16 06:56:38 +00:00
clusterCount := 0
clusters := clusterer.Cluster(availableTemplates)
for _, cluster := range clusters {
2021-02-07 09:42:38 +00:00
if len(cluster) > 1 && !r.options.OfflineHTTP {
executerOpts := protocols.ExecuterOptions{
2021-02-07 20:37:19 +00:00
Output: r.output,
Options: r.options,
Progress: r.progress,
2021-02-26 07:43:11 +00:00
Catalog: r.catalog,
2021-02-07 20:37:19 +00:00
RateLimiter: r.ratelimiter,
IssuesClient: r.issuesClient,
Browser: r.browser,
2021-02-07 20:37:19 +00:00
ProjectFile: r.projectFile,
2021-04-16 11:26:41 +00:00
Interactsh: r.interactsh,
2021-02-07 09:42:38 +00:00
}
clusterID := fmt.Sprintf("cluster-%s", xid.New().String())
finalTemplates = append(finalTemplates, &templates.Template{
ID: clusterID,
RequestsHTTP: cluster[0].RequestsHTTP,
2021-02-07 09:42:38 +00:00
Executer: clusterer.NewExecuter(cluster, &executerOpts),
TotalRequests: len(cluster[0].RequestsHTTP),
})
2021-02-25 07:07:47 +00:00
clusterCount += len(cluster)
} else {
2021-02-26 07:43:11 +00:00
finalTemplates = append(finalTemplates, cluster...)
}
}
for _, workflows := range availableWorkflows {
finalTemplates = append(finalTemplates, workflows)
}
2021-01-15 08:47:34 +00:00
var totalRequests int64 = 0
for _, t := range finalTemplates {
if len(t.Workflows) > 0 {
continue
}
totalRequests += int64(t.TotalRequests) * r.inputCount
}
if totalRequests < unclusteredRequests {
2021-01-16 06:56:38 +00:00
gologger.Info().Msgf("Reduced %d requests to %d (%d templates clustered)", unclusteredRequests, totalRequests, clusterCount)
2021-01-15 08:47:34 +00:00
}
templateCount := originalTemplatesCount + len(availableWorkflows)
2020-08-02 16:33:55 +00:00
// 0 matches means no templates were found in directory
2020-08-02 16:33:55 +00:00
if templateCount == 0 {
gologger.Fatal().Msgf("Error, no templates were found.\n")
}
gologger.Info().Msgf("Using %s rules (%s templates, %s workflows)",
r.colorizer.Bold(templateCount).String(),
r.colorizer.Bold(templateCount-workflowCount).String(),
r.colorizer.Bold(workflowCount).String())
2020-07-23 18:19:19 +00:00
results := &atomic.Bool{}
2020-10-17 00:10:47 +00:00
wgtemplates := sizedwaitgroup.New(r.options.TemplateThreads)
2020-10-23 08:13:34 +00:00
// Starts polling or ignore
collaborator.DefaultCollaborator.Poll()
2020-07-24 16:12:16 +00: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 07:12:13 +00:00
} else {
results.CAS(false, r.processTemplateWithList(template))
}
}(t)
}
wgtemplates.Wait()
2021-04-16 11:26:41 +00:00
if r.interactsh != nil {
2021-04-18 10:40:10 +00:00
matched := r.interactsh.Close()
if matched {
results.CAS(false, true)
}
2021-04-16 11:26:41 +00:00
}
r.progress.Stop()
2020-07-23 18:19:19 +00:00
if r.issuesClient != nil {
r.issuesClient.Close()
}
if !results.Load() {
if r.output != nil {
r.output.Close()
2020-09-10 11:02:01 +00:00
os.Remove(r.options.Output)
}
2021-01-11 14:49:16 +00:00
gologger.Info().Msgf("No results found. Better luck next time!")
}
if r.browser != nil {
r.browser.Close()
}
}
// readNewTemplatesFile reads newly added templates from directory if it exists
func (r *Runner) readNewTemplatesFile() ([]string, error) {
additionsFile := path.Join(r.templatesConfig.TemplatesDirectory, ".new-additions")
file, err := os.Open(additionsFile)
if err != nil {
return nil, err
}
defer file.Close()
2021-03-09 09:30:22 +00:00
templatesList := []string{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if text == "" {
continue
}
2021-03-09 09:30:22 +00:00
templatesList = append(templatesList, text)
}
2021-03-09 09:30:22 +00:00
return templatesList, nil
}