From 39def8df6a73d6787044534d9bf3115825184dfd Mon Sep 17 00:00:00 2001 From: Sol Fisher Romanoff Date: Wed, 6 Sep 2023 12:18:15 +0300 Subject: [PATCH] Add nuclei template parsing support --- cmd/nuclei.go | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++ config.go | 4 + go.mod | 1 + go.sum | 3 + main.go | 4 + 5 files changed, 227 insertions(+) create mode 100644 cmd/nuclei.go diff --git a/cmd/nuclei.go b/cmd/nuclei.go new file mode 100644 index 0000000..921c2c0 --- /dev/null +++ b/cmd/nuclei.go @@ -0,0 +1,215 @@ +package cmd + +import ( + "bufio" + "fmt" + "io" + "net/http" + "os" + "strings" + "sync" + + "github.com/charmbracelet/log" + "gopkg.in/yaml.v3" +) + +const ( + nucleiURL = "https://raw.githubusercontent.com/projectdiscovery/nuclei-templates/v9.6.2/" + nucleiFile = "templates-checksum.txt" +) + +// only process attributes that can be used, with little to no metadata. +// there is no need to run any enum matching, because we trust nuclei-templates to have the proper +// value types, and we take the templates straight from their repository. +type Template struct { + ID string + Info struct { + Severity string + } + HTTP []struct { + Path []string + Raw []string + Attack string + Method string + Body string + Payloads map[string]interface{} + Headers map[string]string + RaceCount int + MaxRedirects int + PipelineCurrentConnections int + PipelineRequestsPerConnection int + Threads int + MaxSize int + Fuzzing []string + CookieReuse bool + ReadAll bool + Redirects bool + HostRedirects bool + Pipeline bool + Unsafe bool + Race bool + ReqCondition bool + StopAtFirstMatch bool + SkipVariablesCheck bool + IterateAll bool + DigestUsername string + DigestPassword string + DisablePathAutomerge bool + } + DNS []struct { + Type string + Retries int + Trace bool + TraceMaxRecursion int + Attack string + Payloads map[string]interface{} + Recursion bool + Resolvers []string + } + File []struct { + Extensions []string + DenyList []string + MaxSize string + Archive bool + MIMEType bool + NoRecursive bool + } + TCP []struct { + Host []string + Attack string + Payloads map[string]interface{} + Inputs []struct { + Data string + Type string + Read int + } + ReadSize int + ReadAll bool + } + Headless []struct { + Attack string + Payloads map[string]interface{} + Steps []struct { + Args map[string]string + Action string + } + UserAgent string + CustomUserAgent string + StopAtFirstMatch bool + Fuzzing []struct { + Type string + Part string + Mode string + Keys []string + KeysRegex []string + Values []string + Fuzz []string + } + CookieReuse bool + } + SSL []struct { + Address string + MinVersion string + MaxVersion string + CipherSuites []string + ScanMode string + } + Websocket []struct { + Address string + Inputs []struct { + Data string + } + Headers map[string]string + Attack string + Payloads map[string]interface{} + } + Whois []struct { + Query string + Server string + } + SelfContained bool + StopAtFirstMatch bool + Signature string + Variables map[string]string + Constants map[string]interface{} +} + +func Nuclei(url string, threads int, logdir string) { + fmt.Println(separator.Render("⚛️ Starting " + statusstyle.Render("nuclei template scanning") + "...")) + + sanitizedURL := strings.Split(url, "://")[1] + + if logdir != "" { + f, err := os.OpenFile(logdir+"/"+sanitizedURL+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + log.Errorf("Error creating log file: %s", err) + return + } + defer f.Close() + f.WriteString(fmt.Sprintf("\n\n--------------\nStarting nuclei template scanning...\n--------------\n")) + } + + logger := log.NewWithOptions(os.Stderr, log.Options{ + Prefix: "nuclei ⚛️", + }) + nucleilog := logger.With("url", url) + + // We don't set timeout because it is specified by nuclei templates. + // This &http.Client is only used for fetching the templates themselves from GitHub. + client := &http.Client{} + + resp, err := client.Get(nucleiURL + nucleiFile) + if err != nil { + log.Errorf("Error downloading nuclei template list: %v", err) + return + } + defer resp.Body.Close() + var templateFiles []string + scanner := bufio.NewScanner(resp.Body) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + templateFiles = append(templateFiles, scanner.Text()) + } + + var wg sync.WaitGroup + wg.Add(threads) + for thread := 0; thread < threads; thread++ { + go func(thread int) { + defer wg.Done() + + for i, templateFile := range templateFiles { + if i%threads != thread { + continue + } + + if !strings.Contains(templateFile, ".yaml:") { + continue + } + + templateFile = strings.Split(templateFile, ":")[0] + resp, err := client.Get(nucleiURL + templateFile) + if err != nil { + nucleilog.Errorf("Error downloading nuclei template: %v", err) + continue + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + + template := Template{} + err = yaml.Unmarshal(data, &template) + if err != nil { + nucleilog.Errorf("Error reading nuclei template: %v", err) + nucleilog.Errorf(string(data)) + continue + } + + if template.Info.Severity == "undefined" || template.Info.Severity == "info" || template.Info.Severity == "unknown" { + continue + } + + log.Info(template.ID) + } + }(thread) + } + wg.Wait() +} diff --git a/config.go b/config.go index dd632f9..16ccaa2 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,7 @@ type Settings struct { Dorking bool Git bool Threads int + Nuclei bool Timeout time.Duration } @@ -37,6 +38,7 @@ func parseURLs() Settings { var noscan = pflag.Bool("noscan", false, "Do not perform base URL (robots.txt, etc) scanning") var git = pflag.Bool("git", false, "Enable git repository scanning") var threads = pflag.Int("threads", 10, "Number of threads to run scans on") + var nuclei = pflag.Bool("nuclei", false, "Scan for vulnerabilities using nuclei templates") pflag.Parse() if len(*url) > 0 { @@ -52,6 +54,7 @@ func parseURLs() Settings { LogDir: *logdir, Threads: *threads, Git: *git, + Nuclei: *nuclei, } } else if *file != "" { if _, err := os.Stat(*file); err != nil { @@ -84,6 +87,7 @@ func parseURLs() Settings { LogDir: *logdir, Threads: *threads, Git: *git, + Nuclei: *nuclei, } } diff --git a/go.mod b/go.mod index 9fe7013..9c258fc 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/log v0.2.4 github.com/rocketlaunchr/google-search v1.1.6 github.com/spf13/pflag v1.0.5 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index 3c8b38c..e4bea07 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,9 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index afa95c2..3e0e618 100644 --- a/main.go +++ b/main.go @@ -108,6 +108,10 @@ func main() { cmd.Git(url, settings.Timeout, settings.Threads, settings.LogDir) } + if settings.Nuclei { + cmd.Nuclei(url, settings.Threads, settings.LogDir) + } + // TODO: WHOIS fmt.Println()