mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-07 17:43:04 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a892ad37c | ||
|
|
59dffcb765 | ||
|
|
1c16e9400a |
No files matched your search
@@ -1,8 +1,10 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/dns/dnsclientpool"
|
||||
fileutil "github.com/projectdiscovery/utils/file"
|
||||
iputil "github.com/projectdiscovery/utils/ip"
|
||||
)
|
||||
|
||||
// Request contains a DNS protocol request to be made from a template
|
||||
@@ -204,6 +207,48 @@ func resolverHost(resolver string) string {
|
||||
return r
|
||||
}
|
||||
|
||||
// pinResolverHost replaces a resolver host name with an address that the
|
||||
// network policy allows. The policy check passes when one address of the name
|
||||
// is allowed, and the dns client resolves the name again when it dials, so it
|
||||
// could otherwise reach a denied address of the same name.
|
||||
// The lookup stops after timeout, so a slow system resolver cannot hold the
|
||||
// request.
|
||||
func pinResolverHost(executionID, resolver, host string, timeout time.Duration) (string, error) {
|
||||
// DoH and DoT resolvers dial through the scan dialer, which checks every
|
||||
// address, and DoT verifies the certificate against the name.
|
||||
if iputil.IsIP(host) || strings.HasPrefix(resolver, "doh:") || strings.HasPrefix(resolver, "dot:") {
|
||||
return resolver, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
addrs, err := net.DefaultResolver.LookupHost(ctx, host)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "could not resolve dns resolver %s", resolver)
|
||||
}
|
||||
prefix, address := resolver, ""
|
||||
if index := strings.LastIndex(resolver, host); index != -1 {
|
||||
prefix, address = resolver[:index], resolver[index:]
|
||||
}
|
||||
port := "53"
|
||||
if _, p, err := net.SplitHostPort(address); err == nil && p != "" {
|
||||
port = p
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if protocolstate.IsHostAllowed(executionID, addr) {
|
||||
return prefix + net.JoinHostPort(addr, port), nil
|
||||
}
|
||||
}
|
||||
return "", errors.Errorf("dns resolver %s is blocked by network policy", resolver)
|
||||
}
|
||||
|
||||
// resolverLookupTimeout is the scan timeout, or ten seconds when it is not set.
|
||||
func resolverLookupTimeout(options *protocols.ExecutorOptions) time.Duration {
|
||||
if options.Options.Timeout > 0 {
|
||||
return time.Duration(options.Options.Timeout) * time.Second
|
||||
}
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (request *Request) getDnsClient(options *protocols.ExecutorOptions, metadata map[string]interface{}) (*dnsclientpool.Client, error) {
|
||||
dnsClientOptions := &dnsclientpool.Configuration{
|
||||
Retries: request.Retries,
|
||||
@@ -237,6 +282,11 @@ func (request *Request) getDnsClient(options *protocols.ExecutorOptions, metadat
|
||||
if !protocolstate.IsHostAllowed(options.Options.ExecutionId, host) {
|
||||
return nil, errors.Errorf("dns resolver %s is blocked by network policy", resolver)
|
||||
}
|
||||
pinned, err := pinResolverHost(options.Options.ExecutionId, resolver, host, resolverLookupTimeout(options))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolver = pinned
|
||||
}
|
||||
resolvers = append(resolvers, resolver)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func Get(options *types.Options, configuration *Configuration) (*Client, error)
|
||||
if (configuration.Retries <= 1) && len(configuration.Resolvers) == 0 {
|
||||
return cachedClient(resolversFor(options))
|
||||
}
|
||||
if len(options.InternalResolversList) == 0 && hasDohResolver(configuration.Resolvers) {
|
||||
if len(options.InternalResolversList) == 0 && hasPolicyResolver(configuration.Resolvers) {
|
||||
return getPolicyClient(options, configuration)
|
||||
}
|
||||
hash := configuration.Hash()
|
||||
|
||||
@@ -3,12 +3,14 @@ package dnsclientpool
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
@@ -19,8 +21,8 @@ import (
|
||||
)
|
||||
|
||||
// Client sends the DNS queries of a template. It sends queries for template
|
||||
// DoH resolvers itself, because the retryabledns DoH client dials and follows
|
||||
// redirects without the network policy of the scan.
|
||||
// DoH and DoT resolvers itself, because the retryabledns clients dial and
|
||||
// follow redirects without the network policy of the scan.
|
||||
type Client struct {
|
||||
*retryabledns.Client
|
||||
|
||||
@@ -29,31 +31,51 @@ type Client struct {
|
||||
index uint32
|
||||
doh *doh.Client
|
||||
transport *http.Transport
|
||||
dial func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// resolver is one template resolver. url is empty for a resolver that
|
||||
// retryabledns queries over UDP, TCP or DoT.
|
||||
// resolver is one template resolver. url is set for a DoH resolver and dot
|
||||
// for a DoT resolver, and both are empty for a resolver that retryabledns
|
||||
// queries over UDP or TCP.
|
||||
type resolver struct {
|
||||
url string
|
||||
method doh.Method
|
||||
dot string
|
||||
}
|
||||
|
||||
var (
|
||||
// policyClients holds the clients with DoH resolvers of each execution,
|
||||
// policyClients holds the clients with DoH or DoT resolvers of each execution,
|
||||
// because each one dials with the dialer of its execution.
|
||||
policyClients = map[string]map[string]*Client{}
|
||||
policyMu sync.Mutex
|
||||
|
||||
// dotRootCAs verifies DoT certificates, and nil uses the system roots.
|
||||
dotRootCAs *x509.CertPool
|
||||
)
|
||||
|
||||
func hasDohResolver(resolvers []string) bool {
|
||||
func hasPolicyResolver(resolvers []string) bool {
|
||||
for _, r := range resolvers {
|
||||
if strings.HasPrefix(r, "doh:") {
|
||||
if strings.HasPrefix(r, "doh:") || strings.HasPrefix(r, "dot:") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseDotResolver returns the host and port of a resolver such as
|
||||
// dot:dns.example or dot:dns.example:853, in the same way as retryabledns.
|
||||
func parseDotResolver(r string) (resolver, bool) {
|
||||
if !strings.HasPrefix(r, "dot:") {
|
||||
return resolver{}, false
|
||||
}
|
||||
address := strings.TrimPrefix(r, "dot:")
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
address = net.JoinHostPort(address, "853")
|
||||
}
|
||||
return resolver{dot: address}, true
|
||||
}
|
||||
|
||||
// parseDohResolver returns the URL and method of a resolver such as
|
||||
// doh:https://dns.example/dns-query:get, in the same way as retryabledns.
|
||||
func parseDohResolver(r string) (resolver, bool) {
|
||||
@@ -91,10 +113,14 @@ func getPolicyClient(options *types.Options, configuration *Configuration) (*Cli
|
||||
client.resolvers = append(client.resolvers, parsed)
|
||||
continue
|
||||
}
|
||||
if parsed, ok := parseDotResolver(r); ok {
|
||||
client.resolvers = append(client.resolvers, parsed)
|
||||
continue
|
||||
}
|
||||
client.resolvers = append(client.resolvers, resolver{})
|
||||
networkResolvers = append(networkResolvers, r)
|
||||
}
|
||||
// Trace and the non DoH resolvers still go through retryabledns.
|
||||
// Trace and the UDP and TCP resolvers still go through retryabledns.
|
||||
if len(networkResolvers) == 0 {
|
||||
networkResolvers = defaultResolvers
|
||||
}
|
||||
@@ -111,14 +137,16 @@ func getPolicyClient(options *types.Options, configuration *Configuration) (*Cli
|
||||
// The scan dialer checks every address it connects to, so redirects
|
||||
// and names that resolve to a denied address are refused too. It also
|
||||
// goes through the scan proxy when one is set.
|
||||
client.dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
dialers := protocolstate.GetDialersWithId(executionID)
|
||||
if dialers == nil || dialers.Fastdialer == nil {
|
||||
return nil, fmt.Errorf("dialers not initialized for %s", executionID)
|
||||
}
|
||||
return dialers.Fastdialer.Dial(ctx, network, address)
|
||||
}
|
||||
client.timeout = options.GetTimeouts().DialTimeout
|
||||
client.transport = &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
dialers := protocolstate.GetDialersWithId(executionID)
|
||||
if dialers == nil || dialers.Fastdialer == nil {
|
||||
return nil, fmt.Errorf("dialers not initialized for %s", executionID)
|
||||
}
|
||||
return dialers.Fastdialer.Dial(ctx, network, address)
|
||||
},
|
||||
DialContext: client.dial,
|
||||
// same as the retryabledns DoH client
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402
|
||||
}
|
||||
@@ -149,6 +177,8 @@ func (c *Client) Do(msg *dns.Msg) (*dns.Msg, error) {
|
||||
r := c.resolvers[index%uint32(len(c.resolvers))]
|
||||
if r.url != "" {
|
||||
resp, err = c.doh.QueryWithDOHMsg(r.method, doh.Resolver{URL: r.url}, msg)
|
||||
} else if r.dot != "" {
|
||||
resp, err = c.queryDoT(r.dot, msg)
|
||||
} else {
|
||||
resp, err = c.Client.Do(msg)
|
||||
}
|
||||
@@ -159,7 +189,35 @@ func (c *Client) Do(msg *dns.Msg) (*dns.Msg, error) {
|
||||
return resp, retryabledns.ErrRetriesExceeded
|
||||
}
|
||||
|
||||
// Close drops the clients with DoH resolvers of an execution.
|
||||
// queryDoT sends a DNS message over TLS to address. It dials through the scan
|
||||
// dialer and verifies the certificate against the resolver host, as the
|
||||
// retryabledns DoT client does.
|
||||
func (c *Client) queryDoT(address string, msg *dns.Msg) (*dns.Msg, error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := c.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = retryabledns.DefaultOptions.Timeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
conn, err := c.dial(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn := tls.Client(conn, &tls.Config{ServerName: host, RootCAs: dotRootCAs, MinVersion: tls.VersionTLS12})
|
||||
defer func() { _ = tlsConn.Close() }()
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsClient := &dns.Client{Net: "tcp-tls", Timeout: timeout}
|
||||
resp, _, err := dnsClient.ExchangeWithConn(msg, &dns.Conn{Conn: tlsConn})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Close drops the clients with DoH or DoT resolvers of an execution.
|
||||
func Close(executionID string) {
|
||||
policyMu.Lock()
|
||||
clients := policyClients[executionID]
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package dnsclientpool
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newDotListener accepts TCP connections on 127.0.0.1, counts them and closes
|
||||
// them, so a test sees each DoT dial that reaches the address.
|
||||
func newDotListener(t *testing.T, hits *atomic.Int32) string {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = l.Close() })
|
||||
go func() {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hits.Add(1)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
require.NoError(t, err)
|
||||
return port
|
||||
}
|
||||
|
||||
func TestDotResolverObeysNetworkPolicy(t *testing.T) {
|
||||
for _, host := range []string{"127.0.0.1", "localhost"} {
|
||||
t.Run("denied address "+host, func(t *testing.T) {
|
||||
var hits atomic.Int32
|
||||
port := newDotListener(t, &hits)
|
||||
// localhost passes the resolver check through ::1, while 127.0.0.1 is denied
|
||||
options := dohOptions(t, []string{"127.0.0.1"})
|
||||
|
||||
client, err := Get(options, &Configuration{Retries: 2, Resolvers: []string{"dot:" + net.JoinHostPort(host, port)}})
|
||||
require.NoError(t, err)
|
||||
_, _ = client.Do(txtQuery())
|
||||
require.Zero(t, hits.Load(), "DoT query reached a denied address")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("allowed address", func(t *testing.T) {
|
||||
var hits atomic.Int32
|
||||
port := newDotListener(t, &hits)
|
||||
options := dohOptions(t, nil)
|
||||
|
||||
client, err := Get(options, &Configuration{Retries: 1, Resolvers: []string{"dot:127.0.0.1:" + port}})
|
||||
require.NoError(t, err)
|
||||
_, _ = client.Do(txtQuery())
|
||||
require.Equal(t, int32(1), hits.Load())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDotResolverAnswers(t *testing.T) {
|
||||
// httptest gives a certificate for 127.0.0.1 and a pool that trusts it.
|
||||
certs := httptest.NewTLSServer(http.NotFoundHandler())
|
||||
t.Cleanup(certs.Close)
|
||||
previous := dotRootCAs
|
||||
dotRootCAs = certs.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs
|
||||
t.Cleanup(func() { dotRootCAs = previous })
|
||||
|
||||
l, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: certs.TLS.Certificates, MinVersion: tls.VersionTLS12})
|
||||
require.NoError(t, err)
|
||||
server := &dns.Server{Listener: l, Net: "tcp-tls", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
rr, _ := dns.NewRR(`example.com. 60 IN TXT "dot-answer"`)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
_ = w.WriteMsg(m)
|
||||
})}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
|
||||
options := dohOptions(t, nil)
|
||||
client, err := Get(options, &Configuration{Retries: 2, Resolvers: []string{"dot:" + l.Addr().String()}})
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Do(txtQuery())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Answer, 1)
|
||||
require.Contains(t, resp.Answer[0].String(), "dot-answer")
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// startDNSServer answers every query with a TXT record, or with the A records
|
||||
// in addrs when the question is for name, and counts the queries.
|
||||
func startDNSServer(t *testing.T, name string, addrs []string, hits *atomic.Int32) string {
|
||||
t.Helper()
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
server := &dns.Server{PacketConn: pc, Handler: dns.HandlerFunc(func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
hits.Add(1)
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
question := q.Question[0]
|
||||
switch {
|
||||
case question.Name == name && question.Qtype == dns.TypeA:
|
||||
for _, addr := range addrs {
|
||||
rr, _ := dns.NewRR(name + " 0 IN A " + addr)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
case question.Name == name:
|
||||
default:
|
||||
rr, _ := dns.NewRR(question.Name + ` 0 IN TXT "answer"`)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
})}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
l, err := net.Listen("tcp", pc.LocalAddr().String())
|
||||
require.NoError(t, err)
|
||||
tcpServer := &dns.Server{Listener: l, Handler: server.Handler}
|
||||
go func() { _ = tcpServer.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = tcpServer.Shutdown() })
|
||||
return pc.LocalAddr().String()
|
||||
}
|
||||
|
||||
// useSystemResolver sends the lookups of the process to the DNS server at address.
|
||||
func useSystemResolver(t *testing.T, address string) {
|
||||
t.Helper()
|
||||
previous := 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", address)
|
||||
},
|
||||
}
|
||||
t.Cleanup(func() { net.DefaultResolver = previous })
|
||||
}
|
||||
|
||||
func TestDNSResolverNameObeysNetworkPolicy(t *testing.T) {
|
||||
// The resolver name has an allowed address and a denied address. The
|
||||
// policy check passes because one address is allowed, so the query must
|
||||
// not go to the denied one.
|
||||
var deniedHits, systemHits atomic.Int32
|
||||
denied := startDNSServer(t, "", nil, &deniedHits)
|
||||
_, port, err := net.SplitHostPort(denied)
|
||||
require.NoError(t, err)
|
||||
system := startDNSServer(t, "resolver.nuclei-test.", []string{"127.0.0.1", "127.0.0.2"}, &systemHits)
|
||||
useSystemResolver(t, system)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.SetExecutionID(t.Name())
|
||||
options.ExcludeTargets = []string{"127.0.0.1"}
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: "dns-resolver-policy",
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Info}, Name: "test"},
|
||||
})
|
||||
|
||||
for _, resolver := range []string{"resolver.nuclei-test:" + port, "tcp:resolver.nuclei-test:" + port} {
|
||||
t.Run(resolver, func(t *testing.T) {
|
||||
deniedHits.Store(0)
|
||||
request := &Request{
|
||||
RequestType: DNSRequestTypeHolder{DNSRequestType: TXT},
|
||||
Class: "INET",
|
||||
Retries: 2,
|
||||
Name: "{{FQDN}}",
|
||||
Resolvers: []string{resolver},
|
||||
}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion("example.com.", dns.TypeTXT)
|
||||
_, _ = request.dnsClient.Do(msg)
|
||||
require.Zero(t, deniedHits.Load(), "dns query reached a denied address")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSResolverNameLookupStopsAtTimeout(t *testing.T) {
|
||||
// The system resolver never answers, so the lookup that pins the resolver
|
||||
// name must stop at the timeout.
|
||||
silent, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = silent.Close() })
|
||||
useSystemResolver(t, silent.LocalAddr().String())
|
||||
|
||||
start := time.Now()
|
||||
_, err = pinResolverHost(t.Name(), "silent.nuclei-test:53", "silent.nuclei-test", time.Second)
|
||||
require.Error(t, err)
|
||||
require.Less(t, time.Since(start), 4*time.Second, "the resolver lookup did not stop at the timeout")
|
||||
}
|
||||
Reference in new issue
Block a user