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:
-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)
-fuzz enable loading fuzzing templates
-fuzz enable loading fuzzing templates (Deprecated: use -dast instead)
-dast only run DAST templates
UNCOVER:
-uc, -uncover enable uncover engine

View File

@ -21,8 +21,6 @@ var fuzzingTestCases = []TestCaseInfo{
{Path: "fuzz/fuzz-type.yaml", TestCase: &fuzzTypeOverride{}},
{Path: "fuzz/fuzz-query.yaml", TestCase: &httpFuzzQuery{}},
{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
// logic in fuzz playground server instead of adding them here
{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)
}
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
var updateNucleiBinary bool
var pdcpauth string
var fuzzFlag bool
flagSet := goflags.NewFlagSet()
flagSet.CaseSensitive = true
@ -313,7 +314,8 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.CreateGroup("fuzzing", "Fuzzing",
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.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",
@ -436,6 +438,12 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
goflags.DisableAutoConfigMigration = true
_ = 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
if pdcpauth == "true" {
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.
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)
r.options.FuzzTemplates = true
loaderConfig.OnlyLoadHTTPFuzzing = true
r.options.DAST = true
}
store, err := loader.New(loaderConfig)
if err != nil {
@ -640,19 +639,27 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
stats.Display(templates.SyntaxWarningStats)
stats.Display(templates.SyntaxErrorStats)
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
stats.DisplayAsWarning(templates.HeadlessFlagWarningStats)
stats.DisplayAsWarning(templates.CodeFlagWarningStats)
stats.DisplayAsWarning(templates.TemplatesExecutedStats)
stats.DisplayAsWarning(templates.HeadlessFlagWarningStats)
stats.DisplayAsWarning(templates.CodeFlagWarningStats)
stats.DisplayAsWarning(templates.FuzzFlagWarningStats)
stats.DisplayAsWarning(templates.TemplatesExecutedStats)
stats.ForceDisplayWarning(templates.ExcludedHeadlessTmplStats)
stats.ForceDisplayWarning(templates.ExcludedCodeTmplStats)
stats.ForceDisplayWarning(templates.ExludedDastTmplStats)
stats.ForceDisplayWarning(templates.TemplatesExcludedStats)
}
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.SkippedRequestSignatureStats)
cfg := config.DefaultConfig
@ -666,25 +673,27 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
}
}
if len(store.Templates()) > 0 {
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 tmplCount > 0 || workflowCount > 0 {
if len(store.Templates()) > 0 {
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 value > 0 {
if k != templates.Unsigned {
gologger.Info().Msgf("Executing %d signed templates from %s", value, k)
} else if !r.options.Silent && !config.DefaultConfig.HideTemplateSigWarning {
gologger.Print().Msgf("[%v] Loaded %d unsigned templates for scan. Use with caution.", aurora.BrightYellow("WRN"), value)
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.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
func EnableFuzzTemplates() NucleiSDKOptions {
// DASTMode only run DAST templates
func DASTMode() NucleiSDKOptions {
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
}
}

View File

@ -61,8 +61,6 @@ type Config struct {
Catalog catalog.Catalog
ExecutorOptions protocols.ExecutorOptions
OnlyLoadHTTPFuzzing bool
}
// Store is a storage for loaded nuclei templates
@ -405,33 +403,42 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templ
stats.Increment(templates.SkippedUnsignedStats)
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
stats.Increment(templates.HeadlessFlagWarningStats)
stats.Increment(templates.ExcludedHeadlessTmplStats)
if config.DefaultConfig.LogAllEvents {
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 {
// 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 {
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 {
// 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
stats.Increment(templates.SkippedUnsignedStats)
if config.DefaultConfig.LogAllEvents {
gologger.Print().Msgf("[%v] Tampered/Unsigned template at %v.\n", aurora.Yellow("WRN").String(), templatePath)
}
} else if parsed.IsFuzzing() && !store.config.ExecutorOptions.Options.FuzzTemplates {
stats.Increment(templates.FuzzFlagWarningStats)
} else if parsed.IsFuzzing() && !store.config.ExecutorOptions.Options.DAST {
stats.Increment(templates.ExludedDastTmplStats)
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 {
loadedTemplates = append(loadedTemplates, parsed)
}
@ -439,7 +446,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templ
}
if err != nil {
if strings.Contains(err.Error(), templates.ErrExcluded.Error()) {
stats.Increment(templates.TemplatesExecutedStats)
stats.Increment(templates.TemplatesExcludedStats)
if cfg.DefaultConfig.LogAllEvents {
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)))
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"
"io"
"mime/multipart"
"net/url"
"strings"
"testing"
"github.com/projectdiscovery/retryablehttp-go"
urlutil "github.com/projectdiscovery/utils/url"
"github.com/stretchr/testify/require"
)
@ -80,7 +80,7 @@ func TestBodyXMLComponent(t *testing.T) {
}
func TestBodyFormComponent(t *testing.T) {
formData := url.Values{}
formData := urlutil.NewOrderedParams()
formData.Set("key1", "value1")
formData.Set("key2", "value2")

View File

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

View File

@ -99,6 +99,14 @@ func (c *Cookie) Rebuild() (*retryablehttp.Request, error) {
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
// must not be fuzzed.
var defaultIgnoredCookieKeys = map[string]struct{}{

View File

@ -102,6 +102,14 @@ func (q *Header) Rebuild() (*retryablehttp.Request, error) {
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
// must not be fuzzed.
var defaultIgnoredHeaderKeys = map[string]struct{}{

View File

@ -83,3 +83,11 @@ func (q *Path) Rebuild() (*retryablehttp.Request, error) {
}
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()
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
}
// 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
func (v *Value) String() string {
return v.data

View File

@ -1,6 +1,9 @@
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
// that is implemented or used by fuzzing package
@ -14,6 +17,18 @@ type KV struct {
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
func (kv *KV) IsNIL() bool {
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
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() {
// for kv fuzzing
return rule.executePartComponentOnKV(input, payload, ruleComponent)
return rule.executePartComponentOnKV(input, payload, ruleComponent.Clone())
} else {
// 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 {
// trim trailing space
// quote non-ascii and non printable characters and then
// unquote quotes (`"`) for readability
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())
if i != len(output.ExtractedResults)-1 {

View File

@ -135,6 +135,12 @@ func (variables *Variable) checkForLazyEval() bool {
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
}

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)
return
}
_ = parsed
for _, p := range s.Data.Params {
parsed.Params.Add(p.Key, p.Value)
}

View File

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

View File

@ -1,13 +1,14 @@
package templates
const (
SyntaxWarningStats = "syntax-warnings"
SyntaxErrorStats = "syntax-errors"
RuntimeWarningsStats = "runtime-warnings"
UnsignedCodeWarning = "unsigned-warnings"
HeadlessFlagWarningStats = "headless-flag-missing-warnings"
TemplatesExecutedStats = "templates-executed"
CodeFlagWarningStats = "code-flag-missing-warnings"
FuzzFlagWarningStats = "fuzz-flag-missing-warnings"
SkippedUnsignedStats = "skipped-unsigned-stats" // tracks loading of unsigned templates
SyntaxWarningStats = "syntax-warnings"
SyntaxErrorStats = "syntax-errors"
RuntimeWarningsStats = "runtime-warnings"
SkippedCodeTmplTamperedStats = "unsigned-warnings"
ExcludedHeadlessTmplStats = "headless-flag-missing-warnings"
TemplatesExcludedStats = "templates-executed"
ExcludedCodeTmplStats = "code-flag-missing-warnings"
ExludedDastTmplStats = "fuzz-flag-missing-warnings"
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(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(UnsignedCodeWarning, "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(CodeFlagWarningStats, "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(FuzzFlagWarningStats, "Excluded %d fuzz template[s] (disabled as default), use -fuzz option to run fuzz templates.")
stats.NewEntry(SkippedCodeTmplTamperedStats, "Found %d unsigned or tampered code template (carefully examine before using it & use -sign flag to sign them)")
stats.NewEntry(ExcludedHeadlessTmplStats, "Excluded %d headless template[s] (disabled as default), use -headless option to run headless templates.")
stats.NewEntry(ExcludedCodeTmplStats, "Excluded %d code template[s] (disabled as default), use -code option to run code templates.")
stats.NewEntry(TemplatesExcludedStats, "Excluded %d template[s] with known weak matchers / tags excluded from default run using .nuclei-ignore")
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(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: |
// 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:
// - "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"`
@ -214,6 +215,11 @@ func (template *Template) IsFuzzing() bool {
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
func (template *Template) HasCodeProtocol() bool {
return len(template.RequestsCode) > 0

View File

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

View File

@ -372,9 +372,6 @@ type Options struct {
ScanID string
// JsConcurrency is the number of concurrent js routines to run
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 goflags.StringSlice
// PreFetchSecrets pre-fetches the secrets from the auth provider
@ -385,6 +382,8 @@ type Options struct {
SkipFormatValidation bool
// PayloadConcurrency is the number of concurrent payloads to run per template
PayloadConcurrency int
// Dast only runs DAST templates
DAST bool
}
// ShouldLoadResume resume file