Compare commits

...
3 Commits
Author SHA1 Message Date
Mzack9999 6692d1241a rename responses 2026-11-07 23:33:46 +01:00
Mzack9999 12f891b9d9 prove trace 2026-11-07 12:48:16 +01:00
Mzack9999 50cdbf5369 filter trace 2026-11-06 22:08:43 +01:00
3 changed files with 213 additions and 1 deletions

No files matched your search

+1 -1
View File
@@ -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)
}
+88
View File
@@ -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
}
+124
View File
@@ -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()
}