mirror of
https://github.com/projectdiscovery/subfinder
synced 2026-06-21 14:05:24 +00:00
Add wildcard certificate detection in JSON output (#1665)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -8,4 +8,5 @@ vendor/
|
||||
.idea
|
||||
.devcontainer
|
||||
.vscode
|
||||
dist
|
||||
dist
|
||||
/subfinder
|
||||
+1
-1
@@ -13,7 +13,7 @@ builds:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- '386'
|
||||
- arm
|
||||
- arm64
|
||||
|
||||
|
||||
+13
-11
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+23
-2
@@ -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)
|
||||
|
||||
+16
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user