Added SMTP smuggling tests

This commit is contained in:
mdecimus
2023-12-29 20:10:15 +01:00
parent b7f9d5ed02
commit 80007961db
12 changed files with 102 additions and 53 deletions

4
Cargo.lock generated
View File

@@ -3115,9 +3115,9 @@ dependencies = [
[[package]]
name = "mail-send"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e38e7453c5cb25cbeaa251e480c74592ec121360a6bcb822928926bf69e3d1e"
checksum = "d0ae928c5d8b258cc56ee9d4efbfc7856a40d8bbd4e6ea2afea33cf4bb1e7b96"
dependencies = [
"base64 0.21.5",
"gethostname",

View File

@@ -10,7 +10,7 @@ store = { path = "../store" }
jmap_proto = { path = "../jmap-proto" }
smtp-proto = { version = "0.1" }
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
tokio = { version = "1.23", features = ["net"] }
tokio-rustls = { version = "0.25.0"}

View File

@@ -33,7 +33,7 @@ impl managed::Manager for SmtpConnectionManager {
type Error = Error;
async fn create(&self) -> Result<SmtpClient, Error> {
let mut client = self.builder.connect().await?;
let mut client = self.builder.connect_opts(false).await?;
let capabilities = client
.capabilities(&self.builder.local_host, self.builder.is_lmtp)
.await?;

View File

@@ -13,7 +13,7 @@ store = { path = "../store" }
nlp = { path = "../nlp" }
utils = { path = "../utils" }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
rustls = "0.22"
rustls-pemfile = "2.0"
tokio = { version = "1.23", features = ["full"] }

View File

@@ -14,7 +14,7 @@ directory = { path = "../directory" }
smtp-proto = { version = "0.1" }
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
sieve-rs = { version = "0.4" }
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"

View File

@@ -13,7 +13,7 @@ directory = { path = "../directory" }
store = { path = "../store" }
utils = { path = "../utils" }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
sieve-rs = { version = "0.4" }
rustls = "0.22"
rustls-pemfile = "2.0"

View File

@@ -17,7 +17,7 @@ utils = { path = "../utils" }
nlp = { path = "../nlp" }
directory = { path = "../directory" }
mail-auth = { version = "0.3" }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
smtp-proto = { version = "0.1" }

View File

@@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"]}
tracing = "0.1"
mail-auth = { version = "0.3" }
smtp-proto = { version = "0.1" }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
tracing-opentelemetry = "0.22.0"

View File

@@ -27,7 +27,7 @@ imap_proto = { path = "../crates/imap-proto" }
smtp = { path = "../crates/smtp", features = ["test_mode", "local_delivery"] }
managesieve = { path = "../crates/managesieve", features = ["test_mode"] }
smtp-proto = { version = "0.1" }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
mail-auth = { version = "0.3", features = ["test"] }
sieve-rs = { version = "0.4" }
utils = { path = "../crates/utils", features = ["test_mode"] }

View File

@@ -46,7 +46,7 @@ sed -i '' -e "s/__STORE__/$STORE/g" \
sed -i '' -e "s|__CERT_PATH__|$BASE_DIR/etc/tls_cert.pem|g" \
-e "s|__PK_PATH__|$BASE_DIR/etc/tls_privatekey.pem|g" "$BASE_DIR/etc/common/tls.toml"
sed -i '' -e 's/method = "log"/method = "stdout"/g' \
-e 's/level = "info"/level = "info"/g' "$BASE_DIR/etc/common/tracing.toml"
-e 's/level = "info"/level = "trace"/g' "$BASE_DIR/etc/common/tracing.toml"
sed -i '' -e 's/%{HOST}%/127.0.0.1/g' "$BASE_DIR/etc/jmap/listener.toml"
sed -i '' -e 's/allow-plain-text = false/allow-plain-text = true/g' \
-e 's/2000\/1m/9999999\/100m/g' \

View File

@@ -67,6 +67,9 @@ impl QueueReceiver {
pub fn assert_empty_queue(&mut self) {
match self.queue_rx.try_recv() {
Err(TryRecvError::Empty) => (),
Ok(queue::Event::Queue(message)) => {
panic!("Unexpected message: {}", message.inner.read_message());
}
Ok(event) => panic!("Expected empty queue but got {event:?}"),
Err(err) => panic!("Queue error: {err:?}"),
}

View File

@@ -41,12 +41,33 @@ use smtp::{
queue::{manager::Queue, DeliveryAttempt, Event, WorkerResult},
};
const SMUGGLER: &str = r#"From: Joe SixPack <john@foobar.net>
To: Suzie Q <suzie@foobar.org>
Subject: Is dinner ready?
Hi.
We lost the game. Are you hungry yet?
.hey
Joe.
<SEP>.
MAIL FROM:<admin@foobar.net>
RCPT TO:<ok@foobar.org>
DATA
From: Joe SixPack <admin@foobar.net>
To: Suzie Q <suzie@foobar.org>
Subject: smuggled message
This is a smuggled message
"#;
#[tokio::test]
#[serial_test::serial]
async fn smtp_delivery() {
/*tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::DEBUG)
.with_max_level(tracing::Level::TRACE)
.finish(),
)
.unwrap();*/
@@ -55,53 +76,32 @@ async fn smtp_delivery() {
let mut core = SMTP::test();
core.session.config.rcpt.relay = IfBlock::new(true);
core.session.config.extensions.dsn = IfBlock::new(true);
core.session.config.extensions.chunking = IfBlock::new(false);
let mut remote_qr = core.init_test_queue("smtp_delivery_remote");
let _rx = start_test_server(core.into(), &[ServerProtocol::Smtp]);
// Add mock DNS entries
let mut core = SMTP::test();
core.resolvers.dns.mx_add(
"foobar.org",
vec![
MX {
exchanges: vec!["mx1.foobar.org".to_string()],
for domain in ["foobar.org", "foobar.net", "foobar.com"] {
core.resolvers.dns.mx_add(
domain,
vec![MX {
exchanges: vec![format!("mx1.{domain}"), format!("mx2.{domain}")],
preference: 10,
},
MX {
exchanges: vec!["mx2.foobar.org".to_string()],
preference: 20,
},
],
Instant::now() + Duration::from_secs(10),
);
core.resolvers.dns.mx_add(
"foobar.net",
vec![MX {
exchanges: vec!["mx1.foobar.net".to_string(), "mx2.foobar.net".to_string()],
preference: 10,
}],
Instant::now() + Duration::from_secs(10),
);
core.resolvers.dns.ipv4_add(
"mx1.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
core.resolvers.dns.ipv4_add(
"mx2.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
core.resolvers.dns.ipv4_add(
"mx1.foobar.net",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
core.resolvers.dns.ipv4_add(
"mx2.foobar.net",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
}],
Instant::now() + Duration::from_secs(10),
);
core.resolvers.dns.ipv4_add(
format!("mx1.{domain}"),
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(30),
);
core.resolvers.dns.ipv4_add(
format!("mx2.{domain}"),
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(30),
);
}
// Multiple delivery attempts
let mut local_qr = core.init_test_queue("smtp_delivery_local");
@@ -111,6 +111,7 @@ async fn smtp_delivery() {
let config = &mut core.queue.config;
config.retry = IfBlock::new(vec![Duration::from_millis(100)]);
config.notify = "[{if = 'rcpt-domain', eq = 'foobar.org', then = ['100ms', '200ms']},
{if = 'rcpt-domain', eq = 'foobar.com', then = ['500ms', '600ms']},
{else = ['100ms']}]"
.parse_if(&ConfigContext::new(&[]));
config.expire = "[{if = 'rcpt-domain', eq = 'foobar.org', then = '650ms'},
@@ -245,4 +246,49 @@ async fn smtp_delivery() {
);
remote_qr.assert_empty_queue();
local_qr.assert_empty_queue();
// SMTP smuggling
for separator in ["\n", "\r"].iter() {
session.data.remote_ip = "10.0.0.2".parse().unwrap();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
let message = SMUGGLER
.replace('\r', "")
.replace('\n', "\r\n")
.replace("<SEP>", separator);
session
.send_message("john@doe.org", &["bill@foobar.com"], &message, "250")
.await;
DeliveryAttempt::from(local_qr.read_event().await.unwrap_message())
.try_deliver(core.clone(), &mut queue)
.await;
let event = local_qr.read_event().await;
assert!(
matches!(event, Event::Done(WorkerResult::Done)),
"event: {:?}",
event
);
let message = remote_qr.read_event().await.unwrap_message().read_message();
assert!(
message.contains("This is a smuggled message"),
"message: {:?}",
message
);
assert!(
message.contains("We lost the game."),
"message: {:?}",
message
);
assert!(
message.contains(&format!("{separator}..\r\nMAIL FROM:<",)),
"message: {:?}",
message
);
}
}