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 }