mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 10:10:18 -10:00
Compare commits
3
Commits
dev
...
dns-trace-policy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6692d1241a | ||
|
|
12f891b9d9 | ||
|
|
50cdbf5369 |
No files matched your search
@@ -195,7 +195,7 @@ func (request *Request) execute(input *contextargs.Context, domain string, metad
|
||||
// perform trace if necessary (excluded from duration — only the query RTT is measured)
|
||||
var traceData *retryabledns.TraceData
|
||||
if request.Trace {
|
||||
traceData, err = request.dnsClient.Trace(domain, request.question, request.TraceMaxRecursion)
|
||||
traceData, err = trace(request.options.Options.ExecutionId, request.dnsClient, domain, request.question, request.TraceMaxRecursion)
|
||||
if err != nil {
|
||||
request.options.Output.Request(request.options.TemplatePath, domain, "dns", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/dns/dnsclientpool"
|
||||
"github.com/projectdiscovery/retryabledns"
|
||||
sliceutil "github.com/projectdiscovery/utils/slice"
|
||||
)
|
||||
|
||||
// traceNameServerPort is the port of the name servers that a trace queries.
|
||||
var traceNameServerPort = "53"
|
||||
|
||||
// trace follows the delegation of host from the root servers in the same way
|
||||
// as retryabledns.Client.Trace, but it skips name server addresses that the
|
||||
// network policy denies. The name servers come from the answers, so the owner
|
||||
// of a zone could otherwise send the queries of a trace to internal addresses.
|
||||
func trace(executionID string, client *dnsclientpool.Client, host string, requestType uint16, maxRecursion int) (*retryabledns.TraceData, error) {
|
||||
var traceData retryabledns.TraceData
|
||||
host = dns.CanonicalName(host)
|
||||
servers := retryabledns.RootDNSServersIPv4
|
||||
seenNS := make(map[string]struct{})
|
||||
seenCName := make(map[string]int)
|
||||
for i := 1; i < maxRecursion; i++ {
|
||||
responses, err := client.QueryParallel(host, requestType, servers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, server := range servers {
|
||||
seenNS[server] = struct{}{}
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return &traceData, nil
|
||||
}
|
||||
for _, dnsData := range responses {
|
||||
if dnsData != nil && len(dnsData.Resolver) > 0 {
|
||||
traceData.DNSData = append(traceData.DNSData, dnsData)
|
||||
}
|
||||
}
|
||||
|
||||
var nameServers []string
|
||||
var nextCname string
|
||||
for _, dnsData := range responses {
|
||||
if dnsData == nil {
|
||||
continue
|
||||
}
|
||||
for _, ns := range dnsData.NS {
|
||||
ips, err := net.DefaultResolver.LookupIP(context.Background(), "ip4", ns)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if !protocolstate.IsHostAllowed(executionID, ip.String()) {
|
||||
continue
|
||||
}
|
||||
nameServers = append(nameServers, net.JoinHostPort(ip.String(), traceNameServerPort))
|
||||
}
|
||||
}
|
||||
if nextCname == "" && len(dnsData.CNAME) > 0 {
|
||||
nextCname = dnsData.CNAME[0]
|
||||
}
|
||||
}
|
||||
nameServers = sliceutil.Dedupe(nameServers)
|
||||
if len(nameServers) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
server := nameServers[rand.Intn(len(nameServers))] // #nosec G404
|
||||
if _, ok := seenNS[server]; ok && nextCname == "" {
|
||||
break
|
||||
}
|
||||
servers = []string{server}
|
||||
|
||||
if nextCname != "" {
|
||||
seenCName[nextCname]++
|
||||
if seenCName[nextCname] > retryabledns.DefaultMaxPerCNAMEFollows {
|
||||
break
|
||||
}
|
||||
host = nextCname
|
||||
}
|
||||
}
|
||||
return &traceData, nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/retryabledns"
|
||||
)
|
||||
|
||||
func serveTraceDNS(t *testing.T, address string, handler dns.HandlerFunc) string {
|
||||
t.Helper()
|
||||
pc, err := net.ListenPacket("udp", address)
|
||||
require.NoError(t, err)
|
||||
server := &dns.Server{PacketConn: pc, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
return pc.LocalAddr().String()
|
||||
}
|
||||
|
||||
func TestDNSTraceObeysNetworkPolicy(t *testing.T) {
|
||||
// The zone of the traced name delegates to a name server on 127.0.0.1.
|
||||
// The trace must query it when the policy allows the address, so that the
|
||||
// test can see a trace that ignores the policy, and must skip it when the
|
||||
// policy denies the address.
|
||||
t.Run("allowed", func(t *testing.T) {
|
||||
require.NotZero(t, runTraceAgainstLoopbackNameServer(t, nil), "trace did not query the allowed name server")
|
||||
})
|
||||
t.Run("denied", func(t *testing.T) {
|
||||
require.Zero(t, runTraceAgainstLoopbackNameServer(t, []string{"127.0.0.1"}), "trace queried a denied name server")
|
||||
})
|
||||
}
|
||||
|
||||
func runTraceAgainstLoopbackNameServer(t *testing.T, excludeTargets []string) int32 {
|
||||
t.Helper()
|
||||
var internalHits atomic.Int32
|
||||
internal := serveTraceDNS(t, "127.0.0.1:0", func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
internalHits.Add(1)
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
rr, _ := dns.NewRR(q.Question[0].Name + ` 0 IN TXT "internal"`)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
_, port, err := net.SplitHostPort(internal)
|
||||
require.NoError(t, err)
|
||||
previousPort := traceNameServerPort
|
||||
traceNameServerPort = port
|
||||
t.Cleanup(func() { traceNameServerPort = previousPort })
|
||||
|
||||
// public stands in for the root servers, the scan resolvers and the system resolver
|
||||
public := serveTraceDNS(t, "127.0.0.1:0", func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
question := q.Question[0]
|
||||
var rr dns.RR
|
||||
switch {
|
||||
case question.Name == "ns.nuclei-test." && question.Qtype == dns.TypeA:
|
||||
rr, _ = dns.NewRR("ns.nuclei-test. 0 IN A 127.0.0.1")
|
||||
case question.Name == "ns.nuclei-test.":
|
||||
case question.Name == "trace.nuclei-test.":
|
||||
rr, _ = dns.NewRR("trace.nuclei-test. 0 IN NS ns.nuclei-test.")
|
||||
default:
|
||||
rr, _ = dns.NewRR(question.Name + ` 0 IN TXT "public"`)
|
||||
}
|
||||
if rr != nil {
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
previousRoots := retryabledns.RootDNSServersIPv4
|
||||
retryabledns.RootDNSServersIPv4 = []string{public}
|
||||
t.Cleanup(func() { retryabledns.RootDNSServersIPv4 = previousRoots })
|
||||
previousResolver := net.DefaultResolver
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "udp", public)
|
||||
},
|
||||
}
|
||||
t.Cleanup(func() { net.DefaultResolver = previousResolver })
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.SetExecutionID(t.Name())
|
||||
options.ExcludeTargets = excludeTargets
|
||||
options.InternalResolversList = []string{public}
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: "dns-trace-policy",
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Info}, Name: "test"},
|
||||
})
|
||||
|
||||
request := &Request{
|
||||
RequestType: DNSRequestTypeHolder{DNSRequestType: TXT},
|
||||
Class: "INET",
|
||||
Name: "{{FQDN}}",
|
||||
// One retry keeps the client keyed by this test's resolvers, so a
|
||||
// client cached by an earlier run cannot point at a closed server.
|
||||
Retries: 1,
|
||||
Trace: true,
|
||||
TraceMaxRecursion: 5,
|
||||
}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
var event output.InternalEvent
|
||||
input := contextargs.NewWithInput(context.Background(), "trace.nuclei-test")
|
||||
err = request.ExecuteWithResults(input, nil, nil, func(e *output.InternalWrappedEvent) {
|
||||
event = e.InternalEvent
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, event["trace"], "ns.nuclei-test", "trace did not query the root servers")
|
||||
return internalHits.Load()
|
||||
}
|
||||
Reference in new issue
Block a user