1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// BREACH (Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext)
// CVE-2013-3587
//
// BREACH exploits HTTP compression to extract secrets from HTTPS responses
// by observing changes in response sizes when injecting known data.
// Similar to CRIME but targets HTTP-level compression instead of TLS compression.
use crate::Result;
use crate::utils::network::Target;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::timeout;
/// BREACH vulnerability tester
pub struct BreachTester {
target: Target,
}
impl BreachTester {
pub fn new(target: Target) -> Self {
Self { target }
}
/// Test for BREACH vulnerability
pub async fn test(&self) -> Result<BreachTestResult> {
let compression_enabled = self.test_http_compression().await?;
let dynamic_content = self.test_dynamic_content().await?;
let sensitive_data = self.test_sensitive_data_reflection().await?;
// BREACH requires all three conditions:
// 1. HTTP compression enabled
// 2. Dynamic content (user input reflected)
// 3. Sensitive data in responses
let vulnerable = compression_enabled && dynamic_content && sensitive_data;
let details = if vulnerable {
"Vulnerable to BREACH (CVE-2013-3587): HTTP compression enabled with dynamic content containing secrets".to_string()
} else if compression_enabled {
let mut reasons = Vec::new();
if !dynamic_content {
reasons.push("no dynamic content detected");
}
if !sensitive_data {
reasons.push("no sensitive data reflection detected");
}
format!(
"Partially vulnerable - HTTP compression enabled but {}",
reasons.join(" and ")
)
} else {
"Not vulnerable - HTTP compression not enabled".to_string()
};
Ok(BreachTestResult {
vulnerable,
compression_enabled,
dynamic_content,
sensitive_data_reflection: sensitive_data,
details,
})
}
/// Test if HTTP compression is enabled
async fn test_http_compression(&self) -> Result<bool> {
let addr = self.target.socket_addrs()[0];
// First establish TLS connection
match timeout(Duration::from_secs(5), TcpStream::connect(addr)).await {
Ok(Ok(stream)) => {
let std_stream = stream.into_std()?;
std_stream.set_nonblocking(false)?;
// Establish TLS
use openssl::ssl::{SslConnector, SslMethod};
let connector = SslConnector::builder(SslMethod::tls())?.build();
match connector.connect(&self.target.hostname, std_stream) {
Ok(mut ssl_stream) => {
use std::io::{Read, Write};
// Send HTTP request with Accept-Encoding header
let request = format!(
"GET / HTTP/1.1\r\n\
Host: {}\r\n\
Accept-Encoding: gzip, deflate\r\n\
User-Agent: Mozilla/5.0\r\n\
Connection: close\r\n\
\r\n",
self.target.hostname
);
ssl_stream.write_all(request.as_bytes())?;
// Read response headers
let mut buffer = vec![0u8; 8192];
let n = ssl_stream.read(&mut buffer)?;
if n > 0 {
let response = String::from_utf8_lossy(&buffer[..n]);
// Check for Content-Encoding header
let compressed = response.lines().any(|line| {
line.to_lowercase().starts_with("content-encoding:")
&& (line.contains("gzip") || line.contains("deflate"))
});
Ok(compressed)
} else {
Ok(false)
}
}
Err(_) => Ok(false),
}
}
_ => Ok(false),
}
}
/// Test if server reflects user input (dynamic content)
async fn test_dynamic_content(&self) -> Result<bool> {
let addr = self.target.socket_addrs()[0];
match timeout(Duration::from_secs(5), TcpStream::connect(addr)).await {
Ok(Ok(stream)) => {
let std_stream = stream.into_std()?;
std_stream.set_nonblocking(false)?;
use openssl::ssl::{SslConnector, SslMethod};
let connector = SslConnector::builder(SslMethod::tls())?.build();
match connector.connect(&self.target.hostname, std_stream) {
Ok(mut ssl_stream) => {
use std::io::{Read, Write};
// Send request with unique marker in query string
let marker = "BREACH_TEST_MARKER_12345";
let request = format!(
"GET /?test={} HTTP/1.1\r\n\
Host: {}\r\n\
Accept-Encoding: gzip, deflate\r\n\
Connection: close\r\n\
\r\n",
marker, self.target.hostname
);
ssl_stream.write_all(request.as_bytes())?;
// Read response
let mut buffer = vec![0u8; 16384];
let n = ssl_stream.read(&mut buffer)?;
if n > 0 {
let response = String::from_utf8_lossy(&buffer[..n]);
// Check if our marker is reflected in the response
Ok(response.contains(marker))
} else {
Ok(false)
}
}
Err(_) => Ok(false),
}
}
_ => Ok(false),
}
}
/// Test if sensitive data might be reflected in responses
async fn test_sensitive_data_reflection(&self) -> Result<bool> {
let addr = self.target.socket_addrs()[0];
match timeout(Duration::from_secs(5), TcpStream::connect(addr)).await {
Ok(Ok(stream)) => {
let std_stream = stream.into_std()?;
std_stream.set_nonblocking(false)?;
use openssl::ssl::{SslConnector, SslMethod};
let connector = SslConnector::builder(SslMethod::tls())?.build();
match connector.connect(&self.target.hostname, std_stream) {
Ok(mut ssl_stream) => {
use std::io::{Read, Write};
// Send request with Cookie header
let request = format!(
"GET / HTTP/1.1\r\n\
Host: {}\r\n\
Cookie: sessionid=test123; csrftoken=abc456\r\n\
Accept-Encoding: gzip, deflate\r\n\
Connection: close\r\n\
\r\n",
self.target.hostname
);
ssl_stream.write_all(request.as_bytes())?;
// Read response
let mut buffer = vec![0u8; 16384];
let n = ssl_stream.read(&mut buffer)?;
if n > 0 {
let response = String::from_utf8_lossy(&buffer[..n]);
// Check for common patterns that indicate sensitive data
let has_sensitive = response.contains("csrf")
|| response.contains("token")
|| response.contains("session")
|| response.contains("Set-Cookie");
Ok(has_sensitive)
} else {
Ok(false)
}
}
Err(_) => Ok(false),
}
}
_ => Ok(false),
}
}
}
/// BREACH test result
#[derive(Debug, Clone)]
pub struct BreachTestResult {
pub vulnerable: bool,
pub compression_enabled: bool,
pub dynamic_content: bool,
pub sensitive_data_reflection: bool,
pub details: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_breach_result_creation() {
let result = BreachTestResult {
vulnerable: true,
compression_enabled: true,
dynamic_content: true,
sensitive_data_reflection: true,
details: "Test".to_string(),
};
assert!(result.vulnerable);
assert!(result.compression_enabled);
assert!(result.dynamic_content);
assert!(result.sensitive_data_reflection);
}
#[test]
fn test_breach_not_vulnerable_no_compression() {
let result = BreachTestResult {
vulnerable: false,
compression_enabled: false,
dynamic_content: true,
sensitive_data_reflection: true,
details: "Not vulnerable".to_string(),
};
assert!(!result.vulnerable);
// Even with dynamic content and sensitive data, not vulnerable without compression
}
#[test]
fn test_breach_partial_vulnerability() {
let result = BreachTestResult {
vulnerable: false,
compression_enabled: true,
dynamic_content: false,
sensitive_data_reflection: true,
details: "Partial".to_string(),
};
// Needs all three conditions for full vulnerability
assert!(!result.vulnerable);
assert!(result.compression_enabled);
}
}