mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-18 15:35:28 +00:00
poc implementation of dns templating
This commit is contained in:
parent
dc6b010625
commit
603456ddbb
@ -7,6 +7,15 @@ import (
|
|||||||
"github.com/projectdiscovery/gologger"
|
"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
|
// Options contains the configuration options for tuning
|
||||||
// the template requesting process.
|
// the template requesting process.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
|||||||
@ -13,17 +13,20 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
"github.com/projectdiscovery/gologger"
|
"github.com/projectdiscovery/gologger"
|
||||||
"github.com/projectdiscovery/nuclei/pkg/extractors"
|
"github.com/projectdiscovery/nuclei/pkg/extractors"
|
||||||
"github.com/projectdiscovery/nuclei/pkg/matchers"
|
"github.com/projectdiscovery/nuclei/pkg/matchers"
|
||||||
"github.com/projectdiscovery/nuclei/pkg/templates"
|
"github.com/projectdiscovery/nuclei/pkg/templates"
|
||||||
|
retryabledns "github.com/projectdiscovery/retryabledns"
|
||||||
retryablehttp "github.com/projectdiscovery/retryablehttp-go"
|
retryablehttp "github.com/projectdiscovery/retryablehttp-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Runner is a client for running the enumeration process.
|
// Runner is a client for running the enumeration process.
|
||||||
type Runner struct {
|
type Runner struct {
|
||||||
// client is the http client with retries
|
// httpClient is the http client with retries
|
||||||
client *retryablehttp.Client
|
httpClient *retryablehttp.Client
|
||||||
|
dnsClient *retryabledns.Client
|
||||||
// output is the output file to write if any
|
// output is the output file to write if any
|
||||||
output *os.File
|
output *os.File
|
||||||
outputMutex *sync.Mutex
|
outputMutex *sync.Mutex
|
||||||
@ -43,7 +46,7 @@ func New(options *Options) (*Runner, error) {
|
|||||||
retryablehttpOptions.RetryMax = options.Retries
|
retryablehttpOptions.RetryMax = options.Retries
|
||||||
|
|
||||||
// Create the HTTP Client
|
// Create the HTTP Client
|
||||||
client := retryablehttp.NewWithHTTPClient(&http.Client{
|
httpClient := retryablehttp.NewWithHTTPClient(&http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
MaxIdleConnsPerHost: -1,
|
MaxIdleConnsPerHost: -1,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
@ -57,9 +60,13 @@ func New(options *Options) (*Runner, error) {
|
|||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
},
|
},
|
||||||
}, retryablehttpOptions)
|
}, 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
|
// Create the output file if asked
|
||||||
if options.Output != "" {
|
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
|
// sendRequest sends a request to the target based on a template
|
||||||
func (r *Runner) sendRequest(template *templates.Template, URL string, writer *bufio.Writer) {
|
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
|
// Compile each request for the template based on the URL
|
||||||
compiledRequest, err := request.MakeRequest(URL)
|
compiledRequest, err := request.MakeHTTPRequest(URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gologger.Warningf("[%s] Could not make request %s: %s\n", template.ID, URL, err)
|
gologger.Warningf("[%s] Could not make request %s: %s\n", template.ID, URL, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the request to the target servers
|
// Send the request to the target servers
|
||||||
reqLoop:
|
reqLoopHTTP:
|
||||||
for _, req := range compiledRequest {
|
for _, req := range compiledRequest {
|
||||||
resp, err := r.client.Do(req)
|
resp, err := r.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
@ -206,7 +214,7 @@ func (r *Runner) sendRequest(template *templates.Template, URL string, writer *b
|
|||||||
|
|
||||||
// Check if the matcher matched
|
// Check if the matcher matched
|
||||||
if !matcher.Match(resp, body, headers) {
|
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
|
// buildOutput builds an output text for writing results
|
||||||
@ -259,3 +307,32 @@ func buildOutput(template *templates.Template, req *retryablehttp.Request, extra
|
|||||||
|
|
||||||
return builder.String()
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@ -2,8 +2,11 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
)
|
)
|
||||||
|
|
||||||
// unsafeToString converts byte slice to string with zero allocations
|
// unsafeToString converts byte slice to string with zero allocations
|
||||||
@ -29,3 +32,25 @@ func headersToString(headers http.Header) string {
|
|||||||
}
|
}
|
||||||
return builder.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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
// extractRegex extracts text from a corpus and returns it
|
||||||
func (e *Extractor) extractRegex(corpus string) []string {
|
func (e *Extractor) extractRegex(corpus string) []string {
|
||||||
results := []string{}
|
results := []string{}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Match matches a http response again a given matcher
|
// 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
|
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
|
// matchStatusCode matches a status code check against an HTTP Response
|
||||||
func (m *Matcher) matchStatusCode(statusCode int) bool {
|
func (m *Matcher) matchStatusCode(statusCode int) bool {
|
||||||
// Iterate over all the status codes accepted as valid
|
// Iterate over all the status codes accepted as valid
|
||||||
|
|||||||
112
pkg/requests/dns-question.go
Normal file
112
pkg/requests/dns-question.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/valyala/fasttemplate"
|
"github.com/valyala/fasttemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request contains a request to be made from a template
|
// HTTPRequest contains a request to be made from a template
|
||||||
type Request struct {
|
type HTTPRequest struct {
|
||||||
// Method is the request method, whether GET, POST, PUT, etc
|
// Method is the request method, whether GET, POST, PUT, etc
|
||||||
Method string `yaml:"method"`
|
Method string `yaml:"method"`
|
||||||
// Path contains the path/s for the request
|
// Path contains the path/s for the request
|
||||||
@ -30,8 +30,8 @@ type Request struct {
|
|||||||
Extractors []*extractors.Extractor `yaml:"extractors,omitempty"`
|
Extractors []*extractors.Extractor `yaml:"extractors,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeRequest creates a *http.Request from a request template
|
// MakeHTTPRequest creates a *http.Request from a request template
|
||||||
func (r *Request) MakeRequest(baseURL string) ([]*retryablehttp.Request, error) {
|
func (r *HTTPRequest) MakeHTTPRequest(baseURL string) ([]*retryablehttp.Request, error) {
|
||||||
parsed, err := url.Parse(baseURL)
|
parsed, err := url.Parse(baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -22,8 +22,23 @@ func ParseTemplate(file string) (*Template, error) {
|
|||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
// Compile the matchers and the extractors
|
// Compile the matchers and the extractors for http requests
|
||||||
for _, request := range template.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 {
|
for _, matcher := range request.Matchers {
|
||||||
if err = matcher.CompileMatchers(); err != nil {
|
if err = matcher.CompileMatchers(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -10,8 +10,10 @@ type Template struct {
|
|||||||
ID string `yaml:"id"`
|
ID string `yaml:"id"`
|
||||||
// Info contains information about the template
|
// Info contains information about the template
|
||||||
Info Info `yaml:"info"`
|
Info Info `yaml:"info"`
|
||||||
// Request contains the request to make in the template
|
// Request contains the http request to make in the template
|
||||||
Requests []*requests.Request `yaml:"requests"`
|
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
|
// Info contains information about the request template
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user