nuclei/internal/runner/runner.go

394 lines
10 KiB
Go
Raw Normal View History

package runner
import (
"bufio"
2020-06-26 10:23:54 +02:00
"context"
"fmt"
"io"
"io/ioutil"
"os"
2020-06-26 10:23:54 +02:00
"path"
"strings"
"sync"
2020-06-26 10:23:54 +02:00
"github.com/d5/tengo/v2"
"github.com/karrick/godirwalk"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/pkg/executor"
"github.com/projectdiscovery/nuclei/pkg/requests"
"github.com/projectdiscovery/nuclei/pkg/templates"
2020-06-26 10:23:54 +02:00
"github.com/projectdiscovery/nuclei/pkg/workflows"
)
// Runner is a client for running the enumeration process.
type Runner struct {
2020-04-04 18:21:05 +05:30
// output is the output file to write if any
output *os.File
outputMutex *sync.Mutex
2020-06-25 03:53:37 +05:30
tempFile string
templatesConfig *nucleiConfig
2020-04-04 18:21:05 +05:30
// options contains configuration options for runner
options *Options
}
// New creates a new client for running enumeration process.
func New(options *Options) (*Runner, error) {
runner := &Runner{
2020-04-04 18:21:05 +05:30
outputMutex: &sync.Mutex{},
options: options,
}
2020-04-04 17:12:29 +05:30
2020-06-25 03:53:37 +05:30
if err := runner.updateTemplates(); err != nil {
return nil, err
}
if (options.Templates == "" || (options.Targets == "" && !options.Stdin && options.Target == "")) && options.UpdateTemplates {
2020-06-25 03:53:37 +05:30
os.Exit(0)
}
// If we have stdin, write it to a new file
if options.Stdin {
tempInput, err := ioutil.TempFile("", "stdin-input-*")
if err != nil {
return nil, err
}
2020-04-26 07:00:28 +05:30
if _, err := io.Copy(tempInput, os.Stdin); err != nil {
return nil, err
}
runner.tempFile = tempInput.Name()
tempInput.Close()
}
// If we have single target, write it to a new file
if options.Target != "" {
tempInput, err := ioutil.TempFile("", "stdin-input-*")
if err != nil {
return nil, err
}
tempInput.WriteString(options.Target)
runner.tempFile = tempInput.Name()
tempInput.Close()
}
2020-04-04 18:21:05 +05:30
// Create the output file if asked
if options.Output != "" {
output, err := os.Create(options.Output)
if err != nil {
gologger.Fatalf("Could not create output file '%s': %s\n", options.Output, err)
}
runner.output = output
}
return runner, nil
}
// Close releases all the resources and cleans up
2020-04-04 18:21:05 +05:30
func (r *Runner) Close() {
r.output.Close()
os.Remove(r.tempFile)
2020-04-04 18:21:05 +05:30
}
2020-04-05 01:18:57 +05:30
// RunEnumeration sets up the input layer for giving input nuclei.
// binary and runs the actual enumeration
func (r *Runner) RunEnumeration() {
var err error
// Check if the template is an absolute path or relative path.
// If the path is absolute, use it. Otherwise,
if r.isRelative(r.options.Templates) {
r.options.Templates, err = r.resolvePath(r.options.Templates)
if err != nil {
gologger.Errorf("Could not find template file '%s': %s\n", r.options.Templates, err)
return
}
}
2020-06-26 14:37:55 +02:00
// Single yaml provided
if strings.HasSuffix(r.options.Templates, ".yaml") {
2020-06-26 14:37:55 +02:00
t := r.parse(r.options.Templates)
switch t.(type) {
case *templates.Template:
2020-06-27 22:01:01 +02:00
var results bool
2020-06-26 14:37:55 +02:00
template := t.(*templates.Template)
2020-06-27 22:01:01 +02:00
// process http requests
2020-06-26 14:37:55 +02:00
for _, request := range template.RequestsHTTP {
results = r.processTemplateRequest(template, request)
}
2020-06-27 22:01:01 +02:00
// process dns requests
2020-06-26 14:37:55 +02:00
for _, request := range template.RequestsDNS {
dnsResults := r.processTemplateRequest(template, request)
if !results {
results = dnsResults
}
}
2020-04-22 22:45:02 +02:00
2020-06-26 14:37:55 +02:00
if !results {
if r.output != nil {
outputFile := r.output.Name()
r.output.Close()
os.Remove(outputFile)
}
gologger.Infof("No results found for the template. Happy hacking!")
}
case *workflows.Workflow:
workflow := t.(*workflows.Workflow)
r.ProcessWorkflowWithList(workflow)
default:
gologger.Errorf("Could not parse file '%s'\n", r.options.Templates)
}
2020-04-04 17:36:20 +05:30
return
}
2020-06-26 14:37:55 +02:00
// If the template passed is a directory
matches := []string{}
// Recursively walk down the Templates directory and run all the template file checks
err = godirwalk.Walk(r.options.Templates, &godirwalk.Options{
Callback: func(path string, d *godirwalk.Dirent) error {
if !d.IsDir() && strings.HasSuffix(path, ".yaml") {
matches = append(matches, path)
}
return nil
},
ErrorCallback: func(path string, err error) godirwalk.ErrorAction {
return godirwalk.SkipNode
},
Unsorted: true,
})
if err != nil {
gologger.Fatalf("Could not find templates in directory '%s': %s\n", r.options.Templates, err)
}
// 0 matches means no templates were found in directory
if len(matches) == 0 {
gologger.Fatalf("Error, no templates found in directory: '%s'\n", r.options.Templates)
}
var results bool
for _, match := range matches {
2020-06-26 14:37:55 +02:00
t := r.parse(match)
switch t.(type) {
case *templates.Template:
template := t.(*templates.Template)
for _, request := range template.RequestsDNS {
dnsResults := r.processTemplateRequest(template, request)
if dnsResults {
results = dnsResults
}
}
2020-06-26 14:37:55 +02:00
for _, request := range template.RequestsHTTP {
httpResults := r.processTemplateRequest(template, request)
if httpResults {
results = httpResults
}
}
2020-06-26 14:37:55 +02:00
case *workflows.Workflow:
workflow := t.(*workflows.Workflow)
r.ProcessWorkflowWithList(workflow)
default:
gologger.Errorf("Could not parse file '%s'\n", r.options.Templates)
}
}
if !results {
if r.output != nil {
outputFile := r.output.Name()
r.output.Close()
os.Remove(outputFile)
}
gologger.Infof("No results found for the template. Happy hacking!")
}
return
}
// processTemplate processes a template and runs the enumeration on all the targets
func (r *Runner) processTemplateRequest(template *templates.Template, request interface{}) bool {
var file *os.File
var err error
// Handle a list of hosts as argument
if r.options.Targets != "" {
file, err = os.Open(r.options.Targets)
} else if r.options.Stdin || r.options.Target != "" {
file, err = os.Open(r.tempFile)
}
if err != nil {
gologger.Fatalf("Could not open targets file '%s': %s\n", r.options.Targets, err)
}
results := r.processTemplateWithList(template, request, file)
file.Close()
return results
}
// processDomain processes the list with a template
func (r *Runner) processTemplateWithList(template *templates.Template, request interface{}, reader io.Reader) bool {
// Display the message for the template
message := fmt.Sprintf("[%s] Loaded template %s (@%s)", template.ID, template.Info.Name, template.Info.Author)
if template.Info.Severity != "" {
message += " [" + template.Info.Severity + "]"
}
gologger.Infof("%s\n", message)
2020-04-04 18:21:05 +05:30
var writer *bufio.Writer
if r.output != nil {
writer = bufio.NewWriter(r.output)
defer writer.Flush()
}
var httpExecutor *executor.HTTPExecutor
var dnsExecutor *executor.DNSExecutor
2020-04-27 23:49:53 +05:30
var err error
// Create an executor based on the request type.
switch value := request.(type) {
case *requests.DNSRequest:
dnsExecutor = executor.NewDNSExecutor(&executor.DNSOptions{
2020-06-22 19:30:01 +05:30
Debug: r.options.Debug,
Template: template,
DNSRequest: value,
Writer: writer,
2020-06-27 20:19:43 +05:30
JSON: r.options.JSON,
})
case *requests.HTTPRequest:
2020-04-27 23:49:53 +05:30
httpExecutor, err = executor.NewHTTPExecutor(&executor.HTTPOptions{
2020-06-22 19:30:01 +05:30
Debug: r.options.Debug,
2020-04-28 13:24:12 +02:00
Template: template,
HTTPRequest: value,
Writer: writer,
Timeout: r.options.Timeout,
Retries: r.options.Retries,
ProxyURL: r.options.ProxyURL,
ProxySocksURL: r.options.ProxySocksURL,
2020-05-22 00:23:38 +02:00
CustomHeaders: r.options.CustomHeaders,
2020-06-27 20:19:43 +05:30
JSON: r.options.JSON,
})
}
2020-04-27 23:49:53 +05:30
if err != nil {
gologger.Warningf("Could not create http client: %s\n", err)
return false
2020-04-27 23:49:53 +05:30
}
limiter := make(chan struct{}, r.options.Threads)
wg := &sync.WaitGroup{}
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
text := scanner.Text()
if text == "" {
continue
}
limiter <- struct{}{}
wg.Add(1)
go func(URL string) {
var err error
if httpExecutor != nil {
2020-06-26 10:23:54 +02:00
err = httpExecutor.ExecuteHTTP(URL)
}
if dnsExecutor != nil {
2020-06-26 10:23:54 +02:00
err = dnsExecutor.ExecuteDNS(URL)
}
if err != nil {
gologger.Warningf("Could not execute step: %s\n", err)
}
<-limiter
wg.Done()
}(text)
}
close(limiter)
wg.Wait()
// See if we got any results from the executors
var results bool
if httpExecutor != nil {
results = httpExecutor.GotResults()
}
if dnsExecutor != nil {
if !results {
results = dnsExecutor.GotResults()
}
}
return results
}
2020-06-26 10:23:54 +02:00
// ProcessWorkflowWithList coming from stdin or list of targets
func (r *Runner) ProcessWorkflowWithList(workflow *workflows.Workflow) {
var file *os.File
var err error
// Handle a list of hosts as argument
if r.options.Targets != "" {
file, err = os.Open(r.options.Targets)
} else if r.options.Stdin {
file, err = os.Open(r.tempFile)
}
if err != nil {
gologger.Fatalf("Could not open targets file '%s': %s\n", r.options.Targets, err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if text == "" {
continue
}
r.ProcessWorkflow(workflow, text)
}
}
// ProcessWorkflow towards an URL
func (r *Runner) ProcessWorkflow(workflow *workflows.Workflow, URL string) error {
script := tengo.NewScript([]byte(workflow.Logic))
for name, value := range workflow.Variables {
var writer *bufio.Writer
if r.output != nil {
writer = bufio.NewWriter(r.output)
defer writer.Flush()
}
templatePath := path.Join(r.options.TemplatesDirectory, value)
template, err := templates.ParseTemplate(templatePath)
if err != nil {
gologger.Errorf("Could not parse template file '%s': %s\n", value, err)
return err
}
httpOptions := &executor.HTTPOptions{
Debug: r.options.Debug,
Writer: writer,
Template: template,
Timeout: r.options.Timeout,
Retries: r.options.Retries,
ProxyURL: r.options.ProxyURL,
ProxySocksURL: r.options.ProxySocksURL,
CustomHeaders: r.options.CustomHeaders,
}
2020-06-26 15:10:42 +02:00
dnsOptions := &executor.DNSOptions{
Debug: r.options.Debug,
Template: template,
Writer: writer,
}
script.Add(name, &workflows.NucleiVar{HTTPOptions: httpOptions, DNSOptions: dnsOptions, URL: URL})
2020-06-26 10:23:54 +02:00
}
_, err := script.RunContext(context.Background())
if err != nil {
gologger.Errorf("Could not execute workflow '%s': %s\n", workflow.ID, err)
return err
}
return nil
}
2020-06-26 14:37:55 +02:00
func (r *Runner) parse(file string) interface{} {
// check if it's a template
template, errTemplate := templates.ParseTemplate(r.options.Templates)
if errTemplate == nil {
return template
}
// check if it's a workflow
workflow, errWorkflow := workflows.Parse(r.options.Templates)
if errWorkflow == nil {
return workflow
}
return nil
}