req_url_pattern for vuln_hash calculation + unit test (#4964)

This commit is contained in:
Tarun Koyalwar 2024-03-30 23:50:31 +05:30 committed by GitHub
parent 5ce912e316
commit 25e7799c09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 154 additions and 10 deletions

View File

@ -439,6 +439,11 @@ func (r *Runner) RunEnumeration() error {
Parser: r.parser, Parser: r.parser,
} }
if env.GetEnvOrDefault("NUCLEI_ARGS", "") == "req_url_pattern=true" {
// Go StdLib style experimental/debug feature switch
executorOpts.ExportReqURLPattern = true
}
if len(r.options.SecretsFile) > 0 && !r.options.Validate { if len(r.options.SecretsFile) > 0 && !r.options.Validate {
authTmplStore, err := GetAuthTmplStore(*r.options, r.catalog, executorOpts) authTmplStore, err := GetAuthTmplStore(*r.options, r.catalog, executorOpts)
if err != nil { if err != nil {

View File

@ -171,6 +171,9 @@ type ResultEvent struct {
// IssueTrackers is the metadata for issue trackers // IssueTrackers is the metadata for issue trackers
IssueTrackers map[string]IssueTrackerMetadata `json:"issue_trackers,omitempty"` IssueTrackers map[string]IssueTrackerMetadata `json:"issue_trackers,omitempty"`
// ReqURLPattern when enabled contains base URL pattern that was used to generate the request
// must be enabled by setting protocols.ExecuterOptions.ExportReqURLPattern to true
ReqURLPattern string `json:"req_url_pattern,omitempty"`
FileToIndexPosition map[string]int `json:"-"` FileToIndexPosition map[string]int `json:"-"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`

View File

@ -32,6 +32,10 @@ import (
urlutil "github.com/projectdiscovery/utils/url" urlutil "github.com/projectdiscovery/utils/url"
) )
const (
ReqURLPatternKey = "req_url_pattern"
)
// ErrEvalExpression // ErrEvalExpression
var ( var (
ErrEvalExpression = errorutil.NewWithTag("expr", "could not evaluate helper expressions") ErrEvalExpression = errorutil.NewWithTag("expr", "could not evaluate helper expressions")
@ -48,6 +52,36 @@ type generatedRequest struct {
dynamicValues map[string]interface{} dynamicValues map[string]interface{}
interactshURLs []string interactshURLs []string
customCancelFunction context.CancelFunc customCancelFunction context.CancelFunc
// requestURLPattern tracks unmodified request url pattern without values ( it is used for constant vuln_hash)
// ex: {{BaseURL}}/api/exp?param={{randstr}}
requestURLPattern string
}
// setReqURLPattern sets the url request pattern for the generated request
func (gr *generatedRequest) setReqURLPattern(reqURLPattern string) {
data := strings.Split(reqURLPattern, "\n")
if len(data) > 1 {
reqURLPattern = strings.TrimSpace(data[0])
// this is raw request (if it has 3 parts after strings.Fields then its valid only use 2nd part)
parts := strings.Fields(reqURLPattern)
if len(parts) >= 3 {
// remove first and last and use all in between
parts = parts[1 : len(parts)-1]
reqURLPattern = strings.Join(parts, " ")
}
} else {
reqURLPattern = strings.TrimSpace(reqURLPattern)
}
// now urlRequestPattern is generated replace preprocessor values with actual placeholders
// that were used (these are called generated 'constants' and contains {{}} in var name)
for k, v := range gr.original.options.Constants {
if strings.HasPrefix(k, "{{") && strings.HasSuffix(k, "}}") {
// this takes care of all preprocessors ( currently we have randstr and its variations)
reqURLPattern = strings.ReplaceAll(reqURLPattern, fmt.Sprint(v), k)
}
}
gr.requestURLPattern = reqURLPattern
} }
// ApplyAuth applies the auth provider to the generated request // ApplyAuth applies the auth provider to the generated request
@ -96,7 +130,13 @@ func (r *requestGenerator) Total() int {
// Make creates a http request for the provided input. // Make creates a http request for the provided input.
// It returns ErrNoMoreRequests as error when all the requests have been exhausted. // It returns ErrNoMoreRequests as error when all the requests have been exhausted.
func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context, reqData string, payloads, dynamicValues map[string]interface{}) (*generatedRequest, error) { func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context, reqData string, payloads, dynamicValues map[string]interface{}) (gr *generatedRequest, err error) {
origReqData := reqData
defer func() {
if gr != nil {
gr.setReqURLPattern(origReqData)
}
}()
// value of `reqData` depends on the type of request specified in template // value of `reqData` depends on the type of request specified in template
// 1. If request is raw request = reqData contains raw request (i.e http request dump) // 1. If request is raw request = reqData contains raw request (i.e http request dump)
// 2. If request is Normal ( simply put not a raw request) (Ex: with placeholders `path`) = reqData contains relative path // 2. If request is Normal ( simply put not a raw request) (Ex: with placeholders `path`) = reqData contains relative path

View File

@ -495,8 +495,9 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
if event == nil { if event == nil {
event := &output.InternalWrappedEvent{ event := &output.InternalWrappedEvent{
InternalEvent: map[string]interface{}{ InternalEvent: map[string]interface{}{
"template-id": request.options.TemplateID, "template-id": request.options.TemplateID,
"host": input.MetaInput.Input, "host": input.MetaInput.Input,
ReqURLPatternKey: generatedRequest.requestURLPattern,
}, },
} }
if request.CompiledOperators != nil && request.CompiledOperators.HasDSL() { if request.CompiledOperators != nil && request.CompiledOperators.HasDSL() {
@ -515,6 +516,7 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
if event.InternalEvent["host"] == nil { if event.InternalEvent["host"] == nil {
event.InternalEvent["host"] = input.MetaInput.Input event.InternalEvent["host"] = input.MetaInput.Input
} }
event.InternalEvent[ReqURLPatternKey] = generatedRequest.requestURLPattern
}() }()
request.setCustomHeaders(generatedRequest) request.setCustomHeaders(generatedRequest)
@ -843,6 +845,14 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
event.UsesInteractsh = true event.UsesInteractsh = true
} }
// if requrlpattern is enabled, only then it is reflected in result event else it is empty string
// consult @Ice3man543 before changing this logic (context: vuln_hash)
if request.options.ExportReqURLPattern {
for _, v := range event.Results {
v.ReqURLPattern = generatedRequest.requestURLPattern
}
}
responseContentType := respChain.Response().Header.Get("Content-Type") responseContentType := respChain.Response().Header.Get("Content-Type")
isResponseTruncated := request.MaxSize > 0 && respChain.Body().Len() >= request.MaxSize isResponseTruncated := request.MaxSize > 0 && respChain.Body().Len() >= request.MaxSize
dumpResponse(event, request, respChain.FullResponse().Bytes(), formedURL, responseContentType, isResponseTruncated, input.MetaInput.Input) dumpResponse(event, request, respChain.FullResponse().Bytes(), formedURL, responseContentType, isResponseTruncated, input.MetaInput.Input)

View File

@ -15,6 +15,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers" "github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
"github.com/projectdiscovery/nuclei/v3/pkg/output" "github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/testutils" "github.com/projectdiscovery/nuclei/v3/pkg/testutils"
) )
@ -184,3 +185,74 @@ func TestDisableTE(t *testing.T) {
require.NotNil(t, finalEvent, "could not get event output from request") require.NotNil(t, finalEvent, "could not get event output from request")
require.Equal(t, 2, matchCount, "could not get correct match count") require.Equal(t, 2, matchCount, "could not get correct match count")
} }
// consult @Ice3man543 before making any breaking changes to this test (context: vuln_hash)
func TestReqURLPattern(t *testing.T) {
options := testutils.DefaultOptions
// assume this was a preprocessor
// {{randstr}} => 2eNU2kbrOcUDzhnUL1RGvSo1it7
testutils.Init(options)
templateID := "testing-http"
request := &Request{
ID: templateID,
Raw: []string{
`GET /{{rand_char("abc")}}/{{interactsh-url}}/123?query={{rand_int(1, 10)}}&data=2eNU2kbrOcUDzhnUL1RGvSo1it7 HTTP/1.1
Host: {{Hostname}}
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
`,
},
Operators: operators.Operators{
Matchers: []*matchers.Matcher{{
Type: matchers.MatcherTypeHolder{MatcherType: matchers.DSLMatcher},
DSL: []string{"true"},
}},
},
IterateAll: true,
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// always return 200
w.WriteHeader(200)
_, _ = w.Write([]byte(`match`))
}))
defer ts.Close()
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
ID: templateID,
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
})
client, _ := interactsh.New(interactsh.DefaultOptions(executerOpts.Output, nil, executerOpts.Progress))
executerOpts.Interactsh = client
defer client.Close()
executerOpts.ExportReqURLPattern = true
// this is how generated constants are added to template
// generated constants are preprocessors that are executed while loading once
executerOpts.Constants = map[string]interface{}{
"{{randstr}}": "2eNU2kbrOcUDzhnUL1RGvSo1it7",
}
err := request.Compile(executerOpts)
require.Nil(t, err, "could not compile network request")
var finalEvent *output.InternalWrappedEvent
var matchCount int
t.Run("test", func(t *testing.T) {
metadata := make(output.InternalEvent)
previous := make(output.InternalEvent)
ctxArgs := contextargs.NewWithInput(ts.URL)
err := request.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
matchCount++
}
finalEvent = event
})
require.Nil(t, err, "could not execute network request")
})
require.NotNil(t, finalEvent, "could not get event output from request")
require.Equal(t, 1, matchCount, "could not get correct match count")
require.NotEmpty(t, finalEvent.Results[0].ReqURLPattern, "could not get req url pattern")
require.Equal(t, `/{{rand_char("abc")}}/{{interactsh-url}}/123?query={{rand_int(1, 10)}}&data={{randstr}}`, finalEvent.Results[0].ReqURLPattern)
}

View File

@ -123,6 +123,9 @@ type ExecutorOptions struct {
//TemporaryDirectory is the directory to store temporary files //TemporaryDirectory is the directory to store temporary files
TemporaryDirectory string TemporaryDirectory string
Parser parser.Parser Parser parser.Parser
// ExportReqURLPattern exports the request URL pattern
// in ResultEvent it contains the exact url pattern (ex: {{BaseURL}}/{{randstr}}/xyz) used in the request
ExportReqURLPattern bool
} }
// GetThreadsForPayloadRequests returns the number of threads to use as default for // GetThreadsForPayloadRequests returns the number of threads to use as default for

View File

@ -18,6 +18,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler" "github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
"github.com/projectdiscovery/nuclei/v3/pkg/operators" "github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols" "github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/offlinehttp" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/offlinehttp"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/signer" "github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec" "github.com/projectdiscovery/nuclei/v3/pkg/tmplexec"
@ -295,14 +296,22 @@ func ParseTemplateFromReader(reader io.Reader, preprocessor Preprocessor, option
SignatureStats[Unsigned].Add(1) SignatureStats[Unsigned].Add(1)
} }
generatedConstants := map[string]interface{}{}
// ==== execute preprocessors ====== // ==== execute preprocessors ======
for _, v := range allPreprocessors { for _, v := range allPreprocessors {
data = v.Process(data) var replaced map[string]interface{}
data, replaced = v.ProcessNReturnData(data)
// preprocess kind of act like a constant and are generated while loading
// and stay constant for the template lifecycle
generatedConstants = generators.MergeMaps(generatedConstants, replaced)
} }
reParsed, err := parseTemplate(data, options) reParsed, err := parseTemplate(data, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// add generated constants to constants map and executer options
reParsed.Constants = generators.MergeMaps(reParsed.Constants, generatedConstants)
reParsed.Options.Constants = reParsed.Constants
reParsed.Verified = isVerified reParsed.Verified = isVerified
return reParsed, nil return reParsed, nil
} }

View File

@ -10,7 +10,7 @@ import (
type Preprocessor interface { type Preprocessor interface {
// Process processes the data and returns the processed data. // Process processes the data and returns the processed data.
Process(data []byte) []byte ProcessNReturnData(data []byte) ([]byte, map[string]interface{})
// Exists check if the preprocessor exists in the data. // Exists check if the preprocessor exists in the data.
Exists(data []byte) bool Exists(data []byte) bool
} }
@ -39,10 +39,10 @@ var _ Preprocessor = &randStrPreprocessor{}
type randStrPreprocessor struct{} type randStrPreprocessor struct{}
// Process processes the data and returns the processed data. // ProcessNReturnData processes the data and returns the key-value pairs of generated/replaced data.
func (r *randStrPreprocessor) Process(data []byte) []byte { func (r *randStrPreprocessor) ProcessNReturnData(data []byte) ([]byte, map[string]interface{}) {
foundMap := make(map[string]struct{}) foundMap := make(map[string]struct{})
dataMap := make(map[string]interface{})
for _, expression := range preprocessorRegex.FindAllStringSubmatch(string(data), -1) { for _, expression := range preprocessorRegex.FindAllStringSubmatch(string(data), -1) {
if len(expression) != 2 { if len(expression) != 2 {
continue continue
@ -57,10 +57,12 @@ func (r *randStrPreprocessor) Process(data []byte) []byte {
} }
foundMap[value] = struct{}{} foundMap[value] = struct{}{}
if strings.EqualFold(value, "randstr") || strings.HasPrefix(value, "randstr_") { if strings.EqualFold(value, "randstr") || strings.HasPrefix(value, "randstr_") {
data = bytes.ReplaceAll(data, []byte(expression[0]), []byte(ksuid.New().String())) randStr := ksuid.New().String()
data = bytes.ReplaceAll(data, []byte(expression[0]), []byte(randStr))
dataMap[expression[0]] = randStr
} }
} }
return data return data, dataMap
} }
// Exists check if the preprocessor exists in the data. // Exists check if the preprocessor exists in the data.