diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index 0b4e2a76..03fb105b 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: "Set up Go" uses: actions/setup-go@v3 @@ -36,7 +38,6 @@ jobs: run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git pull git add SYNTAX-REFERENCE.md nuclei-jsonschema.json git commit -m "Auto Generate Syntax Docs + JSONSchema [$(date)] :robot:" -a diff --git a/SYNTAX-REFERENCE.md b/SYNTAX-REFERENCE.md index 7308db44..b0311441 100755 --- a/SYNTAX-REFERENCE.md +++ b/SYNTAX-REFERENCE.md @@ -3519,6 +3519,19 @@ description: |
+stop-at-first-match bool + +
+
+ +StopAtFirstMatch stops the execution of the requests and template as soon as a match is found. + +
+ +
+ +
+ matchers []matchers.Matcher
diff --git a/nuclei-jsonschema.json b/nuclei-jsonschema.json index ef08a307..cf9b5fd6 100644 --- a/nuclei-jsonschema.json +++ b/nuclei-jsonschema.json @@ -576,6 +576,11 @@ "title": "custom user agent for the headless request", "description": "Custom user agent for the headless request" }, + "stop-at-first-match": { + "type": "boolean", + "title": "stop at first match", + "description": "Stop the execution after a match is found" + }, "matchers": { "items": { "$ref": "#/definitions/matchers.Matcher" diff --git a/v2/pkg/core/inputs/hybrid/hmap.go b/v2/pkg/core/inputs/hybrid/hmap.go index 3937e283..82d2c94d 100644 --- a/v2/pkg/core/inputs/hybrid/hmap.go +++ b/v2/pkg/core/inputs/hybrid/hmap.go @@ -5,7 +5,7 @@ package hybrid import ( "bufio" "io" - "net/url" + "net" "os" "strings" "sync" @@ -22,6 +22,7 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/uncover" "github.com/projectdiscovery/nuclei/v2/pkg/types" + "github.com/projectdiscovery/nuclei/v2/pkg/utils" fileutil "github.com/projectdiscovery/utils/file" iputil "github.com/projectdiscovery/utils/ip" readerutil "github.com/projectdiscovery/utils/reader" @@ -169,39 +170,49 @@ func (i *Input) Set(value string) { if URL == "" { return } - // actual hostname - var host string // parse hostname if url is given - parsedURL, err := url.Parse(value) - if err == nil && parsedURL.Host != "" { - host = parsedURL.Host + host := utils.ParseHostname(value) + if host == "" { + // not a valid url hence scanallips is skipped + gologger.Debug().Msgf("scanAllIps: failed to parse hostname of %v falling back to default", value) + i.setItem(&contextargs.MetaInput{Input: value}) + return } else { - parsedURL = nil - host = value + // case when hostname contains port + hostwithoutport, _, erx := net.SplitHostPort(host) + if erx == nil && hostwithoutport != "" { + // given host contains port + host = hostwithoutport + } } if i.ipOptions.ScanAllIPs { // scan all ips dnsData, err := protocolstate.Dialer.GetDNSData(host) - if err == nil && (len(dnsData.A)+len(dnsData.AAAA)) > 0 { - var ips []string - if i.ipOptions.IPV4 { - ips = append(ips, dnsData.A...) - } - if i.ipOptions.IPV6 { - ips = append(ips, dnsData.AAAA...) - } - for _, ip := range ips { - if ip == "" { - continue + if err == nil { + if (len(dnsData.A) + len(dnsData.AAAA)) > 0 { + var ips []string + if i.ipOptions.IPV4 { + ips = append(ips, dnsData.A...) } - metaInput := &contextargs.MetaInput{Input: value, CustomIP: ip} - i.setItem(metaInput) + if i.ipOptions.IPV6 { + ips = append(ips, dnsData.AAAA...) + } + for _, ip := range ips { + if ip == "" { + continue + } + metaInput := &contextargs.MetaInput{Input: value, CustomIP: ip} + i.setItem(metaInput) + } + return + } else { + gologger.Debug().Msgf("scanAllIps: no ip's found reverting to default") } - return + } else { + // failed to scanallips falling back to defaults + gologger.Debug().Msgf("scanAllIps: dns resolution failed: %v", err) } - // failed to scanallips falling back to defaults - gologger.Error().Msgf("failed to scan all ips reverting to default %v", err) } ips := []string{} @@ -212,7 +223,7 @@ func (i *Input) Set(value string) { // pick/ prefer 1st ips = append(ips, dnsData.AAAA[0]) } else { - gologger.Warning().Msgf("target does not have ipv6 address falling back to ipv4 %s\n", err) + gologger.Warning().Msgf("target does not have ipv6 address falling back to ipv4 %v\n", err) } } if i.ipOptions.IPV4 { diff --git a/v2/pkg/core/inputs/hybrid/hmap_test.go b/v2/pkg/core/inputs/hybrid/hmap_test.go index c4aa453e..60f12972 100644 --- a/v2/pkg/core/inputs/hybrid/hmap_test.go +++ b/v2/pkg/core/inputs/hybrid/hmap_test.go @@ -51,7 +51,7 @@ func Test_expandCIDRInputValue(t *testing.T) { type mockDnsHandler struct{} -func (this *mockDnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { +func (m *mockDnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { msg := dns.Msg{} msg.SetReply(r) switch r.Question[0].Qtype { @@ -85,18 +85,14 @@ func Test_scanallips_normalizeStoreInputValue(t *testing.T) { defaultOpts := types.DefaultOptions() defaultOpts.InternalResolversList = []string{"127.0.0.1:61234"} _ = protocolstate.Init(defaultOpts) - tests := []struct { + type testcase struct { hostname string ipv4 bool ipv6 bool expected []string - }{ + } + tests := []testcase{ { - hostname: "scanme.sh", - ipv4: true, - ipv6: true, - expected: []string{"128.199.158.128", "2400:6180:0:d0::91:1001"}, - }, { hostname: "scanme.sh", ipv4: true, expected: []string{"128.199.158.128"}, @@ -104,12 +100,27 @@ func Test_scanallips_normalizeStoreInputValue(t *testing.T) { hostname: "scanme.sh", ipv6: true, expected: []string{"2400:6180:0:d0::91:1001"}, - }, { - hostname: "http://scanme.sh", + }, + } + // add extra edge cases + urls := []string{ + "https://scanme.sh/", + "http://scanme.sh", + "https://scanme.sh:443/", + "https://scanme.sh:443/somepath", + "http://scanme.sh:80/?with=param", + "scanme.sh/home", + "scanme.sh", + } + resolvedIps := []string{"128.199.158.128", "2400:6180:0:d0::91:1001"} + + for _, v := range urls { + tests = append(tests, testcase{ + hostname: v, ipv4: true, ipv6: true, - expected: []string{"128.199.158.128", "2400:6180:0:d0::91:1001"}, - }, + expected: resolvedIps, + }) } for _, tt := range tests { hm, err := hybrid.New(hybrid.DefaultDiskOptions) @@ -134,7 +145,7 @@ func Test_scanallips_normalizeStoreInputValue(t *testing.T) { got = append(got, metainput.CustomIP) return nil }) - require.ElementsMatch(t, tt.expected, got, "could not get correct ips") + require.ElementsMatchf(t, tt.expected, got, "could not get correct ips for hostname %v", tt.hostname) input.Close() } } diff --git a/v2/pkg/templates/templates_doc.go b/v2/pkg/templates/templates_doc.go index f85b3567..d96fbe4a 100644 --- a/v2/pkg/templates/templates_doc.go +++ b/v2/pkg/templates/templates_doc.go @@ -1542,7 +1542,7 @@ func init() { Value: "Headless response received from client (default)", }, } - HEADLESSRequestDoc.Fields = make([]encoder.Doc, 9) + HEADLESSRequestDoc.Fields = make([]encoder.Doc, 10) HEADLESSRequestDoc.Fields[0].Name = "id" HEADLESSRequestDoc.Fields[0].Type = "string" HEADLESSRequestDoc.Fields[0].Note = "" @@ -1573,22 +1573,27 @@ func init() { HEADLESSRequestDoc.Fields[5].Note = "" HEADLESSRequestDoc.Fields[5].Description = "description: |\n If UserAgent is set to custom, customUserAgent is the custom user-agent to use for the request." HEADLESSRequestDoc.Fields[5].Comments[encoder.LineComment] = " description: |" - HEADLESSRequestDoc.Fields[6].Name = "matchers" - HEADLESSRequestDoc.Fields[6].Type = "[]matchers.Matcher" + HEADLESSRequestDoc.Fields[6].Name = "stop-at-first-match" + HEADLESSRequestDoc.Fields[6].Type = "bool" HEADLESSRequestDoc.Fields[6].Note = "" - HEADLESSRequestDoc.Fields[6].Description = "Matchers contains the detection mechanism for the request to identify\nwhether the request was successful by doing pattern matching\non request/responses.\n\nMultiple matchers can be combined with `matcher-condition` flag\nwhich accepts either `and` or `or` as argument." - HEADLESSRequestDoc.Fields[6].Comments[encoder.LineComment] = "Matchers contains the detection mechanism for the request to identify" - HEADLESSRequestDoc.Fields[7].Name = "extractors" - HEADLESSRequestDoc.Fields[7].Type = "[]extractors.Extractor" + HEADLESSRequestDoc.Fields[6].Description = "StopAtFirstMatch stops the execution of the requests and template as soon as a match is found." + HEADLESSRequestDoc.Fields[6].Comments[encoder.LineComment] = "StopAtFirstMatch stops the execution of the requests and template as soon as a match is found." + HEADLESSRequestDoc.Fields[7].Name = "matchers" + HEADLESSRequestDoc.Fields[7].Type = "[]matchers.Matcher" HEADLESSRequestDoc.Fields[7].Note = "" - HEADLESSRequestDoc.Fields[7].Description = "Extractors contains the extraction mechanism for the request to identify\nand extract parts of the response." - HEADLESSRequestDoc.Fields[7].Comments[encoder.LineComment] = "Extractors contains the extraction mechanism for the request to identify" - HEADLESSRequestDoc.Fields[8].Name = "matchers-condition" - HEADLESSRequestDoc.Fields[8].Type = "string" + HEADLESSRequestDoc.Fields[7].Description = "Matchers contains the detection mechanism for the request to identify\nwhether the request was successful by doing pattern matching\non request/responses.\n\nMultiple matchers can be combined with `matcher-condition` flag\nwhich accepts either `and` or `or` as argument." + HEADLESSRequestDoc.Fields[7].Comments[encoder.LineComment] = "Matchers contains the detection mechanism for the request to identify" + HEADLESSRequestDoc.Fields[8].Name = "extractors" + HEADLESSRequestDoc.Fields[8].Type = "[]extractors.Extractor" HEADLESSRequestDoc.Fields[8].Note = "" - HEADLESSRequestDoc.Fields[8].Description = "MatchersCondition is the condition between the matchers. Default is OR." - HEADLESSRequestDoc.Fields[8].Comments[encoder.LineComment] = "MatchersCondition is the condition between the matchers. Default is OR." - HEADLESSRequestDoc.Fields[8].Values = []string{ + HEADLESSRequestDoc.Fields[8].Description = "Extractors contains the extraction mechanism for the request to identify\nand extract parts of the response." + HEADLESSRequestDoc.Fields[8].Comments[encoder.LineComment] = "Extractors contains the extraction mechanism for the request to identify" + HEADLESSRequestDoc.Fields[9].Name = "matchers-condition" + HEADLESSRequestDoc.Fields[9].Type = "string" + HEADLESSRequestDoc.Fields[9].Note = "" + HEADLESSRequestDoc.Fields[9].Description = "MatchersCondition is the condition between the matchers. Default is OR." + HEADLESSRequestDoc.Fields[9].Comments[encoder.LineComment] = "MatchersCondition is the condition between the matchers. Default is OR." + HEADLESSRequestDoc.Fields[9].Values = []string{ "and", "or", } diff --git a/v2/pkg/utils/utils.go b/v2/pkg/utils/utils.go index 0db1e6b3..73efad3d 100644 --- a/v2/pkg/utils/utils.go +++ b/v2/pkg/utils/utils.go @@ -77,3 +77,24 @@ func StringSliceContains(slice []string, item string) bool { } return false } + +// ParseHostname returns hostname +func ParseHostname(inputURL string) string { + /* + currently if URL is scanme.sh/path or scanme.sh:443 i.e without protocol then + url.Parse considers this as valid url but fails to parse hostname + this can be handled by adding schema + */ + input, err := url.Parse(inputURL) + if err != nil { + return "" + } + if input.Host == "" { + newinput, err := url.Parse("https://" + inputURL) + if err != nil { + return "" + } + return newinput.Host + } + return input.Host +} diff --git a/v2/pkg/utils/utils_test.go b/v2/pkg/utils/utils_test.go index ede9d530..49597214 100644 --- a/v2/pkg/utils/utils_test.go +++ b/v2/pkg/utils/utils_test.go @@ -19,3 +19,24 @@ func TestUnwrapError(t *testing.T) { errThree := fmt.Errorf("error with error: %w", errTwo) require.Equal(t, errOne, UnwrapError(errThree)) } + +func TestParseURL(t *testing.T) { + testcases := []struct { + URL string + Hostname string + }{ + {"https://scanme.sh:443", "scanme.sh:443"}, + {"http://scanme.sh/path", "scanme.sh"}, + {"scanme.sh:443/path", "scanme.sh:443"}, + {"scanme.sh/path", "scanme.sh"}, + } + for _, v := range testcases { + urlx := ParseHostname(v.URL) + if urlx == "" { + t.Errorf("failed to hostname of url %v", v) + } + if urlx != v.Hostname { + t.Errorf("hostname mismatch expected scanme.sh got %v", urlx) + } + } +}