Skip to content

Commit 8f0f333

Browse files
committed
fix smtp VRFY by migrating to emersion/go-smtp
1 parent 416e55c commit 8f0f333

5 files changed

Lines changed: 279 additions & 54 deletions

File tree

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ module github.com/projectdiscovery/interactsh
33
go 1.24.13
44

55
require (
6-
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a
76
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d
87
github.com/caddyserver/certmagic v0.25.0
98
github.com/docker/go-units v0.5.0
9+
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
10+
github.com/emersion/go-smtp v0.24.0
1011
github.com/goburrow/cache v0.1.4
1112
github.com/google/uuid v1.6.0
1213
github.com/json-iterator/go v1.1.12

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
1919
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
2020
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
2121
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
22-
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a h1:3i+FJ7IpSZHL+VAjtpQeZCRhrpP0odl5XfoLBY4fxJ8=
23-
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a/go.mod h1:C7hXLmFmPYPjIDGfQl1clsmQ5TMEQfmzWTrJk475bUs=
2422
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
2523
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
2624
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -126,6 +124,10 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
126124
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
127125
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
128126
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
127+
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
128+
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
129+
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
130+
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
129131
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
130132
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
131133
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=

pkg/server/smtp_backend.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package server
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"net"
8+
"sync/atomic"
9+
"time"
10+
11+
"github.com/emersion/go-sasl"
12+
"github.com/emersion/go-smtp"
13+
"github.com/projectdiscovery/gologger"
14+
)
15+
16+
// interactshBackend is the emersion/go-smtp Backend for collaborator SMTP.
17+
type interactshBackend struct {
18+
srv *SMTPServer
19+
}
20+
21+
func (b *interactshBackend) NewSession(c *smtp.Conn) (smtp.Session, error) {
22+
return &interactshSession{
23+
srv: b.srv,
24+
remote: c.Conn().RemoteAddr(),
25+
}, nil
26+
}
27+
28+
// interactshSession accepts all mail and auth, then forwards message bodies
29+
// to the existing interaction storage path.
30+
type interactshSession struct {
31+
srv *SMTPServer
32+
remote net.Addr
33+
from string
34+
to []string
35+
}
36+
37+
func (s *interactshSession) AuthMechanisms() []string {
38+
return []string{sasl.Plain, sasl.Login}
39+
}
40+
41+
func (s *interactshSession) Auth(mech string) (sasl.Server, error) {
42+
switch mech {
43+
case sasl.Plain:
44+
return sasl.NewPlainServer(func(identity, username, password string) error {
45+
return nil
46+
}), nil
47+
case sasl.Login:
48+
return &acceptLoginServer{}, nil
49+
default:
50+
return nil, smtp.ErrAuthUnknownMechanism
51+
}
52+
}
53+
54+
func (s *interactshSession) Mail(from string, _ *smtp.MailOptions) error {
55+
s.from = from
56+
return nil
57+
}
58+
59+
func (s *interactshSession) Rcpt(to string, _ *smtp.RcptOptions) error {
60+
s.to = append(s.to, to)
61+
return nil
62+
}
63+
64+
func (s *interactshSession) Data(r io.Reader) error {
65+
body, err := io.ReadAll(r)
66+
if err != nil {
67+
return err
68+
}
69+
return s.srv.deliverSMTP(s.remote, s.from, s.to, body)
70+
}
71+
72+
func (s *interactshSession) Reset() {
73+
s.from = ""
74+
s.to = nil
75+
}
76+
77+
func (s *interactshSession) Logout() error {
78+
return nil
79+
}
80+
81+
// acceptLoginServer implements LOGIN auth and always succeeds, matching the
82+
// previous interactsh behavior of accepting any credentials.
83+
type acceptLoginServer struct {
84+
step int
85+
}
86+
87+
func (a *acceptLoginServer) Next(response []byte) (challenge []byte, done bool, err error) {
88+
a.step++
89+
switch a.step {
90+
case 1:
91+
return []byte("Password:"), false, nil
92+
case 2:
93+
return nil, true, nil
94+
default:
95+
return nil, true, nil
96+
}
97+
}
98+
99+
func (h *SMTPServer) deliverSMTP(remoteAddr net.Addr, from string, to []string, body []byte) error {
100+
atomic.AddUint64(&h.options.Stats.Smtp, 1)
101+
102+
buf := bytes.NewBuffer(makeSMTPHeaders(remoteAddr, h.options.Domains[0], to))
103+
buf.Write(body)
104+
dataString := buf.String()
105+
106+
gologger.Debug().Msgf("New SMTP request: %s %s %v %s\n", remoteAddr, from, to, dataString)
107+
h.storeSMTPRecipients(remoteAddr, from, dataString, to)
108+
return nil
109+
}
110+
111+
// makeSMTPHeaders mirrors the Received header the previous smtpd integration
112+
// attached before the message body.
113+
func makeSMTPHeaders(remoteAddr net.Addr, hostname string, to []string) []byte {
114+
if len(to) == 0 {
115+
return nil
116+
}
117+
host, port, _ := net.SplitHostPort(remoteAddr.String())
118+
if host == "" {
119+
host = remoteAddr.String()
120+
}
121+
now := time.Now().Format("Mon, _2 Jan 2006 15:04:05 -0700 (MST)")
122+
var buffer bytes.Buffer
123+
buffer.WriteString(fmt.Sprintf("Received: from %s ([%s])\r\n", host, host))
124+
buffer.WriteString(fmt.Sprintf(" by %s (interactsh) with SMTP\r\n", hostname))
125+
buffer.WriteString(fmt.Sprintf(" for <%s>; %s\r\n", to[0], now))
126+
_ = port
127+
return buffer.Bytes()
128+
}

pkg/server/smtp_server.go

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,55 @@ import (
44
"crypto/tls"
55
"net"
66
"strings"
7-
"sync/atomic"
87
"time"
98

10-
"git.mills.io/prologic/smtpd"
9+
"github.com/emersion/go-smtp"
1110
"github.com/projectdiscovery/gologger"
1211
stringsutil "github.com/projectdiscovery/utils/strings"
1312
)
1413

1514
// SMTPServer is a smtp server instance that listens both
1615
// TLS and Non-TLS based servers.
1716
type SMTPServer struct {
18-
options *Options
19-
smtpServer smtpd.Server
20-
smtpsServer smtpd.Server
17+
options *Options
18+
backend *interactshBackend
19+
smtpServer *smtp.Server
20+
smtpsServer *smtp.Server
21+
smtpAutoTLSServer *smtp.Server
2122
}
2223

2324
// NewSMTPServer returns a new TLS & Non-TLS SMTP server.
2425
func NewSMTPServer(options *Options) (*SMTPServer, error) {
2526
server := &SMTPServer{options: options}
27+
server.backend = &interactshBackend{srv: server}
2628

27-
authHandler := func(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
28-
return true, nil
29-
}
30-
rcptHandler := func(remoteAddr net.Addr, from string, to string) bool {
31-
return true
32-
}
33-
server.smtpServer = smtpd.Server{
34-
Addr: formatAddress(options.ListenIP, options.SmtpPort),
35-
AuthHandler: authHandler,
36-
HandlerRcpt: rcptHandler,
37-
Hostname: options.Domains[0],
38-
Appname: "interactsh",
39-
Handler: smtpd.Handler(server.defaultHandler),
40-
}
41-
server.smtpsServer = smtpd.Server{
42-
Addr: formatAddress(options.ListenIP, options.SmtpsPort),
43-
AuthHandler: authHandler,
44-
HandlerRcpt: rcptHandler,
45-
Hostname: options.Domains[0],
46-
Appname: "interactsh",
47-
Handler: smtpd.Handler(server.defaultHandler),
29+
newEmersionServer := func(addr string, tlsConfig *tls.Config) *smtp.Server {
30+
s := smtp.NewServer(server.backend)
31+
s.Addr = addr
32+
s.Domain = options.Domains[0]
33+
s.AllowInsecureAuth = true
34+
s.TLSConfig = tlsConfig
35+
return s
4836
}
37+
38+
server.smtpServer = newEmersionServer(formatAddress(options.ListenIP, options.SmtpPort), nil)
39+
server.smtpsServer = newEmersionServer(formatAddress(options.ListenIP, options.SmtpsPort), nil)
40+
server.smtpAutoTLSServer = newEmersionServer(formatAddress(options.ListenIP, options.SmtpAutoTLSPort), nil)
4941
return server, nil
5042
}
5143

5244
// ListenAndServe listens on smtp and/or smtps ports for the server.
5345
func (h *SMTPServer) ListenAndServe(tlsConfig *tls.Config, smtpAlive, smtpsAlive chan bool) {
54-
go func() {
55-
if tlsConfig == nil {
56-
return
57-
}
58-
srv := &smtpd.Server{Addr: formatAddress(h.options.ListenIP, h.options.SmtpAutoTLSPort), Handler: h.defaultHandler, Appname: "interactsh", Hostname: h.options.Domains[0]}
59-
srv.TLSConfig = tlsConfig
60-
61-
smtpsAlive <- true
62-
err := srv.ListenAndServe()
63-
if err != nil {
64-
gologger.Error().Msgf("Could not serve smtp with tls on port %d: %s\n", h.options.SmtpAutoTLSPort, err)
65-
smtpsAlive <- false
66-
}
67-
}()
46+
if tlsConfig != nil {
47+
h.smtpAutoTLSServer.TLSConfig = tlsConfig
48+
go func() {
49+
smtpsAlive <- true
50+
if err := h.smtpAutoTLSServer.ListenAndServeTLS(); err != nil {
51+
gologger.Error().Msgf("Could not serve smtp with tls on port %d: %s\n", h.options.SmtpAutoTLSPort, err)
52+
smtpsAlive <- false
53+
}
54+
}()
55+
}
6856

6957
smtpAlive <- true
7058
go func() {
@@ -75,21 +63,23 @@ func (h *SMTPServer) ListenAndServe(tlsConfig *tls.Config, smtpAlive, smtpsAlive
7563
}()
7664
if err := h.smtpsServer.ListenAndServe(); err != nil {
7765
gologger.Error().Msgf("Could not serve smtp on port %d: %s\n", h.options.SmtpsPort, err)
78-
smtpAlive <- false
66+
smtpsAlive <- false
7967
}
8068
}
8169

82-
// defaultHandler is a handler for default collaborator requests
70+
// defaultHandler is kept for unit tests that exercise storage without
71+
// standing up a TCP listener.
8372
func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []string, data []byte) error {
84-
atomic.AddUint64(&h.options.Stats.Smtp, 1)
85-
86-
dataString := string(data)
87-
gologger.Debug().Msgf("New SMTP request: %s %s %s %s\n", remoteAddr, from, to, dataString)
73+
return h.deliverSMTP(remoteAddr, from, to, data)
74+
}
8875

76+
// storeSMTPRecipients persists SMTP interactions for correlation ids found
77+
// in the recipient addresses.
78+
func (h *SMTPServer) storeSMTPRecipients(remoteAddr net.Addr, from, rawRequest string, recipients []string) {
8979
host, _, _ := net.SplitHostPort(remoteAddr.String())
9080

9181
if h.options.RootTLD {
92-
for _, addr := range to {
82+
for _, addr := range recipients {
9383
for _, domain := range h.options.Domains {
9484
if !stringsutil.HasSuffixI(addr, domain) {
9585
continue
@@ -99,7 +89,7 @@ func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []strin
9989
Protocol: "smtp",
10090
UniqueID: address,
10191
FullId: address,
102-
RawRequest: dataString,
92+
RawRequest: rawRequest,
10393
SMTPFrom: from,
10494
RemoteAddress: host,
10595
Timestamp: time.Now(),
@@ -112,7 +102,7 @@ func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []strin
112102
// captured a single uniqueID/fullID and stored it once after the loop, so
113103
// later false-positive labels could overwrite the legitimate match and
114104
// the interaction would be persisted under an unregistered id (issue #1362).
115-
for _, addr := range to {
105+
for _, addr := range recipients {
116106
if len(addr) <= h.options.GetIdLength() || !strings.Contains(addr, "@") {
117107
continue
118108
}
@@ -130,12 +120,11 @@ func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []strin
130120
Protocol: "smtp",
131121
UniqueID: normalizedPart,
132122
FullId: fullID,
133-
RawRequest: dataString,
123+
RawRequest: rawRequest,
134124
SMTPFrom: from,
135125
RemoteAddress: host,
136126
Timestamp: time.Now(),
137127
}, normalizedPart[:h.options.CorrelationIdLength])
138128
}
139129
}
140-
return nil
141130
}

0 commit comments

Comments
 (0)