From 3cb054d97694e1a440725b27bfabee73563198a0 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar <45962551+tarunKoyalwar@users.noreply.github.com> Date: Wed, 5 Nov 2025 21:49:07 +0530 Subject: [PATCH] Add wildcard certificate detection in JSON output (#1665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add wildcard certificate detection in JSON output This commit implements wildcard certificate detection for subdomains that are covered by wildcard certificates (e.g., *.example.com). When sources return results containing wildcard patterns, the subdomain is now marked with a wildcard_certificate field in JSON output mode. Key changes: - Added WildcardCertificate field to HostEntry and Result structs - Detection logic: checks if result.Value contains "*.subdomain" pattern - Propagates wildcard flag through resolution pipeline - JSON output includes wildcard_certificate field (omitted if false) - Updates version to v2.9.1-dev - Fix .goreleaser.yml 386 architecture quoting - Add /subfinder to .gitignore Note: wildcard_certificate field is not included when using -cs flag to avoid breaking changes to the library API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix wildcard certificate flag propagation with -nW flag Fixed a bug where the wildcard_certificate field was being lost when using the -nW flag (DNS resolution with wildcard filtering). The issue occurred because when multiple sources find the same subdomain, only the first occurrence is sent to the resolution pool. If a later source marks the subdomain as having a wildcard certificate, that information was stored in uniqueMap but never propagated to foundResults. Solution: After resolution completes, merge wildcard certificate information from uniqueMap into foundResults. This ensures that if any source marked a subdomain as having a wildcard certificate, that flag is preserved in the final output. This ensures consistent behavior - wildcard_certificate field appears in JSON output regardless of whether -nW flag is used. Validation: - Without -nW: api.nuclei.sh shows wildcard_certificate:true ✓ - With -nW: api.nuclei.sh shows wildcard_certificate:true ✓ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .gitignore | 3 ++- .goreleaser.yml | 2 +- pkg/resolve/resolve.go | 24 +++++++++++++----------- pkg/runner/banners.go | 2 +- pkg/runner/enumerate.go | 25 +++++++++++++++++++++++-- pkg/runner/outputter.go | 28 ++++++++++++++++------------ 6 files changed, 56 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index b0dd47ba..155676bf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ vendor/ .idea .devcontainer .vscode -dist \ No newline at end of file +dist +/subfinder \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index 867267d2..e34562f0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,7 +13,7 @@ builds: - darwin goarch: - amd64 - - 386 + - '386' - arm - arm64 diff --git a/pkg/resolve/resolve.go b/pkg/resolve/resolve.go index d7e4d887..38055b89 100644 --- a/pkg/resolve/resolve.go +++ b/pkg/resolve/resolve.go @@ -25,18 +25,20 @@ type ResolutionPool struct { // HostEntry defines a host with the source type HostEntry struct { - Domain string - Host string - Source string + Domain string + Host string + Source string + WildcardCertificate bool } // Result contains the result for a host resolution type Result struct { - Type ResultType - Host string - IP string - Error error - Source string + Type ResultType + Host string + IP string + Error error + Source string + WildcardCertificate bool } // ResultType is the type of result found @@ -92,13 +94,13 @@ func (r *ResolutionPool) InitWildcards(domain string) error { func (r *ResolutionPool) resolveWorker() { for task := range r.Tasks { if !r.removeWildcard { - r.Results <- Result{Type: Subdomain, Host: task.Host, IP: "", Source: task.Source} + r.Results <- Result{Type: Subdomain, Host: task.Host, IP: "", Source: task.Source, WildcardCertificate: task.WildcardCertificate} continue } hosts, err := r.DNSClient.Lookup(task.Host) if err != nil { - r.Results <- Result{Type: Error, Host: task.Host, Source: task.Source, Error: err} + r.Results <- Result{Type: Error, Host: task.Host, Source: task.Source, Error: err, WildcardCertificate: task.WildcardCertificate} continue } @@ -116,7 +118,7 @@ func (r *ResolutionPool) resolveWorker() { } if !skip { - r.Results <- Result{Type: Subdomain, Host: task.Host, IP: hosts[0], Source: task.Source} + r.Results <- Result{Type: Subdomain, Host: task.Host, IP: hosts[0], Source: task.Source, WildcardCertificate: task.WildcardCertificate} } } r.wg.Done() diff --git a/pkg/runner/banners.go b/pkg/runner/banners.go index d0233478..7e52f2b6 100644 --- a/pkg/runner/banners.go +++ b/pkg/runner/banners.go @@ -17,7 +17,7 @@ const banner = ` const ToolName = `subfinder` // Version is the current version of subfinder -const version = `v2.9.0` +const version = `v2.9.1-dev` // showBanner is used to show the banner to the user func showBanner() { diff --git a/pkg/runner/enumerate.go b/pkg/runner/enumerate.go index f39eab06..bd8953e5 100644 --- a/pkg/runner/enumerate.go +++ b/pkg/runner/enumerate.go @@ -67,6 +67,9 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string gologger.Warning().Msgf("Encountered an error with source %s: %s\n", result.Source, result.Error) case subscraping.Subdomain: subdomain := replacer.Replace(result.Value) + // check if this subdomain is actually a wildcard subdomain + // that may have furthur subdomains associated with it + isWildcard := strings.Contains(result.Value, "*."+subdomain) // Validate the subdomain found and remove wildcards from if !strings.HasSuffix(subdomain, "."+domain) { @@ -90,10 +93,17 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string // send the subdomain for resolution. if _, ok := uniqueMap[subdomain]; ok { skippedCounts[result.Source]++ + // even if it is duplicate if it was not marked as wildcard before but this source says it is wildcard + // then we should mark it as wildcard + if !uniqueMap[subdomain].WildcardCertificate && isWildcard { + val := uniqueMap[subdomain] + val.WildcardCertificate = true + uniqueMap[subdomain] = val + } continue } - hostEntry := resolve.HostEntry{Domain: domain, Host: subdomain, Source: result.Source} + hostEntry := resolve.HostEntry{Domain: domain, Host: subdomain, Source: result.Source, WildcardCertificate: isWildcard} if r.options.ResultCallback != nil && !r.options.RemoveWildcard { r.options.ResultCallback(&hostEntry) } @@ -112,6 +122,7 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string if r.options.RemoveWildcard { close(resolutionPool.Tasks) } + wg.Done() }() @@ -129,11 +140,21 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string if _, ok := foundResults[result.Host]; !ok { foundResults[result.Host] = result if r.options.ResultCallback != nil { - r.options.ResultCallback(&resolve.HostEntry{Domain: domain, Host: result.Host, Source: result.Source}) + r.options.ResultCallback(&resolve.HostEntry{Domain: domain, Host: result.Host, Source: result.Source, WildcardCertificate: result.WildcardCertificate}) } } } } + + // Merge wildcard certificate information from uniqueMap into foundResults + // This handles cases where a later source marked a subdomain as wildcard + // after it was already sent to the resolution pool + for host, result := range foundResults { + if entry, ok := uniqueMap[host]; ok && entry.WildcardCertificate && !result.WildcardCertificate { + result.WildcardCertificate = true + foundResults[host] = result + } + } } wg.Wait() outputWriter := NewOutputWriter(r.options.JSON) diff --git a/pkg/runner/outputter.go b/pkg/runner/outputter.go index cfa4fc1a..e3f1839c 100644 --- a/pkg/runner/outputter.go +++ b/pkg/runner/outputter.go @@ -19,22 +19,25 @@ type OutputWriter struct { } type jsonSourceResult struct { - Host string `json:"host"` - Input string `json:"input"` - Source string `json:"source"` + Host string `json:"host"` + Input string `json:"input"` + Source string `json:"source"` + WildcardCertificate bool `json:"wildcard_certificate,omitempty"` } type jsonSourceIPResult struct { - Host string `json:"host"` - IP string `json:"ip"` - Input string `json:"input"` - Source string `json:"source"` + Host string `json:"host"` + IP string `json:"ip"` + Input string `json:"input"` + Source string `json:"source"` + WildcardCertificate bool `json:"wildcard_certificate,omitempty"` } type jsonSourcesResult struct { - Host string `json:"host"` - Input string `json:"input"` - Sources []string `json:"sources"` + Host string `json:"host"` + Input string `json:"input"` + Sources []string `json:"sources"` + WildcardCertificate bool `json:"wildcard_certificate,omitempty"` } // NewOutputWriter creates a new OutputWriter @@ -117,7 +120,7 @@ func writeJSONHostIP(input string, results map[string]resolve.Result, writer io. data.IP = result.IP data.Input = input data.Source = result.Source - + data.WildcardCertificate = result.WildcardCertificate err := encoder.Encode(&data) if err != nil { return err @@ -130,7 +133,7 @@ func writeJSONHostIP(input string, results map[string]resolve.Result, writer io. func (o *OutputWriter) WriteHostNoWildcard(input string, results map[string]resolve.Result, writer io.Writer) error { hosts := make(map[string]resolve.HostEntry) for host, result := range results { - hosts[host] = resolve.HostEntry{Domain: host, Host: result.Host, Source: result.Source} + hosts[host] = resolve.HostEntry{Domain: host, Host: result.Host, Source: result.Source, WildcardCertificate: result.WildcardCertificate} } return o.WriteHost(input, hosts, writer) @@ -175,6 +178,7 @@ func writeJSONHost(input string, results map[string]resolve.HostEntry, writer io data.Host = result.Host data.Input = input data.Source = result.Source + data.WildcardCertificate = result.WildcardCertificate err := encoder.Encode(data) if err != nil { return err