From d751ce069b54acfdf373a9c6d9cd3de5e8718135 Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Wed, 25 Mar 2026 16:10:30 -0600 Subject: [PATCH] fix: DNS and SMTP drop interactions when nonce lengths differ DNS and SMTP accumulated sliding-window matches in a single variable (last match wins) and stored only once after the loop. This silently dropped valid interactions when the server's correlation-id-nonce-length was shorter than a client's. This change updates both to dispatch per match inside the loop, mirroring the working HTTP/LDAP pattern. SMTP also gains the missing SlideWithLength and ToLower normalization. --- pkg/server/dns_server.go | 58 ++++++++++++++++++------------------- pkg/server/server_test.go | 30 +++++++++++++++++++ pkg/server/smtp_server.go | 61 ++++++++++++++++++++------------------- 3 files changed, 89 insertions(+), 60 deletions(-) diff --git a/pkg/server/dns_server.go b/pkg/server/dns_server.go index c724795c..0e80353e 100644 --- a/pkg/server/dns_server.go +++ b/pkg/server/dns_server.go @@ -299,10 +299,31 @@ func toQType(ttype uint16) (rtype string) { return } +func (h *DNSServer) storeInteraction(uniqueID, fullID, qtype, requestMsg, responseMsg, remoteAddr string) { + correlationID := uniqueID[:h.options.CorrelationIdLength] + interaction := &Interaction{ + Protocol: "dns", + UniqueID: uniqueID, + FullId: fullID, + QType: qtype, + RawRequest: requestMsg, + RawResponse: responseMsg, + RemoteAddress: remoteAddr, + Timestamp: time.Now(), + } + data, err := jsoniter.Marshal(interaction) + if err != nil { + gologger.Warning().Msgf("Could not encode dns interaction: %s\n", err) + } else { + gologger.Debug().Msgf("DNS Interaction: \n%s\n", string(data)) + if err := h.options.Storage.AddInteraction(correlationID, data); err != nil { + gologger.Warning().Msgf("Could not store dns interaction: %s\n", err) + } + } +} + // handleInteraction handles an interaction for the DNS server func (h *DNSServer) handleInteraction(domain string, w dns.ResponseWriter, r *dns.Msg, m *dns.Msg) { - var uniqueID, fullID string - requestMsg := r.String() responseMsg := m.String() @@ -348,14 +369,15 @@ func (h *DNSServer) handleInteraction(domain string, w dns.ResponseWriter, r *dn } if foundDomain != "" { + host, _, _ := net.SplitHostPort(w.RemoteAddr().String()) + qtype := toQType(r.Question[0].Qtype) if h.options.ScanEverywhere { chunks := stringsutil.SplitAny(requestMsg, ".\n\t\"'") for _, chunk := range chunks { for part := range stringsutil.SlideWithLength(chunk, h.options.GetIdLength()) { normalizedPart := strings.ToLower(part) if h.options.isCorrelationID(normalizedPart) { - uniqueID = normalizedPart - fullID = part + h.storeInteraction(normalizedPart, part, qtype, requestMsg, responseMsg, host) } } } @@ -365,40 +387,16 @@ func (h *DNSServer) handleInteraction(domain string, w dns.ResponseWriter, r *dn for partChunk := range stringsutil.SlideWithLength(part, h.options.GetIdLength()) { normalizedPartChunk := strings.ToLower(partChunk) if h.options.isCorrelationID(normalizedPartChunk) { - fullID = part + fullID := part if i+1 <= len(parts) { fullID = strings.Join(parts[:i+1], ".") } - uniqueID = normalizedPartChunk + h.storeInteraction(normalizedPartChunk, fullID, qtype, requestMsg, responseMsg, host) } } } } } - - if uniqueID != "" { - correlationID := uniqueID[:h.options.CorrelationIdLength] - host, _, _ := net.SplitHostPort(w.RemoteAddr().String()) - interaction := &Interaction{ - Protocol: "dns", - UniqueID: uniqueID, - FullId: fullID, - QType: toQType(r.Question[0].Qtype), - RawRequest: requestMsg, - RawResponse: responseMsg, - RemoteAddress: host, - Timestamp: time.Now(), - } - data, err := jsoniter.Marshal(interaction) - if err != nil { - gologger.Warning().Msgf("Could not encode dns interaction: %s\n", err) - } else { - gologger.Debug().Msgf("DNS Interaction: \n%s\n", string(data)) - if err := h.options.Storage.AddInteraction(correlationID, data); err != nil { - gologger.Warning().Msgf("Could not store dns interaction: %s\n", err) - } - } - } } // CustomRecordConfig represents a custom DNS record configuration diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 9391f15a..22c56bc1 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -1,9 +1,12 @@ package server import ( + "strings" "testing" "github.com/projectdiscovery/interactsh/pkg/settings" + stringsutil "github.com/projectdiscovery/utils/strings" + "github.com/rs/xid" "github.com/stretchr/testify/require" ) @@ -12,3 +15,30 @@ func TestGetURLIDComponent(t *testing.T) { random := options.getURLIDComponent("c6rj61aciaeutn2ae680cg5ugboyyyyyn.interactsh.com") require.Equal(t, "c6rj61aciaeutn2ae680cg5ugboyyyyyn", random, "could not get correct component") } + +func TestIsCorrelationID(t *testing.T) { + options := Options{CorrelationIdLength: settings.CorrelationIdLengthDefault, CorrelationIdNonceLength: settings.CorrelationIdNonceLengthDefault} + + t.Run("exact length match", func(t *testing.T) { + id := strings.Repeat("a", options.CorrelationIdLength) + strings.Repeat("b", options.CorrelationIdNonceLength) + require.True(t, options.isCorrelationID(id)) + }) + + t.Run("shorter than expected", func(t *testing.T) { + require.False(t, options.isCorrelationID("tooshort")) + }) + + t.Run("sliding window finds embedded ID", func(t *testing.T) { + shortNonce := Options{CorrelationIdLength: settings.CorrelationIdLengthDefault, CorrelationIdNonceLength: 4} + validID := xid.New().String() + "abcd" + longer := ".." + validID + ".." // non-alphanumeric padding prevents spurious matches + found := false + for chunk := range stringsutil.SlideWithLength(longer, shortNonce.GetIdLength()) { + if shortNonce.isCorrelationID(chunk) { + found = true + break + } + } + require.True(t, found, "sliding window should find embedded correlation ID") + }) +} diff --git a/pkg/server/smtp_server.go b/pkg/server/smtp_server.go index 6c88ca06..837d2ab5 100644 --- a/pkg/server/smtp_server.go +++ b/pkg/server/smtp_server.go @@ -80,12 +80,32 @@ func (h *SMTPServer) ListenAndServe(tlsConfig *tls.Config, smtpAlive, smtpsAlive } } +func (h *SMTPServer) storeInteraction(uniqueID, fullID, dataString, from, remoteAddr string) { + correlationID := uniqueID[:h.options.CorrelationIdLength] + interaction := &Interaction{ + Protocol: "smtp", + UniqueID: uniqueID, + FullId: fullID, + RawRequest: dataString, + SMTPFrom: from, + RemoteAddress: remoteAddr, + Timestamp: time.Now(), + } + data, err := jsoniter.Marshal(interaction) + if err != nil { + gologger.Warning().Msgf("Could not encode smtp interaction: %s\n", err) + } else { + gologger.Debug().Msgf("%s\n", string(data)) + if err := h.options.Storage.AddInteraction(correlationID, data); err != nil { + gologger.Warning().Msgf("Could not store smtp interaction: %s\n", err) + } + } +} + // defaultHandler is a handler for default collaborator requests func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []string, data []byte) error { atomic.AddUint64(&h.options.Stats.Smtp, 1) - var uniqueID, fullID string - dataString := string(data) gologger.Debug().Msgf("New SMTP request: %s %s %s %s\n", remoteAddr, from, to, dataString) @@ -122,40 +142,21 @@ func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []strin for _, addr := range to { if len(addr) > h.options.GetIdLength() && strings.Contains(addr, "@") { + host, _, _ := net.SplitHostPort(remoteAddr.String()) parts := strings.Split(addr[strings.LastIndex(addr, "@")+1:], ".") for i, part := range parts { - if h.options.isCorrelationID(part) { - uniqueID = part - fullID = part - if i+1 <= len(parts) { - fullID = strings.Join(parts[:i+1], ".") + for partChunk := range stringsutil.SlideWithLength(part, h.options.GetIdLength()) { + normalizedPartChunk := strings.ToLower(partChunk) + if h.options.isCorrelationID(normalizedPartChunk) { + fullID := part + if i+1 <= len(parts) { + fullID = strings.Join(parts[:i+1], ".") + } + h.storeInteraction(normalizedPartChunk, fullID, dataString, from, host) } } } } } - if uniqueID != "" { - host, _, _ := net.SplitHostPort(remoteAddr.String()) - - correlationID := uniqueID[:h.options.CorrelationIdLength] - interaction := &Interaction{ - Protocol: "smtp", - UniqueID: uniqueID, - FullId: fullID, - RawRequest: dataString, - SMTPFrom: from, - RemoteAddress: host, - Timestamp: time.Now(), - } - data, err := jsoniter.Marshal(interaction) - if err != nil { - gologger.Warning().Msgf("Could not encode smtp interaction: %s\n", err) - } else { - gologger.Debug().Msgf("%s\n", string(data)) - if err := h.options.Storage.AddInteraction(correlationID, data); err != nil { - gologger.Warning().Msgf("Could not store smtp interaction: %s\n", err) - } - } - } return nil }