Fuzzing additions & enhancements (#5139)

* feat: added fuzzing output enhancements

* changes as requested

* misc

* feat: added dfp flag to display fuzz points + misc additions

* feat: added support for fuzzing nested path segments

* feat: added parts to fuzzing requests

* feat: added tracking for parameter occurence frequency in fuzzing

* added cli flag for fuzz frequency

* fixed broken tests

* fixed path based sqli integration test

* feat: added configurable fuzzing aggression level for payloads

* fixed failing test
This commit is contained in:
Ice3man
2024-06-11 04:43:46 +05:30
committed by GitHub
parent cd40a6aeda
commit 9f3f7fce06
19 changed files with 507 additions and 76 deletions

View File

@@ -326,6 +326,9 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVarP(&options.FuzzingMode, "fuzzing-mode", "fm", "", "overrides fuzzing mode set in template (multiple, single)"),
flagSet.BoolVar(&fuzzFlag, "fuzz", false, "enable loading fuzzing templates (Deprecated: use -dast instead)"),
flagSet.BoolVar(&options.DAST, "dast", false, "enable / run dast (fuzz) nuclei templates"),
flagSet.BoolVarP(&options.DisplayFuzzPoints, "display-fuzz-points", "dfp", false, "display fuzz points in the output for debugging"),
flagSet.IntVar(&options.FuzzParamFrequency, "fuzz-param-frequency", 10, "frequency of uninteresting parameters for fuzzing before skipping"),
flagSet.StringVarP(&options.FuzzAggressionLevel, "fuzz-aggression", "fa", "low", "fuzzing aggression level controls payload count for fuzz (low, medium, high)"),
)
flagSet.CreateGroup("uncover", "Uncover",

View File

@@ -15,21 +15,18 @@ http:
- type: dsl
dsl:
- 'method == "GET"'
- regex("/(.*?/)([0-9]+)(/.*)?",path)
condition: and
payloads:
pathsqli:
- "'OR1=1"
- '%20OR%20True'
fuzzing:
- part: path
type: replace-regex
type: postfix
mode: single
replace-regex: '/(.*?/)([0-9]+)(/.*)?'
fuzz:
- '/${1}${2}{{pathsqli}}${3}'
- '{{pathsqli}}'
matchers:
- type: status

View File

@@ -15,6 +15,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/internal/pdcp"
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/frequency"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
"github.com/projectdiscovery/nuclei/v3/pkg/loader/parser"
@@ -74,21 +75,22 @@ var (
// Runner is a client for running the enumeration process.
type Runner struct {
output output.Writer
interactsh *interactsh.Client
options *types.Options
projectFile *projectfile.ProjectFile
catalog catalog.Catalog
progress progress.Progress
colorizer aurora.Aurora
issuesClient reporting.Client
browser *engine.Browser
rateLimiter *ratelimit.Limiter
hostErrors hosterrorscache.CacheInterface
resumeCfg *types.ResumeCfg
pprofServer *http.Server
pdcpUploadErrMsg string
inputProvider provider.InputProvider
output output.Writer
interactsh *interactsh.Client
options *types.Options
projectFile *projectfile.ProjectFile
catalog catalog.Catalog
progress progress.Progress
colorizer aurora.Aurora
issuesClient reporting.Client
browser *engine.Browser
rateLimiter *ratelimit.Limiter
hostErrors hosterrorscache.CacheInterface
resumeCfg *types.ResumeCfg
pprofServer *http.Server
pdcpUploadErrMsg string
inputProvider provider.InputProvider
fuzzFrequencyCache *frequency.Tracker
//general purpose temporary directory
tmpDir string
parser parser.Parser
@@ -448,24 +450,28 @@ func (r *Runner) RunEnumeration() error {
r.options.ExcludedTemplates = append(r.options.ExcludedTemplates, ignoreFile.Files...)
}
fuzzFreqCache := frequency.New(frequency.DefaultMaxTrackCount, r.options.FuzzParamFrequency)
r.fuzzFrequencyCache = fuzzFreqCache
// Create the executor options which will be used throughout the execution
// stage by the nuclei engine modules.
executorOpts := protocols.ExecutorOptions{
Output: r.output,
Options: r.options,
Progress: r.progress,
Catalog: r.catalog,
IssuesClient: r.issuesClient,
RateLimiter: r.rateLimiter,
Interactsh: r.interactsh,
ProjectFile: r.projectFile,
Browser: r.browser,
Colorizer: r.colorizer,
ResumeCfg: r.resumeCfg,
ExcludeMatchers: excludematchers.New(r.options.ExcludeMatchers),
InputHelper: input.NewHelper(),
TemporaryDirectory: r.tmpDir,
Parser: r.parser,
Output: r.output,
Options: r.options,
Progress: r.progress,
Catalog: r.catalog,
IssuesClient: r.issuesClient,
RateLimiter: r.rateLimiter,
Interactsh: r.interactsh,
ProjectFile: r.projectFile,
Browser: r.browser,
Colorizer: r.colorizer,
ResumeCfg: r.resumeCfg,
ExcludeMatchers: excludematchers.New(r.options.ExcludeMatchers),
InputHelper: input.NewHelper(),
TemporaryDirectory: r.tmpDir,
Parser: r.parser,
FuzzParamsFrequency: fuzzFreqCache,
}
if config.DefaultConfig.IsDebugArgEnabled(config.DebugExportURLPattern) {
@@ -619,6 +625,7 @@ func (r *Runner) RunEnumeration() error {
if executorOpts.InputHelper != nil {
_ = executorOpts.InputHelper.Close()
}
r.fuzzFrequencyCache.Close()
// todo: error propagation without canonical straight error check is required by cloud?
// use safe dereferencing to avoid potential panics in case of previous unchecked errors

View File

@@ -2,10 +2,12 @@ package component
import (
"context"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat"
"github.com/projectdiscovery/retryablehttp-go"
urlutil "github.com/projectdiscovery/utils/url"
)
// Path is a component for a request Path
@@ -31,13 +33,18 @@ func (q *Path) Name() string {
// parsed component
func (q *Path) Parse(req *retryablehttp.Request) (bool, error) {
q.req = req
q.value = NewValue(req.URL.Path)
q.value = NewValue("")
parsed, err := dataformat.Get(dataformat.RawDataFormat).Decode(q.value.String())
if err != nil {
return false, err
splitted := strings.Split(req.URL.Path, "/")
values := make(map[string]interface{})
for i := range splitted {
pathTillNow := strings.Join(splitted[:i+1], "/")
if pathTillNow == "" {
continue
}
values[strconv.Itoa(i)] = pathTillNow
}
q.value.SetParsed(parsed, dataformat.RawDataFormat)
q.value.SetParsed(dataformat.KVMap(values), "")
return true, nil
}
@@ -56,7 +63,8 @@ func (q *Path) Iterate(callback func(key string, value interface{}) error) (err
// SetValue sets a value in the component
// for a key
func (q *Path) SetValue(key string, value string) error {
if !q.value.SetParsedValue(key, value) {
escaped := urlutil.ParamEncode(value)
if !q.value.SetParsedValue(key, escaped) {
return ErrSetValue
}
return nil
@@ -73,13 +81,31 @@ func (q *Path) Delete(key string) error {
// Rebuild returns a new request with the
// component rebuilt
func (q *Path) Rebuild() (*retryablehttp.Request, error) {
encoded, err := q.value.Encode()
if err != nil {
return nil, errors.Wrap(err, "could not encode query")
originalValues := make(map[string]interface{})
splitted := strings.Split(q.req.URL.Path, "/")
for i := range splitted {
pathTillNow := strings.Join(splitted[:i+1], "/")
if pathTillNow == "" {
continue
}
originalValues[strconv.Itoa(i)] = pathTillNow
}
originalPath := q.req.URL.Path
lengthSplitted := len(q.value.parsed.Map)
for i := lengthSplitted; i > 0; i-- {
key := strconv.Itoa(i)
original := originalValues[key].(string)
new := q.value.parsed.Map[key].(string)
originalPath = strings.Replace(originalPath, original, new, 1)
}
rebuiltPath := originalPath
// Clone the request and update the path
cloned := q.req.Clone(context.Background())
if err := cloned.UpdateRelPath(encoded, true); err != nil {
cloned.URL.RawPath = encoded
if err := cloned.UpdateRelPath(rebuiltPath, true); err != nil {
cloned.URL.RawPath = rebuiltPath
}
return cloned, nil
}

View File

@@ -28,10 +28,10 @@ func TestURLComponent(t *testing.T) {
return nil
})
require.Equal(t, []string{"value"}, keys, "unexpected keys")
require.Equal(t, []string{"1"}, keys, "unexpected keys")
require.Equal(t, []string{"/testpath"}, values, "unexpected values")
err = urlComponent.SetValue("value", "/newpath")
err = urlComponent.SetValue("1", "/newpath")
if err != nil {
t.Fatal(err)
}
@@ -40,7 +40,41 @@ func TestURLComponent(t *testing.T) {
if err != nil {
t.Fatal(err)
}
require.Equal(t, "/newpath", rebuilt.URL.Path, "unexpected URL path")
require.Equal(t, "https://example.com/newpath", rebuilt.URL.String(), "unexpected full URL")
}
func TestURLComponent_NestedPaths(t *testing.T) {
path := NewPath()
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com/user/753/profile", nil)
if err != nil {
t.Fatal(err)
}
found, err := path.Parse(req)
if err != nil {
t.Fatal(err)
}
if !found {
t.Fatal("expected path to be found")
}
isSet := false
_ = path.Iterate(func(key string, value interface{}) error {
if !isSet && value.(string) == "/user/753" {
isSet = true
if setErr := path.SetValue(key, "/user/753'"); setErr != nil {
t.Fatal(setErr)
}
}
return nil
})
newReq, err := path.Rebuild()
if err != nil {
t.Fatal(err)
}
if newReq.URL.Path != "/user/753'/profile" {
t.Fatal("expected path to be modified")
}
}

View File

@@ -61,6 +61,7 @@ func (q *Query) Iterate(callback func(key string, value interface{}) error) (err
// SetValue sets a value in the component
// for a key
func (q *Query) SetValue(key string, value string) error {
// Is this safe?
if !q.value.SetParsedValue(key, value) {
return ErrSetValue
}

View File

@@ -1,6 +1,7 @@
package fuzz
import (
"encoding/json"
"fmt"
"io"
"regexp"
@@ -15,6 +16,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/retryablehttp-go"
errorutil "github.com/projectdiscovery/utils/errors"
sliceutil "github.com/projectdiscovery/utils/slice"
urlutil "github.com/projectdiscovery/utils/url"
)
@@ -45,6 +47,8 @@ type ExecuteRuleInput struct {
Values map[string]interface{}
// BaseRequest is the base http request for fuzzing rule
BaseRequest *retryablehttp.Request
// DisplayFuzzPoints is a flag to display fuzz points
DisplayFuzzPoints bool
}
// GeneratedRequest is a single generated request for rule
@@ -76,8 +80,9 @@ func (rule *Rule) Execute(input *ExecuteRuleInput) (err error) {
var finalComponentList []component.Component
// match rule part with component name
displayDebugFuzzPoints := make(map[string]map[string]string)
for _, componentName := range component.Components {
if rule.partType != requestPartType && rule.Part != componentName {
if !(rule.Part == componentName || sliceutil.Contains(rule.Parts, componentName) || rule.partType == requestPartType) {
continue
}
component := component.New(componentName)
@@ -89,12 +94,25 @@ func (rule *Rule) Execute(input *ExecuteRuleInput) (err error) {
if !discovered {
continue
}
// check rule applicable on this component
if !rule.checkRuleApplicableOnComponent(component) {
continue
}
// Debugging display for fuzz points
if input.DisplayFuzzPoints {
displayDebugFuzzPoints[componentName] = make(map[string]string)
_ = component.Iterate(func(key string, value interface{}) error {
displayDebugFuzzPoints[componentName][key] = fmt.Sprintf("%v", value)
return nil
})
}
finalComponentList = append(finalComponentList, component)
}
if len(displayDebugFuzzPoints) > 0 {
marshalled, _ := json.MarshalIndent(displayDebugFuzzPoints, "", " ")
gologger.Info().Msgf("[%s] Fuzz points for %s [%s]\n%s\n", rule.options.TemplateID, input.Input.MetaInput.Input, input.BaseRequest.Method, string(marshalled))
}
if len(finalComponentList) == 0 {
return ErrRuleNotApplicable.Msgf("no component matched on this rule")
@@ -225,7 +243,7 @@ func (rule *Rule) executeRuleValues(input *ExecuteRuleInput, ruleComponent compo
if err != nil {
return err
}
if gotErr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, ""); gotErr != nil {
if gotErr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, "", ""); gotErr != nil {
return gotErr
}
}
@@ -261,8 +279,9 @@ func (rule *Rule) Compile(generator *generators.PayloadGenerator, options *proto
} else {
rule.partType = valueType
}
} else {
rule.partType = queryPartType
}
if rule.Part == "" && len(rule.Parts) == 0 {
return errors.Errorf("no part specified for rule")
}
if rule.Type != "" {

View File

@@ -0,0 +1,152 @@
package frequency
import (
"net"
"net/url"
"os"
"strings"
"sync"
"sync/atomic"
"github.com/bluele/gcache"
"github.com/projectdiscovery/gologger"
)
// Tracker implements a frequency tracker for a given input
// which is used to determine uninteresting input parameters
// which are not that interesting from fuzzing perspective for a template
// and target combination.
//
// This is used to reduce the number of requests made during fuzzing
// for parameters that are less likely to give results for a rule.
type Tracker struct {
frequencies gcache.Cache
paramOccurenceThreshold int
isDebug bool
}
const (
DefaultMaxTrackCount = 10000
DefaultParamOccurenceThreshold = 10
)
type cacheItem struct {
errors atomic.Int32
sync.Once
}
// New creates a new frequency tracker with a given maximum
// number of params to track in LRU fashion with a max error threshold
func New(maxTrackCount, paramOccurenceThreshold int) *Tracker {
gc := gcache.New(maxTrackCount).ARC().Build()
var isDebug bool
if os.Getenv("FREQ_DEBUG") != "" {
isDebug = true
}
return &Tracker{
isDebug: isDebug,
frequencies: gc,
paramOccurenceThreshold: paramOccurenceThreshold,
}
}
func (t *Tracker) Close() {
t.frequencies.Purge()
}
// MarkParameter marks a parameter as frequently occuring once.
//
// The logic requires a parameter to be marked as frequently occuring
// multiple times before it's considered as frequently occuring.
func (t *Tracker) MarkParameter(parameter, target, template string) {
normalizedTarget := normalizeTarget(target)
key := getFrequencyKey(parameter, normalizedTarget, template)
if t.isDebug {
gologger.Verbose().Msgf("[%s] Marking %s as found uninteresting", template, key)
}
existingCacheItem, err := t.frequencies.GetIFPresent(key)
if err != nil || existingCacheItem == nil {
newItem := &cacheItem{errors: atomic.Int32{}}
newItem.errors.Store(1)
_ = t.frequencies.Set(key, newItem)
return
}
existingCacheItemValue := existingCacheItem.(*cacheItem)
existingCacheItemValue.errors.Add(1)
_ = t.frequencies.Set(key, existingCacheItemValue)
}
// IsParameterFrequent checks if a parameter is frequently occuring
// in the input with no much results.
func (t *Tracker) IsParameterFrequent(parameter, target, template string) bool {
normalizedTarget := normalizeTarget(target)
key := getFrequencyKey(parameter, normalizedTarget, template)
if t.isDebug {
gologger.Verbose().Msgf("[%s] Checking if %s is frequently found uninteresting", template, key)
}
existingCacheItem, err := t.frequencies.GetIFPresent(key)
if err != nil {
return false
}
existingCacheItemValue := existingCacheItem.(*cacheItem)
if existingCacheItemValue.errors.Load() >= int32(t.paramOccurenceThreshold) {
existingCacheItemValue.Do(func() {
gologger.Verbose().Msgf("[%s] Skipped %s from parameter for %s as found uninteresting %d times", template, parameter, target, existingCacheItemValue.errors.Load())
})
return true
}
return false
}
// UnmarkParameter unmarks a parameter as frequently occuring. This carries
// more weight and resets the frequency counter for the parameter causing
// it to be checked again. This is done when results are found.
func (t *Tracker) UnmarkParameter(parameter, target, template string) {
normalizedTarget := normalizeTarget(target)
key := getFrequencyKey(parameter, normalizedTarget, template)
if t.isDebug {
gologger.Verbose().Msgf("[%s] Unmarking %s as frequently found uninteresting", template, key)
}
_ = t.frequencies.Remove(key)
}
func getFrequencyKey(parameter, target, template string) string {
var sb strings.Builder
sb.WriteString(target)
sb.WriteString(":")
sb.WriteString(template)
sb.WriteString(":")
sb.WriteString(parameter)
str := sb.String()
return str
}
func normalizeTarget(value string) string {
finalValue := value
if strings.HasPrefix(value, "http") {
if parsed, err := url.Parse(value); err == nil {
hostname := parsed.Host
finalPort := parsed.Port()
if finalPort == "" {
if parsed.Scheme == "https" {
finalPort = "443"
} else {
finalPort = "80"
}
hostname = net.JoinHostPort(parsed.Host, finalPort)
}
finalValue = hostname
}
}
return finalValue
}

View File

@@ -24,12 +24,27 @@ type Rule struct {
ruleType ruleType
// description: |
// Part is the part of request to fuzz.
//
// query fuzzes the query part of url. More parts will be added later.
// values:
// - "query"
// - "header"
// - "path"
// - "body"
// - "cookie"
// - "request"
Part string `yaml:"part,omitempty" json:"part,omitempty" jsonschema:"title=part of rule,description=Part of request rule to fuzz,enum=query,enum=header,enum=path,enum=body,enum=cookie,enum=request"`
partType partType
// description: |
// Parts is the list of parts to fuzz. If multiple parts need to be
// defined while excluding some, this should be used instead of singular part.
// values:
// - "query"
// - "header"
// - "path"
// - "body"
// - "cookie"
// - "request"
Parts []string `yaml:"parts,omitempty" json:"parts,omitempty" jsonschema:"title=parts of rule,description=Part of request rule to fuzz,enum=query,enum=header,enum=path,enum=body,enum=cookie,enum=request"`
// description: |
// Mode is the mode of fuzzing to perform.
//

View File

@@ -7,7 +7,9 @@ import (
)
func TestRuleMatchKeyOrValue(t *testing.T) {
rule := &Rule{}
rule := &Rule{
Part: "query",
}
err := rule.Compile(nil, nil)
require.NoError(t, err, "could not compile rule")
@@ -15,7 +17,7 @@ func TestRuleMatchKeyOrValue(t *testing.T) {
require.True(t, result, "could not get correct result")
t.Run("key", func(t *testing.T) {
rule := &Rule{Keys: []string{"url"}}
rule := &Rule{Keys: []string{"url"}, Part: "query"}
err := rule.Compile(nil, nil)
require.NoError(t, err, "could not compile rule")
@@ -25,7 +27,7 @@ func TestRuleMatchKeyOrValue(t *testing.T) {
require.False(t, result, "could not get correct result")
})
t.Run("value", func(t *testing.T) {
rule := &Rule{ValuesRegex: []string{`https?:\/\/?([-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b)*(\/[\/\d\w\.-]*)*(?:[\?])*(.+)*`}}
rule := &Rule{ValuesRegex: []string{`https?:\/\/?([-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b)*(\/[\/\d\w\.-]*)*(?:[\?])*(.+)*`}, Part: "query"}
err := rule.Compile(nil, nil)
require.NoError(t, err, "could not compile rule")

View File

@@ -2,6 +2,7 @@ package fuzz
import (
"io"
"strconv"
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
@@ -9,6 +10,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/retryablehttp-go"
sliceutil "github.com/projectdiscovery/utils/slice"
)
// executePartRule executes part rules based on type
@@ -18,7 +20,7 @@ func (rule *Rule) executePartRule(input *ExecuteRuleInput, payload ValueOrKeyVal
// checkRuleApplicableOnComponent checks if a rule is applicable on given component
func (rule *Rule) checkRuleApplicableOnComponent(component component.Component) bool {
if rule.Part != component.Name() {
if rule.Part != component.Name() && !sliceutil.Contains(rule.Parts, component.Name()) && rule.partType != requestPartType {
return false
}
foundAny := false
@@ -68,7 +70,7 @@ func (rule *Rule) executePartComponentOnValues(input *ExecuteRuleInput, payloadS
return err
}
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key); qerr != nil {
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key, valueStr); qerr != nil {
return qerr
}
// fmt.Printf("executed with value: %s\n", evaluated)
@@ -90,7 +92,7 @@ func (rule *Rule) executePartComponentOnValues(input *ExecuteRuleInput, payloadS
if err != nil {
return err
}
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, ""); qerr != nil {
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, "", ""); qerr != nil {
err = qerr
return err
}
@@ -125,7 +127,7 @@ func (rule *Rule) executePartComponentOnKV(input *ExecuteRuleInput, payload Valu
return err
}
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key); qerr != nil {
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key, value); qerr != nil {
return err
}
@@ -144,7 +146,23 @@ func (rule *Rule) executePartComponentOnKV(input *ExecuteRuleInput, payload Valu
}
// execWithInput executes a rule with input via callback
func (rule *Rule) execWithInput(input *ExecuteRuleInput, httpReq *retryablehttp.Request, interactURLs []string, component component.Component, parameter string) error {
func (rule *Rule) execWithInput(input *ExecuteRuleInput, httpReq *retryablehttp.Request, interactURLs []string, component component.Component, parameter, parameterValue string) error {
// If the parameter is a number, replace it with the parameter value
// or if the parameter is empty and the parameter value is not empty
// replace it with the parameter value
if _, err := strconv.Atoi(parameter); err == nil || (parameter == "" && parameterValue != "") {
parameter = parameterValue
}
// If the parameter is frequent, skip it if the option is enabled
if rule.options.FuzzParamsFrequency != nil {
if rule.options.FuzzParamsFrequency.IsParameterFrequent(
parameter,
httpReq.URL.String(),
rule.options.TemplateID,
) {
return nil
}
}
request := GeneratedRequest{
Request: httpReq,
InteractURLs: interactURLs,

View File

@@ -25,8 +25,19 @@ func New(payloads map[string]interface{}, attackType AttackType, templatePath st
// Resolve payload paths if they are files.
payloadsFinal := make(map[string]interface{})
for name, payload := range payloads {
payloadsFinal[name] = payload
for payloadName, v := range payloads {
switch value := v.(type) {
case map[interface{}]interface{}:
values, err := parsePayloadsWithAggression(payloadName, value, opts.FuzzAggressionLevel)
if err != nil {
return nil, errors.Wrap(err, "could not parse payloads with aggression")
}
for k, v := range values {
payloadsFinal[k] = v
}
default:
payloadsFinal[payloadName] = v
}
}
generator := &PayloadGenerator{catalog: catalog, options: opts}
@@ -57,6 +68,60 @@ func New(payloads map[string]interface{}, attackType AttackType, templatePath st
return generator, nil
}
type aggressionLevelToPayloads struct {
Low []interface{}
Medium []interface{}
High []interface{}
}
// parsePayloadsWithAggression parses the payloads with the aggression level
//
// Three agression are supported -
// - low
// - medium
// - high
//
// low is the default level. If medium is specified, all templates from
// low and medium are executed. Similarly with high, including all templates
// from low, medium, high.
func parsePayloadsWithAggression(name string, v map[interface{}]interface{}, agression string) (map[string]interface{}, error) {
payloadsLevels := &aggressionLevelToPayloads{}
for k, v := range v {
if _, ok := v.([]interface{}); !ok {
return nil, errors.Errorf("only lists are supported for aggression levels payloads")
}
var ok bool
switch k {
case "low":
payloadsLevels.Low, ok = v.([]interface{})
case "medium":
payloadsLevels.Medium, ok = v.([]interface{})
case "high":
payloadsLevels.High, ok = v.([]interface{})
default:
return nil, errors.Errorf("invalid aggression level %s specified for %s", k, name)
}
if !ok {
return nil, errors.Errorf("invalid aggression level %s specified for %s", k, name)
}
}
payloads := make(map[string]interface{})
switch agression {
case "low":
payloads[name] = payloadsLevels.Low
case "medium":
payloads[name] = append(payloadsLevels.Low, payloadsLevels.Medium...)
case "high":
payloads[name] = append(payloadsLevels.Low, payloadsLevels.Medium...)
payloads[name] = append(payloads[name].([]interface{}), payloadsLevels.High...)
default:
return nil, errors.Errorf("invalid aggression level %s specified for %s", agression, name)
}
return payloads, nil
}
// Iterator is a single instance of an iterator for a generator structure
type Iterator struct {
Type AttackType

View File

@@ -1,9 +1,11 @@
package generators
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
@@ -90,3 +92,49 @@ func getOptions(allowLocalFileAccess bool) *types.Options {
opts.AllowLocalFileAccess = allowLocalFileAccess
return opts
}
func TestParsePayloadsWithAggression(t *testing.T) {
testPayload := `linux_path:
low:
- /etc/passwd
medium:
- ../etc/passwd
- ../../etc/passwd
high:
- ../../../etc/passwd
- ../../../../etc/passwd
- ../../../../../etc/passwd`
var payloads map[string]interface{}
err := yaml.NewDecoder(strings.NewReader(testPayload)).Decode(&payloads)
require.Nil(t, err, "could not unmarshal yaml")
aggressionsToValues := map[string][]string{
"low": {
"/etc/passwd",
},
"medium": {
"/etc/passwd",
"../etc/passwd",
"../../etc/passwd",
},
"high": {
"/etc/passwd",
"../etc/passwd",
"../../etc/passwd",
"../../../etc/passwd",
"../../../../etc/passwd",
"../../../../../etc/passwd",
},
}
for k, v := range payloads {
for aggression, values := range aggressionsToValues {
parsed, err := parsePayloadsWithAggression(k, v.(map[interface{}]interface{}), aggression)
require.Nil(t, err, "could not parse payloads with aggression")
gotValues := parsed[k].([]interface{})
require.Equal(t, len(values), len(gotValues), "could not get correct number of values")
}
}
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/writer"
"github.com/projectdiscovery/retryablehttp-go"
errorutil "github.com/projectdiscovery/utils/errors"
stringsutil "github.com/projectdiscovery/utils/strings"
)
@@ -180,6 +181,14 @@ func (c *Client) processInteractionForRequest(interaction *server.Interaction, d
gologger.DefaultLogger.Print().Msgf("[Interactsh]: got result %v and status %v after processing interaction", result, matched)
}
if c.options.FuzzParamsFrequency != nil {
if !matched {
c.options.FuzzParamsFrequency.MarkParameter(data.Parameter, data.Request.URL.String(), data.Operators.TemplateID)
} else {
c.options.FuzzParamsFrequency.UnmarkParameter(data.Parameter, data.Request.URL.String(), data.Operators.TemplateID)
}
}
// if we don't match, return
if !matched || result == nil {
return false
@@ -320,6 +329,9 @@ type RequestData struct {
Operators *operators.Operators
MatchFunc operators.MatchFunc
ExtractFunc operators.ExtractFunc
Parameter string
Request *retryablehttp.Request
}
// RequestEvent is the event for a network request sent by nuclei.

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/projectdiscovery/interactsh/pkg/client"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/frequency"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
@@ -46,8 +47,9 @@ type Options struct {
// NoColor disables printing colors for matches
NoColor bool
StopAtFirstMatch bool
HTTPClient *retryablehttp.Client
FuzzParamsFrequency *frequency.Tracker
StopAtFirstMatch bool
HTTPClient *retryablehttp.Client
}
// DefaultOptions returns the default options for interactsh client

View File

@@ -120,7 +120,8 @@ func (request *Request) executeAllFuzzingRules(input *contextargs.Context, value
}
err := rule.Execute(&fuzz.ExecuteRuleInput{
Input: input,
Input: input,
DisplayFuzzPoints: request.options.Options.DisplayFuzzPoints,
Callback: func(gr fuzz.GeneratedRequest) bool {
select {
case <-input.Context().Done():
@@ -139,6 +140,7 @@ func (request *Request) executeAllFuzzingRules(input *contextargs.Context, value
continue
}
if fuzz.IsErrRuleNotApplicable(err) {
gologger.Verbose().Msgf("[%s] fuzz: rule not applicable : %s\n", request.options.TemplateID, err)
continue
}
if err == types.ErrNoMoreRequests {
@@ -176,6 +178,7 @@ func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest,
result.FuzzingPosition = gr.Component.Name()
}
setInteractshCallback := false
if hasInteractMarkers && hasInteractMatchers && request.options.Interactsh != nil {
requestData := &interactsh.RequestData{
MakeResultFunc: request.MakeResultEvent,
@@ -183,7 +186,10 @@ func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest,
Operators: request.CompiledOperators,
MatchFunc: request.Match,
ExtractFunc: request.Extract,
Parameter: gr.Parameter,
Request: gr.Request,
}
setInteractshCallback = true
request.options.Interactsh.RequestEvent(gr.InteractURLs, requestData)
gotMatches = request.options.Interactsh.AlreadyMatched(requestData)
} else {
@@ -193,6 +199,13 @@ func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest,
if event.OperatorsResult != nil {
gotMatches = event.OperatorsResult.Matched
}
if request.options.FuzzParamsFrequency != nil && !setInteractshCallback {
if !gotMatches {
request.options.FuzzParamsFrequency.MarkParameter(gr.Parameter, gr.Request.URL.String(), request.options.TemplateID)
} else {
request.options.FuzzParamsFrequency.UnmarkParameter(gr.Parameter, gr.Request.URL.String(), request.options.TemplateID)
}
}
}, 0)
// If a variable is unresolved, skip all further requests
if errors.Is(requestErr, ErrMissingVars) {

View File

@@ -13,6 +13,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/frequency"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
"github.com/projectdiscovery/nuclei/v3/pkg/loader/parser"
@@ -92,6 +93,8 @@ type ExecutorOptions struct {
ExcludeMatchers *excludematchers.ExcludeMatchers
// InputHelper is a helper for input normalization
InputHelper *input.Helper
// FuzzParamsFrequency is a cache for parameter frequency
FuzzParamsFrequency *frequency.Tracker
Operators []*operators.Operators // only used by offlinehttp module

View File

@@ -27,6 +27,7 @@ func GetPlaygroundServer() *echo.Echo {
e.GET("/request", requestHandler)
e.GET("/email", emailHandler)
e.GET("/permissions", permissionsHandler)
e.GET("/blog/post", numIdorHandler) // for num based idors like ?id=44
e.POST("/reset-password", resetPasswordHandler)
e.GET("/host-header-lab", hostHeaderLabHandler)
@@ -47,13 +48,20 @@ var bodyTemplate = `<html>
func indexHandler(ctx echo.Context) error {
return ctx.HTML(200, fmt.Sprintf(bodyTemplate, `<h1>Fuzzing Playground</h1><hr>
<ul>
<li><a href="/info?name=test&another=value&random=data">Info Page XSS</a></li>
<li><a href="/redirect?redirect_url=/info?name=redirected_from_url">Redirect Page OpenRedirect</a></li>
<li><a href="/request?url=https://example.com">Request Page SSRF</a></li>
<li><a href="/email?text=important_user">Email Page SSTI</a></li>
<li><a href="/permissions?cmd=whoami">Permissions Page CMDI</a></li>
</ul>
<ul>
<li><a href="/info?name=test&another=value&random=data">Info Page XSS</a></li>
<li><a href="/redirect?redirect_url=/info?name=redirected_from_url">Redirect Page OpenRedirect</a></li>
<li><a href="/request?url=https://example.com">Request Page SSRF</a></li>
<li><a href="/email?text=important_user">Email Page SSTI</a></li>
<li><a href="/permissions?cmd=whoami">Permissions Page CMDI</a></li>
<li><a href="/host-header-lab">Host Header Lab (X-Forwarded-Host Trusted)</a></li>
<li><a href="/user/75/profile">User Profile Page SQLI (path parameter)</a></li>
<li><a href="/user">POST on /user SQLI (body parameter)</a></li>
<li><a href="/blog/posts">SQLI in cookie lang parameter value (eg. lang=en)</a></li>
</ul>
`))
}

View File

@@ -363,6 +363,12 @@ type Options struct {
FuzzingMode string
// TlsImpersonate enables TLS impersonation
TlsImpersonate bool
// DisplayFuzzPoints enables display of fuzz points for fuzzing
DisplayFuzzPoints bool
// FuzzAggressionLevel is the level of fuzzing aggression (low, medium, high.)
FuzzAggressionLevel string
// FuzzParamFrequency is the frequency of fuzzing parameters
FuzzParamFrequency int
// CodeTemplateSignaturePublicKey is the custom public key used to verify the template signature (algorithm is automatically inferred from the length)
CodeTemplateSignaturePublicKey string
// CodeTemplateSignatureAlgorithm specifies the sign algorithm (rsa, ecdsa)