diff --git a/v2/internal/runner/runner.go b/v2/internal/runner/runner.go index 49f2dc57d..839acc6f8 100644 --- a/v2/internal/runner/runner.go +++ b/v2/internal/runner/runner.go @@ -372,7 +372,10 @@ func (r *Runner) RunEnumeration() { wgtemplates.Wait() if r.interactsh != nil { - r.interactsh.Close() + matched := r.interactsh.Close() + if matched { + results.CAS(false, true) + } } r.progress.Stop() diff --git a/v2/pkg/operators/operators.go b/v2/pkg/operators/operators.go index 423ed40d5..228255d6e 100644 --- a/v2/pkg/operators/operators.go +++ b/v2/pkg/operators/operators.go @@ -65,6 +65,32 @@ type Result struct { PayloadValues map[string]interface{} } +// Merge merges a result structure into the other. +func (r *Result) Merge(result *Result) { + if !r.Matched && result.Matched { + r.Matched = result.Matched + } + if !r.Extracted && result.Extracted { + r.Extracted = result.Extracted + } + + for k, v := range result.Matches { + r.Matches[k] = v + } + for k, v := range result.Extracts { + r.Extracts[k] = v + } + for _, v := range result.OutputExtracts { + r.OutputExtracts = append(r.OutputExtracts, v) + } + for k, v := range result.DynamicValues { + r.DynamicValues[k] = v + } + for k, v := range result.PayloadValues { + r.PayloadValues[k] = v + } +} + // MatchFunc performs matching operation for a matcher on model and returns true or false. type MatchFunc func(data map[string]interface{}, matcher *matchers.Matcher) bool @@ -81,6 +107,7 @@ func (r *Operators) Execute(data map[string]interface{}, match MatchFunc, extrac Extracts: make(map[string][]string), DynamicValues: make(map[string]interface{}), } + // Start with the extractors first and evaluate them. for _, extractor := range r.Extractors { var extractorResults []string diff --git a/v2/pkg/protocols/common/interactsh/interactsh.go b/v2/pkg/protocols/common/interactsh/interactsh.go index 10480ac48..6c11010c5 100644 --- a/v2/pkg/protocols/common/interactsh/interactsh.go +++ b/v2/pkg/protocols/common/interactsh/interactsh.go @@ -22,6 +22,7 @@ type Client struct { // requests is a stored cache for interactsh-url->request-event data. requests *ccache.Cache + matched bool dotHostname string eviction time.Duration pollDuration time.Duration @@ -77,24 +78,38 @@ func New(options *Options) (*Client, error) { pollDuration: options.PollDuration, cooldownDuration: options.ColldownPeriod, } + interactClient.interactsh.StartPolling(interactClient.pollDuration, func(interaction *server.Interaction) { item := interactClient.requests.Get(interaction.UniqueID) if item == nil { return } - data, ok := item.Value().(*internalRequestEvent) + data, ok := item.Value().(*RequestData) if !ok { return } + + data.Event.InternalEvent["interactsh-protocol"] = interaction.Protocol + data.Event.InternalEvent["interactsh-request"] = interaction.RawRequest + data.Event.InternalEvent["interactsh-response"] = interaction.RawResponse + result, matched := data.Operators.Execute(data.Event.InternalEvent, data.MatchFunc, data.ExtractFunc) + if !matched || result == nil { + return // if we don't match, return + } interactClient.requests.Delete(interaction.UniqueID) - data.event.OperatorsResult = &operators.Result{ - Matches: map[string]struct{}{strings.ToLower(interaction.Protocol): {}}, + if data.Event.OperatorsResult != nil { + data.Event.OperatorsResult.Merge(result) + } else { + data.Event.OperatorsResult = result } - data.event.Results = data.makeResultFunc(data.event) - for _, result := range data.event.Results { + data.Event.Results = data.MakeResultFunc(data.Event) + for _, result := range data.Event.Results { result.Interaction = interaction _ = options.Output.Write(result) + if !interactClient.matched { + interactClient.matched = true + } options.Progress.IncrementMatched() } }) @@ -107,12 +122,13 @@ func (c *Client) URL() string { } // Close closes the interactsh clients after waiting for cooldown period. -func (c *Client) Close() { +func (c *Client) Close() bool { if c.cooldownDuration > 0 { time.Sleep(c.cooldownDuration) } c.interactsh.StopPolling() c.interactsh.Close() + return c.matched } // ReplaceMarkers replaces the {{interactsh-url}} placeholders to actual @@ -133,13 +149,36 @@ func (c *Client) ReplaceMarkers(data, interactshURL string) string { // MakeResultEventFunc is a result making function for nuclei type MakeResultEventFunc func(wrapped *output.InternalWrappedEvent) []*output.ResultEvent -type internalRequestEvent struct { - makeResultFunc MakeResultEventFunc - event *output.InternalWrappedEvent +// RequestData contains data for a request event +type RequestData struct { + MakeResultFunc MakeResultEventFunc + Event *output.InternalWrappedEvent + Operators *operators.Operators + MatchFunc operators.MatchFunc + ExtractFunc operators.ExtractFunc } // RequestEvent is the event for a network request sent by nuclei. -func (c *Client) RequestEvent(interactshURL string, event *output.InternalWrappedEvent, makeResult MakeResultEventFunc) { +func (c *Client) RequestEvent(interactshURL string, data *RequestData) { id := strings.TrimSuffix(interactshURL, c.dotHostname) - c.requests.Set(id, &internalRequestEvent{makeResultFunc: makeResult, event: event}, c.eviction) + c.requests.Set(id, data, c.eviction) +} + +// HasMatchers returns true if an operator has interactsh part +// matchers or extractors. +// +// Used by requests to show result or not depending on presence of interact.sh +// data part matchers. +func HasMatchers(operators *operators.Operators) bool { + for _, matcher := range operators.Matchers { + if strings.HasPrefix(matcher.Part, "interactsh-") { + return true + } + } + for _, matcher := range operators.Extractors { + if strings.HasPrefix(matcher.Part, "interactsh-") { + return true + } + } + return false } diff --git a/v2/pkg/protocols/http/request.go b/v2/pkg/protocols/http/request.go index 0e9f5fb80..be2d294f9 100644 --- a/v2/pkg/protocols/http/request.go +++ b/v2/pkg/protocols/http/request.go @@ -17,6 +17,7 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/protocols" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" + "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/tostring" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/httpclientpool" "github.com/projectdiscovery/rawhttp" @@ -219,8 +220,16 @@ func (r *Request) ExecuteWithResults(reqURL string, dynamicValues, previous outp gotOutput = true dynamicValues = generators.MergeMaps(dynamicValues, event.OperatorsResult.DynamicValues) } - if r.options.Interactsh != nil { - r.options.Interactsh.RequestEvent(interactURL, event, r.MakeResultEvent) + if interactsh.HasMatchers(r.CompiledOperators) { + if r.options.Interactsh != nil { + r.options.Interactsh.RequestEvent(interactURL, &interactsh.RequestData{ + MakeResultFunc: r.MakeResultEvent, + Event: event, + Operators: r.CompiledOperators, + MatchFunc: r.Match, + ExtractFunc: r.Extract, + }) + } } callback(event) }, requestCount) @@ -398,14 +407,16 @@ func (r *Request) executeRequest(reqURL string, request *generatedRequest, previ } event := &output.InternalWrappedEvent{InternalEvent: outputEvent} - if r.CompiledOperators != nil { - var ok bool - event.OperatorsResult, ok = r.CompiledOperators.Execute(finalEvent, r.Match, r.Extract) - if ok && event.OperatorsResult != nil { - event.OperatorsResult.PayloadValues = request.meta - event.Results = r.MakeResultEvent(event) + if !interactsh.HasMatchers(r.CompiledOperators) { + if r.CompiledOperators != nil { + var ok bool + event.OperatorsResult, ok = r.CompiledOperators.Execute(finalEvent, r.Match, r.Extract) + if ok && event.OperatorsResult != nil { + event.OperatorsResult.PayloadValues = request.meta + event.Results = r.MakeResultEvent(event) + } + event.InternalEvent = outputEvent } - event.InternalEvent = outputEvent } callback(event) return nil diff --git a/v2/pkg/protocols/network/request.go b/v2/pkg/protocols/network/request.go index bcedcf645..dd12cdbc7 100644 --- a/v2/pkg/protocols/network/request.go +++ b/v2/pkg/protocols/network/request.go @@ -13,6 +13,7 @@ import ( "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/protocols" + "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/replacer" ) @@ -77,6 +78,11 @@ func (r *Request) executeAddress(actualAddress, address, input string, shouldUse defer conn.Close() _ = conn.SetReadDeadline(time.Now().Add(time.Duration(r.options.Options.Timeout) * time.Second)) + var interactURL string + if r.options.Interactsh != nil { + interactURL = r.options.Interactsh.URL() + } + responseBuilder := &strings.Builder{} reqBuilder := &strings.Builder{} @@ -88,6 +94,9 @@ func (r *Request) executeAddress(actualAddress, address, input string, shouldUse case "hex": data, err = hex.DecodeString(input.Data) default: + if r.options.Interactsh != nil { + input.Data = r.options.Interactsh.ReplaceMarkers(input.Data, interactURL) + } data = []byte(input.Data) } if err != nil { @@ -150,11 +159,23 @@ func (r *Request) executeAddress(actualAddress, address, input string, shouldUse } event := &output.InternalWrappedEvent{InternalEvent: outputEvent} - if r.CompiledOperators != nil { - result, ok := r.CompiledOperators.Execute(outputEvent, r.Match, r.Extract) - if ok && result != nil { - event.OperatorsResult = result - event.Results = r.MakeResultEvent(event) + if !interactsh.HasMatchers(r.CompiledOperators) { + if r.CompiledOperators != nil { + result, ok := r.CompiledOperators.Execute(outputEvent, r.Match, r.Extract) + if ok && result != nil { + event.OperatorsResult = result + event.Results = r.MakeResultEvent(event) + } + } + } else { + if r.options.Interactsh != nil { + r.options.Interactsh.RequestEvent(interactURL, &interactsh.RequestData{ + MakeResultFunc: r.MakeResultEvent, + Event: event, + Operators: r.CompiledOperators, + MatchFunc: r.Match, + ExtractFunc: r.Extract, + }) } } callback(event)