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:
Daniil Morozov 2023-09-18 21:31:32 +03:00 committed by GitHub
parent bafde6c633
commit c377221a78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 308 additions and 17 deletions

View 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"

View 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"

View File

@ -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)
}

View File

@ -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

View File

@ -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")
}

View File

@ -99,10 +99,12 @@ type partType int
const (
queryPartType partType = iota + 1
headersPartType
)
var stringToPartType = map[string]partType{
"query": queryPartType,
"headers": headersPartType,
}
// modeType is the mode of rule enum declaration

View File

@ -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

View File

@ -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{

View File

@ -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

View File

@ -232,9 +232,13 @@ 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 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)
hasInteractMarkers := len(gr.InteractURLs) > 0
@ -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()