Added SMTP smuggling tests
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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' \
|
||||
|
||||
@@ -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:?}"),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user