diff --git a/internal/runner/options.go b/internal/runner/options.go index 53321dcd4..91eb0988a 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -7,6 +7,15 @@ import ( "github.com/projectdiscovery/gologger" ) +// DefaultResolvers contains the list of resolvers known to be trusted. +var DefaultResolvers = []string{ + "1.1.1.1:53", // Cloudflare + "1.0.0.1:53", // Cloudflare + "8.8.8.8:53", // Google + "8.8.4.4:53", // Google + "9.9.9.9:53", // Quad9 +} + // Options contains the configuration options for tuning // the template requesting process. type Options struct { diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 7e32ffb42..0ff240614 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -13,17 +13,20 @@ import ( "sync" "time" + "github.com/miekg/dns" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/pkg/extractors" "github.com/projectdiscovery/nuclei/pkg/matchers" "github.com/projectdiscovery/nuclei/pkg/templates" + retryabledns "github.com/projectdiscovery/retryabledns" retryablehttp "github.com/projectdiscovery/retryablehttp-go" ) // Runner is a client for running the enumeration process. type Runner struct { - // client is the http client with retries - client *retryablehttp.Client + // httpClient is the http client with retries + httpClient *retryablehttp.Client + dnsClient *retryabledns.Client // output is the output file to write if any output *os.File outputMutex *sync.Mutex @@ -43,7 +46,7 @@ func New(options *Options) (*Runner, error) { retryablehttpOptions.RetryMax = options.Retries // Create the HTTP Client - client := retryablehttp.NewWithHTTPClient(&http.Client{ + httpClient := retryablehttp.NewWithHTTPClient(&http.Client{ Transport: &http.Transport{ MaxIdleConnsPerHost: -1, TLSClientConfig: &tls.Config{ @@ -57,9 +60,13 @@ func New(options *Options) (*Runner, error) { return http.ErrUseLastResponse }, }, retryablehttpOptions) - client.CheckRetry = retryablehttp.HostSprayRetryPolicy() + httpClient.CheckRetry = retryablehttp.HostSprayRetryPolicy() - runner.client = client + runner.httpClient = httpClient + + // Create the dns client + dnsClient, _ := retryabledns.New(DefaultResolvers, options.Retries) + runner.dnsClient = dnsClient // Create the output file if asked if options.Output != "" { @@ -165,18 +172,19 @@ func (r *Runner) processTemplateWithList(template *templates.Template, reader io // sendRequest sends a request to the target based on a template func (r *Runner) sendRequest(template *templates.Template, URL string, writer *bufio.Writer) { - for _, request := range template.Requests { + // process http requests + for _, request := range template.RequestsHTTP { // Compile each request for the template based on the URL - compiledRequest, err := request.MakeRequest(URL) + compiledRequest, err := request.MakeHTTPRequest(URL) if err != nil { gologger.Warningf("[%s] Could not make request %s: %s\n", template.ID, URL, err) continue } // Send the request to the target servers - reqLoop: + reqLoopHTTP: for _, req := range compiledRequest { - resp, err := r.client.Do(req) + resp, err := r.httpClient.Do(req) if err != nil { if resp != nil { resp.Body.Close() @@ -206,7 +214,7 @@ func (r *Runner) sendRequest(template *templates.Template, URL string, writer *b // Check if the matcher matched if !matcher.Match(resp, body, headers) { - continue reqLoop + continue reqLoopHTTP } } @@ -231,6 +239,46 @@ func (r *Runner) sendRequest(template *templates.Template, URL string, writer *b } } } + + // process dns messages + for _, request := range template.RequestsDNS { + // Compile each request for the template based on the URL + compiledRequest, err := request.MakeDNSRequest(URL) + if err != nil { + gologger.Warningf("[%s] Could not make request %s: %s\n", template.ID, URL, err) + continue + } + + // Send the request to the target servers + resp, err := r.dnsClient.Do(compiledRequest) + if err != nil { + gologger.Warningf("[%s] Could not send request %s: %s\n", template.ID, URL, err) + return + } + + for _, matcher := range request.Matchers { + // Check if the matcher matched + if !matcher.MatchDNS(resp) { + continue + } + } + + // If there is an extractor, run it. + var extractorResults []string + for _, extractor := range request.Extractors { + extractorResults = append(extractorResults, extractor.ExtractDNS(resp.String())...) + } + + // All the matchers matched, print the output on the screen + output := buildOutputDNS(template, resp, extractorResults) + gologger.Silentf("%s", output) + + if writer != nil { + r.outputMutex.Lock() + writer.WriteString(output) + r.outputMutex.Unlock() + } + } } // buildOutput builds an output text for writing results @@ -259,3 +307,32 @@ func buildOutput(template *templates.Template, req *retryablehttp.Request, extra return builder.String() } + +// buildOutput builds an output text for writing results +func buildOutputDNS(template *templates.Template, msg *dns.Msg, extractorResults []string) string { + builder := &strings.Builder{} + builder.WriteRune('[') + builder.WriteString(template.ID) + builder.WriteString("] ") + + // domain name from question + if len(msg.Question) > 0 { + domain := msg.Question[0].Name + builder.WriteString(domain) + } + + // If any extractors, write the results + if len(extractorResults) > 0 { + builder.WriteString(" [") + for i, result := range extractorResults { + builder.WriteString(result) + if i != len(extractorResults)-1 { + builder.WriteRune(',') + } + } + builder.WriteString("]") + } + builder.WriteRune('\n') + + return builder.String() +} diff --git a/internal/runner/utils.go b/internal/runner/utils.go index ab82869ce..f16b33dde 100644 --- a/internal/runner/utils.go +++ b/internal/runner/utils.go @@ -2,8 +2,11 @@ package runner import ( "net/http" + "net/url" "strings" "unsafe" + + "github.com/asaskevich/govalidator" ) // unsafeToString converts byte slice to string with zero allocations @@ -29,3 +32,25 @@ func headersToString(headers http.Header) string { } return builder.String() } + +// isURL tests a string to determine if it is a well-structured url or not. +func isURL(toTest string) bool { + _, err := url.ParseRequestURI(toTest) + if err != nil { + return false + } + + u, err := url.Parse(toTest) + if err != nil || u.Scheme == "" || u.Host == "" { + return false + } + + return true +} + +// isDNS tests a string to determine if it is a well-structured dns or not +// even if it's oneliner, we leave it wrapped in a function call for +// future improvements +func isDNS(toTest string) bool { + return govalidator.IsDNSName(toTest) +} diff --git a/pkg/extractors/extract.go b/pkg/extractors/extract.go index 0391bc7bc..5988759af 100644 --- a/pkg/extractors/extract.go +++ b/pkg/extractors/extract.go @@ -16,6 +16,12 @@ func (e *Extractor) Extract(body, headers string) []string { } } +// ExtractDNS extracts response from dns message using a regex +func (e *Extractor) ExtractDNS(msg string) []string { + // Match the parts as required for regex check + return e.extractRegex(msg) +} + // extractRegex extracts text from a corpus and returns it func (e *Extractor) extractRegex(corpus string) []string { results := []string{} diff --git a/pkg/matchers/match.go b/pkg/matchers/match.go index 50d3933b3..ac2931936 100644 --- a/pkg/matchers/match.go +++ b/pkg/matchers/match.go @@ -4,6 +4,8 @@ import ( "encoding/hex" "net/http" "strings" + + "github.com/miekg/dns" ) // Match matches a http response again a given matcher @@ -53,6 +55,25 @@ func (m *Matcher) Match(resp *http.Response, body, headers string) bool { return false } +// MatchDNS matches a dns response again a given matcher +func (m *Matcher) MatchDNS(msg *dns.Msg) bool { + switch m.matcherType { + // [WIP] add dns status code matcher + case SizeMatcher: + return m.matchSizeCode(msg.Len()) + case WordsMatcher: + // Match for word check + return m.matchWords(msg.String()) + case RegexMatcher: + // Match regex check + return m.matchRegex(msg.String()) + case BinaryMatcher: + // Match binary characters check + return m.matchBinary(msg.String()) + } + return false +} + // matchStatusCode matches a status code check against an HTTP Response func (m *Matcher) matchStatusCode(statusCode int) bool { // Iterate over all the status codes accepted as valid diff --git a/pkg/requests/dns-question.go b/pkg/requests/dns-question.go new file mode 100644 index 000000000..378a05211 --- /dev/null +++ b/pkg/requests/dns-question.go @@ -0,0 +1,112 @@ +package requests + +import ( + "fmt" + "strings" + + "github.com/miekg/dns" + "github.com/projectdiscovery/nuclei/pkg/extractors" + "github.com/projectdiscovery/nuclei/pkg/matchers" + "github.com/valyala/fasttemplate" +) + +// DNSRequest contains a request to be made from a template +type DNSRequest struct { + Recursion bool `yaml:"recursion"` + // Path contains the path/s for the request + Name string `yaml:"name"` + Type string `yaml:"type"` + Class string `yaml:"class"` + + // Matchers contains the detection mechanism for the request to identify + // whether the request was successful + Matchers []*matchers.Matcher `yaml:"matchers,omitempty"` + // Extractors contains the extraction mechanism for the request to identify + // and extract parts of the response. + Extractors []*extractors.Extractor `yaml:"extractors,omitempty"` +} + +// MakeDNSRequest creates a *dns.Request from a request template +func (r *DNSRequest) MakeDNSRequest(domain string) (*dns.Msg, error) { + + domain = dns.Fqdn(domain) + + // Build a request on the specified URL + req := new(dns.Msg) + req.Id = dns.Id() + req.RecursionDesired = r.Recursion + + var q dns.Question + + t := fasttemplate.New(r.Name, "{{", "}}") + q.Name = dns.Fqdn(t.ExecuteString(map[string]interface{}{ + "FQDN": domain, + })) + + qclass, err := toQClass(r.Class) + if err != nil { + return nil, err + } + q.Qclass = qclass + + qtype, err := toQType(r.Type) + if err != nil { + return nil, err + } + q.Qtype = qtype + + req.Question = append(req.Question, q) + + return req, nil +} + +func toQType(ttype string) (rtype uint16, err error) { + ttype = strings.TrimSpace(strings.ToUpper(ttype)) + + switch ttype { + case "A": + rtype = dns.TypeA + case "NS": + rtype = dns.TypeNS + case "CNAME": + rtype = dns.TypeCNAME + case "SOA": + rtype = dns.TypeSOA + case "PTR": + rtype = dns.TypePTR + case "MX": + rtype = dns.TypeMX + case "TXT": + rtype = dns.TypeTXT + case "AAAA": + rtype = dns.TypeAAAA + default: + rtype = dns.TypeNone + err = fmt.Errorf("incorrect type") + } + + return +} + +func toQClass(tclass string) (rclass uint16, err error) { + tclass = strings.TrimSpace(strings.ToUpper(tclass)) + + switch tclass { + case "INET": + rclass = dns.ClassINET + case "CSNET": + rclass = dns.ClassCSNET + case "CHAOS": + rclass = dns.ClassCHAOS + case "HESIOD": + rclass = dns.ClassHESIOD + case "NONE": + rclass = dns.ClassNONE + case "ANY": + rclass = dns.ClassANY + default: + err = fmt.Errorf("incorrect class") + } + + return +} diff --git a/pkg/requests/requests.go b/pkg/requests/http-request.go similarity index 90% rename from pkg/requests/requests.go rename to pkg/requests/http-request.go index 307a960e1..86e3cfa1c 100644 --- a/pkg/requests/requests.go +++ b/pkg/requests/http-request.go @@ -12,8 +12,8 @@ import ( "github.com/valyala/fasttemplate" ) -// Request contains a request to be made from a template -type Request struct { +// HTTPRequest contains a request to be made from a template +type HTTPRequest struct { // Method is the request method, whether GET, POST, PUT, etc Method string `yaml:"method"` // Path contains the path/s for the request @@ -30,8 +30,8 @@ type Request struct { Extractors []*extractors.Extractor `yaml:"extractors,omitempty"` } -// MakeRequest creates a *http.Request from a request template -func (r *Request) MakeRequest(baseURL string) ([]*retryablehttp.Request, error) { +// MakeHTTPRequest creates a *http.Request from a request template +func (r *HTTPRequest) MakeHTTPRequest(baseURL string) ([]*retryablehttp.Request, error) { parsed, err := url.Parse(baseURL) if err != nil { return nil, err diff --git a/pkg/templates/compile.go b/pkg/templates/compile.go index 25c58a0f4..cfe7a8a66 100644 --- a/pkg/templates/compile.go +++ b/pkg/templates/compile.go @@ -22,8 +22,23 @@ func ParseTemplate(file string) (*Template, error) { } f.Close() - // Compile the matchers and the extractors - for _, request := range template.Requests { + // Compile the matchers and the extractors for http requests + for _, request := range template.RequestsHTTP { + for _, matcher := range request.Matchers { + if err = matcher.CompileMatchers(); err != nil { + return nil, err + } + } + + for _, extractor := range request.Extractors { + if err := extractor.CompileExtractors(); err != nil { + return nil, err + } + } + } + + // Compile the matchers and the extractors for dns requests + for _, request := range template.RequestsDNS { for _, matcher := range request.Matchers { if err = matcher.CompileMatchers(); err != nil { return nil, err diff --git a/pkg/templates/templates.go b/pkg/templates/templates.go index 1e73a5eee..8c9352368 100644 --- a/pkg/templates/templates.go +++ b/pkg/templates/templates.go @@ -10,8 +10,10 @@ type Template struct { ID string `yaml:"id"` // Info contains information about the template Info Info `yaml:"info"` - // Request contains the request to make in the template - Requests []*requests.Request `yaml:"requests"` + // Request contains the http request to make in the template + RequestsHTTP []*requests.HTTPRequest `yaml:"requests"` + // Request contains the dns request to make in the template + RequestsDNS []*requests.DNSRequest `yaml:"dns"` } // Info contains information about the request template