-
-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathdns.go
More file actions
289 lines (259 loc) · 8.89 KB
/
Copy pathdns.go
File metadata and controls
289 lines (259 loc) · 8.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package modules
import (
"context"
"fmt"
"net/url"
"regexp"
"strings"
"time"
"github.com/miekg/dns"
retryabledns "github.com/projectdiscovery/retryabledns"
)
// dnsMaxRetries is how many times the resolver rotates through the pool on a
// timeout before giving up.
const dnsMaxRetries = 3
// defaultDNSTimeout bounds a query when the caller passes no timeout.
// retryabledns applies no default of its own: a zero Options.Timeout reaches
// the underlying dns.Client as a literal zero, which blocks forever against a
// non-responsive resolver.
const defaultDNSTimeout = 3 * time.Second
// defaultDNSResolvers is the bundled pool: fast public anycast servers.
var defaultDNSResolvers = []string{"1.1.1.1:53", "8.8.8.8:53", "9.9.9.9:53"}
// dnsRequestType maps a module's record-type string to its dns type code. An
// empty type defaults to A; ANY is deliberately not the default (RFC 8482
// discourages relying on ANY against the public resolvers).
var dnsRequestType = map[string]uint16{
"": dns.TypeA,
"a": dns.TypeA,
"aaaa": dns.TypeAAAA,
"cname": dns.TypeCNAME,
"mx": dns.TypeMX,
"ns": dns.TypeNS,
"txt": dns.TypeTXT,
"soa": dns.TypeSOA,
"srv": dns.TypeSRV,
"caa": dns.TypeCAA,
"ptr": dns.TypePTR,
"any": dns.TypeANY,
}
// dnsResolver is the slice of the retryabledns client the executor needs; tests
// inject a fake through newDNSResolver. Close releases the client's pooled
// connections; ExecuteDNSModule builds a fresh client per call, so leaving this
// out leaks a connection per query.
type dnsResolver interface {
Query(host string, requestType uint16) (*retryabledns.DNSData, error)
Close()
}
// newDNSResolver builds a resolver over the given pool (falling back to the
// bundled default when it is empty) with the given timeout, flooring a
// non-positive timeout to the default so a caller can't request an
// effectively unbounded resolve. It is a package var so tests can supply a
// fake without touching the network.
var newDNSResolver = func(resolvers []string, timeout time.Duration) (dnsResolver, error) {
pool := resolvers
if len(pool) == 0 {
pool = defaultDNSResolvers
}
if timeout <= 0 {
timeout = defaultDNSTimeout
}
opts := retryabledns.Options{
BaseResolvers: pool,
MaxRetries: dnsMaxRetries,
Timeout: timeout,
}
client, err := retryabledns.NewWithOptions(opts)
if err != nil {
return nil, fmt.Errorf("build dns resolver: %w", err)
}
client.TCPFallback = true
return client, nil
}
// dnsResponse holds the parts of a resolved answer a matcher can target.
type dnsResponse struct {
answer []string // the resource records, one per line
rcode string // the response status, e.g. NOERROR or NXDOMAIN
raw string // the full text of the response message
}
// validateDNS rejects, at load time, a dns config the executor cannot run: an
// unknown record type, or a matcher type other than word or regex (status is
// http only).
func validateDNS(cfg *DNSConfig) error {
if _, ok := dnsRequestType[strings.ToLower(cfg.Type)]; !ok {
return fmt.Errorf("unsupported dns record type %q", cfg.Type)
}
for i := range cfg.Matchers {
switch cfg.Matchers[i].Type {
case "word", "regex":
default:
return fmt.Errorf("dns matcher type %q is not supported (use word or regex)", cfg.Matchers[i].Type)
}
}
return nil
}
// ExecuteDNSModule resolves the configured name and record type, then applies
// the module's matchers and extractors to the answer.
func ExecuteDNSModule(ctx context.Context, target string, def *YAMLModule, opts Options) (*Result, error) {
if def.DNS == nil {
return nil, fmt.Errorf("no DNS configuration")
}
cfg := def.DNS
result := &Result{
ModuleID: def.ID,
Target: target,
Findings: make([]Finding, 0),
}
qtype, ok := dnsRequestType[strings.ToLower(cfg.Type)]
if !ok {
return nil, fmt.Errorf("unsupported dns record type %q", cfg.Type)
}
resolver, err := newDNSResolver(opts.Resolvers, opts.Timeout)
if err != nil {
return nil, err
}
defer resolver.Close()
// retryabledns has no context hook, so honor cancellation before the lookup.
if err := ctx.Err(); err != nil {
return result, err
}
name := dnsName(cfg.Name, target)
data, err := resolver.Query(name, qtype)
if err != nil {
return nil, fmt.Errorf("dns query %q: %w", name, err)
}
resp := newDNSResponse(data)
if !checkDNSMatchers(cfg.Matchers, resp) {
return result, nil
}
result.Findings = append(result.Findings, Finding{
Severity: def.Info.Severity,
Evidence: truncateEvidence(resp.raw),
Extracted: runDNSExtractors(cfg.Extractors, resp),
})
return result, nil
}
// newDNSResponse extracts the matchable parts from a resolved answer. The raw
// text comes from RawResp (the single final message) rather than data.Raw, which
// the resolver's retry loop concatenates across attempts.
func newDNSResponse(data *retryabledns.DNSData) dnsResponse {
if data == nil {
return dnsResponse{}
}
raw := data.Raw
if data.RawResp != nil {
raw = data.RawResp.String()
}
return dnsResponse{
answer: data.AllRecords,
rcode: data.StatusCode,
raw: raw,
}
}
// getDNSPart returns the slice of the response a matcher or extractor targets.
// The default (and the explicit "all"/"body") is the full response text;
// "answer" is the record set; "rcode" is the response status.
func getDNSPart(part string, resp dnsResponse) string {
switch strings.ToLower(part) {
case "answer":
return strings.Join(resp.answer, "\n")
case "rcode":
return resp.rcode
default:
return resp.raw
}
}
// checkDNSMatchers evaluates all matchers against the response with AND logic.
func checkDNSMatchers(matchers []Matcher, resp dnsResponse) bool {
if len(matchers) == 0 {
return false
}
for i := range matchers {
matched := checkDNSMatcher(&matchers[i], resp)
if matchers[i].Negative {
matched = !matched
}
if !matched {
return false // AND logic
}
}
return true
}
// checkDNSMatcher evaluates a single matcher. The status matcher type is HTTP
// only; match a response code with a word or regex matcher on part "rcode".
func checkDNSMatcher(m *Matcher, resp dnsResponse) bool {
part := getDNSPart(m.Part, resp)
switch m.Type {
case "word":
return checkWords(part, m.Words, m.Condition, m.CaseInsensitive)
case "regex":
return checkRegex(part, m.Regex, m.Condition)
default:
return false
}
}
// runDNSExtractors pulls regex captures from the response. DNS answers are text,
// so regex is the available extractor; other types are skipped.
func runDNSExtractors(extractors []Extractor, resp dnsResponse) map[string]string {
if len(extractors) == 0 {
return nil
}
result := make(map[string]string)
for _, e := range extractors {
if e.Type != "regex" {
continue
}
part := getDNSPart(e.Part, resp)
for _, pattern := range e.Regex {
re, err := regexp.Compile(pattern)
if err != nil {
continue
}
matches := re.FindStringSubmatch(part)
if len(matches) > e.Group {
result[e.Name] = matches[e.Group]
break
}
}
}
return result
}
// dnsName resolves the lookup name: the module's name with {{FQDN}} replaced by
// the target host, or the bare target host when no name is set.
func dnsName(name, target string) string {
host := dnsHost(target)
if name == "" {
return host
}
name = strings.ReplaceAll(name, "{{FQDN}}", host)
name = strings.ReplaceAll(name, "{{fqdn}}", host)
return name
}
// dnsHost reduces target to its hostname, stripping any scheme, port, path, or
// userinfo. A bare host is returned unchanged.
func dnsHost(target string) string {
target = strings.TrimSpace(target)
if target == "" {
return target
}
// url.Parse only populates Host when a scheme is present; add one for a bare
// host or host:port so the same parse handles every form.
parse := target
if !strings.Contains(parse, "://") {
parse = "//" + parse
}
if u, err := url.Parse(parse); err == nil && u.Hostname() != "" {
return u.Hostname()
}
return target
}