github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/common/interactsh/interactsh.go (about)

     1  package interactsh
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"sync"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	"errors"
    14  
    15  	"github.com/Mzack9999/gcache"
    16  
    17  	"github.com/projectdiscovery/gologger"
    18  	"github.com/projectdiscovery/interactsh/pkg/client"
    19  	"github.com/projectdiscovery/interactsh/pkg/server"
    20  	"github.com/projectdiscovery/nuclei/v2/pkg/operators"
    21  	"github.com/projectdiscovery/nuclei/v2/pkg/output"
    22  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/responsehighlighter"
    23  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/writer"
    24  	errorutil "github.com/projectdiscovery/utils/errors"
    25  	stringsutil "github.com/projectdiscovery/utils/strings"
    26  )
    27  
    28  // Client is a wrapped client for interactsh server.
    29  type Client struct {
    30  	sync.Once
    31  	sync.RWMutex
    32  
    33  	options *Options
    34  
    35  	// interactsh is a client for interactsh server.
    36  	interactsh *client.Client
    37  	// requests is a stored cache for interactsh-url->request-event data.
    38  	requests gcache.Cache[string, *RequestData]
    39  	// interactions is a stored cache for interactsh-interaction->interactsh-url data
    40  	interactions gcache.Cache[string, []*server.Interaction]
    41  	// matchedTemplates is a stored cache to track matched templates
    42  	matchedTemplates gcache.Cache[string, bool]
    43  	// interactshURLs is a stored cache to track multiple interactsh markers
    44  	interactshURLs gcache.Cache[string, string]
    45  
    46  	eviction         time.Duration
    47  	pollDuration     time.Duration
    48  	cooldownDuration time.Duration
    49  
    50  	hostname string
    51  
    52  	// determines if wait the cooldown period in case of generated URL
    53  	generated atomic.Bool
    54  	matched   atomic.Bool
    55  }
    56  
    57  // New returns a new interactsh server client
    58  func New(options *Options) (*Client, error) {
    59  	requestsCache := gcache.New[string, *RequestData](options.CacheSize).LRU().Build()
    60  	interactionsCache := gcache.New[string, []*server.Interaction](defaultMaxInteractionsCount).LRU().Build()
    61  	matchedTemplateCache := gcache.New[string, bool](defaultMaxInteractionsCount).LRU().Build()
    62  	interactshURLCache := gcache.New[string, string](defaultMaxInteractionsCount).LRU().Build()
    63  
    64  	interactClient := &Client{
    65  		eviction:         options.Eviction,
    66  		interactions:     interactionsCache,
    67  		matchedTemplates: matchedTemplateCache,
    68  		interactshURLs:   interactshURLCache,
    69  		options:          options,
    70  		requests:         requestsCache,
    71  		pollDuration:     options.PollDuration,
    72  		cooldownDuration: options.CooldownPeriod,
    73  	}
    74  	return interactClient, nil
    75  }
    76  
    77  func (c *Client) poll() error {
    78  	if c.options.NoInteractsh {
    79  		// do not init if disabled
    80  		return ErrInteractshClientNotInitialized
    81  	}
    82  	interactsh, err := client.New(&client.Options{
    83  		ServerURL:           c.options.ServerURL,
    84  		Token:               c.options.Authorization,
    85  		DisableHTTPFallback: c.options.DisableHttpFallback,
    86  		HTTPClient:          c.options.HTTPClient,
    87  		KeepAliveInterval:   time.Minute,
    88  	})
    89  	if err != nil {
    90  		return errorutil.NewWithErr(err).Msgf("could not create client")
    91  	}
    92  
    93  	c.interactsh = interactsh
    94  
    95  	interactURL := interactsh.URL()
    96  	interactDomain := interactURL[strings.Index(interactURL, ".")+1:]
    97  	gologger.Info().Msgf("Using Interactsh Server: %s", interactDomain)
    98  
    99  	c.setHostname(interactDomain)
   100  
   101  	err = interactsh.StartPolling(c.pollDuration, func(interaction *server.Interaction) {
   102  		request, err := c.requests.Get(interaction.UniqueID)
   103  		// for more context in github actions
   104  		if strings.EqualFold(os.Getenv("GITHUB_ACTIONS"), "true") && c.options.Debug {
   105  			gologger.DefaultLogger.Print().Msgf("[Interactsh]: got interaction of %v for request %v and error %v", interaction, request, err)
   106  		}
   107  		if errors.Is(err, gcache.KeyNotFoundError) || request == nil {
   108  			// If we don't have any request for this ID, add it to temporary
   109  			// lru cache, so we can correlate when we get an add request.
   110  			items, err := c.interactions.Get(interaction.UniqueID)
   111  			if errorutil.IsAny(err, gcache.KeyNotFoundError) || items == nil {
   112  				_ = c.interactions.SetWithExpire(interaction.UniqueID, []*server.Interaction{interaction}, defaultInteractionDuration)
   113  			} else {
   114  				items = append(items, interaction)
   115  				_ = c.interactions.SetWithExpire(interaction.UniqueID, items, defaultInteractionDuration)
   116  			}
   117  			return
   118  		}
   119  
   120  		if requestShouldStopAtFirstMatch(request) || c.options.StopAtFirstMatch {
   121  			if gotItem, err := c.matchedTemplates.Get(hash(request.Event.InternalEvent)); gotItem && err == nil {
   122  				return
   123  			}
   124  		}
   125  
   126  		_ = c.processInteractionForRequest(interaction, request)
   127  	})
   128  
   129  	if err != nil {
   130  		return errorutil.NewWithErr(err).Msgf("could not perform interactsh polling")
   131  	}
   132  	return nil
   133  }
   134  
   135  // requestShouldStopAtFirstmatch checks if further interactions should be stopped
   136  // note: extra care should be taken while using this function since internalEvent is
   137  // synchronized all the time and if caller functions has already acquired lock its best to explicitly specify that
   138  // we could use `TryLock()` but that may over complicate things and need to differentiate
   139  // situations whether to block or skip
   140  func requestShouldStopAtFirstMatch(request *RequestData) bool {
   141  	request.Event.RLock()
   142  	defer request.Event.RUnlock()
   143  
   144  	if stop, ok := request.Event.InternalEvent[stopAtFirstMatchAttribute]; ok {
   145  		if v, ok := stop.(bool); ok {
   146  			return v
   147  		}
   148  	}
   149  	return false
   150  }
   151  
   152  // processInteractionForRequest processes an interaction for a request
   153  func (c *Client) processInteractionForRequest(interaction *server.Interaction, data *RequestData) bool {
   154  	data.Event.Lock()
   155  	data.Event.InternalEvent["interactsh_protocol"] = interaction.Protocol
   156  	data.Event.InternalEvent["interactsh_request"] = interaction.RawRequest
   157  	data.Event.InternalEvent["interactsh_response"] = interaction.RawResponse
   158  	data.Event.InternalEvent["interactsh_ip"] = interaction.RemoteAddress
   159  	data.Event.Unlock()
   160  
   161  	result, matched := data.Operators.Execute(data.Event.InternalEvent, data.MatchFunc, data.ExtractFunc, c.options.Debug || c.options.DebugRequest || c.options.DebugResponse)
   162  
   163  	// for more context in github actions
   164  	if strings.EqualFold(os.Getenv("GITHUB_ACTIONS"), "true") && c.options.Debug {
   165  		gologger.DefaultLogger.Print().Msgf("[Interactsh]: got result %v and status %v after processing interaction", result, matched)
   166  	}
   167  
   168  	// if we don't match, return
   169  	if !matched || result == nil {
   170  		return false
   171  	}
   172  	c.requests.Remove(interaction.UniqueID)
   173  
   174  	if data.Event.OperatorsResult != nil {
   175  		data.Event.OperatorsResult.Merge(result)
   176  	} else {
   177  		data.Event.SetOperatorResult(result)
   178  	}
   179  
   180  	data.Event.Lock()
   181  	data.Event.Results = data.MakeResultFunc(data.Event)
   182  	for _, event := range data.Event.Results {
   183  		event.Interaction = interaction
   184  	}
   185  	data.Event.Unlock()
   186  
   187  	if c.options.Debug || c.options.DebugRequest || c.options.DebugResponse {
   188  		c.debugPrintInteraction(interaction, data.Event.OperatorsResult)
   189  	}
   190  
   191  	// if event is not already matched, write it to output
   192  	if !data.Event.InteractshMatched.Load() && writer.WriteResult(data.Event, c.options.Output, c.options.Progress, c.options.IssuesClient) {
   193  		data.Event.InteractshMatched.Store(true)
   194  		c.matched.Store(true)
   195  		if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {
   196  			_ = c.matchedTemplates.SetWithExpire(hash(data.Event.InternalEvent), true, defaultInteractionDuration)
   197  		}
   198  	}
   199  
   200  	return true
   201  }
   202  
   203  func (c *Client) AlreadyMatched(data *RequestData) bool {
   204  	data.Event.RLock()
   205  	defer data.Event.RUnlock()
   206  
   207  	return c.matchedTemplates.Has(hash(data.Event.InternalEvent))
   208  }
   209  
   210  // URL returns a new URL that can be interacted with
   211  func (c *Client) URL() (string, error) {
   212  	// first time initialization
   213  	var err error
   214  	c.Do(func() {
   215  		err = c.poll()
   216  	})
   217  	if err != nil {
   218  		return "", errorutil.NewWithErr(err).Wrap(ErrInteractshClientNotInitialized)
   219  	}
   220  
   221  	if c.interactsh == nil {
   222  		return "", ErrInteractshClientNotInitialized
   223  	}
   224  
   225  	c.generated.Store(true)
   226  	return c.interactsh.URL(), nil
   227  }
   228  
   229  // Close the interactsh clients after waiting for cooldown period.
   230  func (c *Client) Close() bool {
   231  	if c.cooldownDuration > 0 && c.generated.Load() {
   232  		time.Sleep(c.cooldownDuration)
   233  	}
   234  	if c.interactsh != nil {
   235  		_ = c.interactsh.StopPolling()
   236  		c.interactsh.Close()
   237  	}
   238  
   239  	c.requests.Purge()
   240  	c.interactions.Purge()
   241  	c.matchedTemplates.Purge()
   242  	c.interactshURLs.Purge()
   243  
   244  	return c.matched.Load()
   245  }
   246  
   247  // ReplaceMarkers replaces the default {{interactsh-url}} placeholders with interactsh urls
   248  func (c *Client) Replace(data string, interactshURLs []string) (string, []string) {
   249  	return c.ReplaceWithMarker(data, interactshURLMarkerRegex, interactshURLs)
   250  }
   251  
   252  // ReplaceMarkers replaces the placeholders with interactsh urls and appends them to interactshURLs
   253  func (c *Client) ReplaceWithMarker(data string, regex *regexp.Regexp, interactshURLs []string) (string, []string) {
   254  	for _, interactshURLMarker := range regex.FindAllString(data, -1) {
   255  		if url, err := c.NewURLWithData(interactshURLMarker); err == nil {
   256  			interactshURLs = append(interactshURLs, url)
   257  			data = strings.Replace(data, interactshURLMarker, url, 1)
   258  		}
   259  	}
   260  	return data, interactshURLs
   261  }
   262  
   263  func (c *Client) NewURL() (string, error) {
   264  	return c.NewURLWithData("")
   265  }
   266  
   267  func (c *Client) NewURLWithData(data string) (string, error) {
   268  	url, err := c.URL()
   269  	if err != nil {
   270  		return "", err
   271  	}
   272  	if url == "" {
   273  		return "", errors.New("empty interactsh url")
   274  	}
   275  	_ = c.interactshURLs.SetWithExpire(url, data, defaultInteractionDuration)
   276  	return url, nil
   277  }
   278  
   279  // MakePlaceholders does placeholders for interact URLs and other data to a map
   280  func (c *Client) MakePlaceholders(urls []string, data map[string]interface{}) {
   281  	data["interactsh-server"] = c.getHostname()
   282  	for _, url := range urls {
   283  		if interactshURLMarker, err := c.interactshURLs.Get(url); interactshURLMarker != "" && err == nil {
   284  			interactshMarker := strings.TrimSuffix(strings.TrimPrefix(interactshURLMarker, "{{"), "}}")
   285  
   286  			c.interactshURLs.Remove(url)
   287  
   288  			data[interactshMarker] = url
   289  			urlIndex := strings.Index(url, ".")
   290  			if urlIndex == -1 {
   291  				continue
   292  			}
   293  			data[strings.Replace(interactshMarker, "url", "id", 1)] = url[:urlIndex]
   294  		}
   295  	}
   296  }
   297  
   298  // MakeResultEventFunc is a result making function for nuclei
   299  type MakeResultEventFunc func(wrapped *output.InternalWrappedEvent) []*output.ResultEvent
   300  
   301  // RequestData contains data for a request event
   302  type RequestData struct {
   303  	MakeResultFunc MakeResultEventFunc
   304  	Event          *output.InternalWrappedEvent
   305  	Operators      *operators.Operators
   306  	MatchFunc      operators.MatchFunc
   307  	ExtractFunc    operators.ExtractFunc
   308  }
   309  
   310  // RequestEvent is the event for a network request sent by nuclei.
   311  func (c *Client) RequestEvent(interactshURLs []string, data *RequestData) {
   312  	for _, interactshURL := range interactshURLs {
   313  		id := strings.TrimRight(strings.TrimSuffix(interactshURL, c.getHostname()), ".")
   314  
   315  		if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {
   316  			gotItem, err := c.matchedTemplates.Get(hash(data.Event.InternalEvent))
   317  			if gotItem && err == nil {
   318  				break
   319  			}
   320  		}
   321  
   322  		interactions, err := c.interactions.Get(id)
   323  		if interactions != nil && err == nil {
   324  			for _, interaction := range interactions {
   325  				if c.processInteractionForRequest(interaction, data) {
   326  					c.interactions.Remove(id)
   327  					break
   328  				}
   329  			}
   330  		} else {
   331  			_ = c.requests.SetWithExpire(id, data, c.eviction)
   332  		}
   333  	}
   334  }
   335  
   336  // HasMatchers returns true if an operator has interactsh part
   337  // matchers or extractors.
   338  //
   339  // Used by requests to show result or not depending on presence of interact.sh
   340  // data part matchers.
   341  func HasMatchers(op *operators.Operators) bool {
   342  	if op == nil {
   343  		return false
   344  	}
   345  
   346  	for _, matcher := range op.Matchers {
   347  		for _, dsl := range matcher.DSL {
   348  			if stringsutil.ContainsAnyI(dsl, "interactsh") {
   349  				return true
   350  			}
   351  		}
   352  		if stringsutil.HasPrefixI(matcher.Part, "interactsh") {
   353  			return true
   354  		}
   355  	}
   356  	for _, matcher := range op.Extractors {
   357  		if stringsutil.HasPrefixI(matcher.Part, "interactsh") {
   358  			return true
   359  		}
   360  	}
   361  	return false
   362  }
   363  
   364  // HasMarkers checks if the text contains interactsh markers
   365  func HasMarkers(data string) bool {
   366  	return interactshURLMarkerRegex.Match([]byte(data))
   367  }
   368  
   369  func (c *Client) debugPrintInteraction(interaction *server.Interaction, event *operators.Result) {
   370  	builder := &bytes.Buffer{}
   371  
   372  	switch interaction.Protocol {
   373  	case "dns":
   374  		builder.WriteString(formatInteractionHeader("DNS", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
   375  		if c.options.DebugRequest || c.options.Debug {
   376  			builder.WriteString(formatInteractionMessage("DNS Request", interaction.RawRequest, event, c.options.NoColor))
   377  		}
   378  		if c.options.DebugResponse || c.options.Debug {
   379  			builder.WriteString(formatInteractionMessage("DNS Response", interaction.RawResponse, event, c.options.NoColor))
   380  		}
   381  	case "http":
   382  		builder.WriteString(formatInteractionHeader("HTTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
   383  		if c.options.DebugRequest || c.options.Debug {
   384  			builder.WriteString(formatInteractionMessage("HTTP Request", interaction.RawRequest, event, c.options.NoColor))
   385  		}
   386  		if c.options.DebugResponse || c.options.Debug {
   387  			builder.WriteString(formatInteractionMessage("HTTP Response", interaction.RawResponse, event, c.options.NoColor))
   388  		}
   389  	case "smtp":
   390  		builder.WriteString(formatInteractionHeader("SMTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
   391  		if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {
   392  			builder.WriteString(formatInteractionMessage("SMTP Interaction", interaction.RawRequest, event, c.options.NoColor))
   393  		}
   394  	case "ldap":
   395  		builder.WriteString(formatInteractionHeader("LDAP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))
   396  		if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {
   397  			builder.WriteString(formatInteractionMessage("LDAP Interaction", interaction.RawRequest, event, c.options.NoColor))
   398  		}
   399  	}
   400  	fmt.Fprint(os.Stderr, builder.String())
   401  }
   402  
   403  func formatInteractionHeader(protocol, ID, address string, at time.Time) string {
   404  	return fmt.Sprintf("[%s] Received %s interaction from %s at %s", ID, protocol, address, at.Format("2006-01-02 15:04:05"))
   405  }
   406  
   407  func formatInteractionMessage(key, value string, event *operators.Result, noColor bool) string {
   408  	value = responsehighlighter.Highlight(event, value, noColor, false)
   409  	return fmt.Sprintf("\n------------\n%s\n------------\n\n%s\n\n", key, value)
   410  }
   411  
   412  func hash(internalEvent output.InternalEvent) string {
   413  	templateId := internalEvent[templateIdAttribute].(string)
   414  	host := internalEvent["host"].(string)
   415  	return fmt.Sprintf("%s:%s", templateId, host)
   416  }
   417  
   418  func (c *Client) getHostname() string {
   419  	c.RLock()
   420  	defer c.RUnlock()
   421  
   422  	return c.hostname
   423  }
   424  
   425  func (c *Client) setHostname(hostname string) {
   426  	c.Lock()
   427  	defer c.Unlock()
   428  
   429  	c.hostname = hostname
   430  }