From 9c2fa8f9c4d20300438e06a34e95ca936a6d7f13 Mon Sep 17 00:00:00 2001 From: Shubham Rasal Date: Thu, 11 May 2023 03:26:29 +0530 Subject: [PATCH] Add payload in dns protocol (#3632) * add execute function in dns * Add payload in dns protocol * Add integration test to cover dns payload - also check command line overriding a payload variable * Update matchedAt and remove trailing dot * Consider payload data for request count - Update verbose output to print question - Update dns requests Requests function to consider payload data * update gitignore * bump nuclei version to v2.9.4-dev --------- Co-authored-by: Tarun Koyalwar --- .gitignore | 2 +- integration_tests/dns/payload.yaml | 29 +++++++++++++++++ integration_tests/subdomains.txt | 5 +++ v2/cmd/integration-test/dns.go | 21 ++++++++++++ v2/pkg/protocols/dns/dns.go | 39 ++++++++++++++++++++++ v2/pkg/protocols/dns/dns_test.go | 52 ++++++++++++++++++++++++++++++ v2/pkg/protocols/dns/request.go | 48 ++++++++++++++++++++++----- 7 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 integration_tests/dns/payload.yaml create mode 100644 integration_tests/subdomains.txt diff --git a/.gitignore b/.gitignore index f3aa824d8..99068297b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ v2/pkg/protocols/common/helpers/deserialization/testdata/Deserialize.class v2/pkg/protocols/common/helpers/deserialization/testdata/ValueObject.class v2/pkg/protocols/common/helpers/deserialization/testdata/ValueObject2.ser *.exe - +v2/.gitignore diff --git a/integration_tests/dns/payload.yaml b/integration_tests/dns/payload.yaml new file mode 100644 index 000000000..682752560 --- /dev/null +++ b/integration_tests/dns/payload.yaml @@ -0,0 +1,29 @@ +id: dns-attack + +info: + name: basic dns template + author: pdteam + severity: info + + +dns: + - name: "{{subdomain_wordlist}}.{{FQDN}}" + type: A + + attack: batteringram + payloads: + subdomain_wordlist: + - one + - docs + - drive + + matchers: + - type: word + words: + - "IN\tA" + + extractors: + - type: regex + group: 1 + regex: + - "IN\tA\t(.+)" diff --git a/integration_tests/subdomains.txt b/integration_tests/subdomains.txt new file mode 100644 index 000000000..db0f25a30 --- /dev/null +++ b/integration_tests/subdomains.txt @@ -0,0 +1,5 @@ +one +docs +drive +play + diff --git a/v2/cmd/integration-test/dns.go b/v2/cmd/integration-test/dns.go index 3344b094a..08fac28c9 100644 --- a/v2/cmd/integration-test/dns.go +++ b/v2/cmd/integration-test/dns.go @@ -10,6 +10,7 @@ var dnsTestCases = map[string]testutils.TestCase{ "dns/caa.yaml": &dnsCAA{}, "dns/tlsa.yaml": &dnsTLSA{}, "dns/variables.yaml": &dnsVariables{}, + "dns/payload.yaml": &dnsPayload{}, "dns/dsl-matcher-variable.yaml": &dnsDSLMatcherVariable{}, } @@ -68,6 +69,26 @@ func (h *dnsVariables) Execute(filePath string) error { return expectResultsCount(results, 1) } +type dnsPayload struct{} + +// Execute executes a test case and returns an error if occurred +func (h *dnsPayload) Execute(filePath string) error { + results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "google.com", debug) + if err != nil { + return err + } + if err := expectResultsCount(results, 3); err != nil { + return err + } + + // override payload from CLI + results, err = testutils.RunNucleiTemplateAndGetResults(filePath, "google.com", debug, "-var", "subdomain_wordlist=subdomains.txt") + if err != nil { + return err + } + return expectResultsCount(results, 4) +} + type dnsDSLMatcherVariable struct{} // Execute executes a test case and returns an error if occurred diff --git a/v2/pkg/protocols/dns/dns.go b/v2/pkg/protocols/dns/dns.go index 8dff2143c..4ce665c5c 100644 --- a/v2/pkg/protocols/dns/dns.go +++ b/v2/pkg/protocols/dns/dns.go @@ -9,9 +9,11 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/operators" "github.com/projectdiscovery/nuclei/v2/pkg/protocols" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions" + "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/replacer" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/dns/dnsclientpool" "github.com/projectdiscovery/retryabledns" + fileutil "github.com/projectdiscovery/utils/file" ) // Request contains a DNS protocol request to be made from a template @@ -60,6 +62,21 @@ type Request struct { // value: 100 TraceMaxRecursion int `yaml:"trace-max-recursion,omitempty" jsonschema:"title=trace-max-recursion level for dns request,description=TraceMaxRecursion is the number of max recursion allowed for trace operations"` + // description: | + // Attack is the type of payload combinations to perform. + // + // Batteringram is inserts the same payload into all defined payload positions at once, pitchfork combines multiple payload sets and clusterbomb generates + // permutations and combinations for all payloads. + AttackType generators.AttackTypeHolder `yaml:"attack,omitempty" json:"attack,omitempty" jsonschema:"title=attack is the payload combination,description=Attack is the type of payload combinations to perform,enum=batteringram,enum=pitchfork,enum=clusterbomb"` + // description: | + // Payloads contains any payloads for the current request. + // + // Payloads support both key-values combinations where a list + // of payloads is provided, or optionally a single file can also + // be provided as payload which will be read on run-time. + Payloads map[string]interface{} `yaml:"payloads,omitempty" json:"payloads,omitempty" jsonschema:"title=payloads for the network request,description=Payloads contains any payloads for the current request"` + generator *generators.PayloadGenerator + CompiledOperators *operators.Operators `yaml:"-"` dnsClient *retryabledns.Client options *protocols.ExecuterOptions @@ -143,6 +160,23 @@ func (request *Request) Compile(options *protocols.ExecuterOptions) error { request.class = classToInt(request.Class) request.options = options request.question = questionTypeToInt(request.RequestType.String()) + for name, payload := range options.Options.Vars.AsMap() { + payloadStr, ok := payload.(string) + // check if inputs contains the payload + if ok && fileutil.FileExists(payloadStr) { + if request.Payloads == nil { + request.Payloads = make(map[string]interface{}) + } + request.Payloads[name] = payloadStr + } + } + + if len(request.Payloads) > 0 { + request.generator, err = generators.New(request.Payloads, request.AttackType.Value, request.options.TemplatePath, request.options.Options.Sandbox, request.options.Catalog, request.options.Options.AttackType) + if err != nil { + return errors.Wrap(err, "could not parse payloads") + } + } return nil } @@ -170,6 +204,11 @@ func (request *Request) getDnsClient(options *protocols.ExecuterOptions, metadat // Requests returns the total number of requests the YAML rule will perform func (request *Request) Requests() int { + if request.generator != nil { + payloadRequests := request.generator.NewIterator().Total() + return payloadRequests + } + return 1 } diff --git a/v2/pkg/protocols/dns/dns_test.go b/v2/pkg/protocols/dns/dns_test.go index ba8f43fce..b2262fc7c 100644 --- a/v2/pkg/protocols/dns/dns_test.go +++ b/v2/pkg/protocols/dns/dns_test.go @@ -35,3 +35,55 @@ func TestDNSCompileMake(t *testing.T) { require.Nil(t, err, "could not make dns request") require.Equal(t, "one.one.one.one.", req.Question[0].Name, "could not get correct dns question") } + +func TestDNSRequests(t *testing.T) { + options := testutils.DefaultOptions + + recursion := false + testutils.Init(options) + const templateID = "testing-dns" + + t.Run("dns-regular", func(t *testing.T) { + + request := &Request{ + RequestType: DNSRequestTypeHolder{DNSRequestType: A}, + Class: "INET", + Retries: 5, + ID: templateID, + Recursion: &recursion, + Name: "{{FQDN}}", + } + executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ + ID: templateID, + Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"}, + }) + err := request.Compile(executerOpts) + require.Nil(t, err, "could not compile dns request") + + reqCount := request.Requests() + require.Equal(t, 1, reqCount, "could not get correct dns request count") + }) + + // test payload requests count is correct + t.Run("dns-payload", func(t *testing.T) { + + request := &Request{ + RequestType: DNSRequestTypeHolder{DNSRequestType: A}, + Class: "INET", + Retries: 5, + ID: templateID, + Recursion: &recursion, + Name: "{{subdomain}}.{{FQDN}}", + Payloads: map[string]interface{}{"subdomain": []string{"a", "b", "c"}}, + } + executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ + ID: templateID, + Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"}, + }) + err := request.Compile(executerOpts) + require.Nil(t, err, "could not compile dns request") + + reqCount := request.Requests() + require.Equal(t, 3, reqCount, "could not get correct dns request count") + }) +} diff --git a/v2/pkg/protocols/dns/request.go b/v2/pkg/protocols/dns/request.go index 71b34eeb0..d35cc822f 100644 --- a/v2/pkg/protocols/dns/request.go +++ b/v2/pkg/protocols/dns/request.go @@ -4,9 +4,11 @@ import ( "encoding/hex" "fmt" "net/url" + "strings" "github.com/miekg/dns" "github.com/pkg/errors" + "golang.org/x/exp/maps" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v2/pkg/output" @@ -53,7 +55,29 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, // merge with metadata (eg. from workflow context) vars = generators.MergeMaps(vars, metadata, optionVars) variablesMap := request.options.Variables.Evaluate(vars) - vars = generators.MergeMaps(variablesMap, vars) + vars = generators.MergeMaps(vars, variablesMap) + + if request.generator != nil { + iterator := request.generator.NewIterator() + + for { + value, ok := iterator.Value() + if !ok { + break + } + value = generators.MergeMaps(vars, value) + if err := request.execute(domain, metadata, previous, value, callback); err != nil { + return err + } + } + } else { + value := maps.Clone(vars) + return request.execute(domain, metadata, previous, value, callback) + } + return nil +} + +func (request *Request) execute(domain string, metadata, previous output.InternalEvent, vars map[string]interface{}, callback protocols.OutputEventCallback) error { if vardump.EnableVarDump { gologger.Debug().Msgf("Protocol request variables: \n%s\n", vardump.DumpVariables(vars)) @@ -74,14 +98,20 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, return nil } } + question := domain + if len(compiledRequest.Question) > 0 { + question = compiledRequest.Question[0].Name + } + // remove the last dot + question = strings.TrimSuffix(question, ".") requestString := compiledRequest.String() if varErr := expressions.ContainsUnresolvedVariables(requestString); varErr != nil { - gologger.Warning().Msgf("[%s] Could not make dns request for %s: %v\n", request.options.TemplateID, domain, varErr) + gologger.Warning().Msgf("[%s] Could not make dns request for %s: %v\n", request.options.TemplateID, question, varErr) return nil } if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.StoreResponse { - msg := fmt.Sprintf("[%s] Dumped DNS request for %s", request.options.TemplateID, domain) + msg := fmt.Sprintf("[%s] Dumped DNS request for %s", request.options.TemplateID, question) if request.options.Options.Debug || request.options.Options.DebugRequests { gologger.Info().Str("domain", domain).Msgf(msg) gologger.Print().Msgf("%s", requestString) @@ -98,14 +128,15 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, if err != nil { request.options.Output.Request(request.options.TemplatePath, domain, request.Type().String(), err) request.options.Progress.IncrementFailedRequestsBy(1) + } else { + request.options.Progress.IncrementRequests() } if response == nil { return errors.Wrap(err, "could not send dns request") } - request.options.Progress.IncrementRequests() request.options.Output.Request(request.options.TemplatePath, domain, request.Type().String(), err) - gologger.Verbose().Msgf("[%s] Sent DNS request to %s\n", request.options.TemplateID, domain) + gologger.Verbose().Msgf("[%s] Sent DNS request to %s\n", request.options.TemplateID, question) // perform trace if necessary var traceData *retryabledns.TraceData @@ -116,7 +147,8 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, } } - outputEvent := request.responseToDSLMap(compiledRequest, response, input.MetaInput.Input, input.MetaInput.Input, traceData) + // Create the output event + outputEvent := request.responseToDSLMap(compiledRequest, response, domain, question, traceData) for k, v := range previous { outputEvent[k] = v } @@ -125,9 +157,9 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, } event := eventcreator.CreateEvent(request, outputEvent, request.options.Options.Debug || request.options.Options.DebugResponse) - dumpResponse(event, request, request.options, response.String(), domain) + dumpResponse(event, request, request.options, response.String(), question) if request.Trace { - dumpTraceData(event, request.options, traceToString(traceData, true), domain) + dumpTraceData(event, request.options, traceToString(traceData, true), question) } callback(event)