diff --git a/internal/runner/options.go b/internal/runner/options.go index 16b755fd..b0e50eef 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -28,6 +28,7 @@ type Options struct { CustomHeaders requests.CustomHeaders // Custom global headers UpdateTemplates bool // UpdateTemplates updates the templates installed at startup TemplatesDirectory string // TemplatesDirectory is the directory to use for storing templates + JSON bool // JSON writes json output to files Stdin bool // Stdin specifies whether stdin input was given to the process } @@ -53,6 +54,7 @@ func ParseOptions() *Options { flag.BoolVar(&options.Debug, "debug", false, "Allow debugging of request/responses") flag.BoolVar(&options.UpdateTemplates, "update-templates", false, "Update Templates updates the installed templates (optional)") flag.StringVar(&options.TemplatesDirectory, "templates-directory", "", "Directory to use for storing nuclei-templates") + flag.BoolVar(&options.JSON, "json", false, "Write json output to files") flag.Parse() diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 47960b46..7ae2bbd2 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -253,6 +253,7 @@ func (r *Runner) processTemplateWithList(template *templates.Template, request i Template: template, DNSRequest: value, Writer: writer, + JSON: r.options.JSON, }) case *requests.HTTPRequest: httpExecutor, err = executor.NewHTTPExecutor(&executor.HTTPOptions{ @@ -265,6 +266,7 @@ func (r *Runner) processTemplateWithList(template *templates.Template, request i ProxyURL: r.options.ProxyURL, ProxySocksURL: r.options.ProxySocksURL, CustomHeaders: r.options.CustomHeaders, + JSON: r.options.JSON, }) } if err != nil { diff --git a/pkg/executor/executer_http.go b/pkg/executor/executer_http.go index c64c59e5..058f7544 100644 --- a/pkg/executor/executer_http.go +++ b/pkg/executor/executer_http.go @@ -30,6 +30,7 @@ import ( type HTTPExecutor struct { debug bool results uint32 + jsonOutput bool httpClient *retryablehttp.Client template *templates.Template httpRequest *requests.HTTPRequest @@ -48,6 +49,7 @@ type HTTPOptions struct { ProxyURL string ProxySocksURL string Debug bool + JSON bool CustomHeaders requests.CustomHeaders } @@ -70,6 +72,7 @@ func NewHTTPExecutor(options *HTTPOptions) (*HTTPExecutor, error) { executer := &HTTPExecutor{ debug: options.Debug, + jsonOutput: options.JSON, results: 0, httpClient: client, template: options.Template, diff --git a/pkg/executor/executor_dns.go b/pkg/executor/executor_dns.go index d833ec41..34307678 100644 --- a/pkg/executor/executor_dns.go +++ b/pkg/executor/executor_dns.go @@ -19,6 +19,7 @@ import ( // for a template. type DNSExecutor struct { debug bool + jsonOutput bool results uint32 dnsClient *retryabledns.Client template *templates.Template @@ -38,6 +39,7 @@ var DefaultResolvers = []string{ // DNSOptions contains configuration options for the DNS executor. type DNSOptions struct { Debug bool + JSON bool Template *templates.Template DNSRequest *requests.DNSRequest Writer *bufio.Writer @@ -50,6 +52,7 @@ func NewDNSExecutor(options *DNSOptions) *DNSExecutor { executer := &DNSExecutor{ debug: options.Debug, + jsonOutput: options.JSON, results: 0, dnsClient: dnsClient, template: options.Template, diff --git a/pkg/executor/http_utils.go b/pkg/executor/http_utils.go index eafcc396..341d1f48 100644 --- a/pkg/executor/http_utils.go +++ b/pkg/executor/http_utils.go @@ -6,6 +6,16 @@ import ( "unsafe" ) +type jsonOutput struct { + Template string `json:"template"` + Type string `json:"type"` + Matched string `json:"matched"` + MatcherName string `json:"matcher_name,omitempty"` + ExtractedResults []string `json:"extracted_results,omitempty"` + Severity string `json:"severity"` + Author string `json:"author"` +} + // unsafeToString converts byte slice to string with zero allocations func unsafeToString(bs []byte) string { return *(*string)(unsafe.Pointer(&bs)) diff --git a/pkg/executor/output_dns.go b/pkg/executor/output_dns.go index 00c7d9c1..4b09b749 100644 --- a/pkg/executor/output_dns.go +++ b/pkg/executor/output_dns.go @@ -3,12 +3,43 @@ package executor import ( "strings" + jsoniter "github.com/json-iterator/go" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/pkg/matchers" ) // writeOutputDNS writes dns output to streams func (e *DNSExecutor) writeOutputDNS(domain string, matcher *matchers.Matcher, extractorResults []string) { + if e.jsonOutput { + output := jsonOutput{ + Template: e.template.ID, + Type: "dns", + Matched: domain, + Severity: e.template.Info.Severity, + Author: e.template.Info.Author, + } + if matcher != nil && len(matcher.Name) > 0 { + output.MatcherName = matcher.Name + } + if len(extractorResults) > 0 { + output.ExtractedResults = extractorResults + } + data, err := jsoniter.Marshal(output) + if err != nil { + gologger.Warningf("Could not marshal json output: %s\n", err) + } + + gologger.Silentf("%s", string(data)) + + if e.writer != nil { + e.outputMutex.Lock() + e.writer.Write(data) + e.writer.WriteRune('\n') + e.outputMutex.Unlock() + } + return + } + builder := &strings.Builder{} builder.WriteRune('[') builder.WriteString(e.template.ID) diff --git a/pkg/executor/output_http.go b/pkg/executor/output_http.go index 5d26990d..25a27a5e 100644 --- a/pkg/executor/output_http.go +++ b/pkg/executor/output_http.go @@ -3,6 +3,7 @@ package executor import ( "strings" + jsoniter "github.com/json-iterator/go" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/pkg/matchers" "github.com/projectdiscovery/nuclei/pkg/requests" @@ -10,6 +11,38 @@ import ( // writeOutputHTTP writes http output to streams func (e *HTTPExecutor) writeOutputHTTP(req *requests.CompiledHTTP, matcher *matchers.Matcher, extractorResults []string) { + URL := req.Request.URL.String() + + if e.jsonOutput { + output := jsonOutput{ + Template: e.template.ID, + Type: "http", + Matched: URL, + Severity: e.template.Info.Severity, + Author: e.template.Info.Author, + } + if matcher != nil && len(matcher.Name) > 0 { + output.MatcherName = matcher.Name + } + if len(extractorResults) > 0 { + output.ExtractedResults = extractorResults + } + data, err := jsoniter.Marshal(output) + if err != nil { + gologger.Warningf("Could not marshal json output: %s\n", err) + } + + gologger.Silentf("%s", string(data)) + + if e.writer != nil { + e.outputMutex.Lock() + e.writer.Write(data) + e.writer.WriteRune('\n') + e.outputMutex.Unlock() + } + return + } + builder := &strings.Builder{} builder.WriteRune('[') @@ -21,7 +54,6 @@ func (e *HTTPExecutor) writeOutputHTTP(req *requests.CompiledHTTP, matcher *matc builder.WriteString("] [http] ") // Escape the URL by replacing all % with %% - URL := req.Request.URL.String() escapedURL := strings.Replace(URL, "%", "%%", -1) builder.WriteString(escapedURL)