Path: blob/dev/pkg/protocols/common/interactsh/interactsh.go
2851 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}202// ensure payload values are preserved for interactsh-only matches203data.Event.Lock()204if data.Event.OperatorsResult != nil && len(data.Event.OperatorsResult.PayloadValues) == 0 {205if payloads, ok := data.Event.InternalEvent["payloads"].(map[string]interface{}); ok {206data.Event.OperatorsResult.PayloadValues = payloads207}208}209data.Event.Unlock()210211data.Event.Lock()212data.Event.Results = data.MakeResultFunc(data.Event)213for _, event := range data.Event.Results {214event.Interaction = interaction215}216data.Event.Unlock()217218if c.options.Debug || c.options.DebugRequest || c.options.DebugResponse {219c.debugPrintInteraction(interaction, data.Event.OperatorsResult)220}221222// if event is not already matched, write it to output223if !data.Event.InteractshMatched.Load() && writer.WriteResult(data.Event, c.options.Output, c.options.Progress, c.options.IssuesClient) {224data.Event.InteractshMatched.Store(true)225c.matched.Store(true)226if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {227_ = c.matchedTemplates.SetWithExpire(hash(data.Event.InternalEvent), true, defaultInteractionDuration)228}229}230231return true232}233234func (c *Client) AlreadyMatched(data *RequestData) bool {235data.Event.RLock()236defer data.Event.RUnlock()237238return c.matchedTemplates.Has(hash(data.Event.InternalEvent))239}240241// URL returns a new URL that can be interacted with242func (c *Client) URL() (string, error) {243// first time initialization244var err error245c.Do(func() {246err = c.poll()247})248if err != nil {249return "", errkit.Wrap(ErrInteractshClientNotInitialized, err.Error())250}251252if c.interactsh == nil {253return "", ErrInteractshClientNotInitialized254}255256c.generated.Store(true)257return c.interactsh.URL(), nil258}259260// Close the interactsh clients after waiting for cooldown period.261func (c *Client) Close() bool {262if c.cooldownDuration > 0 && c.generated.Load() {263time.Sleep(c.cooldownDuration)264}265if c.interactsh != nil {266_ = c.interactsh.StopPolling()267_ = c.interactsh.Close()268}269270c.requests.Purge()271c.interactions.Purge()272c.matchedTemplates.Purge()273c.interactshURLs.Purge()274275return c.matched.Load()276}277278// ReplaceMarkers replaces the default {{interactsh-url}} placeholders with interactsh urls279func (c *Client) Replace(data string, interactshURLs []string) (string, []string) {280return c.ReplaceWithMarker(data, interactshURLMarkerRegex, interactshURLs)281}282283// ReplaceMarkers replaces the placeholders with interactsh urls and appends them to interactshURLs284func (c *Client) ReplaceWithMarker(data string, regex *regexp.Regexp, interactshURLs []string) (string, []string) {285for _, interactshURLMarker := range regex.FindAllString(data, -1) {286if url, err := c.NewURLWithData(interactshURLMarker); err == nil {287interactshURLs = append(interactshURLs, url)288data = strings.Replace(data, interactshURLMarker, url, 1)289}290}291return data, interactshURLs292}293294func (c *Client) NewURL() (string, error) {295return c.NewURLWithData("")296}297298func (c *Client) NewURLWithData(data string) (string, error) {299url, err := c.URL()300if err != nil {301return "", err302}303if url == "" {304return "", errors.New("empty interactsh url")305}306_ = c.interactshURLs.SetWithExpire(url, data, defaultInteractionDuration)307return url, nil308}309310// MakePlaceholders does placeholders for interact URLs and other data to a map311func (c *Client) MakePlaceholders(urls []string, data map[string]interface{}) {312data["interactsh-server"] = c.getHostname()313for _, url := range urls {314if interactshURLMarker, err := c.interactshURLs.Get(url); interactshURLMarker != "" && err == nil {315interactshMarker := strings.TrimSuffix(strings.TrimPrefix(interactshURLMarker, "{{"), "}}")316317c.interactshURLs.Remove(url)318319data[interactshMarker] = url320urlIndex := strings.Index(url, ".")321if urlIndex == -1 {322continue323}324data[strings.Replace(interactshMarker, "url", "id", 1)] = url[:urlIndex]325}326}327}328329// MakeResultEventFunc is a result making function for nuclei330type MakeResultEventFunc func(wrapped *output.InternalWrappedEvent) []*output.ResultEvent331332// RequestData contains data for a request event333type RequestData struct {334MakeResultFunc MakeResultEventFunc335Event *output.InternalWrappedEvent336Operators *operators.Operators337MatchFunc operators.MatchFunc338ExtractFunc operators.ExtractFunc339340Parameter string341Request *retryablehttp.Request342}343344// RequestEvent is the event for a network request sent by nuclei.345func (c *Client) RequestEvent(interactshURLs []string, data *RequestData) {346for _, interactshURL := range interactshURLs {347id := strings.TrimRight(strings.TrimSuffix(interactshURL, c.getHostname()), ".")348349if requestShouldStopAtFirstMatch(data) || c.options.StopAtFirstMatch {350gotItem, err := c.matchedTemplates.Get(hash(data.Event.InternalEvent))351if gotItem && err == nil {352break353}354}355356interactions, err := c.interactions.Get(id)357if interactions != nil && err == nil {358for _, interaction := range interactions {359if c.processInteractionForRequest(interaction, data) {360c.interactions.Remove(id)361break362}363}364} else {365_ = c.requests.SetWithExpire(id, data, c.eviction)366}367}368}369370// HasMatchers returns true if an operator has interactsh part371// matchers or extractors.372//373// Used by requests to show result or not depending on presence of interact.sh374// data part matchers.375func HasMatchers(op *operators.Operators) bool {376if op == nil {377return false378}379380for _, matcher := range op.Matchers {381for _, dsl := range matcher.DSL {382if stringsutil.ContainsAnyI(dsl, "interactsh") {383return true384}385}386if stringsutil.HasPrefixI(matcher.Part, "interactsh") {387return true388}389}390for _, matcher := range op.Extractors {391if stringsutil.HasPrefixI(matcher.Part, "interactsh") {392return true393}394}395return false396}397398// HasMarkers checks if the text contains interactsh markers399func HasMarkers(data string) bool {400return interactshURLMarkerRegex.Match([]byte(data))401}402403func (c *Client) debugPrintInteraction(interaction *server.Interaction, event *operators.Result) {404builder := &bytes.Buffer{}405406switch interaction.Protocol {407case "dns":408builder.WriteString(formatInteractionHeader("DNS", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))409if c.options.DebugRequest || c.options.Debug {410builder.WriteString(formatInteractionMessage("DNS Request", interaction.RawRequest, event, c.options.NoColor))411}412if c.options.DebugResponse || c.options.Debug {413builder.WriteString(formatInteractionMessage("DNS Response", interaction.RawResponse, event, c.options.NoColor))414}415case "http":416builder.WriteString(formatInteractionHeader("HTTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))417if c.options.DebugRequest || c.options.Debug {418builder.WriteString(formatInteractionMessage("HTTP Request", interaction.RawRequest, event, c.options.NoColor))419}420if c.options.DebugResponse || c.options.Debug {421builder.WriteString(formatInteractionMessage("HTTP Response", interaction.RawResponse, event, c.options.NoColor))422}423case "smtp":424builder.WriteString(formatInteractionHeader("SMTP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))425if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {426builder.WriteString(formatInteractionMessage("SMTP Interaction", interaction.RawRequest, event, c.options.NoColor))427}428case "ldap":429builder.WriteString(formatInteractionHeader("LDAP", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp))430if c.options.DebugRequest || c.options.Debug || c.options.DebugResponse {431builder.WriteString(formatInteractionMessage("LDAP Interaction", interaction.RawRequest, event, c.options.NoColor))432}433}434_, _ = fmt.Fprint(os.Stderr, builder.String())435}436437func formatInteractionHeader(protocol, ID, address string, at time.Time) string {438return fmt.Sprintf("[%s] Received %s interaction from %s at %s", ID, protocol, address, at.Format("2006-01-02 15:04:05"))439}440441func formatInteractionMessage(key, value string, event *operators.Result, noColor bool) string {442value = responsehighlighter.Highlight(event, value, noColor, false)443return fmt.Sprintf("\n------------\n%s\n------------\n\n%s\n\n", key, value)444}445446func hash(internalEvent output.InternalEvent) string {447templateId := internalEvent[templateIdAttribute].(string)448host := internalEvent["host"].(string)449return fmt.Sprintf("%s:%s", templateId, host)450}451452func (c *Client) getHostname() string {453c.RLock()454defer c.RUnlock()455456return c.hostname457}458459func (c *Client) setHostname(hostname string) {460c.Lock()461defer c.Unlock()462463c.hostname = hostname464}465466// GetHostname returns the configured interactsh server hostname.467func (c *Client) GetHostname() string {468return c.getHostname()469}470471472