2021-04-16 11:26:41 +00:00
package interactsh
import (
2021-08-25 21:13:58 +00:00
"bytes"
"fmt"
2021-04-16 11:26:41 +00:00
"net/url"
2021-08-25 21:13:58 +00:00
"os"
2021-04-16 11:26:41 +00:00
"strings"
2021-09-09 19:02:05 +00:00
"sync"
2021-06-15 06:16:02 +00:00
"sync/atomic"
2021-04-16 11:26:41 +00:00
"time"
"github.com/karlseguin/ccache"
"github.com/pkg/errors"
2021-09-07 14:31:46 +00:00
2021-05-03 08:38:09 +00:00
"github.com/projectdiscovery/gologger"
2021-04-16 11:26:41 +00:00
"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"
2021-11-22 12:23:25 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/writer"
2021-05-03 08:38:09 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
2021-04-16 11:26:41 +00:00
)
// Client is a wrapped client for interactsh server.
type Client struct {
2021-06-15 06:19:32 +00:00
dotHostname string
2021-04-16 11:26:41 +00:00
// interactsh is a client for interactsh server.
interactsh * client . Client
// requests is a stored cache for interactsh-url->request-event data.
requests * ccache . Cache
2021-05-08 20:07:22 +00:00
// interactions is a stored cache for interactsh-interaction->interactsh-url data
interactions * ccache . Cache
2021-04-16 11:26:41 +00:00
2021-05-08 20:07:22 +00:00
options * Options
2021-04-16 11:26:41 +00:00
eviction time . Duration
pollDuration time . Duration
cooldownDuration time . Duration
2021-06-15 06:19:32 +00:00
2021-09-09 19:02:05 +00:00
firstTimeGroup sync . Once
generated uint32 // decide to wait if we have a generated url
matched bool
2021-04-16 11:26:41 +00:00
}
2021-05-08 20:07:22 +00:00
var (
defaultInteractionDuration = 60 * time . Second
interactshURLMarker = "{{interactsh-url}}"
)
2021-04-16 11:26:41 +00:00
// Options contains configuration options for interactsh nuclei integration.
type Options struct {
// ServerURL is the URL of the interactsh server.
ServerURL string
2021-09-01 16:42:15 +00:00
// Authorization is the Authorization header value
Authorization string
2021-04-16 11:26:41 +00:00
// 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
2021-05-03 08:38:09 +00:00
// IssuesClient is a client for issue exporting
IssuesClient * reporting . Client
2021-04-16 11:26:41 +00:00
// Progress is the nuclei progress bar implementation.
Progress progress . Progress
2021-08-25 21:13:58 +00:00
// Debug specifies whether debugging output should be shown for interactsh-client
Debug bool
2021-11-16 14:32:39 +00:00
NoInteractsh bool
2021-04-16 11:26:41 +00:00
}
2021-05-08 20:07:22 +00:00
const defaultMaxInteractionsCount = 5000
2021-04-16 11:26:41 +00:00
// 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-08 20:49:23 +00:00
interactionsCfg := ccache . Configure ( )
interactionsCfg = interactionsCfg . MaxSize ( defaultMaxInteractionsCount )
interactionsCache := ccache . New ( interactionsCfg )
2021-05-08 20:07:22 +00:00
2021-04-16 11:26:41 +00:00
interactClient := & Client {
eviction : options . Eviction ,
2021-05-08 20:07:22 +00:00
interactions : interactionsCache ,
2021-04-16 11:26:41 +00:00
dotHostname : "." + parsed . Host ,
2021-05-08 20:07:22 +00:00
options : options ,
2021-04-16 11:26:41 +00:00
requests : cache ,
pollDuration : options . PollDuration ,
cooldownDuration : options . ColldownPeriod ,
}
2021-09-09 19:02:05 +00:00
return interactClient , nil
}
2021-04-18 10:40:10 +00:00
2021-10-28 21:49:43 +00:00
// NewDefaultOptions returns the default options for interactsh client
func NewDefaultOptions ( output output . Writer , reporting * reporting . Client , progress progress . Progress ) * Options {
return & Options {
ServerURL : "https://interactsh.com" ,
CacheSize : 5000 ,
Eviction : 60 * time . Second ,
ColldownPeriod : 5 * time . Second ,
PollDuration : 5 * time . Second ,
Output : output ,
IssuesClient : reporting ,
Progress : progress ,
}
}
2021-09-09 19:02:05 +00:00
func ( c * Client ) firstTimeInitializeClient ( ) error {
2021-11-16 14:32:39 +00:00
if c . options . NoInteractsh {
return nil // do not init if disabled
}
2021-09-09 19:02:05 +00:00
interactsh , err := client . New ( & client . Options {
ServerURL : c . options . ServerURL ,
Token : c . options . Authorization ,
PersistentSession : false ,
} )
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 . Debug {
2021-08-25 21:13:58 +00:00
debugPrintInteraction ( interaction )
}
2021-09-09 19:02:05 +00:00
item := c . requests . Get ( interaction . UniqueID )
2021-04-16 11:26:41 +00:00
if item == nil {
2021-05-08 20:07:22 +00:00
// If we don't have any request for this ID, add it to temporary
2021-09-07 14:31:46 +00:00
// lru cache, so we can correlate when we get an add request.
2021-09-09 19:02:05 +00:00
gotItem := c . interactions . Get ( interaction . UniqueID )
2021-05-08 20:07:22 +00:00
if gotItem == nil {
2021-09-09 19:02:05 +00:00
c . interactions . Set ( interaction . UniqueID , [ ] * server . Interaction { interaction } , defaultInteractionDuration )
2021-05-08 20:49:23 +00:00
} else if items , ok := gotItem . Value ( ) . ( [ ] * server . Interaction ) ; ok {
items = append ( items , interaction )
2021-09-09 19:02:05 +00:00
c . interactions . Set ( interaction . UniqueID , items , defaultInteractionDuration )
2021-05-08 20:07:22 +00:00
}
2021-04-16 11:26:41 +00:00
return
}
2021-05-08 20:07:22 +00:00
request , ok := item . Value ( ) . ( * RequestData )
2021-04-16 11:26:41 +00:00
if ! ok {
return
}
2021-09-09 19:02:05 +00:00
_ = c . processInteractionForRequest ( interaction , request )
2021-05-08 20:07:22 +00:00
} )
2021-09-09 19:02:05 +00:00
return nil
2021-05-08 20:07:22 +00:00
}
2021-04-18 10:40:10 +00:00
2021-05-08 20:07:22 +00:00
// 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
2021-10-12 17:06:55 +00:00
result , matched := data . Operators . Execute ( data . Event . InternalEvent , data . MatchFunc , data . ExtractFunc , false )
2021-05-08 20:07:22 +00:00
if ! matched || result == nil {
return false // if we don't match, return
}
c . requests . Delete ( interaction . UniqueID )
2021-04-16 11:26:41 +00:00
2021-05-08 20:07:22 +00:00
if data . Event . OperatorsResult != nil {
data . Event . OperatorsResult . Merge ( result )
} else {
data . Event . OperatorsResult = result
}
data . Event . Results = data . MakeResultFunc ( data . Event )
2021-11-22 12:23:25 +00:00
if writer . WriteResult ( data . Event , c . options . Output , c . options . Progress , c . options . IssuesClient ) {
c . matched = true
2021-05-08 20:07:22 +00:00
}
return true
2021-04-16 11:26:41 +00:00
}
// URL returns a new URL that can be interacted with
func ( c * Client ) URL ( ) string {
2021-09-09 19:02:05 +00:00
c . firstTimeGroup . Do ( func ( ) {
if err := c . firstTimeInitializeClient ( ) ; err != nil {
gologger . Error ( ) . Msgf ( "Could not initialize interactsh client: %s" , err )
}
} )
2021-09-15 12:45:22 +00:00
if c . interactsh == nil {
return ""
}
2021-06-15 06:16:02 +00:00
atomic . CompareAndSwapUint32 ( & c . generated , 0 , 1 )
2021-04-16 11:26:41 +00:00
return c . interactsh . URL ( )
}
// Close closes the interactsh clients after waiting for cooldown period.
2021-04-18 10:40:10 +00:00
func ( c * Client ) Close ( ) bool {
2021-06-15 06:16:02 +00:00
if c . cooldownDuration > 0 && atomic . LoadUint32 ( & c . generated ) == 1 {
2021-04-16 11:26:41 +00:00
time . Sleep ( c . cooldownDuration )
}
2021-09-09 19:02:05 +00:00
if c . interactsh != nil {
c . interactsh . StopPolling ( )
c . interactsh . Close ( )
}
2021-04-18 10:40:10 +00:00
return c . matched
2021-04-16 11:26:41 +00:00
}
// 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.
2021-11-04 11:43:47 +00:00
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 11:26:41 +00:00
}
2021-11-04 11:43:47 +00:00
return data , interactshURLs
2021-04-16 11:26:41 +00:00
}
// MakeResultEventFunc is a result making function for nuclei
type MakeResultEventFunc func ( wrapped * output . InternalWrappedEvent ) [ ] * output . ResultEvent
2021-04-18 10:40:10 +00:00
// 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 11:26:41 +00:00
}
// RequestEvent is the event for a network request sent by nuclei.
2021-11-04 11:43:47 +00:00
func ( c * Client ) RequestEvent ( interactshURLs [ ] string , data * RequestData ) {
for _ , interactshURL := range interactshURLs {
id := strings . TrimSuffix ( interactshURL , c . dotHostname )
2021-05-08 20:07:22 +00:00
2021-11-04 11:43:47 +00:00
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
2021-05-08 20:07:22 +00:00
}
2021-11-04 11:43:47 +00:00
for _ , interaction := range interactions {
if c . processInteractionForRequest ( interaction , data ) {
2021-11-05 09:57:49 +00:00
c . interactions . Delete ( id )
2021-11-04 11:43:47 +00:00
break
}
}
} else {
c . requests . Set ( id , data , c . eviction )
2021-05-08 20:07:22 +00:00
}
}
2021-11-04 11:43:47 +00:00
2021-04-18 10:40:10 +00:00
}
// HasMatchers returns true if an operator has interactsh part
// matchers or extractors.
//
2021-09-30 19:10:13 +00:00
// Used by requests to show result or not depending on presence of interactsh.com
2021-04-18 10:40:10 +00:00
// data part matchers.
2021-05-01 12:58:24 +00:00
func HasMatchers ( op * operators . Operators ) bool {
if op == nil {
2021-04-18 11:57:45 +00:00
return false
}
2021-05-01 12:58:24 +00:00
for _ , matcher := range op . Matchers {
2021-04-18 12:23:59 +00:00
for _ , dsl := range matcher . DSL {
2021-04-19 07:43:08 +00:00
if strings . Contains ( dsl , "interactsh" ) {
2021-04-18 12:23:59 +00:00
return true
}
}
if strings . HasPrefix ( matcher . Part , "interactsh" ) {
2021-04-18 10:40:10 +00:00
return true
}
}
2021-05-01 12:58:24 +00:00
for _ , matcher := range op . Extractors {
2021-04-18 12:23:59 +00:00
if strings . HasPrefix ( matcher . Part , "interactsh" ) {
2021-04-18 10:40:10 +00:00
return true
}
}
return false
2021-04-16 11:26:41 +00:00
}
2021-08-25 21:13:58 +00:00
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 ) )
}
fmt . Fprint ( os . Stderr , builder . String ( ) )
}