Path: blob/dev/pkg/protocols/common/interactsh/interactsh.go
2072 views
package interactsh12import (3"bytes"4"fmt"5"os"6"regexp"7"strings"8"sync"9"sync/atomic"10"time"1112"errors"1314"github.com/Mzack9999/gcache"1516"github.com/projectdiscovery/gologger"17"github.com/projectdiscovery/interactsh/pkg/client"18"github.com/projectdiscovery/interactsh/pkg/server"19"github.com/projectdiscovery/nuclei/v3/pkg/operators"20"github.com/projectdiscovery/nuclei/v3/pkg/output"21"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"22"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/writer"23"github.com/projectdiscovery/retryablehttp-go"24"github.com/projectdiscovery/utils/errkit"25stringsutil "github.com/projectdiscovery/utils/strings"26)2728// Client is a wrapped client for interactsh server.29type Client struct {30sync.Once31sync.RWMutex3233options *Options3435// interactsh is a client for interactsh server.36interactsh *client.Client37// requests is a stored cache for interactsh-url->request-event data.38requests gcache.Cache[string, *RequestData]39// interactions is a stored cache for interactsh-interaction->interactsh-url data40interactions gcache.Cache[string, []*server.Interaction]41// matchedTemplates is a stored cache to track matched templates42matchedTemplates gcache.Cache[string, bool]43// interactshURLs is a stored cache to track multiple interactsh markers44interactshURLs gcache.Cache[string, string]4546eviction time.Duration47pollDuration time.Duration48cooldownDuration time.Duration4950hostname string5152// determines if wait the cooldown period in case of generated URL53generated atomic.Bool54matched atomic.Bool55}5657// New returns a new interactsh server client58func New(options *Options) (*Client, error) {59requestsCache := gcache.New[string, *RequestData](options.CacheSize).LRU().Build()60interactionsCache := gcache.New[string, []*server.Interaction](defaultMaxInteractionsCount).LRU().Build()61matchedTemplateCache := gcache.New[string, bool](defaultMaxInteractionsCount).LRU().Build()62interactshURLCache := gcache.New[string, string](defaultMaxInteractionsCount).LRU().Build()6364interactClient := &Client{65eviction: options.Eviction,66interactions: interactionsCache,67matchedTemplates: matchedTemplateCache,68interactshURLs: interactshURLCache,69options: options,70requests: requestsCache,71pollDuration: options.PollDuration,72cooldownDuration: options.CooldownPeriod,73}74return interactClient, nil75}7677func (c *Client) poll() error {78if c.options.NoInteractsh {79// do not init if disabled80return ErrInteractshClientNotInitialized81}82interactsh, err := client.New(&client.Options{83ServerURL: c.options.ServerURL,84Token: c.options.Authorization,85DisableHTTPFallback: c.options.DisableHttpFallback,86HTTPClient: c.options.HTTPClient,87KeepAliveInterval: time.Minute,88})89if err != nil {90return errkit.Wrap(err, "could not create client")91}9293c.interactsh = interactsh9495interactURL := interactsh.URL()96interactDomain := interactURL[strings.Index(interactURL, ".")+1:]97gologger.Info().Msgf("Using Interactsh Server: %s", interactDomain)9899c.setHostname(interactDomain)100101err = interactsh.StartPolling(c.pollDuration, func(interaction *server.Interaction) {102request, err := c.requests.Get(interaction.UniqueID)103// for more context in github actions104if strings.EqualFold(os.Getenv("GITHUB_ACTIONS"), "true") && c.options.Debug {105gologger.DefaultLogger.Print().Msgf("[Interactsh]: got interaction of %v for request %v and error %v", interaction, request, err)106}107if errors.Is(err, gcache.KeyNotFoundError) || request == nil {108// If we don't have any request for this ID, add it to temporary109// lru cache, so we can correlate when we get an add request.110items, err := c.interactions.Get(interaction.UniqueID)111if errkit.Is(err, gcache.KeyNotFoundError) || items == nil {112_ = c.interactions.SetWithExpire(interaction.UniqueID, []*server.Interaction{interaction}, defaultInteractionDuration)113} else {114items = append(items, interaction)115_ = c.interactions.SetWithExpire(interaction.UniqueID, items, defaultInteractionDuration)116}117return118}119120if requestShouldStopAtFirstMatch(request) || c.options.StopAtFirstMatch {121if gotItem, err := c.matchedTemplates.Get(hash(request.Event.InternalEvent)); gotItem && err == nil {122return123}124}125126_ = c.processInteractionForRequest(interaction, request)127})128129if err != nil {130return errkit.Wrap(err, "could not perform interactsh polling")131}132return nil133}134135// requestShouldStopAtFirstmatch checks if further interactions should be stopped136// note: extra care should be taken while using this function since internalEvent is137// synchronized all the time and if caller functions has already acquired lock its best to explicitly specify that138// we could use `TryLock()` but that may over complicate things and need to differentiate139// situations whether to block or skip140func requestShouldStopAtFirstMatch(request *RequestData) bool {141request.Event.RLock()142defer request.Event.RUnlock()143144if stop, ok := request.Event.InternalEvent[stopAtFirstMatchAttribute]; ok {145if v, ok := stop.(bool); ok {146return v147}148}149return false150}151152// processInteractionForRequest processes an interaction for a request153func (c *Client) processInteractionForRequest(interaction *server.Interaction, data *RequestData) bool {154var result *operators.Result155var matched bool156data.Event.Lock()157data.Event.InternalEvent["interactsh_protocol"] = interaction.Protocol158if strings.EqualFold(interaction.Protocol, "dns") {159data.Event.InternalEvent["interactsh_request"] = strings.ToLower(interaction.RawRequest)160} else {161data.Event.InternalEvent["interactsh_request"] = interaction.RawRequest162}163data.Event.InternalEvent["interactsh_response"] = interaction.RawResponse164data.Event.InternalEvent["interactsh_ip"] = interaction.RemoteAddress165data.Event.Unlock()166167if data.Operators != nil {168result, matched = data.Operators.Execute(data.Event.InternalEvent, data.MatchFunc, data.ExtractFunc, c.options.Debug || c.options.DebugRequest || c.options.DebugResponse)169} else {170// this is most likely a bug so error instead of warning171var templateID string172if data.Event.InternalEvent != nil {173templateID = fmt.Sprint(data.Event.InternalEvent[templateIdAttribute])174}175gologger.Error().Msgf("missing compiled operators for '%v' template", templateID)176}177178// for more context in github actions179if strings.EqualFold(os.Getenv("GITHUB_ACTIONS"), "true") && c.options.Debug {180gologger.DefaultLogger.Print().Msgf("[Interactsh]: got result %v and status %v after processing interaction", result, matched)181}182183if c.options.FuzzParamsFrequency != nil {184if !matched {185c.options.FuzzParamsFrequency.MarkParameter(data.Parameter, data.Request.String(), data.Operators.TemplateID)186} else {187c.options.FuzzParamsFrequency.UnmarkParameter(data.Parameter, data.Request.String(), data.Operators.TemplateID)188}189}190191// if we don't match, return192if !matched || result == nil {193return false194}195c.requests.Remove(interaction.UniqueID)196197if data.Event.OperatorsResult != nil {198data.Event.OperatorsResult.Merge(result)199} else {200data.Event.SetOperatorResult(result)201}202203data.Event.Lock()204data.Event.Results = data.MakeResultFunc(data.Event)205for _, event := range data.Event.Results {206event.Interaction = interaction207}208data.Event.Unlock()209210if c.options.Debug || c.options.DebugRequest || c.options.DebugResponse {211c.debugPrintInteraction(interaction, data.Event.OperatorsResult)212}213214// if event is not already matched, write it to output215if !data.Event.InteractshMatched.Load() && writer.WriteResult(data.Event, c.options.Output, c.options.Progress, c.options.IssuesClient) {216data.Event.InteractshMatched.Store(true)217c.matched.Store(true)218if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {219_ = c.matchedTemplates.SetWithExpire(hash(data.Event.InternalEvent), true, defaultInteractionDuration)220}221}222223return true224}225226func (c *Client) AlreadyMatched(data *RequestData) bool {227data.Event.RLock()228defer data.Event.RUnlock()229230return c.matchedTemplates.Has(hash(data.Event.InternalEvent))231}232233// URL returns a new URL that can be interacted with234func (c *Client) URL() (string, error) {235// first time initialization236var err error237c.Do(func() {238err = c.poll()239})240if err != nil {241return "", errkit.Wrap(ErrInteractshClientNotInitialized, err.Error())242}243244if c.interactsh == nil {245return "", ErrInteractshClientNotInitialized246}247248c.generated.Store(true)249return c.interactsh.URL(), nil250}251252// Close the interactsh clients after waiting for cooldown period.253func (c *Client) Close() bool {254if c.cooldownDuration > 0 && c.generated.Load() {255time.Sleep(c.cooldownDuration)256}257if c.interactsh != nil {258_ = c.interactsh.StopPolling()259_ = c.interactsh.Close()260}261262c.requests.Purge()263c.interactions.Purge()264c.matchedTemplates.Purge()265c.interactshURLs.Purge()266267return c.matched.Load()268}269270// ReplaceMarkers replaces the default {{interactsh-url}} placeholders with interactsh urls271func (c *Client) Replace(data string, interactshURLs []string) (string, []string) {272return c.ReplaceWithMarker(data, interactshURLMarkerRegex, interactshURLs)273}274275// ReplaceMarkers replaces the placeholders with interactsh urls and appends them to interactshURLs276func (c *Client) ReplaceWithMarker(data string, regex *regexp.Regexp, interactshURLs []string) (string, []string) {277for _, interactshURLMarker := range regex.FindAllString(data, -1) {278if url, err := c.NewURLWithData(interactshURLMarker); err == nil {279interactshURLs = append(interactshURLs, url)280data = strings.Replace(data, interactshURLMarker, url, 1)281}282}283return data, interactshURLs284}285286func (c *Client) NewURL() (string, error) {287return c.NewURLWithData("")288}289290func (c *Client) NewURLWithData(data string) (string, error) {291url, err := c.URL()292if err != nil {293return "", err294}295if url == "" {296return "", errors.New("empty interactsh url")297}298_ = c.interactshURLs.SetWithExpire(url, data, defaultInteractionDuration)299return url, nil300}301302// MakePlaceholders does placeholders for interact URLs and other data to a map303func (c *Client) MakePlaceholders(urls []string, data map[string]interface{}) {304data["interactsh-server"] = c.getHostname()305for _, url := range urls {306if interactshURLMarker, err := c.interactshURLs.Get(url); interactshURLMarker != "" && err == nil {307interactshMarker := strings.TrimSuffix(strings.TrimPrefix(interactshURLMarker, "{{"), "}}")308309c.interactshURLs.Remove(url)310311data[interactshMarker] = url312urlIndex := strings.Index(url, ".")313if urlIndex == -1 {314continue315}316data[strings.Replace(interactshMarker, "url", "id", 1)] = url[:urlIndex]317}318}319}320321// MakeResultEventFunc is a result making function for nuclei322type MakeResultEventFunc func(wrapped *output.InternalWrappedEvent) []*output.ResultEvent323324// RequestData contains data for a request event325type RequestData struct {326MakeResultFunc MakeResultEventFunc327Event *output.InternalWrappedEvent328Operators *operators.Operators329MatchFunc operators.MatchFunc330ExtractFunc operators.ExtractFunc331332Parameter string333Request *retryablehttp.Request334}335336// RequestEvent is the event for a network request sent by nuclei.337func (c *Client) RequestEvent(interactshURLs []string, data *RequestData) {338for _, interactshURL := range interactshURLs {339id := strings.TrimRight(strings.TrimSuffix(interactshURL, c.getHostname()), ".")340341if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {342gotItem, err := c.matchedTemplates.Get(hash(data.Event.InternalEvent))343if gotItem && err == nil {344break345}346}347348interactions, err := c.interactions.Get(id)349if interactions != nil && err == nil {350for _, interaction := range interactions {351if c.processInteractionForRequest(interaction, data) {352c.interactions.Remove(id)353break354}355}356} else {357_ = c.requests.SetWithExpire(id, data, c.eviction)358}359}360}361362// HasMatchers returns true if an operator has interactsh part363// matchers or extractors.364//365// Used by requests to show result or not depending on presence of interact.sh366// data part matchers.367func HasMatchers(op *operators.Operators) bool {368if op == nil {369return false370}371372for _, matcher := range op.Matchers {373for _, dsl := range matcher.DSL {374if stringsutil.ContainsAnyI(dsl, "interactsh") {375return true376}377}378if stringsutil.HasPrefixI(matcher.Part, "interactsh") {379return true380}381}382for _, matcher := range op.Extractors {383if stringsutil.HasPrefixI(matcher.Part, "interactsh") {384return true385}386}387return false388}389390// HasMarkers checks if the text contains interactsh markers391func HasMarkers(data string) bool {392return interactshURLMarkerRegex.Match([]byte(data))393}394395func (c *Client) debugPrintInteraction(interaction *server.Interaction, event *operators.Result) {396builder := &bytes.Buffer{}397398switch interaction.Protocol {399case "dns":400builder.WriteString(formatInteractionHeader("DNS", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))401if c.options.DebugRequest || c.options.Debug {402builder.WriteString(formatInteractionMessage("DNS Request", interaction.RawRequest, event, c.options.NoColor))403}404if c.options.DebugResponse || c.options.Debug {405builder.WriteString(formatInteractionMessage("DNS Response", interaction.RawResponse, event, c.options.NoColor))406}407case "http":408builder.WriteString(formatInteractionHeader("HTTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))409if c.options.DebugRequest || c.options.Debug {410builder.WriteString(formatInteractionMessage("HTTP Request", interaction.RawRequest, event, c.options.NoColor))411}412if c.options.DebugResponse || c.options.Debug {413builder.WriteString(formatInteractionMessage("HTTP Response", interaction.RawResponse, event, c.options.NoColor))414}415case "smtp":416builder.WriteString(formatInteractionHeader("SMTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))417if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {418builder.WriteString(formatInteractionMessage("SMTP Interaction", interaction.RawRequest, event, c.options.NoColor))419}420case "ldap":421builder.WriteString(formatInteractionHeader("LDAP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))422if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {423builder.WriteString(formatInteractionMessage("LDAP Interaction", interaction.RawRequest, event, c.options.NoColor))424}425}426_, _ = fmt.Fprint(os.Stderr, builder.String())427}428429func formatInteractionHeader(protocol, ID, address string, at time.Time) string {430return fmt.Sprintf("[%s] Received %s interaction from %s at %s", ID, protocol, address, at.Format("2006-01-02 15:04:05"))431}432433func formatInteractionMessage(key, value string, event *operators.Result, noColor bool) string {434value = responsehighlighter.Highlight(event, value, noColor, false)435return fmt.Sprintf("\n------------\n%s\n------------\n\n%s\n\n", key, value)436}437438func hash(internalEvent output.InternalEvent) string {439templateId := internalEvent[templateIdAttribute].(string)440host := internalEvent["host"].(string)441return fmt.Sprintf("%s:%s", templateId, host)442}443444func (c *Client) getHostname() string {445c.RLock()446defer c.RUnlock()447448return c.hostname449}450451func (c *Client) setHostname(hostname string) {452c.Lock()453defer c.Unlock()454455c.hostname = hostname456}457458459