diff --git a/integration_tests/fuzz/fuzz-header-basic.yaml b/integration_tests/fuzz/fuzz-header-basic.yaml new file mode 100644 index 0000000000..1441878a37 --- /dev/null +++ b/integration_tests/fuzz/fuzz-header-basic.yaml @@ -0,0 +1,49 @@ +id: fuzz-header-basic + +info: + name: fuzz header basic + author: pdteam + severity: info + description: | + In this template we check for any reflection when fuzzing Origin header + +variables: + first: "{{rand_int(10000, 99999)}}" + +http: + - raw: + - | + GET /?x=aaa&y=bbb HTTP/1.1 + Host: {{Hostname}} + Origin: https://example.com + X-Fuzz-Header: 1337 + Cookie: z=aaa; bb=aaa + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 + Accept-Language: en-US,en;q=0.9 + Connection: close + + payloads: + reflection: + - "'\"><{{first}}" + + fuzzing: + - part: headers + type: replace + mode: single + keys: ["Origin"] + fuzz: + - "{{reflection}}" + + stop-at-first-match: true + matchers-condition: and + matchers: + - type: word + part: body + words: + - "{{reflection}}" + + - type: word + part: header + words: + - "text/html" \ No newline at end of file diff --git a/integration_tests/fuzz/fuzz-header-multiple.yaml b/integration_tests/fuzz/fuzz-header-multiple.yaml new file mode 100644 index 0000000000..04b88b1ff6 --- /dev/null +++ b/integration_tests/fuzz/fuzz-header-multiple.yaml @@ -0,0 +1,41 @@ +id: fuzz-header-multiple + +info: + name: fuzz header multiple + author: pdteam + severity: info + description: | + In this template we fuzz multiple headers with single payload + +http: + - raw: + - | + GET /?x=aaa&y=bbb HTTP/1.1 + Host: {{Hostname}} + Origin: https://example.com + X-Forwared-For: 1337 + Cookie: z=aaa; bb=aaa + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 + Accept-Language: en-US,en;q=0.9 + Connection: close + + payloads: + reflection: + - "secret.local" + + fuzzing: + - part: headers + type: replace + mode: multiple + keys: ["Origin", "X-Forwared-For"] + fuzz: + - "{{reflection}}" + + stop-at-first-match: true + matchers-condition: and + matchers: + - type: word + part: body + words: + - "admin" \ No newline at end of file diff --git a/v2/cmd/integration-test/fuzz.go b/v2/cmd/integration-test/fuzz.go index 5dd048d32b..93dc616201 100644 --- a/v2/cmd/integration-test/fuzz.go +++ b/v2/cmd/integration-test/fuzz.go @@ -17,6 +17,8 @@ var fuzzingTestCases = []TestCaseInfo{ {Path: "fuzz/fuzz-type.yaml", TestCase: &fuzzTypeOverride{}}, {Path: "fuzz/fuzz-query.yaml", TestCase: &httpFuzzQuery{}}, {Path: "fuzz/fuzz-headless.yaml", TestCase: &HeadlessFuzzingQuery{}}, + {Path: "fuzz/fuzz-header-basic.yaml", TestCase: &FuzzHeaderBasic{}}, + {Path: "fuzz/fuzz-header-multiple.yaml", TestCase: &FuzzHeaderMultiple{}}, } type httpFuzzQuery struct{} @@ -147,3 +149,52 @@ func (h *HeadlessFuzzingQuery) Execute(filePath string) error { } return expectResultsCount(got, 2) } + +type FuzzHeaderBasic struct{} + +// Execute executes a test case and returns an error if occurred +func (h *FuzzHeaderBasic) Execute(filePath string) error { + router := httprouter.New() + router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + host := r.Header.Get("Origin") + // redirect to different domain + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Click Here") + }) + ts := httptest.NewTLSServer(router) + defer ts.Close() + + got, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + return expectResultsCount(got, 1) +} + +type FuzzHeaderMultiple struct{} + +// Execute executes a test case and returns an error if occurred +func (h *FuzzHeaderMultiple) Execute(filePath string) error { + router := httprouter.New() + router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + host1 := r.Header.Get("Origin") + host2 := r.Header.Get("X-Forwared-For") + + fmt.Printf("host1: %s, host2: %s\n", host1, host2) + if host1 == host2 && host2 == "secret.local" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "welcome! to secret admin panel") + return + } + // redirect to different domain + w.WriteHeader(http.StatusForbidden) + }) + ts := httptest.NewTLSServer(router) + defer ts.Close() + + got, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + return expectResultsCount(got, 1) +} diff --git a/v2/pkg/protocols/common/fuzz/execute.go b/v2/pkg/protocols/common/fuzz/execute.go index eca6d8d90f..86a18cae93 100644 --- a/v2/pkg/protocols/common/fuzz/execute.go +++ b/v2/pkg/protocols/common/fuzz/execute.go @@ -9,7 +9,7 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" "github.com/projectdiscovery/retryablehttp-go" - urlutil "github.com/projectdiscovery/utils/url" + errorutil "github.com/projectdiscovery/utils/errors" ) // ExecuteRuleInput is the input for rule Execute function @@ -42,8 +42,11 @@ type GeneratedRequest struct { // Input is not thread safe and should not be shared between concurrent // goroutines. func (rule *Rule) Execute(input *ExecuteRuleInput) error { - if !rule.isExecutable(input.Input) { - return nil + if input.BaseRequest == nil { + return errorutil.NewWithTag("fuzz", "base request is nil for rule %v", rule) + } + if !rule.isExecutable(input.BaseRequest) { + return errorutil.NewWithTag("fuzz", "rule is not executable on %v", input.BaseRequest.URL.String()) } baseValues := input.Values if rule.generator == nil { @@ -70,12 +73,11 @@ func (rule *Rule) Execute(input *ExecuteRuleInput) error { } // isExecutable returns true if the rule can be executed based on provided input -func (rule *Rule) isExecutable(input *contextargs.Context) bool { - parsed, err := urlutil.Parse(input.MetaInput.Input) - if err != nil { - return false +func (rule *Rule) isExecutable(req *retryablehttp.Request) bool { + if !req.Query().IsEmpty() && rule.partType == queryPartType { + return true } - if !parsed.Query().IsEmpty() && rule.partType == queryPartType { + if len(req.Header) > 0 && rule.partType == headersPartType { return true } return false diff --git a/v2/pkg/protocols/common/fuzz/execute_test.go b/v2/pkg/protocols/common/fuzz/execute_test.go index 88582e1b35..a922fbb7f7 100644 --- a/v2/pkg/protocols/common/fuzz/execute_test.go +++ b/v2/pkg/protocols/common/fuzz/execute_test.go @@ -1,9 +1,9 @@ package fuzz import ( + "github.com/projectdiscovery/retryablehttp-go" "testing" - "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs" "github.com/stretchr/testify/require" ) @@ -12,11 +12,15 @@ func TestRuleIsExecutable(t *testing.T) { err := rule.Compile(nil, nil) require.NoError(t, err, "could not compile rule") - input := contextargs.NewWithInput("https://example.com/?url=localhost") - result := rule.isExecutable(input) + req, err := retryablehttp.NewRequest("GET", "https://example.com/?url=localhost", nil) + require.NoError(t, err, "could not build request") + + result := rule.isExecutable(req) require.True(t, result, "could not get correct result") - input = contextargs.NewWithInput("https://example.com/") - result = rule.isExecutable(input) + req, err = retryablehttp.NewRequest("GET", "https://example.com/", nil) + require.NoError(t, err, "could not build request") + + result = rule.isExecutable(req) require.False(t, result, "could not get correct result") } diff --git a/v2/pkg/protocols/common/fuzz/fuzz.go b/v2/pkg/protocols/common/fuzz/fuzz.go index 7f872e816a..1ce0561e09 100644 --- a/v2/pkg/protocols/common/fuzz/fuzz.go +++ b/v2/pkg/protocols/common/fuzz/fuzz.go @@ -99,10 +99,12 @@ type partType int const ( queryPartType partType = iota + 1 + headersPartType ) var stringToPartType = map[string]partType{ - "query": queryPartType, + "query": queryPartType, + "headers": headersPartType, } // modeType is the mode of rule enum declaration diff --git a/v2/pkg/protocols/common/fuzz/parts.go b/v2/pkg/protocols/common/fuzz/parts.go index 0e09aaf802..afae9f928e 100644 --- a/v2/pkg/protocols/common/fuzz/parts.go +++ b/v2/pkg/protocols/common/fuzz/parts.go @@ -2,9 +2,13 @@ package fuzz import ( "context" + "io" "net/http" "strings" + "github.com/pkg/errors" + "github.com/projectdiscovery/gologger" + "github.com/corpix/uarand" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" @@ -19,6 +23,46 @@ func (rule *Rule) executePartRule(input *ExecuteRuleInput, payload string) error switch rule.partType { case queryPartType: return rule.executeQueryPartRule(input, payload) + case headersPartType: + return rule.executeHeadersPartRule(input, payload) + } + return nil +} + +// executeHeadersPartRule executes headers part rules +func (rule *Rule) executeHeadersPartRule(input *ExecuteRuleInput, payload string) error { + // clone the request to avoid modifying the original + originalRequest := input.BaseRequest + req := originalRequest.Clone(context.TODO()) + // Also clone headers + headers := req.Header.Clone() + + for key, values := range originalRequest.Header { + cloned := sliceutil.Clone(values) + for i, value := range values { + if !rule.matchKeyOrValue(key, value) { + continue + } + var evaluated string + evaluated, input.InteractURLs = rule.executeEvaluate(input, key, value, payload, input.InteractURLs) + cloned[i] = evaluated + + if rule.modeType == singleModeType { + headers[key] = cloned + if err := rule.buildHeadersInput(input, headers, input.InteractURLs); err != nil && err != io.EOF { + gologger.Error().Msgf("Could not build request for headers part rule %v: %s\n", rule, err) + return err + } + cloned[i] = value // change back to previous value for headers + } + } + headers[key] = cloned + } + + if rule.modeType == multipleModeType { + if err := rule.buildHeadersInput(input, headers, input.InteractURLs); err != nil { + return err + } } return nil } @@ -67,6 +111,29 @@ func (rule *Rule) executeQueryPartRule(input *ExecuteRuleInput, payload string) return err } +// buildHeadersInput returns created request for a Headers Input +func (rule *Rule) buildHeadersInput(input *ExecuteRuleInput, headers http.Header, interactURLs []string) error { + var req *retryablehttp.Request + if input.BaseRequest == nil { + return errors.New("Base request cannot be nil when fuzzing headers") + } else { + req = input.BaseRequest.Clone(context.TODO()) + req.Header = headers + // update host of request and not URL + // URL.Host is used to dial the connection + req.Request.Host = req.Header.Get("Host") + } + request := GeneratedRequest{ + Request: req, + InteractURLs: interactURLs, + DynamicValues: input.Values, + } + if !input.Callback(request) { + return io.EOF + } + return nil +} + // buildQueryInput returns created request for a Query Input func (rule *Rule) buildQueryInput(input *ExecuteRuleInput, parsed *urlutil.URL, interactURLs []string) error { var req *retryablehttp.Request diff --git a/v2/pkg/protocols/common/fuzz/parts_test.go b/v2/pkg/protocols/common/fuzz/parts_test.go index a8f6e141bf..805c08cfc4 100644 --- a/v2/pkg/protocols/common/fuzz/parts_test.go +++ b/v2/pkg/protocols/common/fuzz/parts_test.go @@ -1,6 +1,8 @@ package fuzz import ( + "github.com/projectdiscovery/retryablehttp-go" + "net/http" "testing" "github.com/projectdiscovery/nuclei/v2/pkg/protocols" @@ -9,6 +11,68 @@ import ( "github.com/stretchr/testify/require" ) +func TestExecuteHeadersPartRule(t *testing.T) { + options := &protocols.ExecutorOptions{ + Interactsh: &interactsh.Client{}, + } + req, err := retryablehttp.NewRequest("GET", "http://localhost:8080/", nil) + require.NoError(t, err, "can't build request") + + req.Header.Set("X-Custom-Foo", "foo") + req.Header.Set("X-Custom-Bar", "bar") + + t.Run("single", func(t *testing.T) { + rule := &Rule{ + ruleType: postfixRuleType, + partType: headersPartType, + modeType: singleModeType, + options: options, + } + var generatedHeaders []http.Header + err := rule.executeHeadersPartRule(&ExecuteRuleInput{ + Input: contextargs.New(), + BaseRequest: req, + Callback: func(gr GeneratedRequest) bool { + generatedHeaders = append(generatedHeaders, gr.Request.Header.Clone()) + return true + }, + }, "1337'") + require.NoError(t, err, "could not execute part rule") + require.ElementsMatch(t, []http.Header{ + { + "X-Custom-Foo": {"foo1337'"}, + "X-Custom-Bar": {"bar"}, + }, + { + "X-Custom-Foo": {"foo"}, + "X-Custom-Bar": {"bar1337'"}, + }, + }, generatedHeaders, "could not get generated headers") + }) + + t.Run("multiple", func(t *testing.T) { + rule := &Rule{ + ruleType: postfixRuleType, + partType: headersPartType, + modeType: multipleModeType, + options: options, + } + var generatedHeaders http.Header + err := rule.executeHeadersPartRule(&ExecuteRuleInput{ + Input: contextargs.New(), + BaseRequest: req, + Callback: func(gr GeneratedRequest) bool { + generatedHeaders = gr.Request.Header.Clone() + return true + }, + }, "1337'") + require.NoError(t, err, "could not execute part rule") + require.Equal(t, http.Header{ + "X-Custom-Foo": {"foo1337'"}, + "X-Custom-Bar": {"bar1337'"}, + }, generatedHeaders, "could not get generated headers") + }) +} func TestExecuteQueryPartRule(t *testing.T) { URL := "http://localhost:8080/?url=localhost&mode=multiple&file=passwdfile" options := &protocols.ExecutorOptions{ diff --git a/v2/pkg/protocols/headless/request.go b/v2/pkg/protocols/headless/request.go index 942eb603f5..ac6f3c93a7 100644 --- a/v2/pkg/protocols/headless/request.go +++ b/v2/pkg/protocols/headless/request.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/projectdiscovery/retryablehttp-go" + "github.com/pkg/errors" "golang.org/x/exp/maps" @@ -211,12 +213,16 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, payloads if _, err := urlutil.Parse(input.MetaInput.Input); err != nil { return errors.Wrap(err, "could not parse url") } + baseRequest, err := retryablehttp.NewRequest("GET", input.MetaInput.Input, nil) + if err != nil { + return errors.Wrap(err, "could not create base request") + } for _, rule := range request.Fuzzing { err := rule.Execute(&fuzz.ExecuteRuleInput{ Input: input, Callback: fuzzRequestCallback, Values: payloads, - BaseRequest: nil, + BaseRequest: baseRequest, }) if err == types.ErrNoMoreRequests { return nil diff --git a/v2/pkg/protocols/http/request.go b/v2/pkg/protocols/http/request.go index f1a49e27fa..ee6010d811 100644 --- a/v2/pkg/protocols/http/request.go +++ b/v2/pkg/protocols/http/request.go @@ -229,8 +229,12 @@ func (request *Request) executeTurboHTTP(input *contextargs.Context, dynamicValu // executeFuzzingRule executes fuzzing request for a URL func (request *Request) executeFuzzingRule(input *contextargs.Context, previous output.InternalEvent, callback protocols.OutputEventCallback) error { - if _, err := urlutil.Parse(input.MetaInput.Input); err != nil { - return errors.Wrap(err, "could not parse url") + // If request is self-contained we don't need to parse any input. + if !request.SelfContained { + // If it's not self-contained we parse user provided input + if _, err := urlutil.Parse(input.MetaInput.Input); err != nil { + return errors.Wrap(err, "could not parse url") + } } fuzzRequestCallback := func(gr fuzz.GeneratedRequest) bool { hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators) @@ -273,6 +277,7 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, previous if request.options.HostErrorsCache != nil { request.options.HostErrorsCache.MarkFailed(input.MetaInput.Input, requestErr) } + gologger.Verbose().Msgf("[%s] Error occurred in request: %s\n", request.options.TemplateID, requestErr) } request.options.Progress.IncrementRequests()