mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 19:55:26 +00:00
header fuzzing support in http templates (#4114)
* Add headersPartType for fuzzing * fix nil pointer dereference for headless mode * minor changes+ add integration test * update template in fuzz-header-multiple --------- Co-authored-by: 0x123456789 <0x123456789> Co-authored-by: Tarun Koyalwar <tarun@projectdiscovery.io>
This commit is contained in:
parent
bafde6c633
commit
c377221a78
49
integration_tests/fuzz/fuzz-header-basic.yaml
Normal file
49
integration_tests/fuzz/fuzz-header-basic.yaml
Normal file
@ -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"
|
||||
41
integration_tests/fuzz/fuzz-header-multiple.yaml
Normal file
41
integration_tests/fuzz/fuzz-header-multiple.yaml
Normal file
@ -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"
|
||||
@ -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, "<html><body><a href="+host+">Click Here</a></body></html>")
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -232,8 +232,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)
|
||||
@ -276,6 +280,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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user