mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-01-31 15:53:10 +08:00
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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
152
pkg/fuzz/frequency/tracker.go
Normal file
152
pkg/fuzz/frequency/tracker.go
Normal 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
|
||||
}
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
`))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user