358 lines
12 KiB
Go
Raw Normal View History

2021-04-16 16:56:41 +05:30
package interactsh
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
2021-04-16 16:56:41 +05:30
"net/url"
"os"
2021-04-16 16:56:41 +05:30
"strings"
"sync"
2021-06-15 11:46:02 +05:30
"sync/atomic"
2021-04-16 16:56:41 +05:30
"time"
"github.com/karlseguin/ccache"
"github.com/pkg/errors"
2021-09-07 17:31:46 +03:00
"github.com/projectdiscovery/gologger"
2021-04-16 16:56:41 +05:30
"github.com/projectdiscovery/interactsh/pkg/client"
"github.com/projectdiscovery/interactsh/pkg/server"
"github.com/projectdiscovery/nuclei/v2/pkg/operators"
"github.com/projectdiscovery/nuclei/v2/pkg/output"
"github.com/projectdiscovery/nuclei/v2/pkg/progress"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/writer"
"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
2021-04-16 16:56:41 +05:30
)
// Client is a wrapped client for interactsh server.
type Client struct {
2021-06-15 11:49:32 +05:30
dotHostname string
2021-04-16 16:56:41 +05:30
// interactsh is a client for interactsh server.
interactsh *client.Client
// requests is a stored cache for interactsh-url->request-event data.
requests *ccache.Cache
// interactions is a stored cache for interactsh-interaction->interactsh-url data
interactions *ccache.Cache
// matchedTemplates is a stored cache to track matched templates
matchedTemplates *ccache.Cache
2021-04-16 16:56:41 +05:30
options *Options
2021-04-16 16:56:41 +05:30
eviction time.Duration
pollDuration time.Duration
cooldownDuration time.Duration
2021-06-15 11:49:32 +05:30
firstTimeGroup sync.Once
generated uint32 // decide to wait if we have a generated url
matched bool
2021-04-16 16:56:41 +05:30
}
var (
defaultInteractionDuration = 60 * time.Second
interactshURLMarker = "{{interactsh-url}}"
)
2021-04-16 16:56:41 +05:30
// Options contains configuration options for interactsh nuclei integration.
type Options struct {
// ServerURL is the URL of the interactsh server.
ServerURL string
// Authorization is the Authorization header value
Authorization string
2021-04-16 16:56:41 +05:30
// CacheSize is the numbers of requests to keep track of at a time.
// Older items are discarded in LRU manner in favor of new requests.
CacheSize int64
// Eviction is the period of time after which to automatically discard
// interaction requests.
Eviction time.Duration
// CooldownPeriod is additional time to wait for interactions after closing
// of the poller.
ColldownPeriod time.Duration
// PollDuration is the time to wait before each poll to the server for interactions.
PollDuration time.Duration
// Output is the output writer for nuclei
Output output.Writer
// IssuesClient is a client for issue exporting
IssuesClient *reporting.Client
2021-04-16 16:56:41 +05:30
// Progress is the nuclei progress bar implementation.
Progress progress.Progress
// Debug specifies whether debugging output should be shown for interactsh-client
Debug bool
// HttpFallback controls http retry in case of https failure for server url
HttpFallback bool
// NoInteractsh disables the engine
NoInteractsh bool
StopAtFirstMatch bool
2021-04-16 16:56:41 +05:30
}
const defaultMaxInteractionsCount = 5000
2021-04-16 16:56:41 +05:30
// New returns a new interactsh server client
func New(options *Options) (*Client, error) {
parsed, err := url.Parse(options.ServerURL)
if err != nil {
return nil, errors.Wrap(err, "could not parse server url")
}
configure := ccache.Configure()
configure = configure.MaxSize(options.CacheSize)
cache := ccache.New(configure)
2021-05-09 02:19:23 +05:30
interactionsCfg := ccache.Configure()
interactionsCfg = interactionsCfg.MaxSize(defaultMaxInteractionsCount)
interactionsCache := ccache.New(interactionsCfg)
matchedTemplateCache := ccache.New(ccache.Configure().MaxSize(defaultMaxInteractionsCount))
2021-04-16 16:56:41 +05:30
interactClient := &Client{
eviction: options.Eviction,
interactions: interactionsCache,
matchedTemplates: matchedTemplateCache,
2021-04-16 16:56:41 +05:30
dotHostname: "." + parsed.Host,
options: options,
2021-04-16 16:56:41 +05:30
requests: cache,
pollDuration: options.PollDuration,
cooldownDuration: options.ColldownPeriod,
}
return interactClient, nil
}
2021-04-18 16:10:10 +05:30
// NewDefaultOptions returns the default options for interactsh client
func NewDefaultOptions(output output.Writer, reporting *reporting.Client, progress progress.Progress) *Options {
return &Options{
ServerURL: "https://interact.sh",
CacheSize: 5000,
Eviction: 60 * time.Second,
ColldownPeriod: 5 * time.Second,
PollDuration: 5 * time.Second,
Output: output,
IssuesClient: reporting,
Progress: progress,
HttpFallback: true,
}
}
func (c *Client) firstTimeInitializeClient() error {
if c.options.NoInteractsh {
return nil // do not init if disabled
}
interactsh, err := client.New(&client.Options{
ServerURL: c.options.ServerURL,
Token: c.options.Authorization,
PersistentSession: false,
HTTPFallback: c.options.HttpFallback,
})
if err != nil {
return errors.Wrap(err, "could not create client")
}
c.interactsh = interactsh
interactsh.StartPolling(c.pollDuration, func(interaction *server.Interaction) {
if c.options.StopAtFirstMatch && c.matched {
return
}
if c.options.Debug {
debugPrintInteraction(interaction)
}
item := c.requests.Get(interaction.UniqueID)
2021-04-16 16:56:41 +05:30
if item == nil {
// If we don't have any request for this ID, add it to temporary
2021-09-07 17:31:46 +03:00
// lru cache, so we can correlate when we get an add request.
gotItem := c.interactions.Get(interaction.UniqueID)
if gotItem == nil {
c.interactions.Set(interaction.UniqueID, []*server.Interaction{interaction}, defaultInteractionDuration)
2021-05-09 02:19:23 +05:30
} else if items, ok := gotItem.Value().([]*server.Interaction); ok {
items = append(items, interaction)
c.interactions.Set(interaction.UniqueID, items, defaultInteractionDuration)
}
2021-04-16 16:56:41 +05:30
return
}
request, ok := item.Value().(*RequestData)
2021-04-16 16:56:41 +05:30
if !ok {
return
}
if _, ok := request.Event.InternalEvent["stop-at-first-match"]; ok {
2021-12-29 18:07:48 +05:30
gotItem := c.matchedTemplates.Get(hash(request.Event.InternalEvent["template-id"].(string), request.Event.InternalEvent["host"].(string)))
if gotItem != nil {
return
}
}
_ = c.processInteractionForRequest(interaction, request)
})
return nil
}
2021-04-18 16:10:10 +05:30
// processInteractionForRequest processes an interaction for a request
func (c *Client) processInteractionForRequest(interaction *server.Interaction, data *RequestData) bool {
data.Event.InternalEvent["interactsh_protocol"] = interaction.Protocol
data.Event.InternalEvent["interactsh_request"] = interaction.RawRequest
data.Event.InternalEvent["interactsh_response"] = interaction.RawResponse
data.Event.InternalEvent["interactsh_ip"] = interaction.RemoteAddress
result, matched := data.Operators.Execute(data.Event.InternalEvent, data.MatchFunc, data.ExtractFunc, false)
if !matched || result == nil {
return false // if we don't match, return
}
c.requests.Delete(interaction.UniqueID)
2021-04-16 16:56:41 +05:30
if data.Event.OperatorsResult != nil {
data.Event.OperatorsResult.Merge(result)
} else {
data.Event.OperatorsResult = result
}
data.Event.Results = data.MakeResultFunc(data.Event)
for _, event := range data.Event.Results {
event.Interaction = interaction
}
if writer.WriteResult(data.Event, c.options.Output, c.options.Progress, c.options.IssuesClient) {
c.matched = true
if _, ok := data.Event.InternalEvent["stop-at-first-match"]; ok {
2021-12-29 18:07:48 +05:30
c.matchedTemplates.Set(hash(data.Event.InternalEvent["template-id"].(string), data.Event.InternalEvent["host"].(string)), true, defaultInteractionDuration)
}
}
return true
2021-04-16 16:56:41 +05:30
}
// URL returns a new URL that can be interacted with
func (c *Client) URL() string {
c.firstTimeGroup.Do(func() {
if err := c.firstTimeInitializeClient(); err != nil {
gologger.Error().Msgf("Could not initialize interactsh client: %s", err)
}
})
if c.interactsh == nil {
return ""
}
2021-06-15 11:46:02 +05:30
atomic.CompareAndSwapUint32(&c.generated, 0, 1)
2021-04-16 16:56:41 +05:30
return c.interactsh.URL()
}
// Close closes the interactsh clients after waiting for cooldown period.
2021-04-18 16:10:10 +05:30
func (c *Client) Close() bool {
2021-06-15 11:46:02 +05:30
if c.cooldownDuration > 0 && atomic.LoadUint32(&c.generated) == 1 {
2021-04-16 16:56:41 +05:30
time.Sleep(c.cooldownDuration)
}
if c.interactsh != nil {
c.interactsh.StopPolling()
c.interactsh.Close()
}
2021-04-18 16:10:10 +05:30
return c.matched
2021-04-16 16:56:41 +05:30
}
// ReplaceMarkers replaces the {{interactsh-url}} placeholders to actual
// URLs pointing to interactsh-server.
//
// It accepts data to replace as well as the URL to replace placeholders
// with generated uniquely for each request.
func (c *Client) ReplaceMarkers(data string, interactshURLs []string) (string, []string) {
for strings.Contains(data, interactshURLMarker) {
url := c.URL()
interactshURLs = append(interactshURLs, url)
data = strings.Replace(data, interactshURLMarker, url, 1)
2021-04-16 16:56:41 +05:30
}
return data, interactshURLs
2021-04-16 16:56:41 +05:30
}
// SetStopAtFirstMatch sets StopAtFirstMatch true for interactsh client options
func (c *Client) SetStopAtFirstMatch() {
c.options.StopAtFirstMatch = true
}
2021-04-16 16:56:41 +05:30
// MakeResultEventFunc is a result making function for nuclei
type MakeResultEventFunc func(wrapped *output.InternalWrappedEvent) []*output.ResultEvent
2021-04-18 16:10:10 +05:30
// RequestData contains data for a request event
type RequestData struct {
MakeResultFunc MakeResultEventFunc
Event *output.InternalWrappedEvent
Operators *operators.Operators
MatchFunc operators.MatchFunc
ExtractFunc operators.ExtractFunc
2021-04-16 16:56:41 +05:30
}
// RequestEvent is the event for a network request sent by nuclei.
func (c *Client) RequestEvent(interactshURLs []string, data *RequestData) {
for _, interactshURL := range interactshURLs {
if c.options.StopAtFirstMatch && c.matched {
break
}
id := strings.TrimSuffix(interactshURL, c.dotHostname)
interaction := c.interactions.Get(id)
if interaction != nil {
// If we have previous interactions, get them and process them.
interactions, ok := interaction.Value().([]*server.Interaction)
if !ok {
c.requests.Set(id, data, c.eviction)
return
}
for _, interaction := range interactions {
if c.processInteractionForRequest(interaction, data) {
c.interactions.Delete(id)
break
}
}
} else {
c.requests.Set(id, data, c.eviction)
}
}
2021-04-18 16:10:10 +05:30
}
// HasMatchers returns true if an operator has interactsh part
// matchers or extractors.
//
// Used by requests to show result or not depending on presence of interact.sh
2021-04-18 16:10:10 +05:30
// data part matchers.
2021-05-01 18:28:24 +05:30
func HasMatchers(op *operators.Operators) bool {
if op == nil {
return false
}
2021-05-01 18:28:24 +05:30
for _, matcher := range op.Matchers {
2021-04-18 17:53:59 +05:30
for _, dsl := range matcher.DSL {
if strings.Contains(dsl, "interactsh") {
2021-04-18 17:53:59 +05:30
return true
}
}
if strings.HasPrefix(matcher.Part, "interactsh") {
2021-04-18 16:10:10 +05:30
return true
}
}
2021-05-01 18:28:24 +05:30
for _, matcher := range op.Extractors {
2021-04-18 17:53:59 +05:30
if strings.HasPrefix(matcher.Part, "interactsh") {
2021-04-18 16:10:10 +05:30
return true
}
}
return false
2021-04-16 16:56:41 +05:30
}
func debugPrintInteraction(interaction *server.Interaction) {
builder := &bytes.Buffer{}
switch interaction.Protocol {
case "dns":
builder.WriteString(fmt.Sprintf("[%s] Received DNS interaction (%s) from %s at %s", interaction.FullId, interaction.QType, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05")))
builder.WriteString(fmt.Sprintf("\n-----------\nDNS Request\n-----------\n\n%s\n\n------------\nDNS Response\n------------\n\n%s\n\n", interaction.RawRequest, interaction.RawResponse))
case "http":
builder.WriteString(fmt.Sprintf("[%s] Received HTTP interaction from %s at %s", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05")))
builder.WriteString(fmt.Sprintf("\n------------\nHTTP Request\n------------\n\n%s\n\n-------------\nHTTP Response\n-------------\n\n%s\n\n", interaction.RawRequest, interaction.RawResponse))
case "smtp":
builder.WriteString(fmt.Sprintf("[%s] Received SMTP interaction from %s at %s", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05")))
builder.WriteString(fmt.Sprintf("\n------------\nSMTP Interaction\n------------\n\n%s\n\n", interaction.RawRequest))
case "ldap":
builder.WriteString(fmt.Sprintf("[%s] Received LDAP interaction from %s at %s", interaction.FullId, interaction.RemoteAddress, interaction.Timestamp.Format("2006-01-02 15:04:05")))
builder.WriteString(fmt.Sprintf("\n------------\nLDAP Interaction\n------------\n\n%s\n\n", interaction.RawRequest))
}
fmt.Fprint(os.Stderr, builder.String())
}
2021-12-29 18:07:48 +05:30
func hash(templateID, host string) string {
2021-12-29 12:33:54 +05:30
h := sha1.New()
2021-12-29 18:07:48 +05:30
h.Write([]byte(templateID))
h.Write([]byte(host))
2021-12-29 12:33:54 +05:30
return hex.EncodeToString(h.Sum(nil))
}