Skip to content

Commit

Permalink
Added SMTP smuggling tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mdecimus committed Dec 29, 2023
1 parent b7f9d5e commit 8000796
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 53 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/directory/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
2 changes: 1 addition & 1 deletion crates/directory/src/backend/smtp/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down
2 changes: 1 addition & 1 deletion crates/imap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/jmap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/managesieve/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/smtp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
2 changes: 1 addition & 1 deletion crates/utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion tests/resources/scripts/create_test_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand Down
3 changes: 3 additions & 0 deletions tests/src/smtp/inbound/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}"),
}
Expand Down
130 changes: 88 additions & 42 deletions tests/src/smtp/outbound/smtp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,33 @@ use smtp::{
queue::{manager::Queue, DeliveryAttempt, Event, WorkerResult},
};

const SMUGGLER: &str = r#"From: Joe SixPack <[email protected]>
To: Suzie Q <[email protected]>
Subject: Is dinner ready?
Hi.
We lost the game. Are you hungry yet?
.hey
Joe.
<SEP>.
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
From: Joe SixPack <[email protected]>
To: Suzie Q <[email protected]>
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();*/
Expand All @@ -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");
Expand All @@ -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'},
Expand Down Expand Up @@ -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("[email protected]", &["[email protected]"], &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
);
}
}

0 comments on commit 8000796

Please sign in to comment.