From 80007961db961a794a6f8034ca3ebadbeeb1aa9e Mon Sep 17 00:00:00 2001 From: mdecimus Date: Fri, 29 Dec 2023 20:10:15 +0100 Subject: [PATCH] Added SMTP smuggling tests --- Cargo.lock | 4 +- crates/directory/Cargo.toml | 2 +- crates/directory/src/backend/smtp/pool.rs | 2 +- crates/imap/Cargo.toml | 2 +- crates/jmap/Cargo.toml | 2 +- crates/managesieve/Cargo.toml | 2 +- crates/smtp/Cargo.toml | 2 +- crates/utils/Cargo.toml | 2 +- tests/Cargo.toml | 2 +- tests/resources/scripts/create_test_env.sh | 2 +- tests/src/smtp/inbound/mod.rs | 3 + tests/src/smtp/outbound/smtp.rs | 130 ++++++++++++++------- 12 files changed, 102 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c760e273..7c8f533b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index 974559a8..2e56eab8 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -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"} diff --git a/crates/directory/src/backend/smtp/pool.rs b/crates/directory/src/backend/smtp/pool.rs index 34ef0ddb..0151d352 100644 --- a/crates/directory/src/backend/smtp/pool.rs +++ b/crates/directory/src/backend/smtp/pool.rs @@ -33,7 +33,7 @@ impl managed::Manager for SmtpConnectionManager { type Error = Error; async fn create(&self) -> Result { - 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?; diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 1788ef66..413a9a6b 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -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"] } diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 04e1edd3..1ea025a6 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -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" diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index a2c14123..74cb0465 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -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" diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 27e8fdf2..be0331c4 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -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" } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 84de5b7a..881ff990 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -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" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 40a7a97e..8a090366 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -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"] } diff --git a/tests/resources/scripts/create_test_env.sh b/tests/resources/scripts/create_test_env.sh index c8b2302b..8f61a22b 100644 --- a/tests/resources/scripts/create_test_env.sh +++ b/tests/resources/scripts/create_test_env.sh @@ -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' \ diff --git a/tests/src/smtp/inbound/mod.rs b/tests/src/smtp/inbound/mod.rs index 326dfd78..91e549d5 100644 --- a/tests/src/smtp/inbound/mod.rs +++ b/tests/src/smtp/inbound/mod.rs @@ -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:?}"), } diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs index 35e12809..6ad958f6 100644 --- a/tests/src/smtp/outbound/smtp.rs +++ b/tests/src/smtp/outbound/smtp.rs @@ -41,12 +41,33 @@ use smtp::{ queue::{manager::Queue, DeliveryAttempt, Event, WorkerResult}, }; +const SMUGGLER: &str = r#"From: Joe SixPack +To: Suzie Q +Subject: Is dinner ready? + +Hi. + +We lost the game. Are you hungry yet? +.hey +Joe. + +. +MAIL FROM: +RCPT TO: +DATA +From: Joe SixPack +To: Suzie Q +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("", 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 + ); + } }