-
Notifications
You must be signed in to change notification settings - Fork 309
feat: auto wildcard domain detection for multi-domain runs (#924) #949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
64b5c10
71e3573
05b82ba
cc8b7ed
1674779
ffd7b98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,9 +28,16 @@ import ( | |||||||||||||||||||||||||||||
| iputil "github.com/projectdiscovery/utils/ip" | ||||||||||||||||||||||||||||||
| mapsutil "github.com/projectdiscovery/utils/maps" | ||||||||||||||||||||||||||||||
| sliceutil "github.com/projectdiscovery/utils/slice" | ||||||||||||||||||||||||||||||
| "golang.org/x/net/publicsuffix" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Runner is a client for running the enumeration process. | ||||||||||||||||||||||||||||||
| // wildcardTask carries the input host and the wildcard root domain used for matching. | ||||||||||||||||||||||||||||||
| type wildcardTask struct { | ||||||||||||||||||||||||||||||
| host string | ||||||||||||||||||||||||||||||
| domain string | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| type Runner struct { | ||||||||||||||||||||||||||||||
| options *Options | ||||||||||||||||||||||||||||||
| dnsx *dnsx.DNSX | ||||||||||||||||||||||||||||||
|
|
@@ -39,7 +46,7 @@ type Runner struct { | |||||||||||||||||||||||||||||
| wgwildcardworker *sync.WaitGroup | ||||||||||||||||||||||||||||||
| workerchan chan string | ||||||||||||||||||||||||||||||
| outputchan chan string | ||||||||||||||||||||||||||||||
| wildcardworkerchan chan string | ||||||||||||||||||||||||||||||
| wildcardworkerchan chan wildcardTask | ||||||||||||||||||||||||||||||
| wildcards *mapsutil.SyncLockMap[string, struct{}] | ||||||||||||||||||||||||||||||
| wildcardscache map[string][]string | ||||||||||||||||||||||||||||||
| wildcardscachemutex sync.Mutex | ||||||||||||||||||||||||||||||
|
|
@@ -50,6 +57,10 @@ type Runner struct { | |||||||||||||||||||||||||||||
| aurora aurora.Aurora | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func normalizeHostname(host string) string { | ||||||||||||||||||||||||||||||
| return strings.TrimSuffix(strings.TrimSpace(host), ".") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func New(options *Options) (*Runner, error) { | ||||||||||||||||||||||||||||||
| retryabledns.CheckInternalIPs = true | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -115,7 +126,7 @@ func New(options *Options) (*Runner, error) { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // If no option is specified or wildcard filter has been requested use query type A | ||||||||||||||||||||||||||||||
| if len(questionTypes) == 0 || options.WildcardDomain != "" { | ||||||||||||||||||||||||||||||
| if len(questionTypes) == 0 || options.WildcardDomain != "" || options.AutoWildcard { | ||||||||||||||||||||||||||||||
| options.A = true | ||||||||||||||||||||||||||||||
| questionTypes = append(questionTypes, dns.TypeA) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
@@ -156,7 +167,7 @@ func New(options *Options) (*Runner, error) { | |||||||||||||||||||||||||||||
| wgresolveworkers: &sync.WaitGroup{}, | ||||||||||||||||||||||||||||||
| wgwildcardworker: &sync.WaitGroup{}, | ||||||||||||||||||||||||||||||
| workerchan: make(chan string), | ||||||||||||||||||||||||||||||
| wildcardworkerchan: make(chan string), | ||||||||||||||||||||||||||||||
| wildcardworkerchan: make(chan wildcardTask), | ||||||||||||||||||||||||||||||
| wildcards: mapsutil.NewSyncLockMap[string, struct{}](), | ||||||||||||||||||||||||||||||
| wildcardscache: make(map[string][]string), | ||||||||||||||||||||||||||||||
| limiter: limiter, | ||||||||||||||||||||||||||||||
|
|
@@ -437,6 +448,10 @@ func (r *Runner) SaveResumeConfig() error { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func (r *Runner) Run() error { | ||||||||||||||||||||||||||||||
| if r.options.Stream { | ||||||||||||||||||||||||||||||
| if r.options.WildcardDomain != "" || r.options.AutoWildcard { | ||||||||||||||||||||||||||||||
| gologger.Warning().Msgf("Wildcard filtering enabled in stream mode: falling back to buffered execution") | ||||||||||||||||||||||||||||||
| return r.run() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+451
to
+454
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stream fallback currently breaks stdin-backed wildcard runs. At Line 453, 🔧 Proposed fix func (r *Runner) Run() error {
if r.options.Stream {
if r.options.WildcardDomain != "" || r.options.AutoWildcard {
gologger.Warning().Msgf("Wildcard filtering enabled in stream mode: falling back to buffered execution")
+ r.options.Stream = false
return r.run()
}
return r.runStream()
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| return r.runStream() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -467,7 +482,7 @@ func (r *Runner) run() error { | |||||||||||||||||||||||||||||
| close(r.outputchan) | ||||||||||||||||||||||||||||||
| r.wgoutputworker.Wait() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if r.options.WildcardDomain != "" { | ||||||||||||||||||||||||||||||
| if r.options.WildcardDomain != "" || r.options.AutoWildcard { | ||||||||||||||||||||||||||||||
| gologger.Print().Msgf("Starting to filter wildcard subdomains\n") | ||||||||||||||||||||||||||||||
| ipDomain := make(map[string]map[string]struct{}) | ||||||||||||||||||||||||||||||
| listIPs := []string{} | ||||||||||||||||||||||||||||||
|
|
@@ -503,13 +518,22 @@ func (r *Runner) run() error { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| seen := make(map[string]struct{}) | ||||||||||||||||||||||||||||||
| hostToWildcardDomain := make(map[string]string) | ||||||||||||||||||||||||||||||
| for _, a := range listIPs { | ||||||||||||||||||||||||||||||
| hosts := ipDomain[a] | ||||||||||||||||||||||||||||||
| if len(hosts) >= r.options.WildcardThreshold { | ||||||||||||||||||||||||||||||
| for host := range hosts { | ||||||||||||||||||||||||||||||
| wildcardDomain, ok := r.getWildcardDomainForHost(host) | ||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| hostToWildcardDomain[host] = wildcardDomain | ||||||||||||||||||||||||||||||
| if r.isWildcardApexHost(host, wildcardDomain) { | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if _, ok := seen[host]; !ok { | ||||||||||||||||||||||||||||||
| seen[host] = struct{}{} | ||||||||||||||||||||||||||||||
| r.wildcardworkerchan <- host | ||||||||||||||||||||||||||||||
| r.wildcardworkerchan <- wildcardTask{host: host, domain: wildcardDomain} | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
@@ -524,7 +548,8 @@ func (r *Runner) run() error { | |||||||||||||||||||||||||||||
| numRemovedSubdomains := 0 | ||||||||||||||||||||||||||||||
| for _, A := range listIPs { | ||||||||||||||||||||||||||||||
| for host := range ipDomain[A] { | ||||||||||||||||||||||||||||||
| if host == r.options.WildcardDomain { | ||||||||||||||||||||||||||||||
| wildcardDomain, hasDomain := hostToWildcardDomain[host] | ||||||||||||||||||||||||||||||
| if hasDomain && r.isWildcardApexHost(host, wildcardDomain) { | ||||||||||||||||||||||||||||||
| if _, ok := seen[host]; !ok { | ||||||||||||||||||||||||||||||
| seen[host] = struct{}{} | ||||||||||||||||||||||||||||||
| _ = r.lookupAndOutput(host) | ||||||||||||||||||||||||||||||
|
|
@@ -551,6 +576,38 @@ func (r *Runner) run() error { | |||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // getWildcardDomainForHost resolves the wildcard root domain for a host. | ||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||
| // Resolution order: | ||||||||||||||||||||||||||||||
| // - explicit options.WildcardDomain | ||||||||||||||||||||||||||||||
| // - auto-derived eTLD+1 when options.AutoWildcard is enabled | ||||||||||||||||||||||||||||||
| func (r *Runner) getWildcardDomainForHost(host string) (string, bool) { | ||||||||||||||||||||||||||||||
| if domain := normalizeHostname(r.options.WildcardDomain); domain != "" { | ||||||||||||||||||||||||||||||
| return domain, true | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if !r.options.AutoWildcard { | ||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| h := normalizeHostname(host) | ||||||||||||||||||||||||||||||
| switch { | ||||||||||||||||||||||||||||||
| case h == "": | ||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||
| case strings.Contains(h, ":"): | ||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| domain, err := publicsuffix.EffectiveTLDPlusOne(h) | ||||||||||||||||||||||||||||||
| if err != nil || domain == "" { | ||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return domain, true | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func (r *Runner) isWildcardApexHost(host, wildcardDomain string) bool { | ||||||||||||||||||||||||||||||
| return normalizeHostname(host) == normalizeHostname(wildcardDomain) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func (r *Runner) lookupAndOutput(host string) error { | ||||||||||||||||||||||||||||||
| if r.options.JSON { | ||||||||||||||||||||||||||||||
| if data, ok := r.hm.Get(host); ok { | ||||||||||||||||||||||||||||||
|
|
@@ -731,7 +788,7 @@ func (r *Runner) worker() { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| // if wildcard filtering just store the data | ||||||||||||||||||||||||||||||
| if r.options.WildcardDomain != "" { | ||||||||||||||||||||||||||||||
| if r.options.WildcardDomain != "" || r.options.AutoWildcard { | ||||||||||||||||||||||||||||||
| if err := r.storeDNSData(dnsData.DNSData); err != nil { | ||||||||||||||||||||||||||||||
| gologger.Debug().Msgf("Failed to store DNS data for %s: %v\n", domain, err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
@@ -935,13 +992,13 @@ func (r *Runner) wildcardWorker() { | |||||||||||||||||||||||||||||
| defer r.wgwildcardworker.Done() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for { | ||||||||||||||||||||||||||||||
| host, more := <-r.wildcardworkerchan | ||||||||||||||||||||||||||||||
| task, more := <-r.wildcardworkerchan | ||||||||||||||||||||||||||||||
| if !more { | ||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if r.IsWildcard(host) { | ||||||||||||||||||||||||||||||
| if r.IsWildcard(task.host, task.domain) { | ||||||||||||||||||||||||||||||
| // mark this host as a wildcard subdomain | ||||||||||||||||||||||||||||||
| _ = r.wildcards.Set(host, struct{}{}) | ||||||||||||||||||||||||||||||
| _ = r.wildcards.Set(task.host, struct{}{}) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normalize host/domain case before apex comparison.
Line 608 currently compares case-sensitively. DNS labels are case-insensitive, so
Example.COMvsexample.comshould still match.🔧 Proposed fix
func normalizeHostname(host string) string { - return strings.TrimSuffix(strings.TrimSpace(host), ".") + return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".") }Also applies to: 607-609
🤖 Prompt for AI Agents