mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-18 06:15:27 +00:00
306 lines
11 KiB
Go
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
|
||
|
|
}
|