nuclei/pkg/protocols/http/request_fuzz.go
Ice3man fa56800fcc
Fuzzing layer enhancements + input-types support (#4477)
* feat: move fuzz package to root directory

* feat: added support for input providers like openapi,postman,etc

* feat: integration of new fuzzing logic in engine

* bugfix: use and instead of or

* fixed lint errors

* go mod tidy

* add new reqresp type + bump utils

* custom http request parser

* use new struct type RequestResponse

* introduce unified input/target provider

* abstract input formats via new inputprovider

* completed input provider refactor

* remove duplicated code

* add sdk method to load targets

* rename component url->path

* add new yaml format + remove duplicated code

* use gopkg.in/yaml.v3 for parsing

* update .gitignore

* refactor/move + docs fuzzing in http protocol

* fuzz: header + query integration test using fuzzplayground

* fix integration test runner in windows

* feat add support for filter in http fuzz

* rewrite header/query integration test with filter

* add replace regex rule

* support kv fuzzing + misc updates

* add path fuzzing example + misc improvements

* fix matchedURL + skip httpx on multi formats

* cookie fuzz integration test

* add json body + params body tests

* feat add multipart/form-data fuzzing support

* add all fuzz body integration test

* misc bug fixes + minor refactor

* add multipart form + body form unit tests

* only run fuzzing templates if -fuzz flag is given

* refactor/move fuzz playground server to pkg

* fix integration test + refactor

* add auth types and strategies

* add file auth provider

* start implementing auth logic in http

* add logic in http protocol

* static auth implemented for http

* default :80,:443 normalization

* feat: dynamic auth init

* feat: dynamic auth using templates

* validate targets count in openapi+swagger

* inputformats: add support to accept variables

* fix workflow integration test

* update lazy cred fetch logic

* fix unit test

* drop postman support

* domain related normalization

* update secrets.yaml file format + misc updates

* add auth prefetch option

* remove old secret files

* add fuzzing+auth related sdk options

* fix/support multiple mode in kv header fuzzing

* rename 'headers' -> 'header' in fuzzing rules

* fix deadlock due to merge conflict resolution

* misc update

* add bool type in parsed value

* add openapi validation+override+ new flags

* misc updates

* remove optional path parameters when unavailable

* fix swagger.yaml file

* misc updates

* update print msg

* multiple openapi validation enchancements + appMode

* add optional params in required_openapi_vars.yaml file

* improve warning/verbose msgs in format

* fix skip-format-validation not working

* use 'params/parameter' instead of 'variable' in openapi

* add retry support for falky tests

* fix nuclei loading ignored templates (#4849)

* fix tag include logic

* fix unit test

* remove quoting in extractor output

* remove quote in debug code command

* feat: issue tracker URLs in JSON + misc fixes (#4855)

* feat: issue tracker URLs in JSON + misc fixes

* misc changes

* feat: status update support for issues

* feat: report metadata generation hook support

* feat: added CLI summary of tickets created

* misc changes

* introduce `disable-unsigned-templates` flag (#4820)

* introduce `disable-unsigned-templates` flag

* minor

* skip instead of exit

* remove duplicate imports

* use stats package + misc enhancements

* force display warning + adjust skipped stats in unsigned count

* include unsigned skipped templates without -dut flag

---------

Co-authored-by: Tarun Koyalwar <tarun@projectdiscovery.io>

* Purge cache on global callback set (#4840)

* purge cache on global callback set

* lint

* purging cache

* purge cache in runner after loading templates

* include internal cache from parsers + add global cache register/purge via config

* remove disable cache purge option

---------

Co-authored-by: Tarun Koyalwar <tarun@projectdiscovery.io>

* misc update

* add application/octet-stream support

* openapi: support path specific params

* misc option + readme update

---------

Co-authored-by: Sandeep Singh <sandeep@projectdiscovery.io>
Co-authored-by: sandeep <8293321+ehsandeep@users.noreply.github.com>
Co-authored-by: Tarun Koyalwar <tarun@projectdiscovery.io>
Co-authored-by: Tarun Koyalwar <45962551+tarunKoyalwar@users.noreply.github.com>
Co-authored-by: Dogan Can Bakir <65292895+dogancanbakir@users.noreply.github.com>
Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
2024-03-14 03:08:53 +05:30

306 lines
11 KiB
Go

package http
// === Fuzzing Documentation (Scoped to this File) =====
// -> request.executeFuzzingRule [iterates over payloads(+requests) and executes]
// -> request.executePayloadUsingRules [executes single payload on all rules (if more than 1)]
// -> request.executeGeneratedFuzzingRequest [execute final generated fuzzing request and get result]
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/retryablehttp-go"
)
// executeFuzzingRule executes fuzzing request for a URL
// TODO:
// 1. use SPMHandler and rewrite stop at first match logic here
// 2. use scanContext instead of contextargs.Context
func (request *Request) executeFuzzingRule(input *contextargs.Context, _ output.InternalEvent, callback protocols.OutputEventCallback) error {
// methdology:
// to check applicablity of rule, we first try to execute it with one value
// if it is applicable, we execute all requests
// if it is not applicable, we log and fail silently
// check if target should be fuzzed or not
if !request.ShouldFuzzTarget(input) {
urlx, _ := input.MetaInput.URL()
if urlx != nil {
gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, urlx.String())
} else {
gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, input.MetaInput.Input)
}
return nil
}
// Iterate through all requests for template and queue them for fuzzing
generator := request.newGenerator(true)
// this will generate next value along with request it is meant to be used with
currRequest, payloads, result := generator.nextValue()
if !result && input.MetaInput.ReqResp == nil {
// this case is only true if input is not a full http request
return fmt.Errorf("no values to generate requests")
}
// if it is a full http request obtained from target file
if input.MetaInput.ReqResp != nil {
// Note: in case of full http request, we only need to build it once
// and then reuse it for all requests and completely abandon the request
// returned by generator
_ = currRequest
generated, err := input.MetaInput.ReqResp.BuildRequest()
if err != nil {
return errors.Wrap(err, "fuzz: could not build request obtained from target file")
}
input.MetaInput.Input = generated.URL.String()
// execute with one value first to checks its applicability
err = request.executePayloadUsingRules(input, payloads, generated, callback)
if err != nil {
// in case of any error, return it
if fuzz.IsErrRuleNotApplicable(err) {
// log and fail silently
gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err)
return nil
}
if errors.Is(err, errStopExecution) {
return err
}
gologger.Verbose().Msgf("[%s] fuzz: inital payload request execution failed : %s\n", request.options.TemplateID, err)
}
// if it is applicable, execute all requests
for {
_, payloads, result := generator.nextValue()
if !result {
break
}
err = request.executePayloadUsingRules(input, payloads, generated, callback)
if err != nil {
// continue to next request since this is payload specific
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed : %s\n", request.options.TemplateID, err)
continue
}
}
return nil
}
// ==== fuzzing when only URL is provided =====
generated, err := generator.Make(context.Background(), input, currRequest, payloads, nil)
if err != nil {
return errors.Wrap(err, "fuzz: could not build request from url")
}
// we need to use this url instead of input
inputx := input.Clone()
inputx.MetaInput.Input = generated.request.URL.String()
// execute with one value first to checks its applicability
err = request.executePayloadUsingRules(inputx, generated.dynamicValues, generated.request, callback)
if err != nil {
// in case of any error, return it
if fuzz.IsErrRuleNotApplicable(err) {
// log and fail silently
gologger.Verbose().Msgf("[%s] fuzz: rule not applicable : %s\n", request.options.TemplateID, err)
return nil
}
if errors.Is(err, errStopExecution) {
return err
}
gologger.Verbose().Msgf("[%s] fuzz: inital payload request execution failed : %s\n", request.options.TemplateID, err)
}
// continue to next request since this is payload specific
for {
currRequest, payloads, result = generator.nextValue()
if !result {
break
}
generated, err := generator.Make(context.Background(), input, currRequest, payloads, nil)
if err != nil {
return errors.Wrap(err, "fuzz: could not build request from url")
}
// we need to use this url instead of input
inputx := input.Clone()
inputx.MetaInput.Input = generated.request.URL.String()
// execute with one value first to checks its applicability
err = request.executePayloadUsingRules(inputx, generated.dynamicValues, generated.request, callback)
if err != nil {
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed : %s\n", request.options.TemplateID, err)
continue
}
}
return nil
}
// executePayloadUsingRules executes a payload using rules with given payload i.e values
func (request *Request) executePayloadUsingRules(input *contextargs.Context, values map[string]interface{}, baseRequest *retryablehttp.Request, callback protocols.OutputEventCallback) error {
applicable := false
for _, rule := range request.Fuzzing {
err := rule.Execute(&fuzz.ExecuteRuleInput{
Input: input,
Callback: func(gr fuzz.GeneratedRequest) bool {
// TODO: replace this after scanContext Refactor
return request.executeGeneratedFuzzingRequest(gr, input, callback)
},
Values: values,
BaseRequest: baseRequest,
})
if err == nil {
applicable = true
continue
}
if fuzz.IsErrRuleNotApplicable(err) {
continue
}
if err == types.ErrNoMoreRequests {
return nil
}
return errors.Wrap(err, "could not execute rule")
}
if !applicable {
return fuzz.ErrRuleNotApplicable.Msgf(fmt.Sprintf("no rule was applicable for this request: %v", input.MetaInput.Input))
}
return nil
}
// executeGeneratedFuzzingRequest executes a generated fuzzing request after building it using rules and payloads
func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest, input *contextargs.Context, callback protocols.OutputEventCallback) bool {
hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)
hasInteractMarkers := len(gr.InteractURLs) > 0
if request.options.HostErrorsCache != nil && request.options.HostErrorsCache.Check(input.MetaInput.Input) {
return false
}
request.options.RateLimiter.Take()
req := &generatedRequest{
request: gr.Request,
dynamicValues: gr.DynamicValues,
interactshURLs: gr.InteractURLs,
original: request,
}
var gotMatches bool
requestErr := request.executeRequest(input, req, gr.DynamicValues, hasInteractMatchers, func(event *output.InternalWrappedEvent) {
if hasInteractMarkers && hasInteractMatchers && request.options.Interactsh != nil {
requestData := &interactsh.RequestData{
MakeResultFunc: request.MakeResultEvent,
Event: event,
Operators: request.CompiledOperators,
MatchFunc: request.Match,
ExtractFunc: request.Extract,
}
request.options.Interactsh.RequestEvent(gr.InteractURLs, requestData)
gotMatches = request.options.Interactsh.AlreadyMatched(requestData)
} else {
callback(event)
}
// Add the extracts to the dynamic values if any.
if event.OperatorsResult != nil {
gotMatches = event.OperatorsResult.Matched
}
}, 0)
// If a variable is unresolved, skip all further requests
if errors.Is(requestErr, errStopExecution) {
return false
}
if requestErr != nil {
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()
// If this was a match, and we want to stop at first match, skip all further requests.
shouldStopAtFirstMatch := request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch
if shouldStopAtFirstMatch && gotMatches {
return false
}
return true
}
// ShouldFuzzTarget checks if given target should be fuzzed or not using `filter` field in template
func (request *Request) ShouldFuzzTarget(input *contextargs.Context) bool {
if len(request.FuzzingFilter) == 0 {
return true
}
status := []bool{}
for index, filter := range request.FuzzingFilter {
isMatch, _ := request.Match(request.filterDataMap(input), filter)
status = append(status, isMatch)
if request.options.Options.MatcherStatus {
gologger.Debug().Msgf("[%s] [%s] Filter => %s : %v", input.MetaInput.Target(), request.options.TemplateID, operators.GetMatcherName(filter, index), isMatch)
}
}
if len(status) == 0 {
return true
}
var matched bool
if request.fuzzingFilterCondition == matchers.ANDCondition {
matched = operators.EvalBoolSlice(status, true)
} else {
matched = operators.EvalBoolSlice(status, false)
}
if request.options.Options.MatcherStatus {
gologger.Debug().Msgf("[%s] [%s] Final Filter Status => %v", input.MetaInput.Target(), request.options.TemplateID, matched)
}
return matched
}
// input data map returns map[string]interface{} from input
func (request *Request) filterDataMap(input *contextargs.Context) map[string]interface{} {
m := make(map[string]interface{})
parsed, err := input.MetaInput.URL()
if err != nil {
m["host"] = input.MetaInput.Input
return m
}
m = protocolutils.GenerateVariables(parsed, true, m)
for k, v := range m {
m[strings.ToLower(k)] = v
}
m["path"] = parsed.Path // override existing
m["query"] = parsed.RawQuery
// add request data like headers, body etc
if input.MetaInput.ReqResp != nil && input.MetaInput.ReqResp.Request != nil {
req := input.MetaInput.ReqResp.Request
m["method"] = req.Method
m["body"] = req.Body
sb := &strings.Builder{}
req.Headers.Iterate(func(k, v string) bool {
k = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(k), "-", "_"))
if strings.EqualFold(k, "Cookie") {
m["cookie"] = v
}
if strings.EqualFold(k, "User_Agent") {
m["user_agent"] = v
}
if strings.EqualFold(k, "content_type") {
m["content_type"] = v
}
sb.WriteString(fmt.Sprintf("%s: %s\n", k, v))
return true
})
m["header"] = sb.String()
}
// dump if svd is enabled
if request.options.Options.ShowVarDump {
gologger.Debug().Msgf("Fuzz Filter Variables: \n%s\n", vardump.DumpVariables(m))
}
return m
}