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