package gobusterdns import ( "bufio" "bytes" "context" "fmt" "net" "net/netip" "strings" "text/tabwriter" "time" "git.sual.in/casual/gobuster-lib/libgobuster" "github.com/google/uuid" ) // ErrWildcard is returned if a wildcard response is found type ErrWildcard struct { wildcardIps libgobuster.Set[netip.Addr] } // Error is the implementation of the error interface func (e *ErrWildcard) Error() string { return fmt.Sprintf("the DNS Server returned the same IP for every domain. IP address(es) returned: %s", e.wildcardIps.Stringify()) } // GobusterDNS is the main type to implement the interface type GobusterDNS struct { resolver *net.Resolver globalopts *libgobuster.Options options *OptionsDNS isWildcard bool wildcardIps libgobuster.Set[netip.Addr] } func newCustomDialer(server string) func(ctx context.Context, network, address string) (net.Conn, error) { return func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{} if !strings.Contains(server, ":") { server = fmt.Sprintf("%s:53", server) } return d.DialContext(ctx, "udp", server) } } // NewGobusterDNS creates a new initialized GobusterDNS func NewGobusterDNS(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDNS, error) { if globalopts == nil { return nil, fmt.Errorf("please provide valid global options") } if opts == nil { return nil, fmt.Errorf("please provide valid plugin options") } resolver := net.DefaultResolver if opts.Resolver != "" { resolver = &net.Resolver{ PreferGo: true, Dial: newCustomDialer(opts.Resolver), } } g := GobusterDNS{ options: opts, globalopts: globalopts, wildcardIps: libgobuster.NewSet[netip.Addr](), resolver: resolver, } return &g, nil } // Name should return the name of the plugin func (d *GobusterDNS) Name() string { return "DNS enumeration" } // PreRun is the pre run implementation of gobusterdns func (d *GobusterDNS) PreRun(ctx context.Context, progress *libgobuster.Progress) error { // Resolve a subdomain that probably shouldn't exist guid := uuid.New() wildcardIps, err := d.dnsLookup(ctx, fmt.Sprintf("%s.%s", guid, d.options.Domain)) if err == nil { d.isWildcard = true d.wildcardIps.AddRange(wildcardIps) if !d.options.WildcardForced { return &ErrWildcard{wildcardIps: d.wildcardIps} } } if !d.globalopts.Quiet { // Provide a warning if the base domain doesn't resolve (in case of typo) _, err = d.dnsLookup(ctx, d.options.Domain) if err != nil { // Not an error, just a warning. Eg. `yp.to` doesn't resolve, but `cr.yp.to` does! progress.MessageChan <- libgobuster.Message{ Level: libgobuster.LevelInfo, Message: fmt.Sprintf("[-] Unable to validate base domain: %s (%v)", d.options.Domain, err), } progress.MessageChan <- libgobuster.Message{ Level: libgobuster.LevelDebug, Message: fmt.Sprintf("%#v", err), } } } return nil } // ProcessWord is the process implementation of gobusterdns func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain) if !d.options.NoFQDN && !strings.HasSuffix(subdomain, ".") { // add a . to indicate this is the full domain and we do not want to traverse the search domains on the system subdomain = fmt.Sprintf("%s.", subdomain) } ips, err := d.dnsLookup(ctx, subdomain) if err == nil { if !d.isWildcard || !d.wildcardIps.ContainsAny(ips) { result := Result{ Subdomain: subdomain, Found: true, ShowIPs: d.options.ShowIPs, ShowCNAME: d.options.ShowCNAME, NoFQDN: d.options.NoFQDN, } if d.options.ShowIPs { result.IPs = ips } else if d.options.ShowCNAME { cname, err := d.dnsLookupCname(ctx, subdomain) if err == nil { result.CNAME = cname } } progress.ResultChan <- result } } else if d.globalopts.Verbose { progress.ResultChan <- Result{ Subdomain: subdomain, Found: false, ShowIPs: d.options.ShowIPs, ShowCNAME: d.options.ShowCNAME, } } return nil } func (d *GobusterDNS) AdditionalWords(word string) []string { return []string{} } // GetConfigString returns the string representation of the current config func (d *GobusterDNS) GetConfigString() (string, error) { var buffer bytes.Buffer bw := bufio.NewWriter(&buffer) tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0) o := d.options if _, err := fmt.Fprintf(tw, "[+] Domain:\t%s\n", o.Domain); err != nil { return "", err } if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil { return "", err } if d.globalopts.Delay > 0 { if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil { return "", err } } if o.Resolver != "" { if _, err := fmt.Fprintf(tw, "[+] Resolver:\t%s\n", o.Resolver); err != nil { return "", err } } if o.ShowCNAME { if _, err := fmt.Fprintf(tw, "[+] Show CNAME:\ttrue\n"); err != nil { return "", err } } if o.ShowIPs { if _, err := fmt.Fprintf(tw, "[+] Show IPs:\ttrue\n"); err != nil { return "", err } } if o.WildcardForced { if _, err := fmt.Fprintf(tw, "[+] Wildcard forced:\ttrue\n"); err != nil { return "", err } } if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { return "", err } wordlist := "stdin (pipe)" if d.globalopts.Wordlist != "-" { wordlist = d.globalopts.Wordlist } if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { return "", err } if d.globalopts.PatternFile != "" { if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil { return "", err } } if d.globalopts.Verbose { if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { return "", err } } if err := tw.Flush(); err != nil { return "", fmt.Errorf("error on tostring: %w", err) } if err := bw.Flush(); err != nil { return "", fmt.Errorf("error on tostring: %w", err) } return strings.TrimSpace(buffer.String()), nil } func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([]netip.Addr, error) { ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout) defer cancel() return d.resolver.LookupNetIP(ctx2, "ip", domain) } func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain string) (string, error) { ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout) defer cancel() time.Sleep(time.Second) return d.resolver.LookupCNAME(ctx2, domain) }