use crate::Result;
use crate::constants::{CONTENT_TYPE_ALERT, CONTENT_TYPE_APPLICATION_DATA, VERSION_TLS_1_2};
use crate::protocols::Protocol;
use crate::protocols::handshake::ClientHelloBuilder;
use crate::utils::network::Target;
use crate::utils::{VulnSslConfig, test_vuln_ssl_connection};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{Instant, timeout};
pub struct PoodleTester<'a> {
target: &'a Target,
}
impl<'a> PoodleTester<'a> {
pub fn new(target: &'a Target) -> Self {
Self { target }
}
pub async fn test(&self) -> Result<PoodleTestResult> {
let ssl3_supported = self.test_ssl3().await?;
let tls_poodle = if !ssl3_supported {
self.test_tls_poodle().await?
} else {
false
};
let vulnerable = ssl3_supported || tls_poodle;
let details = if ssl3_supported {
"Vulnerable: SSL 3.0 is supported (CVE-2014-3566)".to_string()
} else if tls_poodle {
"Vulnerable: TLS implementation vulnerable to POODLE (CVE-2014-8730)".to_string()
} else {
"Not vulnerable: SSL 3.0 disabled and TLS not vulnerable".to_string()
};
Ok(PoodleTestResult {
vulnerable,
ssl3_supported,
tls_poodle,
details,
variants: Vec::new(),
})
}
pub async fn test_all_variants(&self) -> Result<PoodleTestResult> {
let mut variants = Vec::new();
let ssl3_supported = self.test_ssl3().await?;
variants.push(PoodleVariantResult {
variant: PoodleVariant::SslV3,
vulnerable: ssl3_supported,
details: if ssl3_supported {
"SSL 3.0 is supported - vulnerable to classic POODLE attack".to_string()
} else {
"SSL 3.0 is not supported".to_string()
},
timing_data: None,
});
let tls_poodle = self.test_tls_poodle().await?;
variants.push(PoodleVariantResult {
variant: PoodleVariant::Tls,
vulnerable: tls_poodle,
details: if tls_poodle {
"TLS implementation vulnerable to POODLE-style attack".to_string()
} else {
"TLS implementation not vulnerable to POODLE".to_string()
},
timing_data: None,
});
let zombie_result = self.test_zombie_poodle().await?;
variants.push(zombie_result);
let golden_result = self.test_goldendoodle().await?;
variants.push(golden_result);
let sleeping_result = self.test_sleeping_poodle().await?;
variants.push(sleeping_result);
let zero_length_result = self.test_openssl_0length().await?;
variants.push(zero_length_result);
let vulnerable = variants.iter().any(|v| v.vulnerable);
let details = if vulnerable {
let vuln_variants: Vec<_> = variants
.iter()
.filter(|v| v.vulnerable)
.map(|v| v.variant.name())
.collect();
format!("Vulnerable to: {}", vuln_variants.join(", "))
} else {
"Not vulnerable to any POODLE variants".to_string()
};
Ok(PoodleTestResult {
vulnerable,
ssl3_supported,
tls_poodle,
details,
variants,
})
}
async fn test_ssl3(&self) -> Result<bool> {
test_vuln_ssl_connection(self.target, VulnSslConfig::ssl3_only())
.await
.map_err(crate::TlsError::from)
}
async fn test_tls_poodle(&self) -> Result<bool> {
let config = VulnSslConfig::tls10_with_ciphers("AES128-SHA:AES256-SHA:DES-CBC3-SHA");
let connected = test_vuln_ssl_connection(self.target, config)
.await
.map_err(crate::TlsError::from)?;
let _ = connected; Ok(false)
}
async fn test_zombie_poodle(&self) -> Result<PoodleVariantResult> {
if !self.supports_cbc_ciphers().await? {
return Ok(PoodleVariantResult {
variant: PoodleVariant::ZombiePoodle,
vulnerable: false,
details: "CBC ciphers not supported - not vulnerable".to_string(),
timing_data: None,
});
}
const ITERATIONS: usize = 5;
let mut valid_mac_responses = Vec::new();
let mut invalid_mac_responses = Vec::new();
for _ in 0..ITERATIONS {
if let Ok(response) = self
.send_malformed_record(MalformedRecordType::InvalidPaddingValidMac)
.await
{
valid_mac_responses.push(response);
}
if let Ok(response) = self
.send_malformed_record(MalformedRecordType::InvalidPaddingInvalidMac)
.await
{
invalid_mac_responses.push(response);
}
}
let oracle_detected =
self.detect_response_oracle(&valid_mac_responses, &invalid_mac_responses);
Ok(PoodleVariantResult {
variant: PoodleVariant::ZombiePoodle,
vulnerable: oracle_detected,
details: if oracle_detected {
format!(
"Vulnerable to Zombie POODLE - Observable MAC validity oracle detected ({} iterations)",
ITERATIONS
)
} else {
"Not vulnerable to Zombie POODLE - No observable MAC oracle".to_string()
},
timing_data: None,
})
}
async fn test_goldendoodle(&self) -> Result<PoodleVariantResult> {
if !self.supports_cbc_ciphers().await? {
return Ok(PoodleVariantResult {
variant: PoodleVariant::GoldenDoodle,
vulnerable: false,
details: "CBC ciphers not supported - not vulnerable".to_string(),
timing_data: None,
});
}
const ITERATIONS: usize = 5;
let mut valid_pad_responses = Vec::new();
let mut invalid_pad_responses = Vec::new();
for _ in 0..ITERATIONS {
if let Ok(response) = self
.send_malformed_record(MalformedRecordType::ValidPaddingInvalidMac)
.await
{
valid_pad_responses.push(response);
}
if let Ok(response) = self
.send_malformed_record(MalformedRecordType::InvalidPaddingInvalidMac)
.await
{
invalid_pad_responses.push(response);
}
}
let oracle_detected =
self.detect_response_oracle(&valid_pad_responses, &invalid_pad_responses);
Ok(PoodleVariantResult {
variant: PoodleVariant::GoldenDoodle,
vulnerable: oracle_detected,
details: if oracle_detected {
format!(
"Vulnerable to GOLDENDOODLE - Padding oracle detected via error differentiation ({} iterations)",
ITERATIONS
)
} else {
"Not vulnerable to GOLDENDOODLE - No padding oracle detected".to_string()
},
timing_data: None,
})
}
async fn test_sleeping_poodle(&self) -> Result<PoodleVariantResult> {
if !self.supports_cbc_ciphers().await? {
return Ok(PoodleVariantResult {
variant: PoodleVariant::SleepingPoodle,
vulnerable: false,
details: "CBC ciphers not supported - not vulnerable".to_string(),
timing_data: None,
});
}
const SAMPLES: usize = 10;
const TIMING_THRESHOLD_MS: f64 = 5.0; let mut valid_timings = Vec::new();
let mut invalid_timings = Vec::new();
for _ in 0..SAMPLES {
if let Ok(timing) = self
.measure_response_time(MalformedRecordType::ValidPaddingInvalidMac)
.await
{
valid_timings.push(timing);
}
if let Ok(timing) = self
.measure_response_time(MalformedRecordType::InvalidPaddingInvalidMac)
.await
{
invalid_timings.push(timing);
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
if valid_timings.is_empty() || invalid_timings.is_empty() {
return Ok(PoodleVariantResult {
variant: PoodleVariant::SleepingPoodle,
vulnerable: false,
details: "Could not collect timing samples".to_string(),
timing_data: None,
});
}
let valid_avg = valid_timings.iter().sum::<f64>() / valid_timings.len() as f64;
let invalid_avg = invalid_timings.iter().sum::<f64>() / invalid_timings.len() as f64;
let timing_diff = (valid_avg - invalid_avg).abs();
let oracle_detected = timing_diff > TIMING_THRESHOLD_MS;
let timing_data = Some(TimingData {
valid_padding_avg_ms: valid_avg,
invalid_padding_avg_ms: invalid_avg,
timing_difference_ms: timing_diff,
samples_collected: valid_timings.len().min(invalid_timings.len()),
});
Ok(PoodleVariantResult {
variant: PoodleVariant::SleepingPoodle,
vulnerable: oracle_detected,
details: if oracle_detected {
format!(
"Vulnerable to Sleeping POODLE - Timing oracle detected: valid={:.2}ms, invalid={:.2}ms, diff={:.2}ms",
valid_avg, invalid_avg, timing_diff
)
} else {
format!(
"Not vulnerable to Sleeping POODLE - No significant timing difference: valid={:.2}ms, invalid={:.2}ms, diff={:.2}ms",
valid_avg, invalid_avg, timing_diff
)
},
timing_data,
})
}
async fn test_openssl_0length(&self) -> Result<PoodleVariantResult> {
if !self.supports_cbc_ciphers().await? {
return Ok(PoodleVariantResult {
variant: PoodleVariant::OpenSsl0Length,
vulnerable: false,
details: "CBC ciphers not supported - not vulnerable".to_string(),
timing_data: None,
});
}
const ITERATIONS: usize = 3;
let mut vulnerable_count = 0;
for _ in 0..ITERATIONS {
match self
.send_malformed_record(MalformedRecordType::ZeroLengthFragment)
.await
{
Ok(response) => {
if response.connection_accepted || response.shows_differential_behavior {
vulnerable_count += 1;
}
}
Err(_) => {
}
}
}
let vulnerable = vulnerable_count >= 2;
Ok(PoodleVariantResult {
variant: PoodleVariant::OpenSsl0Length,
vulnerable,
details: if vulnerable {
format!(
"Vulnerable to OpenSSL 0-Length Fragment (CVE-2011-4576) - Server accepts zero-length records ({}/{} iterations)",
vulnerable_count, ITERATIONS
)
} else {
"Not vulnerable to OpenSSL 0-Length Fragment - Server properly rejects zero-length records".to_string()
},
timing_data: None,
})
}
async fn supports_cbc_ciphers(&self) -> Result<bool> {
const CBC_CIPHERS: &str = "AES128-SHA:AES256-SHA:AES128-SHA256:AES256-SHA256:DES-CBC3-SHA";
test_vuln_ssl_connection(self.target, VulnSslConfig::with_ciphers(CBC_CIPHERS))
.await
.map_err(crate::TlsError::from)
}
async fn send_malformed_record(
&self,
record_type: MalformedRecordType,
) -> Result<ServerResponse> {
let addr = self.target.socket_addrs()[0];
let start_time = Instant::now();
match timeout(Duration::from_secs(5), TcpStream::connect(addr)).await {
Ok(Ok(mut stream)) => {
let client_hello = self.build_client_hello_cbc();
stream.write_all(&client_hello).await?;
let mut buffer = vec![0u8; 8192];
let bytes_read =
timeout(Duration::from_secs(3), stream.read(&mut buffer)).await??;
if bytes_read == 0 {
return Ok(ServerResponse {
connection_accepted: false,
alert_type: None,
response_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
shows_differential_behavior: false,
});
}
let malformed = self.build_malformed_record(record_type);
stream.write_all(&malformed).await?;
let mut response = vec![0u8; 1024];
let alert_type =
match timeout(Duration::from_secs(2), stream.read(&mut response)).await {
Ok(Ok(n)) if n > 0 => {
if response[0] == CONTENT_TYPE_ALERT && n >= 7 {
Some(response[6]) } else {
None
}
}
_ => None,
};
Ok(ServerResponse {
connection_accepted: true,
alert_type,
response_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
shows_differential_behavior: alert_type.is_some(),
})
}
_ => Ok(ServerResponse {
connection_accepted: false,
alert_type: None,
response_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
shows_differential_behavior: false,
}),
}
}
async fn measure_response_time(&self, record_type: MalformedRecordType) -> Result<f64> {
let response = self.send_malformed_record(record_type).await?;
Ok(response.response_time_ms)
}
fn detect_response_oracle(
&self,
responses_a: &[ServerResponse],
responses_b: &[ServerResponse],
) -> bool {
if responses_a.is_empty() || responses_b.is_empty() {
return false;
}
let alert_types_a: Vec<_> = responses_a.iter().filter_map(|r| r.alert_type).collect();
let alert_types_b: Vec<_> = responses_b.iter().filter_map(|r| r.alert_type).collect();
if !alert_types_a.is_empty() && !alert_types_b.is_empty() {
let avg_a = alert_types_a.iter().map(|&x| x as u32).sum::<u32>() as f64
/ alert_types_a.len() as f64;
let avg_b = alert_types_b.iter().map(|&x| x as u32).sum::<u32>() as f64
/ alert_types_b.len() as f64;
if (avg_a - avg_b).abs() > 0.5 {
return true;
}
}
let avg_time_a =
responses_a.iter().map(|r| r.response_time_ms).sum::<f64>() / responses_a.len() as f64;
let avg_time_b =
responses_b.iter().map(|r| r.response_time_ms).sum::<f64>() / responses_b.len() as f64;
(avg_time_a - avg_time_b).abs() > 10.0 }
fn build_client_hello_cbc(&self) -> Vec<u8> {
let mut builder = ClientHelloBuilder::new(Protocol::TLS12);
builder.for_cbc_ciphers();
builder.build_minimal().unwrap_or_else(|_| Vec::new())
}
fn build_malformed_record(&self, record_type: MalformedRecordType) -> Vec<u8> {
match record_type {
MalformedRecordType::InvalidPaddingValidMac => {
self.build_record_invalid_padding_valid_mac()
}
MalformedRecordType::ValidPaddingInvalidMac => {
self.build_record_valid_padding_invalid_mac()
}
MalformedRecordType::InvalidPaddingInvalidMac => {
self.build_record_invalid_padding_invalid_mac()
}
MalformedRecordType::ZeroLengthFragment => self.build_zero_length_record(),
}
}
fn build_record_invalid_padding_valid_mac(&self) -> Vec<u8> {
let mut record = vec![
CONTENT_TYPE_APPLICATION_DATA, (VERSION_TLS_1_2 >> 8) as u8, (VERSION_TLS_1_2 & 0xff) as u8, 0x00,
0x30, ];
record.extend_from_slice(&[0x41; 32]);
record.extend_from_slice(&[0x00; 16]);
for i in 0..7 {
record.push((i * 3) as u8); }
record
}
fn build_record_valid_padding_invalid_mac(&self) -> Vec<u8> {
let mut record = vec![
CONTENT_TYPE_APPLICATION_DATA, (VERSION_TLS_1_2 >> 8) as u8, (VERSION_TLS_1_2 & 0xff) as u8, 0x00,
0x30, ];
record.extend_from_slice(&[0x41; 32]);
record.extend_from_slice(&[0xff; 16]);
record.extend(std::iter::repeat_n(0x06, 7));
record
}
fn build_record_invalid_padding_invalid_mac(&self) -> Vec<u8> {
let mut record = vec![
CONTENT_TYPE_APPLICATION_DATA, (VERSION_TLS_1_2 >> 8) as u8, (VERSION_TLS_1_2 & 0xff) as u8, 0x00,
0x30, ];
record.extend_from_slice(&[0x41; 32]);
record.extend_from_slice(&[0xff; 16]);
for i in 0..7 {
record.push((i * 5) as u8);
}
record
}
fn build_zero_length_record(&self) -> Vec<u8> {
vec![
CONTENT_TYPE_APPLICATION_DATA, (VERSION_TLS_1_2 >> 8) as u8, (VERSION_TLS_1_2 & 0xff) as u8, 0x00,
0x00, ]
}
}
#[derive(Debug, Clone, Copy)]
enum MalformedRecordType {
InvalidPaddingValidMac,
ValidPaddingInvalidMac,
InvalidPaddingInvalidMac,
ZeroLengthFragment,
}
#[derive(Debug, Clone)]
struct ServerResponse {
connection_accepted: bool,
alert_type: Option<u8>,
response_time_ms: f64,
shows_differential_behavior: bool,
}
#[derive(Debug, Clone)]
pub struct PoodleTestResult {
pub vulnerable: bool,
pub ssl3_supported: bool,
pub tls_poodle: bool,
pub details: String,
pub variants: Vec<PoodleVariantResult>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoodleVariant {
SslV3,
Tls,
ZombiePoodle,
GoldenDoodle,
SleepingPoodle,
OpenSsl0Length,
}
impl PoodleVariant {
pub fn name(&self) -> &'static str {
match self {
Self::SslV3 => "POODLE (SSLv3)",
Self::Tls => "POODLE (TLS)",
Self::ZombiePoodle => "Zombie POODLE",
Self::GoldenDoodle => "GOLDENDOODLE",
Self::SleepingPoodle => "Sleeping POODLE",
Self::OpenSsl0Length => "OpenSSL 0-Length Fragment",
}
}
pub fn cve(&self) -> &'static str {
match self {
Self::SslV3 => "CVE-2014-3566",
Self::Tls => "CVE-2014-8730",
Self::ZombiePoodle | Self::GoldenDoodle | Self::SleepingPoodle => "CVE-2019-5592",
Self::OpenSsl0Length => "CVE-2011-4576",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::SslV3 => "SSL 3.0 CBC padding oracle - allows plaintext recovery",
Self::Tls => "TLS CBC padding oracle - similar to SSLv3 POODLE",
Self::ZombiePoodle => "Observable MAC validity oracle despite invalid padding",
Self::GoldenDoodle => "Padding oracle through error response differentiation",
Self::SleepingPoodle => "Timing-based padding oracle vulnerability",
Self::OpenSsl0Length => "Zero-length TLS fragment padding vulnerability",
}
}
}
#[derive(Debug, Clone)]
pub struct PoodleVariantResult {
pub variant: PoodleVariant,
pub vulnerable: bool,
pub details: String,
pub timing_data: Option<TimingData>,
}
#[derive(Debug, Clone)]
pub struct TimingData {
pub valid_padding_avg_ms: f64,
pub invalid_padding_avg_ms: f64,
pub timing_difference_ms: f64,
pub samples_collected: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_poodle_result() {
let result = PoodleTestResult {
vulnerable: true,
ssl3_supported: true,
tls_poodle: false,
details: "Test".to_string(),
variants: Vec::new(),
};
assert!(result.vulnerable);
assert!(result.ssl3_supported);
}
#[test]
fn test_poodle_variant_names() {
assert_eq!(PoodleVariant::SslV3.name(), "POODLE (SSLv3)");
assert_eq!(PoodleVariant::Tls.name(), "POODLE (TLS)");
assert_eq!(PoodleVariant::ZombiePoodle.name(), "Zombie POODLE");
assert_eq!(PoodleVariant::GoldenDoodle.name(), "GOLDENDOODLE");
assert_eq!(PoodleVariant::SleepingPoodle.name(), "Sleeping POODLE");
assert_eq!(
PoodleVariant::OpenSsl0Length.name(),
"OpenSSL 0-Length Fragment"
);
}
#[test]
fn test_poodle_variant_cves() {
assert_eq!(PoodleVariant::SslV3.cve(), "CVE-2014-3566");
assert_eq!(PoodleVariant::Tls.cve(), "CVE-2014-8730");
assert_eq!(PoodleVariant::ZombiePoodle.cve(), "CVE-2019-5592");
assert_eq!(PoodleVariant::GoldenDoodle.cve(), "CVE-2019-5592");
assert_eq!(PoodleVariant::SleepingPoodle.cve(), "CVE-2019-5592");
assert_eq!(PoodleVariant::OpenSsl0Length.cve(), "CVE-2011-4576");
}
#[test]
fn test_variant_result_structure() {
let timing_data = TimingData {
valid_padding_avg_ms: 15.5,
invalid_padding_avg_ms: 10.2,
timing_difference_ms: 5.3,
samples_collected: 10,
};
let result = PoodleVariantResult {
variant: PoodleVariant::SleepingPoodle,
vulnerable: true,
details: "Timing oracle detected".to_string(),
timing_data: Some(timing_data),
};
assert!(result.vulnerable);
assert_eq!(result.variant, PoodleVariant::SleepingPoodle);
assert!(result.timing_data.is_some());
let timing = result.timing_data.expect("test assertion should succeed");
assert_eq!(timing.samples_collected, 10);
assert_eq!(timing.timing_difference_ms, 5.3);
}
#[test]
fn test_malformed_record_building() {
let target = crate::utils::network::Target::with_ips(
"example.com".to_string(),
443,
vec!["127.0.0.1".parse().unwrap()],
)
.unwrap();
let tester = PoodleTester::new(&target);
let record = tester.build_record_invalid_padding_valid_mac();
assert_eq!(record[0], CONTENT_TYPE_APPLICATION_DATA); assert_eq!(record[1], (VERSION_TLS_1_2 >> 8) as u8); assert_eq!(record[2], (VERSION_TLS_1_2 & 0xff) as u8);
assert!(record.len() > 48);
let padding = &record[record.len() - 7..];
let first = padding[0];
assert!(
padding.iter().any(|&b| b != first),
"Padding should be inconsistent"
);
let record = tester.build_record_valid_padding_invalid_mac();
assert_eq!(record[0], CONTENT_TYPE_APPLICATION_DATA);
let padding = &record[record.len() - 7..];
assert!(padding.iter().all(|&b| b == 0x06), "Padding should be 0x06");
let record = tester.build_zero_length_record();
assert_eq!(record.len(), 5); assert_eq!(record[3], 0x00); assert_eq!(record[4], 0x00); }
#[test]
fn test_client_hello_cbc_structure() {
let target = crate::utils::network::Target::with_ips(
"example.com".to_string(),
443,
vec!["127.0.0.1".parse().unwrap()],
)
.unwrap();
let tester = PoodleTester::new(&target);
let hello = tester.build_client_hello_cbc();
assert_eq!(hello[0], 0x16); assert_eq!(hello[1], 0x03); assert_eq!(hello[2], 0x01);
assert_eq!(hello[5], 0x01);
assert_eq!(hello[9], 0x03);
assert_eq!(hello[10], 0x03);
assert!(hello.len() > 50, "ClientHello should contain cipher suites");
}
#[tokio::test]
#[ignore] async fn test_poodle_ssl3_modern_server() {
let target = crate::utils::network::Target::parse("www.google.com:443")
.await
.expect("test assertion should succeed");
let tester = PoodleTester::new(&target);
let result = tester.test().await.expect("test assertion should succeed");
assert!(!result.ssl3_supported);
assert!(!result.vulnerable);
}
#[tokio::test]
#[ignore] async fn test_all_variants_modern_server() {
let target = crate::utils::network::Target::parse("www.google.com:443")
.await
.expect("test assertion should succeed");
let tester = PoodleTester::new(&target);
let result = tester
.test_all_variants()
.await
.expect("test assertion should succeed");
assert!(!result.vulnerable);
assert_eq!(result.variants.len(), 6);
for variant_result in &result.variants {
println!(
"{}: {}",
variant_result.variant.name(),
variant_result.details
);
}
}
}