Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,13 @@ $ interactsh-server -domain hackwithautomation.com -wildcard
[DNS] Listening on UDP 157.230.223.165:53
```

In wildcard mode, each connected client independently receives all interactions for the shared domain. The server keeps a buffer of recent interactions so that multiple clients can poll without missing data. By default, the buffer holds up to **10,000** interactions per shared key. This can be adjusted via the `INTERACTSH_MAX_SHARED_INTERACTIONS` environment variable:

```console
$ export INTERACTSH_MAX_SHARED_INTERACTIONS=50000
$ interactsh-server -domain hackwithautomation.com -wildcard
```

## LDAP Interaction

As default, Interactsh server support LDAP interaction for the payload included in [search query](https://ldapwiki.com/wiki/LDAP%20Query%20Examples), additionally `ldap` flag can be used for complete logging.
Expand Down
5 changes: 5 additions & 0 deletions cmd/interactsh-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ func main() {
}
storeOptions.DbPath = cliOptions.DiskStoragePath
}
if v := os.Getenv("INTERACTSH_MAX_SHARED_INTERACTIONS"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
storeOptions.MaxSharedInteractions = n
}
}

var err error
store, err = storage.New(&storeOptions)
Expand Down
12 changes: 10 additions & 2 deletions pkg/server/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,14 @@ func (h *HTTPServer) deregisterHandler(w http.ResponseWriter, req *http.Request)
jsonError(w, fmt.Sprintf("could not remove id: %s", err), http.StatusBadRequest)
return
}
if h.options.RootTLD {
for _, domain := range h.options.Domains {
_ = h.options.Storage.RemoveConsumer(domain, r.CorrelationID)
}
}
if h.options.Token != "" {
_ = h.options.Storage.RemoveConsumer(h.options.Token, r.CorrelationID)
}
jsonMsg(w, "deregistration successful", http.StatusOK)
gologger.Debug().Msgf("Deregistered correlationID %s for key\n", r.CorrelationID)
}
Expand Down Expand Up @@ -460,14 +468,14 @@ func (h *HTTPServer) pollHandler(w http.ResponseWriter, req *http.Request) {
var tlddata, extradata []string
if h.options.RootTLD {
for _, domain := range h.options.Domains {
interactions, _ := h.options.Storage.GetInteractionsWithId(domain)
interactions, _ := h.options.Storage.GetInteractionsWithIdForConsumer(domain, ID)
// root domains interaction are not encrypted
tlddata = append(tlddata, interactions...)
}
}
if h.options.Token != "" {
// auth token interactions are not encrypted
extradata, _ = h.options.Storage.GetInteractionsWithId(h.options.Token)
extradata, _ = h.options.Storage.GetInteractionsWithIdForConsumer(h.options.Token, ID)
}
response := &PollResponse{Data: data, AESKey: aesKey, TLDData: tlddata, Extra: extradata}

Expand Down
16 changes: 10 additions & 6 deletions pkg/storage/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ const (
)

type Options struct {
DbPath string
EvictionTTL time.Duration
MaxSize int
EvictionStrategy EvictionStrategy
DbPath string
EvictionTTL time.Duration
MaxSize int
MaxSharedInteractions int
EvictionStrategy EvictionStrategy
}

func (options *Options) UseDisk() bool {
return options.DbPath != ""
}

const defaultMaxSharedInteractions = 10000

var DefaultOptions = Options{
MaxSize: 2500000,
EvictionStrategy: EvictionStrategySliding,
MaxSize: 2500000,
MaxSharedInteractions: defaultMaxSharedInteractions,
EvictionStrategy: EvictionStrategySliding,
}
2 changes: 2 additions & 0 deletions pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type Storage interface {
AddInteractionWithId(id string, data []byte) error
GetInteractions(correlationID, secret string) ([]string, string, error)
GetInteractionsWithId(id string) ([]string, error)
GetInteractionsWithIdForConsumer(id, consumerID string) ([]string, error)
RemoveConsumer(id, consumerID string) error
RemoveID(correlationID, secret string) error
GetCacheItem(token string) (*CorrelationData, error)
Close() error
Expand Down
182 changes: 182 additions & 0 deletions pkg/storage/storagedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"strings"
"time"

"github.com/goburrow/cache"
"github.com/pkg/errors"
Expand All @@ -32,6 +33,9 @@ type StorageDB struct {

// New creates a new storage instance for interactsh data.
func New(options *Options) (*StorageDB, error) {
if options.MaxSharedInteractions <= 0 {
options.MaxSharedInteractions = defaultMaxSharedInteractions
}
storageDB := &StorageDB{Options: options}
cacheOptions := []cache.Option{
cache.WithMaximumSize(options.MaxSize),
Expand Down Expand Up @@ -240,6 +244,184 @@ func (s *StorageDB) GetInteractionsWithId(id string) ([]string, error) {
return s.getInteractions(value, id)
}

// GetInteractionsWithIdForConsumer returns unseen interactions for a consumer
// using per-consumer read offsets.
func (s *StorageDB) GetInteractionsWithIdForConsumer(id, consumerID string) ([]string, error) {
item, ok := s.cache.GetIfPresent(id)
if !ok {
return nil, errors.New("could not get id from cache")
}
value, ok := item.(*CorrelationData)
if !ok {
return nil, errors.New("invalid id cache value found")
}

value.Lock()
defer value.Unlock()

if value.ReadOffsets == nil {
value.ReadOffsets = make(map[string]int)
}
if value.LastSeen == nil {
value.LastSeen = make(map[string]time.Time)
}

var allData []string
switch {
case s.Options.UseDisk():
raw, err := s.db.Get([]byte(id), nil)
if err != nil {
if errors.Is(err, leveldb.ErrNotFound) {
return nil, nil
}
return nil, err
}
for _, d := range bytes.Split(raw, []byte("\n")) {
if len(d) > 0 {
allData = append(allData, string(d))
}
}
default:
allData = value.Data
}

offset := min(value.ReadOffsets[consumerID], len(allData))

var unseen []string
if offset < len(allData) {
unseen = make([]string, len(allData)-offset)
copy(unseen, allData[offset:])
}

value.ReadOffsets[consumerID] = len(allData)
value.LastSeen[consumerID] = time.Now()

s.evictAndEnforceBuffer(value, id)

return unseen, nil
}

// RemoveConsumer removes a consumer's read offset and compacts consumed data.
func (s *StorageDB) RemoveConsumer(id, consumerID string) error {
item, ok := s.cache.GetIfPresent(id)
if !ok {
return nil
}
value, ok := item.(*CorrelationData)
if !ok {
return nil
}

value.Lock()
defer value.Unlock()

delete(value.ReadOffsets, consumerID)
delete(value.LastSeen, consumerID)

s.compactConsumedData(value, id)
return nil
}

func (s *StorageDB) evictAndEnforceBuffer(value *CorrelationData, id string) {
s.evictStaleConsumers(value, id)
s.enforceMaxBuffer(value, id)
}

func (s *StorageDB) compactConsumedData(value *CorrelationData, id string) {
s.evictStaleConsumers(value, id)
if len(value.ReadOffsets) == 0 {
return
}

minOffset := -1
for _, off := range value.ReadOffsets {
if minOffset < 0 || off < minOffset {
minOffset = off
}
}
if minOffset > 0 {
s.applyTrim(value, id, minOffset)
}
s.enforceMaxBuffer(value, id)
}

func (s *StorageDB) evictStaleConsumers(value *CorrelationData, id string) {
if s.Options.EvictionTTL > 0 {
now := time.Now()
for cid, lastSeen := range value.LastSeen {
if now.Sub(lastSeen) > s.Options.EvictionTTL {
delete(value.ReadOffsets, cid)
delete(value.LastSeen, cid)
}
}
}

if len(value.ReadOffsets) == 0 {
value.Data = nil
if s.Options.UseDisk() {
_ = s.db.Delete([]byte(id), nil)
}
}
}

func (s *StorageDB) enforceMaxBuffer(value *CorrelationData, id string) {
dataLen := s.dataLen(value, id)
if dataLen <= s.Options.MaxSharedInteractions {
return
}
excess := dataLen - s.Options.MaxSharedInteractions
s.applyTrim(value, id, excess)
}

func (s *StorageDB) applyTrim(value *CorrelationData, id string, trimCount int) {
switch {
case s.Options.UseDisk():
raw, err := s.db.Get([]byte(id), nil)
if err != nil {
return
}
var allData []string
for _, d := range bytes.Split(raw, []byte("\n")) {
if len(d) > 0 {
allData = append(allData, string(d))
}
}
if trimCount >= len(allData) {
_ = s.db.Delete([]byte(id), nil)
} else {
remaining := allData[trimCount:]
_ = s.db.Put([]byte(id), []byte(strings.Join(remaining, "\n")), nil)
}
default:
if trimCount >= len(value.Data) {
value.Data = nil
} else {
value.Data = value.Data[trimCount:]
}
}

for cid, off := range value.ReadOffsets {
value.ReadOffsets[cid] = max(off-trimCount, 0)
}
}

func (s *StorageDB) dataLen(value *CorrelationData, id string) int {
if s.Options.UseDisk() {
raw, err := s.db.Get([]byte(id), nil)
if err != nil {
return 0
}
count := 0
for _, d := range bytes.Split(raw, []byte("\n")) {
if len(d) > 0 {
count++
}
}
return count
}
return len(value.Data)
}

// RemoveID removes data for a correlation ID and data related to it.
func (s *StorageDB) RemoveID(correlationID, secret string) error {
item, ok := s.cache.GetIfPresent(correlationID)
Expand Down
Loading
Loading