Skip to content

Commit fd13189

Browse files
committed
adding to smtp
1 parent 1a8d15e commit fd13189

4 files changed

Lines changed: 229 additions & 116 deletions

File tree

pkg/server/dns_server.go

Lines changed: 31 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"sync/atomic"
1010
"time"
1111

12-
jsoniter "github.com/json-iterator/go"
1312
"github.com/miekg/dns"
1413
"github.com/pkg/errors"
1514
"github.com/projectdiscovery/gologger"
@@ -315,21 +314,49 @@ func (h *DNSServer) handleInteraction(domain string, w dns.ResponseWriter, r *dn
315314
}
316315
}
317316

317+
host, _, _ := net.SplitHostPort(w.RemoteAddr().String())
318+
318319
if h.options.RootTLD && foundDomain != "" {
319-
h.storeRootTLDInteraction(foundDomain, domain, requestMsg, responseMsg, w, r)
320+
interaction := &Interaction{
321+
Protocol: "dns",
322+
UniqueID: domain,
323+
FullId: domain,
324+
QType: toQType(r.Question[0].Qtype),
325+
RawRequest: requestMsg,
326+
RawResponse: responseMsg,
327+
RemoteAddress: host,
328+
Timestamp: time.Now(),
329+
}
330+
if h.options.OnResult != nil {
331+
h.options.OnResult(interaction)
332+
}
333+
h.options.storeRootTLDInteraction(interaction, foundDomain)
320334
}
321335

322336
if foundDomain == "" {
323337
return
324338
}
325339

340+
storeMatch := func(uniqueID, fullID string) {
341+
h.options.storeInteraction(&Interaction{
342+
Protocol: "dns",
343+
UniqueID: uniqueID,
344+
FullId: fullID,
345+
QType: toQType(r.Question[0].Qtype),
346+
RawRequest: requestMsg,
347+
RawResponse: responseMsg,
348+
RemoteAddress: host,
349+
Timestamp: time.Now(),
350+
}, uniqueID[:h.options.CorrelationIdLength])
351+
}
352+
326353
if h.options.ScanEverywhere {
327354
chunks := stringsutil.SplitAny(requestMsg, ".\n\t\"'")
328355
for _, chunk := range chunks {
329356
for part := range stringsutil.SlideWithLength(chunk, h.options.GetIdLength()) {
330357
normalizedPart := strings.ToLower(part)
331358
if h.options.isCorrelationID(normalizedPart) {
332-
h.storeMatchedInteraction(normalizedPart, part, requestMsg, responseMsg, w, r)
359+
storeMatch(normalizedPart, part)
333360
}
334361
}
335362
}
@@ -347,63 +374,11 @@ func (h *DNSServer) handleInteraction(domain string, w dns.ResponseWriter, r *dn
347374
if i+1 <= len(parts) {
348375
fullID = strings.Join(parts[:i+1], ".")
349376
}
350-
h.storeMatchedInteraction(normalizedPartChunk, fullID, requestMsg, responseMsg, w, r)
377+
storeMatch(normalizedPartChunk, fullID)
351378
}
352379
}
353380
}
354381

355-
func (h *DNSServer) storeRootTLDInteraction(rootDomain, fqdn, requestMsg, responseMsg string, w dns.ResponseWriter, r *dns.Msg) {
356-
host, _, _ := net.SplitHostPort(w.RemoteAddr().String())
357-
interaction := &Interaction{
358-
Protocol: "dns",
359-
UniqueID: fqdn,
360-
FullId: fqdn,
361-
QType: toQType(r.Question[0].Qtype),
362-
RawRequest: requestMsg,
363-
RawResponse: responseMsg,
364-
RemoteAddress: host,
365-
Timestamp: time.Now(),
366-
}
367-
368-
if h.options.OnResult != nil {
369-
h.options.OnResult(interaction)
370-
}
371-
372-
data, err := jsoniter.Marshal(interaction)
373-
if err != nil {
374-
gologger.Warning().Msgf("Could not encode root tld dns interaction: %s\n", err)
375-
return
376-
}
377-
gologger.Debug().Msgf("Root TLD DNS Interaction: \n%s\n", string(data))
378-
if err := h.options.Storage.AddInteractionWithId(rootDomain, data); err != nil {
379-
gologger.Warning().Msgf("Could not store dns interaction: %s\n", err)
380-
}
381-
}
382-
383-
func (h *DNSServer) storeMatchedInteraction(uniqueID, fullID, requestMsg, responseMsg string, w dns.ResponseWriter, r *dns.Msg) {
384-
correlationID := uniqueID[:h.options.CorrelationIdLength]
385-
host, _, _ := net.SplitHostPort(w.RemoteAddr().String())
386-
interaction := &Interaction{
387-
Protocol: "dns",
388-
UniqueID: uniqueID,
389-
FullId: fullID,
390-
QType: toQType(r.Question[0].Qtype),
391-
RawRequest: requestMsg,
392-
RawResponse: responseMsg,
393-
RemoteAddress: host,
394-
Timestamp: time.Now(),
395-
}
396-
data, err := jsoniter.Marshal(interaction)
397-
if err != nil {
398-
gologger.Warning().Msgf("Could not encode dns interaction: %s\n", err)
399-
return
400-
}
401-
gologger.Debug().Msgf("DNS Interaction: \n%s\n", string(data))
402-
if err := h.options.Storage.AddInteraction(correlationID, data); err != nil {
403-
gologger.Warning().Msgf("Could not store dns interaction: %s\n", err)
404-
}
405-
}
406-
407382
// CustomRecordConfig represents a custom DNS record configuration
408383
type CustomRecordConfig struct {
409384
Type string `yaml:"type"`

pkg/server/smtp_server.go

Lines changed: 40 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"time"
99

1010
"git.mills.io/prologic/smtpd"
11-
jsoniter "github.com/json-iterator/go"
1211
"github.com/projectdiscovery/gologger"
1312
stringsutil "github.com/projectdiscovery/utils/strings"
1413
)
@@ -84,77 +83,58 @@ func (h *SMTPServer) ListenAndServe(tlsConfig *tls.Config, smtpAlive, smtpsAlive
8483
func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []string, data []byte) error {
8584
atomic.AddUint64(&h.options.Stats.Smtp, 1)
8685

87-
var uniqueID, fullID string
88-
8986
dataString := string(data)
9087
gologger.Debug().Msgf("New SMTP request: %s %s %s %s\n", remoteAddr, from, to, dataString)
9188

92-
// if root-tld is enabled stores any interaction towards the main domain
93-
for _, addr := range to {
94-
if h.options.RootTLD {
89+
host, _, _ := net.SplitHostPort(remoteAddr.String())
90+
91+
if h.options.RootTLD {
92+
for _, addr := range to {
9593
for _, domain := range h.options.Domains {
96-
if stringsutil.HasSuffixI(addr, domain) {
97-
ID := domain
98-
host, _, _ := net.SplitHostPort(remoteAddr.String())
99-
address := addr[strings.LastIndex(addr, "@"):]
100-
interaction := &Interaction{
101-
Protocol: "smtp",
102-
UniqueID: address,
103-
FullId: address,
104-
RawRequest: dataString,
105-
SMTPFrom: from,
106-
RemoteAddress: host,
107-
Timestamp: time.Now(),
108-
}
109-
data, err := jsoniter.Marshal(interaction)
110-
if err != nil {
111-
gologger.Warning().Msgf("Could not encode root tld SMTP interaction: %s\n", err)
112-
} else {
113-
gologger.Debug().Msgf("Root TLD SMTP Interaction: \n%s\n", string(data))
114-
if err := h.options.Storage.AddInteractionWithId(ID, data); err != nil {
115-
gologger.Warning().Msgf("Could not store root tld smtp interaction: %s\n", err)
116-
}
117-
}
94+
if !stringsutil.HasSuffixI(addr, domain) {
95+
continue
11896
}
97+
address := addr[strings.LastIndex(addr, "@"):]
98+
h.options.storeRootTLDInteraction(&Interaction{
99+
Protocol: "smtp",
100+
UniqueID: address,
101+
FullId: address,
102+
RawRequest: dataString,
103+
SMTPFrom: from,
104+
RemoteAddress: host,
105+
Timestamp: time.Now(),
106+
}, domain)
119107
}
120108
}
121109
}
122110

111+
// Each matched correlation id is stored independently; previously the loop
112+
// captured a single uniqueID/fullID and stored it once after the loop, so
113+
// later false-positive labels could overwrite the legitimate match and
114+
// the interaction would be persisted under an unregistered id (issue #1362).
123115
for _, addr := range to {
124-
if len(addr) > h.options.GetIdLength() && strings.Contains(addr, "@") {
125-
parts := strings.Split(addr[strings.LastIndex(addr, "@")+1:], ".")
126-
for i, part := range parts {
127-
if h.options.isCorrelationID(part) {
128-
uniqueID = part
129-
fullID = part
130-
if i+1 <= len(parts) {
131-
fullID = strings.Join(parts[:i+1], ".")
132-
}
133-
}
134-
}
116+
if len(addr) <= h.options.GetIdLength() || !strings.Contains(addr, "@") {
117+
continue
135118
}
136-
}
137-
if uniqueID != "" {
138-
host, _, _ := net.SplitHostPort(remoteAddr.String())
139-
140-
correlationID := uniqueID[:h.options.CorrelationIdLength]
141-
interaction := &Interaction{
142-
Protocol: "smtp",
143-
UniqueID: uniqueID,
144-
FullId: fullID,
145-
RawRequest: dataString,
146-
SMTPFrom: from,
147-
RemoteAddress: host,
148-
Timestamp: time.Now(),
149-
}
150-
data, err := jsoniter.Marshal(interaction)
151-
if err != nil {
152-
gologger.Warning().Msgf("Could not encode smtp interaction: %s\n", err)
153-
} else {
154-
gologger.Debug().Msgf("%s\n", string(data))
155-
if err := h.options.Storage.AddInteraction(correlationID, data); err != nil {
156-
gologger.Warning().Msgf("Could not store smtp interaction: %s\n", err)
119+
parts := strings.Split(addr[strings.LastIndex(addr, "@")+1:], ".")
120+
for i, part := range parts {
121+
normalizedPart := strings.ToLower(part)
122+
if !h.options.isCorrelationID(normalizedPart) {
123+
continue
124+
}
125+
fullID := part
126+
if i+1 <= len(parts) {
127+
fullID = strings.Join(parts[:i+1], ".")
157128
}
129+
h.options.storeInteraction(&Interaction{
130+
Protocol: "smtp",
131+
UniqueID: normalizedPart,
132+
FullId: fullID,
133+
RawRequest: dataString,
134+
SMTPFrom: from,
135+
RemoteAddress: host,
136+
Timestamp: time.Now(),
137+
}, normalizedPart[:h.options.CorrelationIdLength])
158138
}
159139
}
160140
return nil
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package server
2+
3+
import (
4+
"net"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// Regression suite for https://github.com/projectdiscovery/interactsh/issues/1362
12+
// covering the SMTP path. Each row exercises a different shape of correlation
13+
// id and parent domain to guard against false-positive matches in ordinary
14+
// domain labels overwriting legitimate ones, and against alphabet mismatches.
15+
func TestSMTPInteractionStored(t *testing.T) {
16+
cases := []struct {
17+
name string
18+
cidl, cidn int
19+
correlationID string
20+
nonce string
21+
parentDomain string
22+
toAddress func(preamble, parent string) string
23+
expectStored int
24+
}{
25+
{
26+
name: "short ids with parent label rejected by alphabet (example.com)",
27+
cidl: 3, cidn: 3,
28+
correlationID: "d82", nonce: "yyy",
29+
parentDomain: "example.com",
30+
expectStored: 1,
31+
},
32+
{
33+
name: "short ids with alphabet-compatible parent label (oast.online)",
34+
cidl: 3, cidn: 3,
35+
correlationID: "d82", nonce: "yyy",
36+
parentDomain: "oast.online",
37+
expectStored: 1,
38+
},
39+
{
40+
name: "default lengths",
41+
cidl: 20, cidn: 13,
42+
correlationID: "c6rj61aciaeutn2ae680", nonce: "cg5ugboyyyyyn",
43+
parentDomain: "example.com",
44+
expectStored: 1,
45+
},
46+
{
47+
name: "uppercase recipient is normalized",
48+
cidl: 3, cidn: 3,
49+
correlationID: "d82", nonce: "yyy",
50+
parentDomain: "example.com",
51+
toAddress: func(preamble, parent string) string {
52+
return "victim@" + strings.ToUpper(preamble) + "." + parent
53+
},
54+
expectStored: 1,
55+
},
56+
{
57+
name: "multi-level subdomain prefix",
58+
cidl: 3, cidn: 3,
59+
correlationID: "d82", nonce: "yyy",
60+
parentDomain: "example.com",
61+
toAddress: func(preamble, parent string) string {
62+
return "victim@extra." + preamble + "." + parent
63+
},
64+
expectStored: 1,
65+
},
66+
}
67+
68+
for _, tc := range cases {
69+
tc := tc
70+
t.Run(tc.name, func(t *testing.T) {
71+
store := newTestStorage(t)
72+
registerTestKey(t, store, tc.correlationID)
73+
74+
opts := &Options{
75+
Domains: []string{tc.parentDomain},
76+
ListenIP: "127.0.0.1",
77+
Storage: store,
78+
CorrelationIdLength: tc.cidl,
79+
CorrelationIdNonceLength: tc.cidn,
80+
Stats: &Metrics{},
81+
}
82+
srv := &SMTPServer{options: opts}
83+
84+
addr := defaultRecipient(tc.correlationID+tc.nonce, tc.parentDomain)
85+
if tc.toAddress != nil {
86+
addr = tc.toAddress(tc.correlationID+tc.nonce, tc.parentDomain)
87+
}
88+
remote := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}
89+
require.NoError(t, srv.defaultHandler(remote, "attacker@example.org", []string{addr}, []byte("DATA")))
90+
91+
interactions, _, err := store.GetInteractions(tc.correlationID, "secret")
92+
require.NoError(t, err)
93+
require.Len(t, interactions, tc.expectStored)
94+
})
95+
}
96+
}
97+
98+
// Unregistered preambles must not produce stored interactions, even though
99+
// they pass the alphabet check. This is the structural defense against the
100+
// previous "last match wins" overwrite bug.
101+
func TestSMTPInteractionNotStored_UnregisteredPreamble(t *testing.T) {
102+
store := newTestStorage(t)
103+
registerTestKey(t, store, "d82")
104+
105+
opts := &Options{
106+
Domains: []string{"example.com"},
107+
ListenIP: "127.0.0.1",
108+
Storage: store,
109+
CorrelationIdLength: 3,
110+
CorrelationIdNonceLength: 3,
111+
Stats: &Metrics{},
112+
}
113+
srv := &SMTPServer{options: opts}
114+
115+
remote := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}
116+
require.NoError(t, srv.defaultHandler(remote, "attacker@example.org", []string{"victim@abcybn.example.com"}, []byte("DATA")))
117+
118+
interactions, _, err := store.GetInteractions("d82", "secret")
119+
require.NoError(t, err)
120+
require.Empty(t, interactions, "recipients that do not target a registered preamble must not be stored under another id")
121+
}
122+
123+
func defaultRecipient(preamble, parent string) string {
124+
return "victim@" + preamble + "." + parent
125+
}

0 commit comments

Comments
 (0)