Skip to content

Commit

Permalink
header fuzzing support in http templates (#4114)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
0x123456789 and tarunKoyalwar authored Sep 18, 2023
1 parent bafde6c commit c377221
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 17 deletions.
49 changes: 49 additions & 0 deletions integration_tests/fuzz/fuzz-header-basic.yaml
Original file line number Diff line number Diff line change
@@ -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 changes: 41 additions & 0 deletions integration_tests/fuzz/fuzz-header-multiple.yaml
Original file line number Diff line number Diff line change
@@ -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"
51 changes: 51 additions & 0 deletions v2/cmd/integration-test/fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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)
}
18 changes: 10 additions & 8 deletions v2/pkg/protocols/common/fuzz/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
14 changes: 9 additions & 5 deletions v2/pkg/protocols/common/fuzz/execute_test.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -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")
}
4 changes: 3 additions & 1 deletion v2/pkg/protocols/common/fuzz/fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions v2/pkg/protocols/common/fuzz/parts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit c377221

Please sign in to comment.