use crate::Result;
use crate::utils::network::Target;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{Duration, timeout};
pub struct StarttlsInjectionTester {
target: Target,
}
impl StarttlsInjectionTester {
pub fn new(target: Target) -> Self {
Self { target }
}
pub async fn test_smtp_injection(&self) -> Result<bool> {
let addr = format!("{}:{}", self.target.hostname, self.target.port);
let stream = match timeout(Duration::from_secs(5), TcpStream::connect(&addr)).await {
Ok(Ok(s)) => s,
_ => return Ok(false),
};
if self.test_command_injection_smtp(stream).await? {
return Ok(true);
}
Ok(false)
}
async fn test_command_injection_smtp(&self, mut stream: TcpStream) -> Result<bool> {
let mut buf = vec![0u8; 4096];
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if !response.starts_with("220") {
return Ok(false);
}
stream.write_all(b"EHLO test.local\r\n").await?;
let _n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let injection_payload = b"STARTTLS\r\nMAIL FROM:<injection@test.com>\r\n";
stream.write_all(injection_payload).await?;
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if response.contains("250") && response.contains("220") {
return Ok(true);
}
Ok(false)
}
pub async fn test_imap_injection(&self) -> Result<bool> {
let addr = format!("{}:{}", self.target.hostname, self.target.port);
let stream = match timeout(Duration::from_secs(5), TcpStream::connect(&addr)).await {
Ok(Ok(s)) => s,
_ => return Ok(false),
};
self.test_command_injection_imap(stream).await
}
async fn test_command_injection_imap(&self, mut stream: TcpStream) -> Result<bool> {
let mut buf = vec![0u8; 4096];
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if !response.starts_with("* OK") {
return Ok(false);
}
stream.write_all(b"a001 CAPABILITY\r\n").await?;
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if !response.contains("STARTTLS") {
return Ok(false);
}
let injection_payload = b"a002 STARTTLS\r\na003 LOGIN test test\r\n";
stream.write_all(injection_payload).await?;
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if response.contains("a003") {
return Ok(true);
}
Ok(false)
}
pub async fn test_pop3_injection(&self) -> Result<bool> {
let addr = format!("{}:{}", self.target.hostname, self.target.port);
let stream = match timeout(Duration::from_secs(5), TcpStream::connect(&addr)).await {
Ok(Ok(s)) => s,
_ => return Ok(false),
};
self.test_command_injection_pop3(stream).await
}
async fn test_command_injection_pop3(&self, mut stream: TcpStream) -> Result<bool> {
let mut buf = vec![0u8; 4096];
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if !response.starts_with("+OK") {
return Ok(false);
}
stream.write_all(b"CAPA\r\n").await?;
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if !response.contains("STLS") {
return Ok(false);
}
let injection_payload = b"STLS\r\nUSER injection\r\n";
stream.write_all(injection_payload).await?;
let n = timeout(Duration::from_secs(2), stream.read(&mut buf)).await??;
let response = String::from_utf8_lossy(&buf[..n]);
if response.matches("+OK").count() >= 2 {
return Ok(true);
}
Ok(false)
}
pub async fn test_all(&self) -> Result<StarttlsInjectionResult> {
let mut result = StarttlsInjectionResult {
vulnerable: false,
smtp_vulnerable: false,
imap_vulnerable: false,
pop3_vulnerable: false,
details: Vec::new(),
};
if self.target.port == 25 || self.target.port == 587 {
match self.test_smtp_injection().await {
Ok(vuln) => {
result.smtp_vulnerable = vuln;
if vuln {
result.vulnerable = true;
result.details.push(
"SMTP STARTTLS injection detected - commands can be injected before TLS upgrade".to_string()
);
}
}
Err(e) => {
result.details.push(format!("SMTP test error: {}", e));
}
}
}
if self.target.port == 143 {
match self.test_imap_injection().await {
Ok(vuln) => {
result.imap_vulnerable = vuln;
if vuln {
result.vulnerable = true;
result.details.push(
"IMAP STARTTLS injection detected - commands can be injected before TLS upgrade".to_string()
);
}
}
Err(e) => {
result.details.push(format!("IMAP test error: {}", e));
}
}
}
if self.target.port == 110 {
match self.test_pop3_injection().await {
Ok(vuln) => {
result.pop3_vulnerable = vuln;
if vuln {
result.vulnerable = true;
result.details.push(
"POP3 STARTTLS injection detected - commands can be injected before TLS upgrade".to_string()
);
}
}
Err(e) => {
result.details.push(format!("POP3 test error: {}", e));
}
}
}
if result.details.is_empty() {
result.details.push(format!(
"Port {} is not a standard STARTTLS port (25, 143, 110, 587)",
self.target.port
));
}
Ok(result)
}
}
#[derive(Debug, Clone)]
pub struct StarttlsInjectionResult {
pub vulnerable: bool,
pub smtp_vulnerable: bool,
pub imap_vulnerable: bool,
pub pop3_vulnerable: bool,
pub details: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_starttls_injection_struct() {
let result = StarttlsInjectionResult {
vulnerable: true,
smtp_vulnerable: true,
imap_vulnerable: false,
pop3_vulnerable: false,
details: vec!["Test".to_string()],
};
assert!(result.vulnerable);
assert!(result.smtp_vulnerable);
}
}