Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fuzzing additions & enhancements #5139

Merged
merged 13 commits into from
Jun 10, 2024
1 change: 1 addition & 0 deletions cmd/nuclei/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ 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.CreateGroup("uncover", "Uncover",
Expand Down
24 changes: 22 additions & 2 deletions pkg/fuzz/execute.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fuzz

import (
"encoding/json"
"fmt"
"io"
"regexp"
Expand Down Expand Up @@ -45,6 +46,8 @@
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
Expand All @@ -57,6 +60,8 @@
DynamicValues map[string]interface{}
// Component is the component for the request
Component component.Component
// Parameter being fuzzed
Parameter string
}

// Execute executes a fuzzing rule accepting a callback on which
Expand All @@ -74,8 +79,9 @@

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 || rule.partType == requestPartType) {
continue
}
component := component.New(componentName)
Expand All @@ -87,12 +93,26 @@
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 {

Check failure on line 104 in pkg/fuzz/execute.go

View workflow job for this annotation

GitHub Actions / Lint Test

Error return value of `component.Iterate` is not checked (errcheck)
displayDebugFuzzPoints[componentName][key] = fmt.Sprintf("%v", value)
return nil
})
}
finalComponentList = append(finalComponentList, component)
}
if len(displayDebugFuzzPoints) > 0 {
gologger.Info().Msgf("Fuzz points for %s\n", rule.options.TemplateID)
marshalled, _ := json.MarshalIndent(displayDebugFuzzPoints, "", " ")
gologger.Silent().Msgf("%s\n", string(marshalled))
}

if len(finalComponentList) == 0 {
return ErrRuleNotApplicable.Msgf("no component matched on this rule")
Expand Down Expand Up @@ -223,7 +243,7 @@
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
}
}
Expand Down
11 changes: 6 additions & 5 deletions pkg/fuzz/parts.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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() && rule.partType != requestPartType {
return false
}
foundAny := false
Expand Down Expand Up @@ -68,7 +68,7 @@ func (rule *Rule) executePartComponentOnValues(input *ExecuteRuleInput, payloadS
return err
}

if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent); qerr != nil {
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key); qerr != nil {
return qerr
}
// fmt.Printf("executed with value: %s\n", evaluated)
Expand All @@ -90,7 +90,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
}
Expand Down Expand Up @@ -125,7 +125,7 @@ func (rule *Rule) executePartComponentOnKV(input *ExecuteRuleInput, payload Valu
return err
}

if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent); qerr != nil {
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key); qerr != nil {
return err
}

Expand All @@ -144,12 +144,13 @@ 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) error {
func (rule *Rule) execWithInput(input *ExecuteRuleInput, httpReq *retryablehttp.Request, interactURLs []string, component component.Component, parameter string) error {
request := GeneratedRequest{
Request: httpReq,
InteractURLs: interactURLs,
DynamicValues: input.Values,
Component: component,
Parameter: parameter,
}
if !input.Callback(request) {
return types.ErrNoMoreRequests
Expand Down
16 changes: 16 additions & 0 deletions pkg/output/format_screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,21 @@ func (w *StandardWriter) formatScreen(output *ResultEvent) []byte {
}
builder.WriteString("]")
}

// If it is a fuzzing output, enrich with additional
// metadata for the match.
if output.IsFuzzingResult {
if output.FuzzingParameter != "" {
builder.WriteString(" [")
builder.WriteString(output.FuzzingPosition)
builder.WriteRune(':')
builder.WriteString(w.aurora.BrightMagenta(output.FuzzingParameter).String())
builder.WriteString("]")
}

builder.WriteString(" [")
builder.WriteString(output.FuzzingMethod)
builder.WriteString("]")
}
return builder.Bytes()
}
8 changes: 8 additions & 0 deletions pkg/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ type ResultEvent struct {
// must be enabled by setting protocols.ExecuterOptions.ExportReqURLPattern to true
ReqURLPattern string `json:"req_url_pattern,omitempty"`

// Fields related to HTTP Fuzzing functionality of nuclei.
// The output contains additional fields when the result is
// for a fuzzing template.
IsFuzzingResult bool `json:"is_fuzzing_result,omitempty"`
FuzzingMethod string `json:"fuzzing_method,omitempty"`
FuzzingParameter string `json:"fuzzing_parameter,omitempty"`
FuzzingPosition string `json:"fuzzing_position,omitempty"`

FileToIndexPosition map[string]int `json:"-"`
Error string `json:"error,omitempty"`
}
Expand Down
11 changes: 10 additions & 1 deletion pkg/protocols/http/request_fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,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():
Expand All @@ -140,6 +141,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 {
Expand Down Expand Up @@ -170,6 +172,13 @@ func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest,
}
var gotMatches bool
requestErr := request.executeRequest(input, req, gr.DynamicValues, hasInteractMatchers, func(event *output.InternalWrappedEvent) {
for _, result := range event.Results {
result.IsFuzzingResult = true
result.FuzzingMethod = gr.Request.Method
result.FuzzingParameter = gr.Parameter
result.FuzzingPosition = gr.Component.Name()
}

if hasInteractMarkers && hasInteractMatchers && request.options.Interactsh != nil {
requestData := &interactsh.RequestData{
MakeResultFunc: request.MakeResultEvent,
Expand Down
22 changes: 15 additions & 7 deletions pkg/testutils/fuzzplayground/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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>
`))
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ type Options struct {
FuzzingMode string
// TlsImpersonate enables TLS impersonation
TlsImpersonate bool
// DisplayFuzzPoints enables display of fuzz points for fuzzing
DisplayFuzzPoints bool
// 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)
Expand Down
Loading