cipherrun 0.3.0

A fast, modular, and scalable TLS/SSL security scanner written in Rust
// Ticketbleed Vulnerability Test
// CVE-2016-9244
//
// Ticketbleed is a vulnerability in F5 BIG-IP that leaks 31 bytes of uninitialized memory
// when processing TLS session tickets. This can expose sensitive information including
// session keys, passwords, and other confidential data.

use crate::Result;
use crate::protocols::Protocol;
use crate::protocols::handshake::ClientHelloBuilder;
use crate::utils::network::Target;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;

/// Ticketbleed vulnerability tester
pub struct TicketbleedTester {
    target: Target,
}

impl TicketbleedTester {
    pub fn new(target: Target) -> Self {
        Self { target }
    }

    /// Test for Ticketbleed vulnerability
    pub async fn test(&self) -> Result<TicketbleedTestResult> {
        let vulnerable = self.test_session_ticket_leak().await?;

        let details = if vulnerable {
            "Vulnerable to Ticketbleed (CVE-2016-9244) - Server leaks memory in session ticket responses".to_string()
        } else {
            "Not vulnerable - No memory leak detected in session ticket handling".to_string()
        };

        Ok(TicketbleedTestResult {
            vulnerable,
            details,
        })
    }

    /// Test for session ticket memory leak
    async fn test_session_ticket_leak(&self) -> Result<bool> {
        let addr = self.target.socket_addrs()[0];

        match timeout(Duration::from_secs(5), TcpStream::connect(addr)).await {
            Ok(Ok(mut stream)) => {
                // Send ClientHello with SessionTicket extension
                let client_hello = self.build_client_hello_with_session_ticket();
                stream.write_all(&client_hello).await?;

                // Read ServerHello and NewSessionTicket
                let mut buffer = vec![0u8; 16384];
                match timeout(Duration::from_secs(3), stream.read(&mut buffer)).await {
                    Ok(Ok(n)) if n > 0 => {
                        // Parse response for NewSessionTicket message
                        let has_new_ticket = self.parse_new_session_ticket(&buffer[..n])?;

                        if has_new_ticket {
                            // Send second ClientHello with the received ticket
                            let client_hello2 =
                                self.build_client_hello_with_received_ticket(&buffer[..n]);
                            stream.write_all(&client_hello2).await?;

                            // Read response
                            let mut response = vec![0u8; 16384];
                            match timeout(Duration::from_secs(3), stream.read(&mut response)).await
                            {
                                Ok(Ok(m)) if m > 0 => {
                                    // Check for anomalous ticket lengths (31 extra bytes)
                                    let leaked = self.detect_memory_leak(&response[..m])?;
                                    Ok(leaked)
                                }
                                _ => Ok(false),
                            }
                        } else {
                            Ok(false)
                        }
                    }
                    _ => Ok(false),
                }
            }
            _ => Ok(false),
        }
    }

    /// Build ClientHello with SessionTicket extension using ClientHelloBuilder
    fn build_client_hello_with_session_ticket(&self) -> Vec<u8> {
        let mut builder = ClientHelloBuilder::new(Protocol::TLS12);
        builder.for_vulnerability_testing().add_session_ticket(); // Add empty session ticket for ticketbleed testing
        builder.build().unwrap_or_else(|_| Vec::new())
    }

    /// Build ClientHello with received session ticket
    fn build_client_hello_with_received_ticket(&self, _server_response: &[u8]) -> Vec<u8> {
        // Extract ticket from NewSessionTicket message
        // For simplicity, we'll send a ClientHello with an empty ticket
        // Real implementation would parse and extract the actual ticket
        self.build_client_hello_with_session_ticket()
    }

    /// Parse NewSessionTicket message from server response
    fn parse_new_session_ticket(&self, response: &[u8]) -> Result<bool> {
        // Look for Handshake type 0x04 (NewSessionTicket)
        for i in 0..response.len().saturating_sub(10) {
            if response[i] == 0x16 && // Handshake record
               i + 5 < response.len() &&
               response[i + 5] == 0x04
            {
                // NewSessionTicket
                return Ok(true);
            }
        }
        Ok(false)
    }

    /// Detect memory leak in session ticket response
    fn detect_memory_leak(&self, response: &[u8]) -> Result<bool> {
        // Look for NewSessionTicket with anomalous length
        // Ticketbleed leaks 31 extra bytes
        for i in 0..response.len().saturating_sub(10) {
            if response[i] == 0x16 && // Handshake record
               i + 5 < response.len() &&
               response[i + 5] == 0x04
            {
                // NewSessionTicket

                // Check ticket length
                if i + 10 < response.len() {
                    let ticket_len = u16::from_be_bytes([response[i + 8], response[i + 9]]);

                    // F5 BIG-IP vulnerable versions leak 31 bytes
                    // Look for patterns indicating uninitialized memory
                    if ticket_len > 0 && i + 10 + ticket_len as usize <= response.len() {
                        let ticket_data = &response[i + 10..i + 10 + ticket_len as usize];

                        // Check for suspicious patterns (null bytes, repetitive patterns)
                        let null_count = ticket_data.iter().filter(|&&b| b == 0).count();
                        let is_suspicious = null_count > ticket_data.len() / 4;

                        return Ok(is_suspicious);
                    }
                }
            }
        }
        Ok(false)
    }
}

/// Ticketbleed test result
#[derive(Debug, Clone)]
pub struct TicketbleedTestResult {
    pub vulnerable: bool,
    pub details: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ticketbleed_result() {
        let result = TicketbleedTestResult {
            vulnerable: false,
            details: "Test".to_string(),
        };
        assert!(!result.vulnerable);
    }

    #[test]
    fn test_client_hello_with_session_ticket() {
        let target = Target::with_ips(
            "example.com".to_string(),
            443,
            vec!["93.184.216.34".parse().unwrap()],
        )
        .unwrap();

        let tester = TicketbleedTester::new(target);
        let hello = tester.build_client_hello_with_session_ticket();

        assert!(hello.len() > 50);
        assert_eq!(hello[0], 0x16); // Handshake
        assert_eq!(hello[5], 0x01); // ClientHello

        // Check for SessionTicket extension (0x0023)
        let has_ticket_ext = hello.windows(2).any(|w| w == [0x00, 0x23]);
        assert!(has_ticket_ext);
    }
}