nuclei/pkg/protocols/common/interactsh/interactsh.go

446 lines
15 KiB
Go

package interactsh
import (
"bytes"
"fmt"
"os"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"errors"
"github.com/Mzack9999/gcache"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/interactsh/pkg/client"
"github.com/projectdiscovery/interactsh/pkg/server"
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/writer"
errorutil "github.com/projectdiscovery/utils/errors"
stringsutil "github.com/projectdiscovery/utils/strings"
)
// Client is a wrapped client for interactsh server.
type Client struct {
sync.Once
sync.RWMutex
options *Options
// interactsh is a client for interactsh server.
interactsh *client.Client
// requests is a stored cache for interactsh-url->request-event data.
requests gcache.Cache[string, *RequestData]
// interactions is a stored cache for interactsh-interaction->interactsh-url data
interactions gcache.Cache[string, []*server.Interaction]
// matchedTemplates is a stored cache to track matched templates
matchedTemplates gcache.Cache[string, bool]
// interactshURLs is a stored cache to track multiple interactsh markers
interactshURLs gcache.Cache[string, string]
eviction time.Duration
pollDuration time.Duration
cooldownDuration time.Duration
hostname string
// determines if wait the cooldown period in case of generated URL
generated atomic.Bool
matched atomic.Bool
}
// New returns a new interactsh server client
func New(options *Options) (*Client, error) {
requestsCache := gcache.New[string, *RequestData](options.CacheSize).LRU().Build()
interactionsCache := gcache.New[string, []*server.Interaction](defaultMaxInteractionsCount).LRU().Build()
matchedTemplateCache := gcache.New[string, bool](defaultMaxInteractionsCount).LRU().Build()
interactshURLCache := gcache.New[string, string](defaultMaxInteractionsCount).LRU().Build()
interactClient := &Client{
eviction: options.Eviction,
interactions: interactionsCache,
matchedTemplates: matchedTemplateCache,
interactshURLs: interactshURLCache,
options: options,
requests: requestsCache,
pollDuration: options.PollDuration,
cooldownDuration: options.CooldownPeriod,
}
return interactClient, nil
}
func (c *Client) poll() error {
if c.options.NoInteractsh {
// do not init if disabled
return ErrInteractshClientNotInitialized
}
interactsh, err := client.New(&client.Options{
ServerURL: c.options.ServerURL,
Token: c.options.Authorization,
DisableHTTPFallback: c.options.DisableHttpFallback,
HTTPClient: c.options.HTTPClient,
KeepAliveInterval: time.Minute,
})
if err != nil {
return errorutil.NewWithErr(err).Msgf("could not create client")
}
c.interactsh = interactsh
interactURL := interactsh.URL()
interactDomain := interactURL[strings.Index(interactURL, ".")+1:]
gologger.Info().Msgf("Using Interactsh Server: %s", interactDomain)
c.setHostname(interactDomain)
err = interactsh.StartPolling(c.pollDuration, func(interaction *server.Interaction) {
request, err := c.requests.Get(interaction.UniqueID)
// for more context in github actions
if strings.EqualFold(os.Getenv("GITHUB_ACTIONS"), "true") && c.options.Debug {
gologger.DefaultLogger.Print().Msgf("[Interactsh]: got interaction of %v for request %v and error %v", interaction, request, err)
}
if errors.Is(err, gcache.KeyNotFoundError) || request == nil {
// If we don't have any request for this ID, add it to temporary
// lru cache, so we can correlate when we get an add request.
items, err := c.interactions.Get(interaction.UniqueID)
if errorutil.IsAny(err, gcache.KeyNotFoundError) || items == nil {
_ = c.interactions.SetWithExpire(interaction.UniqueID, []*server.Interaction{interaction}, defaultInteractionDuration)
} else {
items = append(items, interaction)
_ = c.interactions.SetWithExpire(interaction.UniqueID, items, defaultInteractionDuration)
}
return
}
if requestShouldStopAtFirstMatch(request) || c.options.StopAtFirstMatch {
if gotItem, err := c.matchedTemplates.Get(hash(request.Event.InternalEvent)); gotItem && err == nil {
return
}
}
_ = c.processInteractionForRequest(interaction, request)
})
if err != nil {
return errorutil.NewWithErr(err).Msgf("could not perform interactsh polling")
}
return nil
}
// requestShouldStopAtFirstmatch checks if further interactions should be stopped
// note: extra care should be taken while using this function since internalEvent is
// synchronized all the time and if caller functions has already acquired lock its best to explicitly specify that
// we could use `TryLock()` but that may over complicate things and need to differentiate
// situations whether to block or skip
func requestShouldStopAtFirstMatch(request *RequestData) bool {
request.Event.RLock()
defer request.Event.RUnlock()
if stop, ok := request.Event.InternalEvent[stopAtFirstMatchAttribute]; ok {
if v, ok := stop.(bool); ok {
return v
}
}
return false
}
// processInteractionForRequest processes an interaction for a request
func (c *Client) processInteractionForRequest(interaction *server.Interaction, data *RequestData) bool {
var result *operators.Result
var matched bool
data.Event.Lock()
data.Event.InternalEvent["interactsh_protocol"] = interaction.Protocol
if strings.EqualFold(interaction.Protocol, "dns") {
data.Event.InternalEvent["interactsh_request"] = strings.ToLower(interaction.RawRequest)
} else {
data.Event.InternalEvent["interactsh_request"] = interaction.RawRequest
}
data.Event.InternalEvent["interactsh_response"] = interaction.RawResponse
data.Event.InternalEvent["interactsh_ip"] = interaction.RemoteAddress
data.Event.Unlock()
if data.Operators != nil {
result, matched = data.Operators.Execute(data.Event.InternalEvent, data.MatchFunc, data.ExtractFunc, c.options.Debug || c.options.DebugRequest || c.options.DebugResponse)
} else {
// this is most likely a bug so error instead of warning
var templateID string
if data.Event.InternalEvent != nil {
templateID = fmt.Sprint(data.Event.InternalEvent[templateIdAttribute])
}
gologger.Error().Msgf("missing compiled operators for '%v' template", templateID)
}
// for more context in github actions
if strings.EqualFold(os.Getenv("GITHUB_ACTIONS"), "true") && c.options.Debug {
gologger.DefaultLogger.Print().Msgf("[Interactsh]: got result %v and status %v after processing interaction", result, matched)
}
// if we don't match, return
if !matched || result == nil {
return false
}
c.requests.Remove(interaction.UniqueID)
if data.Event.OperatorsResult != nil {
data.Event.OperatorsResult.Merge(result)
} else {
data.Event.SetOperatorResult(result)
}
data.Event.Lock()
data.Event.Results = data.MakeResultFunc(data.Event)
for _, event := range data.Event.Results {
event.Interaction = interaction
}
data.Event.Unlock()
if c.options.Debug || c.options.DebugRequest || c.options.DebugResponse {
c.debugPrintInteraction(interaction, data.Event.OperatorsResult)
}
// if event is not already matched, write it to output
if !data.Event.InteractshMatched.Load() && writer.WriteResult(data.Event, c.options.Output, c.options.Progress, c.options.IssuesClient) {
data.Event.InteractshMatched.Store(true)
c.matched.Store(true)
if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {
_ = c.matchedTemplates.SetWithExpire(hash(data.Event.InternalEvent), true, defaultInteractionDuration)
}
}
return true
}
func (c *Client) AlreadyMatched(data *RequestData) bool {
data.Event.RLock()
defer data.Event.RUnlock()
return c.matchedTemplates.Has(hash(data.Event.InternalEvent))
}
// URL returns a new URL that can be interacted with
func (c *Client) URL() (string, error) {
// first time initialization
var err error
c.Do(func() {
err = c.poll()
})
if err != nil {
return "", errorutil.NewWithErr(err).Wrap(ErrInteractshClientNotInitialized)
}
if c.interactsh == nil {
return "", ErrInteractshClientNotInitialized
}
c.generated.Store(true)
return c.interactsh.URL(), nil
}
// Close the interactsh clients after waiting for cooldown period.
func (c *Client) Close() bool {
if c.cooldownDuration > 0 && c.generated.Load() {
time.Sleep(c.cooldownDuration)
}
if c.interactsh != nil {
_ = c.interactsh.StopPolling()
c.interactsh.Close()
}
c.requests.Purge()
c.interactions.Purge()
c.matchedTemplates.Purge()
c.interactshURLs.Purge()
return c.matched.Load()
}
// ReplaceMarkers replaces the default {{interactsh-url}} placeholders with interactsh urls
func (c *Client) Replace(data string, interactshURLs []string) (string, []string) {
return c.ReplaceWithMarker(data, interactshURLMarkerRegex, interactshURLs)
}
// ReplaceMarkers replaces the placeholders with interactsh urls and appends them to interactshURLs
func (c *Client) ReplaceWithMarker(data string, regex *regexp.Regexp, interactshURLs []string) (string, []string) {
for _, interactshURLMarker := range regex.FindAllString(data, -1) {
if url, err := c.NewURLWithData(interactshURLMarker); err == nil {
interactshURLs = append(interactshURLs, url)
data = strings.Replace(data, interactshURLMarker, url, 1)
}
}
return data, interactshURLs
}
func (c *Client) NewURL() (string, error) {
return c.NewURLWithData("")
}
func (c *Client) NewURLWithData(data string) (string, error) {
url, err := c.URL()
if err != nil {
return "", err
}
if url == "" {
return "", errors.New("empty interactsh url")
}
_ = c.interactshURLs.SetWithExpire(url, data, defaultInteractionDuration)
return url, nil
}
// MakePlaceholders does placeholders for interact URLs and other data to a map
func (c *Client) MakePlaceholders(urls []string, data map[string]interface{}) {
data["interactsh-server"] = c.getHostname()
for _, url := range urls {
if interactshURLMarker, err := c.interactshURLs.Get(url); interactshURLMarker != "" && err == nil {
interactshMarker := strings.TrimSuffix(strings.TrimPrefix(interactshURLMarker, "{{"), "}}")
c.interactshURLs.Remove(url)
data[interactshMarker] = url
urlIndex := strings.Index(url, ".")
if urlIndex == -1 {
continue
}
data[strings.Replace(interactshMarker, "url", "id", 1)] = url[:urlIndex]
}
}
}
// MakeResultEventFunc is a result making function for nuclei
type MakeResultEventFunc func(wrapped *output.InternalWrappedEvent) []*output.ResultEvent
// 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(interactshURLs []string, data *RequestData) {
for _, interactshURL := range interactshURLs {
id := strings.TrimRight(strings.TrimSuffix(interactshURL, c.getHostname()), ".")
if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {
gotItem, err := c.matchedTemplates.Get(hash(data.Event.InternalEvent))
if gotItem && err == nil {
break
}
}
interactions, err := c.interactions.Get(id)
if interactions != nil && err == nil {
for _, interaction := range interactions {
if c.processInteractionForRequest(interaction, data) {
c.interactions.Remove(id)
break
}
}
} else {
_ = c.requests.SetWithExpire(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(op *operators.Operators) bool {
if op == nil {
return false
}
for _, matcher := range op.Matchers {
for _, dsl := range matcher.DSL {
if stringsutil.ContainsAnyI(dsl, "interactsh") {
return true
}
}
if stringsutil.HasPrefixI(matcher.Part, "interactsh") {
return true
}
}
for _, matcher := range op.Extractors {
if stringsutil.HasPrefixI(matcher.Part, "interactsh") {
return true
}
}
return false
}
// HasMarkers checks if the text contains interactsh markers
func HasMarkers(data string) bool {
return interactshURLMarkerRegex.Match([]byte(data))
}
func (c *Client) debugPrintInteraction(interaction *server.Interaction, event *operators.Result) {
builder := &bytes.Buffer{}
switch interaction.Protocol {
case "dns":
builder.WriteString(formatInteractionHeader("DNS", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
if c.options.DebugRequest || c.options.Debug {
builder.WriteString(formatInteractionMessage("DNS Request", interaction.RawRequest, event, c.options.NoColor))
}
if c.options.DebugResponse || c.options.Debug {
builder.WriteString(formatInteractionMessage("DNS Response", interaction.RawResponse, event, c.options.NoColor))
}
case "http":
builder.WriteString(formatInteractionHeader("HTTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
if c.options.DebugRequest || c.options.Debug {
builder.WriteString(formatInteractionMessage("HTTP Request", interaction.RawRequest, event, c.options.NoColor))
}
if c.options.DebugResponse || c.options.Debug {
builder.WriteString(formatInteractionMessage("HTTP Response", interaction.RawResponse, event, c.options.NoColor))
}
case "smtp":
builder.WriteString(formatInteractionHeader("SMTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {
builder.WriteString(formatInteractionMessage("SMTP Interaction", interaction.RawRequest, event, c.options.NoColor))
}
case "ldap":
builder.WriteString(formatInteractionHeader("LDAP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {
builder.WriteString(formatInteractionMessage("LDAP Interaction", interaction.RawRequest, event, c.options.NoColor))
}
}
fmt.Fprint(os.Stderr, builder.String())
}
func formatInteractionHeader(protocol, ID, address string, at time.Time) string {
return fmt.Sprintf("[%s] Received %s interaction from %s at %s", ID, protocol, address, at.Format("2006-01-02 15:04:05"))
}
func formatInteractionMessage(key, value string, event *operators.Result, noColor bool) string {
value = responsehighlighter.Highlight(event, value, noColor, false)
return fmt.Sprintf("\n------------\n%s\n------------\n\n%s\n\n", key, value)
}
func hash(internalEvent output.InternalEvent) string {
templateId := internalEvent[templateIdAttribute].(string)
host := internalEvent["host"].(string)
return fmt.Sprintf("%s:%s", templateId, host)
}
func (c *Client) getHostname() string {
c.RLock()
defer c.RUnlock()
return c.hostname
}
func (c *Client) setHostname(hostname string) {
c.Lock()
defer c.Unlock()
c.hostname = hostname
}