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 <tarun@projectdiscovery.io>
dev
Shubham Rasal 2023-05-11 03:26:29 +05:30 committed by GitHub
parent 59376180b1
commit 9c2fa8f9c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 187 additions and 9 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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(.+)"

View File

@ -0,0 +1,5 @@
one
docs
drive
play

View File

@ -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

View File

@ -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
}

View File

@ -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")
})
}

View File

@ -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)