Compare commits

...
2 Commits
22 changed files with 485 additions and 1 deletions

No files matched your search

+2
View File
@@ -206,6 +206,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -240,6 +241,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -204,6 +204,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -237,6 +238,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -204,6 +204,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -237,6 +238,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -221,6 +221,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -254,6 +255,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -204,6 +204,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -237,6 +238,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -204,6 +204,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -237,6 +238,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -204,6 +204,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -237,6 +238,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -204,6 +204,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -237,6 +238,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -204,6 +204,7 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -237,6 +238,7 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
+2
View File
@@ -356,6 +356,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVarP(&options.Output, "output", "o", "", "output file to write found issues/vulnerabilities"),
flagSet.BoolVarP(&options.StoreResponse, "store-resp", "sresp", false, "store all request/response passed through nuclei to output directory"),
flagSet.StringVarP(&options.StoreResponseDir, "store-resp-dir", "srd", runner.DefaultDumpTrafficOutputFolder, "store all request/response passed through nuclei to custom directory"),
flagSet.StringVar(&options.StoreEvents, "store-events", "", "store what the matchers of every request see, for any protocol, to a jsonl file for -replay"),
flagSet.BoolVar(&options.Silent, "silent", false, "display findings only"),
flagSet.BoolVarP(&options.NoColor, "no-color", "nc", false, "disable output content coloring (ANSI escape codes)"),
flagSet.BoolVarP(&options.JSONL, "jsonl", "j", false, "write output in JSONL(ines) format"),
@@ -391,6 +392,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&options.SystemResolvers, "system-resolvers", "sr", false, "use system DNS resolving as error fallback"),
flagSet.BoolVarP(&options.DisableClustering, "disable-clustering", "dc", false, "disable clustering of requests"),
flagSet.BoolVar(&options.OfflineHTTP, "passive", false, "enable passive HTTP response processing mode"),
flagSet.StringVar(&options.ReplayFile, "replay", "", "run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests"),
flagSet.BoolVarP(&options.ForceAttemptHTTP2, "force-http2", "fh2", false, "force http2 connection on requests"),
flagSet.BoolVarP(&options.EnvironmentVariables, "env-vars", "ev", false, "enable environment variables to be used in template"),
flagSet.StringVarP(&options.ClientCertFile, "client-cert", "cc", "", "client certificate file (PEM-encoded) used for authenticating against scanned hosts"),
+2 -1
View File
@@ -64,7 +64,8 @@ func ConfigureOptions() error {
// would hold the scan until it does.
func readsTargetsFromStdin(options *types.Options, stdinIsPipe bool) bool {
hasOtherTargets := len(options.Targets) > 0 || options.TargetsFilePath != "" || options.Uncover || len(options.UncoverQuery) > 0
return stdinIsPipe && !options.DisableStdin && !hasOtherTargets
// a replay runs over stored events and has no targets to wait for
return stdinIsPipe && !options.DisableStdin && !hasOtherTargets && options.ReplayFile == ""
}
// ParseOptions parses the command line flags provided by a user
+1
View File
@@ -84,6 +84,7 @@ func TestReadsTargetsFromStdin(t *testing.T) {
{"targets given with -l", &types.Options{TargetsFilePath: "targets.txt"}, true, false},
{"uncover query", &types.Options{UncoverQuery: goflags.StringSlice{"example"}}, true, false},
{"uncover enabled", &types.Options{Uncover: true}, true, false},
{"replay", &types.Options{ReplayFile: "events.jsonl"}, true, false},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, readsTargetsFromStdin(tc.options, tc.stdinIsPipe))
+24
View File
@@ -0,0 +1,24 @@
package runner
import (
"slices"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/replay"
)
// replay writes the findings of the loaded templates over a stored corpus
// through the usual output, so -o, -j and the other output options apply.
func (r *Runner) replay(store *loader.Store) error {
results, err := replay.Replay(r.options.ReplayFile, slices.Concat(store.Templates(), store.Workflows()))
if err != nil {
return err
}
for _, result := range results {
if err := r.output.Write(result); err != nil {
return err
}
}
r.Logger.Info().Msgf("Replayed %s: %d findings", r.options.ReplayFile, len(results))
return nil
}
+18
View File
@@ -57,6 +57,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/automaticscan"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
@@ -69,6 +70,7 @@ import (
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/replay"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
@@ -837,6 +839,22 @@ func (r *Runner) RunEnumeration() error {
if r.options.PlanFile != "" {
return r.writePlan(store, executorEngine)
}
if r.options.ReplayFile != "" {
return r.replay(store)
}
if r.options.StoreEvents != "" {
recorder, err := replay.NewRecorder(r.options.StoreEvents, slices.Concat(store.Templates(), store.Workflows()))
if err != nil {
return errors.Wrap(err, "could not create events file")
}
eventcreator.Record = recorder.Record
defer func() {
eventcreator.Record = nil
if err := recorder.Close(); err != nil {
r.Logger.Error().Msgf("Could not write %s: %s", r.options.StoreEvents, err)
}
}()
}
// TODO: remove below functions after v3 or update warning messages
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
@@ -46,6 +46,35 @@ func TestNetwork(t *testing.T) {
}
})
t.Run("ReplayStoredEvents", func(t *testing.T) {
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
data, err := reader.ConnReadNWithTimeout(conn, 4, 5*time.Second)
if err == nil && string(data) == "PING" {
_, _ = conn.Write([]byte("PONG"))
}
})
events := filepath.Join(t.TempDir(), "events.jsonl")
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/network/basic.yaml", server.URL, suite.debug, "-store-events", events)
server.Close()
if err != nil {
t.Fatalf("recording network events failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
// the server is gone, so the finding can only come from the stored event
results, err = testutils.RunNucleiBareArgsAndGetResults(suite.debug, nil, "-t", "protocols/network/basic.yaml", "-replay", events)
if err != nil {
t.Fatalf("replaying network events failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("PinnedTargetLine", func(t *testing.T) {
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
@@ -10,6 +10,11 @@ import (
"golang.org/x/text/language"
)
// Record, when set, receives the event of every request before its operators
// run, which is what a replay needs to run them again. Every protocol builds
// its result here, so setting it covers all of them.
var Record func(request protocols.Request, event output.InternalEvent)
// CreateEvent wraps the outputEvent with the result of the operators defined on the request
func CreateEvent(request protocols.Request, outputEvent output.InternalEvent, isResponseDebug bool) *output.InternalWrappedEvent {
return CreateEventWithAdditionalOptions(request, outputEvent, isResponseDebug, nil)
@@ -19,6 +24,9 @@ func CreateEvent(request protocols.Request, outputEvent output.InternalEvent, is
// and enables extending the resulting event with additional attributes or values.
func CreateEventWithAdditionalOptions(request protocols.Request, outputEvent output.InternalEvent, isResponseDebug bool,
addAdditionalOptions func(internalWrappedEvent *output.InternalWrappedEvent)) *output.InternalWrappedEvent {
if Record != nil {
Record(request, outputEvent)
}
event := &output.InternalWrappedEvent{InternalEvent: outputEvent}
// Dump response variables if ran in debug mode
+93
View File
@@ -0,0 +1,93 @@
package replay
import (
"encoding/base64"
"unicode/utf8"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
// Matchers type-assert some values, such as an int status code or a []string
// of dns answers, and binary responses are not valid UTF-8, so plain JSON
// would change what they see. Those values are wrapped in a one-key object
// naming their Go type; everything else is stored as JSON.
const (
tagBytes = "$bytes"
tagBinary = "$binary"
tagInt = "$int"
tagStrings = "$strings"
)
func encodeEvent(event output.InternalEvent) map[string]any {
encoded := make(map[string]any, len(event))
for key, value := range event {
switch v := value.(type) {
case string:
if utf8.ValidString(v) {
encoded[key] = v
} else {
encoded[key] = map[string]string{tagBinary: base64.StdEncoding.EncodeToString([]byte(v))}
}
case []byte:
encoded[key] = map[string]string{tagBytes: base64.StdEncoding.EncodeToString(v)}
case int:
encoded[key] = map[string]int{tagInt: v}
case []string:
encoded[key] = map[string][]string{tagStrings: v}
default:
// values JSON cannot hold, such as funcs, are not something a
// matcher reads
if _, err := json.Marshal(v); err == nil {
encoded[key] = v
}
}
}
return encoded
}
func decodeEvent(raw map[string]json.Message) (output.InternalEvent, error) {
event := make(output.InternalEvent, len(raw))
for key, message := range raw {
value, err := decodeValue(message)
if err != nil {
return nil, err
}
event[key] = value
}
return event, nil
}
func decodeValue(message json.Message) (any, error) {
var tagged map[string]json.Message
if json.Unmarshal(message, &tagged) == nil && len(tagged) == 1 {
for tag, inner := range tagged {
switch tag {
case tagBytes, tagBinary:
var text string
if err := json.Unmarshal(inner, &text); err != nil {
return nil, err
}
data, err := base64.StdEncoding.DecodeString(text)
if err != nil {
return nil, err
}
if tag == tagBinary {
return string(data), nil
}
return data, nil
case tagInt:
var number int
err := json.Unmarshal(inner, &number)
return number, err
case tagStrings:
var list []string
err := json.Unmarshal(inner, &list)
return list, err
}
}
}
var value any
err := json.Unmarshal(message, &value)
return value, err
}
+39
View File
@@ -0,0 +1,39 @@
package replay
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/stretchr/testify/require"
)
func TestEventRoundTripKeepsWhatMatchersRead(t *testing.T) {
event := output.InternalEvent{
"body": "<title>admin</title>",
"raw": "\x00\xffBIN\xfe",
"data": []byte{0, 1, 2},
"status_code": 200,
"answer": []string{"a", "b"},
"duration": 1.5,
"matched": true,
"info": map[string]any{"name": "x"},
"callback": func() {},
}
data, err := json.Marshal(encodeEvent(event))
require.NoError(t, err)
var raw map[string]json.Message
require.NoError(t, json.Unmarshal(data, &raw))
decoded, err := decodeEvent(raw)
require.NoError(t, err)
require.Equal(t, "<title>admin</title>", decoded["body"])
require.Equal(t, "\x00\xffBIN\xfe", decoded["raw"], "binary responses keep every byte")
require.Equal(t, []byte{0, 1, 2}, decoded["data"])
require.Equal(t, 200, decoded["status_code"], "the status matcher reads an int")
require.Equal(t, []string{"a", "b"}, decoded["answer"])
require.Equal(t, 1.5, decoded["duration"])
require.Equal(t, true, decoded["matched"])
require.Equal(t, map[string]any{"name": "x"}, decoded["info"])
require.NotContains(t, decoded, "callback", "values JSON cannot hold are dropped")
}
+135
View File
@@ -0,0 +1,135 @@
// Package replay stores the event every request hands its matchers and
// extractors, for any protocol, and runs templates over a stored corpus again
// without sending anything. Comparing the findings of two template sets or
// two builds on the same corpus shows what a change moved.
package replay
import (
"os"
"strconv"
"sync"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
// record is one line of a corpus.
type record[E any] struct {
TemplateID string `json:"template-id,omitempty"`
// Request is the position of the request in its template.
Request int `json:"request"`
// ClusterKey identifies the request of a single-request template; every
// template sending the same request replays the event, as the scan
// clusters them onto one.
ClusterKey string `json:"cluster-key,omitempty"`
Event E `json:"event"`
}
type requestRef struct {
templateID string
index int
single bool
}
// Recorder appends the events of a scan to a corpus file.
type Recorder struct {
mu sync.Mutex
file *os.File
requests map[protocols.Request]requestRef
singleIDs map[string]bool
err error
}
// clusterKeyed is a request the scan can cluster. The key leaves out what
// makes a request unclusterable, such as a raw request or a body, so only a
// clusterable request may share events by it.
type clusterKeyed interface {
TmplClusterKey() uint64
IsClusterable() bool
}
func clusterKey(request protocols.Request) (string, bool) {
keyed, ok := request.(clusterKeyed)
if !ok || !keyed.IsClusterable() {
return "", false
}
return strconv.FormatUint(keyed.TmplClusterKey(), 10), true
}
type requestLister interface {
ProtocolRequests() []protocols.Request
}
// NewRecorder creates the corpus at path for a scan of templatesList.
func NewRecorder(path string, templatesList []*templates.Template) (*Recorder, error) {
file, err := os.Create(path)
if err != nil {
return nil, err
}
recorder := &Recorder{file: file, requests: make(map[protocols.Request]requestRef), singleIDs: make(map[string]bool)}
for _, template := range templatesList {
requests := templateRequests(template)
single := len(requests) == 1 && sharesEvents(template)
for i, request := range requests {
recorder.requests[request] = requestRef{templateID: template.ID, index: i, single: single}
}
if single {
recorder.singleIDs[template.ID] = true
}
}
return recorder, nil
}
// sharesEvents mirrors templates.Cluster: a template the scan never
// clusters does not take another template's event either.
func sharesEvents(template *templates.Template) bool {
return template.Flow == "" && (template.Options == nil || !template.Options.IsMultiProtocol) && !template.Info.HasPlaceholders()
}
func templateRequests(template *templates.Template) []protocols.Request {
if lister, ok := template.Executer.(requestLister); ok {
return lister.ProtocolRequests()
}
return nil
}
// Record stores event, which request produced. It runs before the operators,
// which add their own values to the event.
func (r *Recorder) Record(request protocols.Request, event output.InternalEvent) {
ref, known := r.requests[request]
if !known {
// a cluster runs the first member's request, which the scan compiled
// apart from the template list
templateID, _ := event["template-id"].(string)
if !r.singleIDs[templateID] {
return
}
ref = requestRef{templateID: templateID, single: true}
}
line := record[map[string]any]{TemplateID: ref.templateID, Request: ref.index, Event: encodeEvent(event)}
if key, ok := clusterKey(request); ok && ref.single {
line.ClusterKey = key
}
data, err := json.Marshal(line)
if err != nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
// written straight to the file: ctrl+c exits without running defers
if _, err := r.file.Write(append(data, '\n')); err != nil && r.err == nil {
r.err = err
}
}
// Close closes the corpus and reports the first write error.
func (r *Recorder) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
if err := r.file.Close(); err != nil && r.err == nil {
r.err = err
}
return r.err
}
+103
View File
@@ -0,0 +1,103 @@
package replay
import (
"bufio"
"bytes"
"cmp"
"fmt"
"io"
"maps"
"os"
"slices"
"strconv"
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
type target struct {
template *templates.Template
request protocols.Request
}
// Replay runs the matchers and extractors of templatesList over every event
// in the corpus at path and returns the findings sorted, so two replays diff
// line by line. An event replays on the template and request that produced
// it, and a single-request event also on every template sending the same
// request.
func Replay(path string, templatesList []*templates.Template) ([]*output.ResultEvent, error) {
byRequest := make(map[string][]target)
byCluster := make(map[string][]target)
for _, template := range templatesList {
requests := templateRequests(template)
for i, request := range requests {
byRequest[template.ID+"\x00"+strconv.Itoa(i)] = []target{{template: template, request: request}}
}
if len(requests) == 1 && sharesEvents(template) {
if key, ok := clusterKey(requests[0]); ok {
byCluster[key] = append(byCluster[key], target{template: template, request: requests[0]})
}
}
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
var results []*output.ResultEvent
reader := bufio.NewReader(file)
for number := 1; ; number++ {
line, readErr := reader.ReadBytes('\n')
if line = bytes.TrimSpace(line); len(line) > 0 {
var stored record[map[string]json.Message]
if err := json.Unmarshal(line, &stored); err != nil {
return nil, fmt.Errorf("%s:%d: %w", path, number, err)
}
event, err := decodeEvent(stored.Event)
if err != nil {
return nil, fmt.Errorf("%s:%d: %w", path, number, err)
}
targets := byRequest[stored.TemplateID+"\x00"+strconv.Itoa(stored.Request)]
if stored.ClusterKey != "" {
targets = byCluster[stored.ClusterKey]
}
for _, target := range targets {
results = append(results, replayEvent(target, event)...)
}
}
if readErr == io.EOF {
break
}
if readErr != nil {
return nil, readErr
}
}
slices.SortStableFunc(results, compareResults)
return results, nil
}
func replayEvent(target target, event output.InternalEvent) []*output.ResultEvent {
// operators add their values to the event, and a cluster event carries
// the first member's identity
event = maps.Clone(event)
event["template-id"] = target.template.ID
event["template-path"] = target.template.Path
event["template-info"] = target.template.Info
return eventcreator.CreateEvent(target.request, event, false).Results
}
func compareResults(a, b *output.ResultEvent) int {
return cmp.Or(
strings.Compare(a.TemplateID, b.TemplateID),
strings.Compare(a.Host, b.Host),
strings.Compare(a.Matched, b.Matched),
strings.Compare(a.MatcherName, b.MatcherName),
strings.Compare(a.ExtractorName, b.ExtractorName),
strings.Compare(strings.Join(a.ExtractedResults, "\x00"), strings.Join(b.ExtractedResults, "\x00")),
)
}
+5
View File
@@ -88,6 +88,11 @@ func (e *TemplateExecuter) Compile() error {
return e.engine.Compile()
}
// ProtocolRequests returns the compiled requests in execution order.
func (e *TemplateExecuter) ProtocolRequests() []protocols.Request {
return e.requests
}
// Requests returns the total number of requests the rule will perform
func (e *TemplateExecuter) Requests() int {
var count int
+8
View File
@@ -254,6 +254,12 @@ type Options struct {
// PlanFile is where the work a scan would do is written as JSON lines;
// nuclei exits after writing it, without sending requests.
PlanFile string
// StoreEvents is where the scan writes the event of every request, as
// JSON lines, for -replay.
StoreEvents string
// ReplayFile is a corpus written by -store-events whose events the
// templates are run over, without sending requests.
ReplayFile string
// NoStrictSyntax disables strict syntax check on nuclei templates (allows custom key-value pairs).
NoStrictSyntax bool
// Verbose flag indicates whether to show verbose output or not
@@ -715,6 +721,8 @@ func (options *Options) Copy() *Options {
Silent: options.Silent,
Validate: options.Validate,
PlanFile: options.PlanFile,
StoreEvents: options.StoreEvents,
ReplayFile: options.ReplayFile,
NoStrictSyntax: options.NoStrictSyntax,
Verbose: options.Verbose,
VerboseVerbose: options.VerboseVerbose,