add `-dast` flag and multiple bug fixes for dast templates (#4941)

* add default get method

* remove residual payload logic from old implementation

* fuzz: clone current state of component

* fuzz: bug fix stacking of payloads in multiple mode

* improve stdout template loading stats

* stdout: force display warnings if no templates are loaded

* update flags in README.md

* quote non-ascii chars in extractor output

* aws request signature can only be used in signed & verified tmpls

* deprecate request signature

* remove logic related to deprecated fuzzing input

* update test to use ordered params

* fix interactsh-url lazy eval: #4946

* output: skip unnecessary updates when unescaping

* updates as per requested changes
dev
Tarun Koyalwar 2024-03-29 13:31:30 +05:30 committed by GitHub
parent 78300e3250
commit e88889b263
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 216 additions and 274 deletions

View File

@ -221,7 +221,8 @@ INTERACTSH:
FUZZING: FUZZING:
-ft, -fuzzing-type string overrides fuzzing type set in template (replace, prefix, postfix, infix) -ft, -fuzzing-type string overrides fuzzing type set in template (replace, prefix, postfix, infix)
-fm, -fuzzing-mode string overrides fuzzing mode set in template (multiple, single) -fm, -fuzzing-mode string overrides fuzzing mode set in template (multiple, single)
-fuzz enable loading fuzzing templates -fuzz enable loading fuzzing templates (Deprecated: use -dast instead)
-dast only run DAST templates
UNCOVER: UNCOVER:
-uc, -uncover enable uncover engine -uc, -uncover enable uncover engine

View File

@ -21,8 +21,6 @@ var fuzzingTestCases = []TestCaseInfo{
{Path: "fuzz/fuzz-type.yaml", TestCase: &fuzzTypeOverride{}}, {Path: "fuzz/fuzz-type.yaml", TestCase: &fuzzTypeOverride{}},
{Path: "fuzz/fuzz-query.yaml", TestCase: &httpFuzzQuery{}}, {Path: "fuzz/fuzz-query.yaml", TestCase: &httpFuzzQuery{}},
{Path: "fuzz/fuzz-headless.yaml", TestCase: &HeadlessFuzzingQuery{}}, {Path: "fuzz/fuzz-headless.yaml", TestCase: &HeadlessFuzzingQuery{}},
{Path: "fuzz/fuzz-header-basic.yaml", TestCase: &FuzzHeaderBasic{}},
{Path: "fuzz/fuzz-header-multiple.yaml", TestCase: &FuzzHeaderMultiple{}},
// for fuzzing we should prioritize adding test case related backend // for fuzzing we should prioritize adding test case related backend
// logic in fuzz playground server instead of adding them here // logic in fuzz playground server instead of adding them here
{Path: "fuzz/fuzz-query-num-replace.yaml", TestCase: &genericFuzzTestCase{expectedResults: 2}}, {Path: "fuzz/fuzz-query-num-replace.yaml", TestCase: &genericFuzzTestCase{expectedResults: 2}},
@ -176,52 +174,3 @@ func (h *HeadlessFuzzingQuery) Execute(filePath string) error {
} }
return expectResultsCount(got, 2) return expectResultsCount(got, 2)
} }
type FuzzHeaderBasic struct{}
// Execute executes a test case and returns an error if occurred
func (h *FuzzHeaderBasic) Execute(filePath string) error {
router := httprouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
host := r.Header.Get("Origin")
// redirect to different domain
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "<html><body><a href="+host+">Click Here</a></body></html>")
})
ts := httptest.NewTLSServer(router)
defer ts.Close()
got, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-fuzz")
if err != nil {
return err
}
return expectResultsCount(got, 1)
}
type FuzzHeaderMultiple struct{}
// Execute executes a test case and returns an error if occurred
func (h *FuzzHeaderMultiple) Execute(filePath string) error {
router := httprouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
host1 := r.Header.Get("Origin")
host2 := r.Header.Get("X-Forwared-For")
fmt.Printf("host1: %s, host2: %s\n", host1, host2)
if host1 == host2 && host2 == "secret.local" {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "welcome! to secret admin panel")
return
}
// redirect to different domain
w.WriteHeader(http.StatusForbidden)
})
ts := httptest.NewTLSServer(router)
defer ts.Close()
got, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-fuzz")
if err != nil {
return err
}
return expectResultsCount(got, 1)
}

View File

@ -186,6 +186,7 @@ func readConfig() *goflags.FlagSet {
// when true updates nuclei binary to latest version // when true updates nuclei binary to latest version
var updateNucleiBinary bool var updateNucleiBinary bool
var pdcpauth string var pdcpauth string
var fuzzFlag bool
flagSet := goflags.NewFlagSet() flagSet := goflags.NewFlagSet()
flagSet.CaseSensitive = true flagSet.CaseSensitive = true
@ -313,7 +314,8 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.CreateGroup("fuzzing", "Fuzzing", flagSet.CreateGroup("fuzzing", "Fuzzing",
flagSet.StringVarP(&options.FuzzingType, "fuzzing-type", "ft", "", "overrides fuzzing type set in template (replace, prefix, postfix, infix)"), flagSet.StringVarP(&options.FuzzingType, "fuzzing-type", "ft", "", "overrides fuzzing type set in template (replace, prefix, postfix, infix)"),
flagSet.StringVarP(&options.FuzzingMode, "fuzzing-mode", "fm", "", "overrides fuzzing mode set in template (multiple, single)"), flagSet.StringVarP(&options.FuzzingMode, "fuzzing-mode", "fm", "", "overrides fuzzing mode set in template (multiple, single)"),
flagSet.BoolVar(&options.FuzzTemplates, "fuzz", false, "enable loading fuzzing templates"), flagSet.BoolVar(&fuzzFlag, "fuzz", false, "enable loading fuzzing templates (Deprecated: use -dast instead)"),
flagSet.BoolVar(&options.DAST, "dast", false, "only run DAST templates"),
) )
flagSet.CreateGroup("uncover", "Uncover", flagSet.CreateGroup("uncover", "Uncover",
@ -436,6 +438,12 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
goflags.DisableAutoConfigMigration = true goflags.DisableAutoConfigMigration = true
_ = flagSet.Parse() _ = flagSet.Parse()
// when fuzz flag is enabled, set the dast flag to true
if fuzzFlag {
// backwards compatibility for fuzz flag
options.DAST = true
}
// api key hierarchy: cli flag > env var > .pdcp/credential file // api key hierarchy: cli flag > env var > .pdcp/credential file
if pdcpauth == "true" { if pdcpauth == "true" {
runner.AuthWithPDCP() runner.AuthWithPDCP()

View File

@ -1,49 +0,0 @@
id: fuzz-header-basic
info:
name: fuzz header basic
author: pdteam
severity: info
description: |
In this template we check for any reflection when fuzzing Origin header
variables:
first: "{{rand_int(10000, 99999)}}"
http:
- raw:
- |
GET /?x=aaa&y=bbb HTTP/1.1
Host: {{Hostname}}
Origin: https://example.com
X-Fuzz-Header: 1337
Cookie: z=aaa; bb=aaa
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Connection: close
payloads:
reflection:
- "'\"><{{first}}"
fuzzing:
- part: header
type: replace
mode: single
keys: ["Origin"]
fuzz:
- "{{reflection}}"
stop-at-first-match: true
matchers-condition: and
matchers:
- type: word
part: body
words:
- "{{reflection}}"
- type: word
part: header
words:
- "text/html"

View File

@ -1,41 +0,0 @@
id: fuzz-header-multiple
info:
name: fuzz header multiple
author: pdteam
severity: info
description: |
In this template we fuzz multiple headers with single payload
http:
- raw:
- |
GET /?x=aaa&y=bbb HTTP/1.1
Host: {{Hostname}}
Origin: https://example.com
X-Forwared-For: 1337
Cookie: z=aaa; bb=aaa
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Connection: close
payloads:
reflection:
- "secret.local"
fuzzing:
- part: header
type: replace
mode: multiple
keys: ["Origin", "X-Forwared-For"]
fuzz:
- "{{reflection}}"
stop-at-first-match: true
matchers-condition: and
matchers:
- type: word
part: body
words:
- "admin"

View File

@ -475,10 +475,9 @@ func (r *Runner) RunEnumeration() error {
// If using input-file flags, only load http fuzzing based templates. // If using input-file flags, only load http fuzzing based templates.
loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts) loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts)
if !strings.EqualFold(r.options.InputFileMode, "list") || r.options.FuzzTemplates { if !strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST {
// if input type is not list (implicitly enable fuzzing) // if input type is not list (implicitly enable fuzzing)
r.options.FuzzTemplates = true r.options.DAST = true
loaderConfig.OnlyLoadHTTPFuzzing = true
} }
store, err := loader.New(loaderConfig) store, err := loader.New(loaderConfig)
if err != nil { if err != nil {
@ -640,19 +639,27 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
stats.Display(templates.SyntaxWarningStats) stats.Display(templates.SyntaxWarningStats)
stats.Display(templates.SyntaxErrorStats) stats.Display(templates.SyntaxErrorStats)
stats.Display(templates.RuntimeWarningsStats) stats.Display(templates.RuntimeWarningsStats)
if r.options.Verbose { tmplCount := len(store.Templates())
workflowCount := len(store.Workflows())
if r.options.Verbose || (tmplCount == 0 && workflowCount == 0) {
// only print these stats in verbose mode // only print these stats in verbose mode
stats.DisplayAsWarning(templates.HeadlessFlagWarningStats) stats.ForceDisplayWarning(templates.ExcludedHeadlessTmplStats)
stats.DisplayAsWarning(templates.CodeFlagWarningStats) stats.ForceDisplayWarning(templates.ExcludedCodeTmplStats)
stats.DisplayAsWarning(templates.TemplatesExecutedStats) stats.ForceDisplayWarning(templates.ExludedDastTmplStats)
stats.DisplayAsWarning(templates.HeadlessFlagWarningStats) stats.ForceDisplayWarning(templates.TemplatesExcludedStats)
stats.DisplayAsWarning(templates.CodeFlagWarningStats)
stats.DisplayAsWarning(templates.FuzzFlagWarningStats)
stats.DisplayAsWarning(templates.TemplatesExecutedStats)
} }
stats.DisplayAsWarning(templates.UnsignedCodeWarning) if tmplCount == 0 && workflowCount == 0 {
// if dast flag is used print explicit warning
if r.options.DAST {
gologger.DefaultLogger.Print().Msgf("[%v] No DAST templates found", aurora.BrightYellow("WRN"))
}
stats.ForceDisplayWarning(templates.SkippedCodeTmplTamperedStats)
} else {
stats.DisplayAsWarning(templates.SkippedCodeTmplTamperedStats)
}
stats.ForceDisplayWarning(templates.SkippedUnsignedStats) stats.ForceDisplayWarning(templates.SkippedUnsignedStats)
stats.ForceDisplayWarning(templates.SkippedRequestSignatureStats)
cfg := config.DefaultConfig cfg := config.DefaultConfig
@ -666,25 +673,27 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
} }
} }
if len(store.Templates()) > 0 { if tmplCount > 0 || workflowCount > 0 {
gologger.Info().Msgf("New templates added in latest release: %d", len(config.DefaultConfig.GetNewAdditions())) if len(store.Templates()) > 0 {
gologger.Info().Msgf("Templates loaded for current scan: %d", len(store.Templates())) gologger.Info().Msgf("New templates added in latest release: %d", len(config.DefaultConfig.GetNewAdditions()))
} gologger.Info().Msgf("Templates loaded for current scan: %d", len(store.Templates()))
if len(store.Workflows()) > 0 {
gologger.Info().Msgf("Workflows loaded for current scan: %d", len(store.Workflows()))
}
for k, v := range templates.SignatureStats {
value := v.Load()
if k == templates.Unsigned && value > 0 {
// adjust skipped unsigned templates via code or -dut flag
value = value - uint64(stats.GetValue(templates.SkippedUnsignedStats))
value = value - uint64(stats.GetValue(templates.CodeFlagWarningStats))
} }
if value > 0 { if len(store.Workflows()) > 0 {
if k != templates.Unsigned { gologger.Info().Msgf("Workflows loaded for current scan: %d", len(store.Workflows()))
gologger.Info().Msgf("Executing %d signed templates from %s", value, k) }
} else if !r.options.Silent && !config.DefaultConfig.HideTemplateSigWarning { for k, v := range templates.SignatureStats {
gologger.Print().Msgf("[%v] Loaded %d unsigned templates for scan. Use with caution.", aurora.BrightYellow("WRN"), value) value := v.Load()
if k == templates.Unsigned && value > 0 {
// adjust skipped unsigned templates via code or -dut flag
value = value - uint64(stats.GetValue(templates.SkippedUnsignedStats))
value = value - uint64(stats.GetValue(templates.ExcludedCodeTmplStats))
}
if value > 0 {
if k == templates.Unsigned && !r.options.Silent && !config.DefaultConfig.HideTemplateSigWarning {
gologger.Print().Msgf("[%v] Loading %d unsigned templates for scan. Use with caution.", aurora.BrightYellow("WRN"), value)
} else {
gologger.Info().Msgf("Executing %d signed templates from %s", value, k)
}
} }
} }
} }

View File

@ -376,10 +376,18 @@ func LoadSecretsFromFile(files []string, prefetch bool) NucleiSDKOptions {
} }
} }
// EnableFuzzTemplates allows enabling template fuzzing // DASTMode only run DAST templates
func EnableFuzzTemplates() NucleiSDKOptions { func DASTMode() NucleiSDKOptions {
return func(e *NucleiEngine) error { return func(e *NucleiEngine) error {
e.opts.FuzzTemplates = true e.opts.DAST = true
return nil
}
}
// SignedTemplatesOnly only run signed templates and disabled loading all unsigned templates
func SignedTemplatesOnly() NucleiSDKOptions {
return func(e *NucleiEngine) error {
e.opts.DisableUnsignedTemplates = true
return nil return nil
} }
} }

View File

@ -61,8 +61,6 @@ type Config struct {
Catalog catalog.Catalog Catalog catalog.Catalog
ExecutorOptions protocols.ExecutorOptions ExecutorOptions protocols.ExecutorOptions
OnlyLoadHTTPFuzzing bool
} }
// Store is a storage for loaded nuclei templates // Store is a storage for loaded nuclei templates
@ -405,33 +403,42 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templ
stats.Increment(templates.SkippedUnsignedStats) stats.Increment(templates.SkippedUnsignedStats)
continue continue
} }
if len(parsed.RequestsHeadless) > 0 && !store.config.ExecutorOptions.Options.Headless { // if template has request signature like aws then only signed and verified templates are allowed
if parsed.UsesRequestSignature() && !parsed.Verified {
stats.Increment(templates.SkippedRequestSignatureStats)
continue
}
// DAST only templates
if store.config.ExecutorOptions.Options.DAST {
// check if the template is a DAST template
if parsed.IsFuzzing() {
loadedTemplates = append(loadedTemplates, parsed)
}
} else if len(parsed.RequestsHeadless) > 0 && !store.config.ExecutorOptions.Options.Headless {
// donot include headless template in final list if headless flag is not set // donot include headless template in final list if headless flag is not set
stats.Increment(templates.HeadlessFlagWarningStats) stats.Increment(templates.ExcludedHeadlessTmplStats)
if config.DefaultConfig.LogAllEvents { if config.DefaultConfig.LogAllEvents {
gologger.Print().Msgf("[%v] Headless flag is required for headless template '%s'.\n", aurora.Yellow("WRN").String(), templatePath) gologger.Print().Msgf("[%v] Headless flag is required for headless template '%s'.\n", aurora.Yellow("WRN").String(), templatePath)
} }
} else if len(parsed.RequestsCode) > 0 && !store.config.ExecutorOptions.Options.EnableCodeTemplates { } else if len(parsed.RequestsCode) > 0 && !store.config.ExecutorOptions.Options.EnableCodeTemplates {
// donot include 'Code' protocol custom template in final list if code flag is not set // donot include 'Code' protocol custom template in final list if code flag is not set
stats.Increment(templates.CodeFlagWarningStats) stats.Increment(templates.ExcludedCodeTmplStats)
if config.DefaultConfig.LogAllEvents { if config.DefaultConfig.LogAllEvents {
gologger.Print().Msgf("[%v] Code flag is required for code protocol template '%s'.\n", aurora.Yellow("WRN").String(), templatePath) gologger.Print().Msgf("[%v] Code flag is required for code protocol template '%s'.\n", aurora.Yellow("WRN").String(), templatePath)
} }
} else if len(parsed.RequestsCode) > 0 && !parsed.Verified && len(parsed.Workflows) == 0 { } else if len(parsed.RequestsCode) > 0 && !parsed.Verified && len(parsed.Workflows) == 0 {
// donot include unverified 'Code' protocol custom template in final list // donot include unverified 'Code' protocol custom template in final list
stats.Increment(templates.UnsignedCodeWarning) stats.Increment(templates.SkippedCodeTmplTamperedStats)
// these will be skipped so increment skip counter // these will be skipped so increment skip counter
stats.Increment(templates.SkippedUnsignedStats) stats.Increment(templates.SkippedUnsignedStats)
if config.DefaultConfig.LogAllEvents { if config.DefaultConfig.LogAllEvents {
gologger.Print().Msgf("[%v] Tampered/Unsigned template at %v.\n", aurora.Yellow("WRN").String(), templatePath) gologger.Print().Msgf("[%v] Tampered/Unsigned template at %v.\n", aurora.Yellow("WRN").String(), templatePath)
} }
} else if parsed.IsFuzzing() && !store.config.ExecutorOptions.Options.FuzzTemplates { } else if parsed.IsFuzzing() && !store.config.ExecutorOptions.Options.DAST {
stats.Increment(templates.FuzzFlagWarningStats) stats.Increment(templates.ExludedDastTmplStats)
if config.DefaultConfig.LogAllEvents { if config.DefaultConfig.LogAllEvents {
gologger.Print().Msgf("[%v] Fuzz flag is required for fuzzing template '%s'.\n", aurora.Yellow("WRN").String(), templatePath) gologger.Print().Msgf("[%v] -dast flag is required for DAST template '%s'.\n", aurora.Yellow("WRN").String(), templatePath)
} }
} else if store.config.OnlyLoadHTTPFuzzing && !parsed.IsFuzzing() {
gologger.Warning().Msgf("Non-Fuzzing template '%s' can only be run on list input mode targets\n", templatePath)
} else { } else {
loadedTemplates = append(loadedTemplates, parsed) loadedTemplates = append(loadedTemplates, parsed)
} }
@ -439,7 +446,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templ
} }
if err != nil { if err != nil {
if strings.Contains(err.Error(), templates.ErrExcluded.Error()) { if strings.Contains(err.Error(), templates.ErrExcluded.Error()) {
stats.Increment(templates.TemplatesExecutedStats) stats.Increment(templates.TemplatesExcludedStats)
if cfg.DefaultConfig.LogAllEvents { if cfg.DefaultConfig.LogAllEvents {
gologger.Print().Msgf("[%v] %v\n", aurora.Yellow("WRN").String(), err.Error()) gologger.Print().Msgf("[%v] %v\n", aurora.Yellow("WRN").String(), err.Error())
} }

View File

@ -141,3 +141,10 @@ func (b *Body) Rebuild() (*retryablehttp.Request, error) {
cloned.Header.Set("Content-Length", strconv.Itoa(len(encoded))) cloned.Header.Set("Content-Length", strconv.Itoa(len(encoded)))
return cloned, nil return cloned, nil
} }
func (b *Body) Clone() Component {
return &Body{
value: b.value.Clone(),
req: b.req.Clone(context.Background()),
}
}

View File

@ -4,11 +4,11 @@ import (
"bytes" "bytes"
"io" "io"
"mime/multipart" "mime/multipart"
"net/url"
"strings" "strings"
"testing" "testing"
"github.com/projectdiscovery/retryablehttp-go" "github.com/projectdiscovery/retryablehttp-go"
urlutil "github.com/projectdiscovery/utils/url"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -80,7 +80,7 @@ func TestBodyXMLComponent(t *testing.T) {
} }
func TestBodyFormComponent(t *testing.T) { func TestBodyFormComponent(t *testing.T) {
formData := url.Values{} formData := urlutil.NewOrderedParams()
formData.Set("key1", "value1") formData.Set("key1", "value1")
formData.Set("key2", "value2") formData.Set("key2", "value2")

View File

@ -46,6 +46,8 @@ type Component interface {
// Rebuild returns a new request with the // Rebuild returns a new request with the
// component rebuilt // component rebuilt
Rebuild() (*retryablehttp.Request, error) Rebuild() (*retryablehttp.Request, error)
// Clones current state of this component
Clone() Component
} }
const ( const (

View File

@ -99,6 +99,14 @@ func (c *Cookie) Rebuild() (*retryablehttp.Request, error) {
return cloned, nil return cloned, nil
} }
// Clone clones current state of this component
func (c *Cookie) Clone() Component {
return &Cookie{
value: c.value.Clone(),
req: c.req.Clone(context.Background()),
}
}
// A list of cookies that are essential to the request and // A list of cookies that are essential to the request and
// must not be fuzzed. // must not be fuzzed.
var defaultIgnoredCookieKeys = map[string]struct{}{ var defaultIgnoredCookieKeys = map[string]struct{}{

View File

@ -102,6 +102,14 @@ func (q *Header) Rebuild() (*retryablehttp.Request, error) {
return cloned, nil return cloned, nil
} }
// Clones current state of this component
func (q *Header) Clone() Component {
return &Header{
value: q.value.Clone(),
req: q.req.Clone(context.Background()),
}
}
// A list of headers that are essential to the request and // A list of headers that are essential to the request and
// must not be fuzzed. // must not be fuzzed.
var defaultIgnoredHeaderKeys = map[string]struct{}{ var defaultIgnoredHeaderKeys = map[string]struct{}{

View File

@ -83,3 +83,11 @@ func (q *Path) Rebuild() (*retryablehttp.Request, error) {
} }
return cloned, nil return cloned, nil
} }
// Clones current state to a new component
func (q *Path) Clone() Component {
return &Path{
value: q.value.Clone(),
req: q.req.Clone(context.Background()),
}
}

View File

@ -92,3 +92,11 @@ func (q *Query) Rebuild() (*retryablehttp.Request, error) {
cloned.Update() cloned.Update()
return cloned, nil return cloned, nil
} }
// Clones current state to a new component
func (q *Query) Clone() Component {
return &Query{
value: q.value.Clone(),
req: q.req.Clone(context.Background()),
}
}

View File

@ -36,6 +36,15 @@ func NewValue(data string) *Value {
return v return v
} }
// Clones current state of this value
func (v *Value) Clone() *Value {
return &Value{
data: v.data,
parsed: v.parsed.Clone(),
dataFormat: v.dataFormat,
}
}
// String returns the string representation of the value // String returns the string representation of the value
func (v *Value) String() string { func (v *Value) String() string {
return v.data return v.data

View File

@ -1,6 +1,9 @@
package dataformat package dataformat
import mapsutil "github.com/projectdiscovery/utils/maps" import (
mapsutil "github.com/projectdiscovery/utils/maps"
"golang.org/x/exp/maps"
)
// KV is a key-value struct // KV is a key-value struct
// that is implemented or used by fuzzing package // that is implemented or used by fuzzing package
@ -14,6 +17,18 @@ type KV struct {
OrderedMap *mapsutil.OrderedMap[string, any] OrderedMap *mapsutil.OrderedMap[string, any]
} }
// Clones the current state of the KV struct
func (kv *KV) Clone() KV {
newKV := KV{}
if kv.OrderedMap == nil {
newKV.Map = maps.Clone(kv.Map)
return newKV
}
clonedOrderedMap := kv.OrderedMap.Clone()
newKV.OrderedMap = &clonedOrderedMap
return newKV
}
// IsNIL returns true if the KV struct is nil // IsNIL returns true if the KV struct is nil
func (kv *KV) IsNIL() bool { func (kv *KV) IsNIL() bool {
return kv.Map == nil && kv.OrderedMap == nil return kv.Map == nil && kv.OrderedMap == nil

View File

@ -34,12 +34,13 @@ func (rule *Rule) checkRuleApplicableOnComponent(component component.Component)
// executePartComponent executes this rule on a given component and payload // executePartComponent executes this rule on a given component and payload
func (rule *Rule) executePartComponent(input *ExecuteRuleInput, payload ValueOrKeyValue, ruleComponent component.Component) error { func (rule *Rule) executePartComponent(input *ExecuteRuleInput, payload ValueOrKeyValue, ruleComponent component.Component) error {
// Note: component needs to be cloned because they contain values copied by reference
if payload.IsKV() { if payload.IsKV() {
// for kv fuzzing // for kv fuzzing
return rule.executePartComponentOnKV(input, payload, ruleComponent) return rule.executePartComponentOnKV(input, payload, ruleComponent.Clone())
} else { } else {
// for value only fuzzing // for value only fuzzing
return rule.executePartComponentOnValues(input, payload.Value, ruleComponent) return rule.executePartComponentOnValues(input, payload.Value, ruleComponent.Clone())
} }
} }

View File

@ -59,8 +59,12 @@ func (w *StandardWriter) formatScreen(output *ResultEvent) []byte {
for i, item := range output.ExtractedResults { for i, item := range output.ExtractedResults {
// trim trailing space // trim trailing space
// quote non-ascii and non printable characters and then
// unquote quotes (`"`) for readability
item = strings.TrimSpace(item) item = strings.TrimSpace(item)
item = strings.ReplaceAll(item, "\n", "\\n") // only replace newlines item = strconv.QuoteToASCII(item)
item = strings.ReplaceAll(item, `\"`, `"`)
builder.WriteString(w.aurora.BrightCyan(item).String()) builder.WriteString(w.aurora.BrightCyan(item).String())
if i != len(output.ExtractedResults)-1 { if i != len(output.ExtractedResults)-1 {

View File

@ -135,6 +135,12 @@ func (variables *Variable) checkForLazyEval() bool {
return return
} }
} }
// this is a hotfix and not the best way to do it
// will be refactored once we move scan state to scanContext (see: https://github.com/projectdiscovery/nuclei/issues/4631)
if strings.Contains(types.ToString(value), "interactsh-url") {
variables.LazyEval = true
return
}
}) })
return variables.LazyEval return variables.LazyEval
} }

View File

@ -283,7 +283,6 @@ func (r *Request) ApplyAuthStrategy(strategy authx.AuthStrategy) {
gologger.Error().Msgf("auth strategy failed to parse url: %s got %v", r.FullURL, err) gologger.Error().Msgf("auth strategy failed to parse url: %s got %v", r.FullURL, err)
return return
} }
_ = parsed
for _, p := range s.Data.Params { for _, p := range s.Data.Params {
parsed.Params.Add(p.Key, p.Value) parsed.Params.Add(p.Key, p.Value)
} }

View File

@ -8,6 +8,7 @@ package http
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -23,13 +24,14 @@ import (
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils" protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/types" "github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/retryablehttp-go" "github.com/projectdiscovery/retryablehttp-go"
urlutil "github.com/projectdiscovery/utils/url"
) )
// executeFuzzingRule executes fuzzing request for a URL // executeFuzzingRule executes fuzzing request for a URL
// TODO: // TODO:
// 1. use SPMHandler and rewrite stop at first match logic here // 1. use SPMHandler and rewrite stop at first match logic here
// 2. use scanContext instead of contextargs.Context // 2. use scanContext instead of contextargs.Context
func (request *Request) executeFuzzingRule(input *contextargs.Context, _ output.InternalEvent, callback protocols.OutputEventCallback) error { func (request *Request) executeFuzzingRule(input *contextargs.Context, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
// methdology: // methdology:
// to check applicablity of rule, we first try to execute it with one value // 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 applicable, we execute all requests
@ -46,29 +48,20 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, _ output.
return nil return nil
} }
// Iterate through all requests for template and queue them for fuzzing if input.MetaInput.Input == "" && input.MetaInput.ReqResp == nil {
generator := request.newGenerator(true) return errors.New("empty input provided for fuzzing")
// 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 // ==== fuzzing when full HTTP request is provided =====
if input.MetaInput.ReqResp != nil { if input.MetaInput.ReqResp != nil {
// Note: in case of full http request, we only need to build it once baseRequest, err := input.MetaInput.ReqResp.BuildRequest()
// 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 { if err != nil {
return errors.Wrap(err, "fuzz: could not build request obtained from target file") return errors.Wrap(err, "fuzz: could not build request obtained from target file")
} }
input.MetaInput.Input = generated.URL.String() input.MetaInput.Input = baseRequest.URL.String()
// execute with one value first to checks its applicability // execute with one value first to checks its applicability
err = request.executePayloadUsingRules(input, payloads, generated, callback) err = request.executeAllFuzzingRules(input, previous, baseRequest, callback)
if err != nil { if err != nil {
// in case of any error, return it // in case of any error, return it
if fuzz.IsErrRuleNotApplicable(err) { if fuzz.IsErrRuleNotApplicable(err) {
@ -79,36 +72,25 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, _ output.
if errors.Is(err, errStopExecution) { if errors.Is(err, errStopExecution) {
return err return err
} }
gologger.Verbose().Msgf("[%s] fuzz: inital payload request execution failed : %s\n", request.options.TemplateID, err) gologger.Verbose().Msgf("[%s] fuzz: 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 return nil
} }
// ==== fuzzing when only URL is provided ===== // ==== fuzzing when only URL is provided =====
generated, err := generator.Make(context.Background(), input, currRequest, payloads, nil) // we need to use this url instead of input
inputx := input.Clone()
parsed, err := urlutil.ParseAbsoluteURL(input.MetaInput.Input, true)
if err != nil {
return errors.Wrap(err, "fuzz: could not parse input url")
}
baseRequest, err := retryablehttp.NewRequestFromURL(http.MethodGet, parsed, nil)
if err != nil { if err != nil {
return errors.Wrap(err, "fuzz: could not build request from url") 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 // execute with one value first to checks its applicability
err = request.executePayloadUsingRules(inputx, generated.dynamicValues, generated.request, callback) err = request.executeAllFuzzingRules(inputx, previous, baseRequest, callback)
if err != nil { if err != nil {
// in case of any error, return it // in case of any error, return it
if fuzz.IsErrRuleNotApplicable(err) { if fuzz.IsErrRuleNotApplicable(err) {
@ -119,34 +101,13 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, _ output.
if errors.Is(err, errStopExecution) { if errors.Is(err, errStopExecution) {
return err return err
} }
gologger.Verbose().Msgf("[%s] fuzz: inital payload request execution failed : %s\n", request.options.TemplateID, err) gologger.Verbose().Msgf("[%s] fuzz: 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 return nil
} }
// executePayloadUsingRules executes a payload using rules with given payload i.e values // executeAllFuzzingRules executes all fuzzing rules defined in template for a given base request
func (request *Request) executePayloadUsingRules(input *contextargs.Context, values map[string]interface{}, baseRequest *retryablehttp.Request, callback protocols.OutputEventCallback) error { func (request *Request) executeAllFuzzingRules(input *contextargs.Context, values map[string]interface{}, baseRequest *retryablehttp.Request, callback protocols.OutputEventCallback) error {
applicable := false applicable := false
for _, rule := range request.Fuzzing { for _, rule := range request.Fuzzing {
err := rule.Execute(&fuzz.ExecuteRuleInput{ err := rule.Execute(&fuzz.ExecuteRuleInput{
@ -156,7 +117,7 @@ func (request *Request) executePayloadUsingRules(input *contextargs.Context, val
return request.executeGeneratedFuzzingRequest(gr, input, callback) return request.executeGeneratedFuzzingRequest(gr, input, callback)
}, },
Values: values, Values: values,
BaseRequest: baseRequest, BaseRequest: baseRequest.Clone(context.TODO()),
}) })
if err == nil { if err == nil {
applicable = true applicable = true
@ -295,6 +256,9 @@ func (request *Request) filterDataMap(input *contextargs.Context) map[string]int
return true return true
}) })
m["header"] = sb.String() m["header"] = sb.String()
} else {
// add default method value
m["method"] = http.MethodGet
} }
// dump if svd is enabled // dump if svd is enabled

View File

@ -1,13 +1,14 @@
package templates package templates
const ( const (
SyntaxWarningStats = "syntax-warnings" SyntaxWarningStats = "syntax-warnings"
SyntaxErrorStats = "syntax-errors" SyntaxErrorStats = "syntax-errors"
RuntimeWarningsStats = "runtime-warnings" RuntimeWarningsStats = "runtime-warnings"
UnsignedCodeWarning = "unsigned-warnings" SkippedCodeTmplTamperedStats = "unsigned-warnings"
HeadlessFlagWarningStats = "headless-flag-missing-warnings" ExcludedHeadlessTmplStats = "headless-flag-missing-warnings"
TemplatesExecutedStats = "templates-executed" TemplatesExcludedStats = "templates-executed"
CodeFlagWarningStats = "code-flag-missing-warnings" ExcludedCodeTmplStats = "code-flag-missing-warnings"
FuzzFlagWarningStats = "fuzz-flag-missing-warnings" ExludedDastTmplStats = "fuzz-flag-missing-warnings"
SkippedUnsignedStats = "skipped-unsigned-stats" // tracks loading of unsigned templates SkippedUnsignedStats = "skipped-unsigned-stats" // tracks loading of unsigned templates
SkippedRequestSignatureStats = "skipped-request-signature-stats"
) )

View File

@ -6,10 +6,11 @@ func init() {
stats.NewEntry(SyntaxWarningStats, "Found %d templates with syntax warning (use -validate flag for further examination)") stats.NewEntry(SyntaxWarningStats, "Found %d templates with syntax warning (use -validate flag for further examination)")
stats.NewEntry(SyntaxErrorStats, "Found %d templates with syntax error (use -validate flag for further examination)") stats.NewEntry(SyntaxErrorStats, "Found %d templates with syntax error (use -validate flag for further examination)")
stats.NewEntry(RuntimeWarningsStats, "Found %d templates with runtime error (use -validate flag for further examination)") stats.NewEntry(RuntimeWarningsStats, "Found %d templates with runtime error (use -validate flag for further examination)")
stats.NewEntry(UnsignedCodeWarning, "Found %d unsigned or tampered code template (carefully examine before using it & use -sign flag to sign them)") stats.NewEntry(SkippedCodeTmplTamperedStats, "Found %d unsigned or tampered code template (carefully examine before using it & use -sign flag to sign them)")
stats.NewEntry(HeadlessFlagWarningStats, "Excluded %d headless template[s] (disabled as default), use -headless option to run headless templates.") stats.NewEntry(ExcludedHeadlessTmplStats, "Excluded %d headless template[s] (disabled as default), use -headless option to run headless templates.")
stats.NewEntry(CodeFlagWarningStats, "Excluded %d code template[s] (disabled as default), use -code option to run code templates.") stats.NewEntry(ExcludedCodeTmplStats, "Excluded %d code template[s] (disabled as default), use -code option to run code templates.")
stats.NewEntry(TemplatesExecutedStats, "Excluded %d template[s] with known weak matchers / tags excluded from default run using .nuclei-ignore") stats.NewEntry(TemplatesExcludedStats, "Excluded %d template[s] with known weak matchers / tags excluded from default run using .nuclei-ignore")
stats.NewEntry(FuzzFlagWarningStats, "Excluded %d fuzz template[s] (disabled as default), use -fuzz option to run fuzz templates.") stats.NewEntry(ExludedDastTmplStats, "Excluded %d dast template[s] (disabled as default), use -dast option to run dast templates.")
stats.NewEntry(SkippedUnsignedStats, "Skipping %d unsigned template[s]") stats.NewEntry(SkippedUnsignedStats, "Skipping %d unsigned template[s]")
stats.NewEntry(SkippedRequestSignatureStats, "Skipping %d templates, HTTP Request signatures can only be used in Signed & Verified templates.")
} }

View File

@ -132,6 +132,7 @@ type Template struct {
// description: | // description: |
// Signature is the request signature method // Signature is the request signature method
// WARNING: 'signature' will be deprecated and will be removed in a future release. Prefer using 'code' protocol for writing cloud checks
// values: // values:
// - "AWS" // - "AWS"
Signature http.SignatureTypeHolder `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=signature is the http request signature method,description=Signature is the HTTP Request signature Method,enum=AWS,deprecated=true"` Signature http.SignatureTypeHolder `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=signature is the http request signature method,description=Signature is the HTTP Request signature Method,enum=AWS,deprecated=true"`
@ -214,6 +215,11 @@ func (template *Template) IsFuzzing() bool {
return false return false
} }
// UsesRequestSignature returns true if the template uses a request signature like AWS
func (template *Template) UsesRequestSignature() bool {
return template.Signature.Value.String() != ""
}
// HasCodeProtocol returns true if the template has a code protocol section // HasCodeProtocol returns true if the template has a code protocol section
func (template *Template) HasCodeProtocol() bool { func (template *Template) HasCodeProtocol() bool {
return len(template.RequestsCode) > 0 return len(template.RequestsCode) > 0

View File

@ -86,6 +86,10 @@ func parseWorkflowTemplate(workflow *workflows.WorkflowTemplate, preprocessor Pr
stats.Increment(SkippedUnsignedStats) stats.Increment(SkippedUnsignedStats)
continue continue
} }
if template.UsesRequestSignature() && !template.Verified {
stats.Increment(SkippedRequestSignatureStats)
continue
}
if len(template.RequestsCode) > 0 { if len(template.RequestsCode) > 0 {
if !options.Options.EnableCodeTemplates { if !options.Options.EnableCodeTemplates {

View File

@ -372,9 +372,6 @@ type Options struct {
ScanID string ScanID string
// JsConcurrency is the number of concurrent js routines to run // JsConcurrency is the number of concurrent js routines to run
JsConcurrency int JsConcurrency int
// Fuzz enabled execution of fuzzing templates
// Note: when Fuzz is enabled other templates will not be executed
FuzzTemplates bool
// SecretsFile is file containing secrets for nuclei // SecretsFile is file containing secrets for nuclei
SecretsFile goflags.StringSlice SecretsFile goflags.StringSlice
// PreFetchSecrets pre-fetches the secrets from the auth provider // PreFetchSecrets pre-fetches the secrets from the auth provider
@ -385,6 +382,8 @@ type Options struct {
SkipFormatValidation bool SkipFormatValidation bool
// PayloadConcurrency is the number of concurrent payloads to run per template // PayloadConcurrency is the number of concurrent payloads to run per template
PayloadConcurrency int PayloadConcurrency int
// Dast only runs DAST templates
DAST bool
} }
// ShouldLoadResume resume file // ShouldLoadResume resume file