Skip to content
Open
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
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ module github.com/projectdiscovery/interactsh
go 1.24.13

require (
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d
github.com/caddyserver/certmagic v0.25.0
github.com/docker/go-units v0.5.0
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.24.0
github.com/goburrow/cache v0.1.4
github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a h1:3i+FJ7IpSZHL+VAjtpQeZCRhrpP0odl5XfoLBY4fxJ8=
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a/go.mod h1:C7hXLmFmPYPjIDGfQl1clsmQ5TMEQfmzWTrJk475bUs=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down Expand Up @@ -126,6 +124,10 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
Expand Down
127 changes: 127 additions & 0 deletions pkg/server/smtp_backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package server

import (
"bytes"
"fmt"
"io"
"net"
"sync/atomic"
"time"

"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/projectdiscovery/gologger"
)

// interactshBackend is the emersion/go-smtp Backend for collaborator SMTP.
type interactshBackend struct {
srv *SMTPServer
}

func (b *interactshBackend) NewSession(c *smtp.Conn) (smtp.Session, error) {
return &interactshSession{
srv: b.srv,
remote: c.Conn().RemoteAddr(),
}, nil
}

// interactshSession accepts all mail and auth, then forwards message bodies
// to the existing interaction storage path.
type interactshSession struct {
srv *SMTPServer
remote net.Addr
from string
to []string
}

func (s *interactshSession) AuthMechanisms() []string {
return []string{sasl.Plain, sasl.Login}
}

func (s *interactshSession) Auth(mech string) (sasl.Server, error) {
switch mech {
case sasl.Plain:
return sasl.NewPlainServer(func(identity, username, password string) error {
return nil
}), nil
case sasl.Login:
return &acceptLoginServer{}, nil
default:
return nil, smtp.ErrAuthUnknownMechanism
}
}

func (s *interactshSession) Mail(from string, _ *smtp.MailOptions) error {
s.from = from
return nil
}

func (s *interactshSession) Rcpt(to string, _ *smtp.RcptOptions) error {
s.to = append(s.to, to)
return nil
}

func (s *interactshSession) Data(r io.Reader) error {
body, err := io.ReadAll(r)
if err != nil {
return err
}
return s.srv.deliverSMTP(s.remote, s.from, s.to, body)
}

func (s *interactshSession) Reset() {
s.from = ""
s.to = nil
}

func (s *interactshSession) Logout() error {
return nil
}

// acceptLoginServer implements LOGIN auth and always succeeds, matching the
// previous interactsh behavior of accepting any credentials.
type acceptLoginServer struct {
step int
}

func (a *acceptLoginServer) Next(response []byte) (challenge []byte, done bool, err error) {
a.step++
switch a.step {
case 1:
return []byte("Password:"), false, nil
case 2:
return nil, true, nil
default:
return nil, true, nil
}
}

func (h *SMTPServer) deliverSMTP(remoteAddr net.Addr, from string, to []string, body []byte) error {
atomic.AddUint64(&h.options.Stats.Smtp, 1)

buf := bytes.NewBuffer(makeSMTPHeaders(remoteAddr, h.options.Domains[0], to))
buf.Write(body)
dataString := buf.String()

gologger.Debug().Msgf("New SMTP request: %s %s %v %s\n", remoteAddr, from, to, dataString)
h.storeSMTPRecipients(remoteAddr, from, dataString, to)
return nil
}

// makeSMTPHeaders mirrors the Received header the previous smtpd integration
// attached before the message body.
func makeSMTPHeaders(remoteAddr net.Addr, hostname string, to []string) []byte {
if len(to) == 0 {
return nil
}
host, _, _ := net.SplitHostPort(remoteAddr.String())
if host == "" {
host = remoteAddr.String()
}
now := time.Now().Format("Mon, _2 Jan 2006 15:04:05 -0700 (MST)")
var buffer bytes.Buffer
fmt.Fprintf(&buffer, "Received: from %s ([%s])\r\n", host, host)
fmt.Fprintf(&buffer, " by %s (interactsh) with SMTP\r\n", hostname)
fmt.Fprintf(&buffer, " for <%s>; %s\r\n", to[0], now)
return buffer.Bytes()
}
91 changes: 40 additions & 51 deletions pkg/server/smtp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,55 @@ import (
"crypto/tls"
"net"
"strings"
"sync/atomic"
"time"

"git.mills.io/prologic/smtpd"
"github.com/emersion/go-smtp"
"github.com/projectdiscovery/gologger"
stringsutil "github.com/projectdiscovery/utils/strings"
)

// SMTPServer is a smtp server instance that listens both
// TLS and Non-TLS based servers.
type SMTPServer struct {
options *Options
smtpServer smtpd.Server
smtpsServer smtpd.Server
options *Options
backend *interactshBackend
smtpServer *smtp.Server
smtpsServer *smtp.Server
smtpAutoTLSServer *smtp.Server
}

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

authHandler := func(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
return true, nil
}
rcptHandler := func(remoteAddr net.Addr, from string, to string) bool {
return true
}
server.smtpServer = smtpd.Server{
Addr: formatAddress(options.ListenIP, options.SmtpPort),
AuthHandler: authHandler,
HandlerRcpt: rcptHandler,
Hostname: options.Domains[0],
Appname: "interactsh",
Handler: smtpd.Handler(server.defaultHandler),
}
server.smtpsServer = smtpd.Server{
Addr: formatAddress(options.ListenIP, options.SmtpsPort),
AuthHandler: authHandler,
HandlerRcpt: rcptHandler,
Hostname: options.Domains[0],
Appname: "interactsh",
Handler: smtpd.Handler(server.defaultHandler),
newEmersionServer := func(addr string, tlsConfig *tls.Config) *smtp.Server {
s := smtp.NewServer(server.backend)
s.Addr = addr
s.Domain = options.Domains[0]
s.AllowInsecureAuth = true
s.TLSConfig = tlsConfig
return s
}

server.smtpServer = newEmersionServer(formatAddress(options.ListenIP, options.SmtpPort), nil)
server.smtpsServer = newEmersionServer(formatAddress(options.ListenIP, options.SmtpsPort), nil)
server.smtpAutoTLSServer = newEmersionServer(formatAddress(options.ListenIP, options.SmtpAutoTLSPort), nil)
return server, nil
}

// ListenAndServe listens on smtp and/or smtps ports for the server.
func (h *SMTPServer) ListenAndServe(tlsConfig *tls.Config, smtpAlive, smtpsAlive chan bool) {
go func() {
if tlsConfig == nil {
return
}
srv := &smtpd.Server{Addr: formatAddress(h.options.ListenIP, h.options.SmtpAutoTLSPort), Handler: h.defaultHandler, Appname: "interactsh", Hostname: h.options.Domains[0]}
srv.TLSConfig = tlsConfig

smtpsAlive <- true
err := srv.ListenAndServe()
if err != nil {
gologger.Error().Msgf("Could not serve smtp with tls on port %d: %s\n", h.options.SmtpAutoTLSPort, err)
smtpsAlive <- false
}
}()
if tlsConfig != nil {
h.smtpAutoTLSServer.TLSConfig = tlsConfig
go func() {
smtpsAlive <- true
if err := h.smtpAutoTLSServer.ListenAndServeTLS(); err != nil {
gologger.Error().Msgf("Could not serve smtp with tls on port %d: %s\n", h.options.SmtpAutoTLSPort, err)
smtpsAlive <- false
}
}()
}

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

// defaultHandler is a handler for default collaborator requests
// defaultHandler is kept for unit tests that exercise storage without
// standing up a TCP listener.
func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []string, data []byte) error {
atomic.AddUint64(&h.options.Stats.Smtp, 1)

dataString := string(data)
gologger.Debug().Msgf("New SMTP request: %s %s %s %s\n", remoteAddr, from, to, dataString)
return h.deliverSMTP(remoteAddr, from, to, data)
}

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

if h.options.RootTLD {
for _, addr := range to {
for _, addr := range recipients {
for _, domain := range h.options.Domains {
if !stringsutil.HasSuffixI(addr, domain) {
continue
Expand All @@ -99,7 +89,7 @@ func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []strin
Protocol: "smtp",
UniqueID: address,
FullId: address,
RawRequest: dataString,
RawRequest: rawRequest,
SMTPFrom: from,
RemoteAddress: host,
Timestamp: time.Now(),
Expand All @@ -112,7 +102,7 @@ func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []strin
// captured a single uniqueID/fullID and stored it once after the loop, so
// later false-positive labels could overwrite the legitimate match and
// the interaction would be persisted under an unregistered id (issue #1362).
for _, addr := range to {
for _, addr := range recipients {
if len(addr) <= h.options.GetIdLength() || !strings.Contains(addr, "@") {
continue
}
Expand All @@ -130,12 +120,11 @@ func (h *SMTPServer) defaultHandler(remoteAddr net.Addr, from string, to []strin
Protocol: "smtp",
UniqueID: normalizedPart,
FullId: fullID,
RawRequest: dataString,
RawRequest: rawRequest,
SMTPFrom: from,
RemoteAddress: host,
Timestamp: time.Now(),
}, normalizedPart[:h.options.CorrelationIdLength])
}
}
return nil
}
Loading
Loading