Skip to content

Commit

Permalink
защита от SMTP Smuggling и Pipelining
Browse files Browse the repository at this point in the history
  • Loading branch information
vodolaz095 committed Jan 24, 2024
1 parent cd8c476 commit 8f955e7
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 0 deletions.
1 change: 1 addition & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const lineLength = 76

// Karma related
const tlsHandshakeFailedHate = 1
const wrongCommandOrderPenalty = 1
const missingParameterPenalty = 1
const unknownCommandPenalty = 2
const tooManyRecipientsPenalty = 5
Expand Down
136 changes: 136 additions & 0 deletions smtp_smuggling_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package msmtpd

import (
"bytes"
"fmt"
"net/mail"
"net/smtp"
"strings"
"sync/atomic"
"testing"
"time"
)

// makeSmugglingMessage makes test email message with attempt to smuggle
func makeSmugglingMessage(separator, from string, to ...string) string {
now := time.Now()
buh := bytes.NewBufferString("Date: " + now.Format(time.RFC1123Z) + "\r\n")
buh.WriteString("From: " + from + "\r\n")
buh.WriteString("To: " + strings.Join(to, ",") + "\r\n")
buh.WriteString(fmt.Sprintf("Subject: Test email send on %s\r\n", now.Format(time.RFC1123Z)))
buh.WriteString(fmt.Sprintf("Message-Id: <%s@localhost>\r\n", now.Format("20060102150405")))
buh.WriteString("\r\n")
buh.WriteString(fmt.Sprintf("This is test message send from %s to %s on %s\r\n",
from, strings.Join(to, ","), now.Format(time.Stamp),
))
buh.WriteString(separator)
buh.WriteString("HELO lol\r\n")
buh.WriteString("MAIL FROM:<[email protected]>\r\n")
buh.WriteString("RCPT TO:<[email protected]>\r\n")
return buh.String()
}

func TestSMTPSmugglingNotWorks(t *testing.T) {
type testCase struct {
separator string
}

// list of separators
// https://github.com/The-Login/SMTP-Smuggling-Tools/blob/235cbf27ec66437f767013ae9f37c56a30648932/smtp_smuggling_scanner.py#L13
testCases := []testCase{
{"\r\n.\r\n"}, // correct one
{"\n.\n"},
{"\n.\r"},
{"\r.\n"},
{"\r.\r"},
{"\n.\r\n"},
{"\r.\r\n"},
{"\r\n\x00.\r\n"},
{"\r\n.\r\r\n"},
{"\r\r\n.\r\r\n"},
{"\r\n\x00.\r\n"},
}

for i := range testCases {
t.Run(fmt.Sprintf("Case %v with separator %q", i, testCases[i].separator), func(tt *testing.T) {
var numberOfMessagesAccepted uint32
addr, closer := RunTestServerWithoutTLS(tt, &Server{
HeloCheckers: []HelloChecker{
func(tr *Transaction) error {
if tr.HeloName == "lol" {
tt.Errorf("smuggling encountered, helo accepted from message body")
}
return nil
},
},
SenderCheckers: []SenderChecker{
func(tr *Transaction) error {
if tr.MailFrom.Address == "[email protected]" {
tt.Errorf("smuggling encountered, MAIL FROM accepted from message body")
}
return nil
},
},
RecipientCheckers: []RecipientChecker{
func(tr *Transaction, recipient *mail.Address) error {
if recipient.Address == "[email protected]" {
tt.Errorf("smuggling encountered, RCPT TO accepted from message body")
}
return nil
},
},
DataHandlers: []DataHandler{
func(tr *Transaction) error {
atomic.AddUint32(&numberOfMessagesAccepted, 1)
tt.Log("Message content: ", string(tr.Body))
return nil
},
},
})
defer closer()
c, err := smtp.Dial(addr)
if err != nil {
tt.Errorf("Dial failed: %v", err)
return
}
err = c.Hello("localhost")
if err != nil {
tt.Errorf("helo failed: %v", err)
return
}
if err = c.Mail("[email protected]"); err != nil {
tt.Errorf("MAIL failed: %v", err)
return
}
if err = c.Rcpt("[email protected]"); err != nil {
tt.Errorf("RCPT failed: %v", err)
return
}
wc, err := c.Data()
if err != nil {
tt.Errorf("error calling data: %v", err)
return
}
n, err := fmt.Fprint(c.Text.W, makeSmugglingMessage(
testCases[i].separator,
"[email protected]",
"[email protected]",
))
if err != nil {
tt.Errorf("error writing data: %v", err)
return
}
tt.Logf("%v bytes written", n)
err = wc.Close()
if err != nil {
tt.Errorf("error closing channel: %v", err)
return
}
if numberOfMessagesAccepted != 1 {
tt.Errorf("smuggling encountered after connection close")
}
tt.Logf("Case %v finished for separator %q: %v messages accepted", i, testCases[i].separator,
numberOfMessagesAccepted)
})
}
}
12 changes: 12 additions & 0 deletions transaction_helo.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ func (t *Transaction) handleHELO(cmd command) {
t.Hate(missingParameterPenalty)
return
}
if t.dataHandlersCalledProperly {
t.LogWarn("HELO called after DATA accepted")
t.reply(502, "wrong order of commands")
t.Hate(wrongCommandOrderPenalty)
return
}
if t.HeloName != "" {
// Reset envelope in case of duplicate HELO
t.reset()
Expand Down Expand Up @@ -61,6 +67,12 @@ func (t *Transaction) handleEHLO(cmd command) {
t.Hate(missingParameterPenalty)
return
}
if t.dataHandlersCalledProperly {
t.LogWarn("EHLO called after DATA accepted")
t.reply(502, "wrong order of commands")
t.Hate(wrongCommandOrderPenalty)
return
}
if t.HeloName != "" {
// Reset envelope in case of duplicate HELO
t.reset()
Expand Down
6 changes: 6 additions & 0 deletions transaction_mailfrom.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ func (t *Transaction) handleMAIL(cmd command) {
t.reply(502, "Invalid syntax.")
return
}
if t.dataHandlersCalledProperly {
t.LogWarn("MAIL FROM called after DATA accepted")
t.Hate(wrongCommandOrderPenalty)
t.reply(502, "wrong order of commands")
return
}
if t.HeloName == "" {
t.Hate(missingParameterPenalty)
t.LogDebug("MAIL FROM called without HELO/EHLO")
Expand Down
6 changes: 6 additions & 0 deletions transaction_rcpt_to.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ func (t *Transaction) handleRCPT(cmd command) {
t.reply(502, "Invalid syntax.")
return
}
if t.dataHandlersCalledProperly {
t.LogWarn("RCPT TO called after DATA accepted")
t.Hate(wrongCommandOrderPenalty)
t.reply(502, "wrong order of commands")
return
}
if t.HeloName == "" {
t.LogDebug("RCPT TO called without HELO/EHLO")
t.Hate(missingParameterPenalty)
Expand Down

0 comments on commit 8f955e7

Please sign in to comment.