Skip to main content

web_analyzer/
cloudflare_bypass.rs

1use regex::Regex;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5use std::time::Duration;
6
7/// Known Cloudflare IPv4 CIDR ranges (simplified to prefix checks)
8const CF_PREFIXES: &[&str] = &[
9    "173.245.", "103.21.", "103.22.", "103.31.", "141.101.", "108.162.", "190.93.", "188.114.",
10    "197.234.", "198.41.", "162.158.", "162.159.", "104.16.", "104.17.", "104.18.", "104.19.",
11    "104.20.", "104.21.", "104.22.", "104.23.", "104.24.", "104.25.", "104.26.", "104.27.",
12    "172.64.", "172.65.", "172.66.", "172.67.", "131.0.",
13];
14
15/// Headers that may leak origin IPs
16const HEADERS_TO_CHECK: &[&str] = &[
17    "x-forwarded-for",
18    "x-real-ip",
19    "x-origin-ip",
20    "cf-connecting-ip",
21    "x-server-ip",
22    "server-ip",
23    "x-backend-server",
24    "x-origin-server",
25];
26
27/// IP history lookup sources
28const IP_HISTORY_SOURCES: &[(&str, &str)] = &[
29    ("ViewDNS", "https://viewdns.info/iphistory/?domain={}"),
30    (
31        "SecurityTrails",
32        "https://securitytrails.com/domain/{}/history/a",
33    ),
34    ("WhoIs", "https://who.is/whois/{}"),
35];
36
37/// Private IP prefixes (RFC 1918 + loopback + link-local)
38const PRIVATE_PREFIXES: &[&str] = &[
39    "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.", "172.23.",
40    "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.",
41    "192.168.", "127.", "0.", "169.254.",
42];
43
44// ── Structs ─────────────────────────────────────────────────────────────────
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct FoundIp {
48    pub ip: String,
49    pub source: String,
50    pub confidence: String,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub description: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub status: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CloudflareBypassResult {
59    pub domain: String,
60    pub cloudflare_protected: bool,
61    pub found_ips: Vec<FoundIp>,
62    pub scan_time_ms: u128,
63}
64
65// ── IP classification helpers ───────────────────────────────────────────────
66
67fn is_cloudflare_ip(ip: &str) -> bool {
68    CF_PREFIXES.iter().any(|prefix| ip.starts_with(prefix))
69}
70
71fn is_private_ip(ip: &str) -> bool {
72    PRIVATE_PREFIXES.iter().any(|prefix| ip.starts_with(prefix))
73}
74
75fn is_valid_ip(ip: &str) -> bool {
76    let parts: Vec<&str> = ip.split('.').collect();
77    if parts.len() != 4 {
78        return false;
79    }
80    parts.iter().all(|p| p.parse::<u8>().is_ok())
81}
82
83fn confidence_score(c: &str) -> u8 {
84    match c {
85        "Very High" => 4,
86        "High" => 3,
87        "Medium" => 2,
88        "Low" => 1,
89        _ => 0,
90    }
91}
92
93// ── Main scanner ────────────────────────────────────────────────────────────
94
95pub async fn find_real_ip(
96    domain: &str,
97    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
98) -> Result<CloudflareBypassResult, Box<dyn std::error::Error + Send + Sync>> {
99    let start = std::time::Instant::now();
100
101    let clean_domain = domain
102        .trim_start_matches("https://")
103        .trim_start_matches("http://");
104
105    let client = crate::http_client_builder()
106        .timeout(Duration::from_secs(8))
107        .danger_accept_invalid_certs(true)
108        .redirect(reqwest::redirect::Policy::limited(3))
109        .build()?;
110
111    let ip_regex = Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap();
112    let mut found_ips: Vec<FoundIp> = Vec::new();
113
114    if let Some(t) = &progress_tx {
115        let _ = t
116            .send(crate::ScanProgress {
117                module: "Cloudflare Bypass".into(),
118                percentage: 5.0,
119                message: "Started real IP discovery...".into(),
120                status: "Info".into(),
121            })
122            .await;
123    }
124
125    // ── 1. Direct DNS resolution ────────────────────────────────────────
126    if let Some(t) = &progress_tx {
127        let _ = t
128            .send(crate::ScanProgress {
129                module: "Cloudflare Bypass".into(),
130                percentage: 10.0,
131                message: "Performing direct DNS resolution...".into(),
132                status: "Info".into(),
133            })
134            .await;
135    }
136    let dns_ip = tokio::net::lookup_host(format!("{}:80", clean_domain))
137        .await
138        .ok()
139        .and_then(|mut addrs| addrs.next())
140        .map(|a| a.ip().to_string());
141
142    let cloudflare_protected = if let Some(ref ip) = dns_ip {
143        is_cloudflare_ip(ip)
144    } else {
145        false
146    };
147
148    if let Some(ref ip) = dns_ip {
149        if !is_cloudflare_ip(ip) && !is_private_ip(ip) {
150            found_ips.push(FoundIp {
151                ip: ip.clone(),
152                source: "direct_dns".into(),
153                confidence: "Very High".into(),
154                description: None,
155                status: None,
156            });
157        }
158    }
159
160    // Only run bypass techniques if CF-protected
161    if cloudflare_protected {
162        // ── 2. Check common + domain-specific subdomains ────────────────
163        if let Some(t) = &progress_tx {
164            let _ = t
165                .send(crate::ScanProgress {
166                    module: "Cloudflare Bypass".into(),
167                    percentage: 30.0,
168                    message: "Checking infrastructure subdomains...".into(),
169                    status: "Info".into(),
170                })
171                .await;
172        }
173        let mut subdomains: Vec<String> =
174            vec!["direct", "origin", "api", "mail", "cpanel", "server", "ftp"]
175                .into_iter()
176                .map(|s| s.to_string())
177                .collect();
178
179        // Domain-specific subdomains
180        let name_part = clean_domain.split('.').next().unwrap_or("");
181        if !name_part.is_empty() {
182            subdomains.push(format!("origin-{}", name_part));
183            subdomains.push(format!("{}-origin", name_part));
184            subdomains.push(format!("direct-{}", name_part));
185            subdomains.push(format!("{}-direct", name_part));
186        }
187
188        for sub in &subdomains {
189            let full: String = format!("{}.{}:80", sub, clean_domain);
190            if let Ok(addrs) = tokio::net::lookup_host(full.as_str().to_owned()).await {
191                let resolved: Vec<_> = addrs.collect();
192                if let Some(addr) = resolved.first() {
193                    let ip = addr.ip().to_string();
194                    if !is_cloudflare_ip(&ip) && !is_private_ip(&ip) && is_valid_ip(&ip) {
195                        found_ips.push(FoundIp {
196                            ip,
197                            source: format!("subdomain_{}", sub),
198                            confidence: "Medium".into(),
199                            description: None,
200                            status: None,
201                        });
202                    }
203                }
204            }
205        }
206
207        // ── 3. Check response headers for IP leaks ──────────────────────
208        if let Some(t) = &progress_tx {
209            let _ = t
210                .send(crate::ScanProgress {
211                    module: "Cloudflare Bypass".into(),
212                    percentage: 60.0,
213                    message: "Analyzing response origin headers...".into(),
214                    status: "Info".into(),
215                })
216                .await;
217        }
218        if let Ok(resp) = client.get(format!("https://{}", clean_domain)).send().await {
219            for header in HEADERS_TO_CHECK {
220                if let Some(val) = resp.headers().get(*header) {
221                    if let Ok(val_str) = val.to_str() {
222                        for cap in ip_regex.find_iter(val_str) {
223                            let ip = cap.as_str().to_string();
224                            if is_valid_ip(&ip) && !is_cloudflare_ip(&ip) && !is_private_ip(&ip) {
225                                found_ips.push(FoundIp {
226                                    ip,
227                                    source: format!("header_{}", header),
228                                    confidence: "High".into(),
229                                    description: None,
230                                    status: None,
231                                });
232                            }
233                        }
234                    }
235                }
236            }
237        }
238
239        // ── 4. IP History lookup ────────────────────────────────────────
240        if let Some(t) = &progress_tx {
241            let _ = t
242                .send(crate::ScanProgress {
243                    module: "Cloudflare Bypass".into(),
244                    percentage: 75.0,
245                    message: "Querying historical DNS databases...".into(),
246                    status: "Info".into(),
247                })
248                .await;
249        }
250        let history_ips = check_ip_history(&client, clean_domain, &ip_regex, &progress_tx).await;
251        found_ips.extend(history_ips);
252    }
253
254    // ── Deduplicate, keeping highest confidence ─────────────────────────
255    let mut best: HashMap<String, FoundIp> = HashMap::new();
256    for ip_info in found_ips {
257        let key = ip_info.ip.clone();
258        let new_score = confidence_score(&ip_info.confidence);
259        if let Some(existing) = best.get(&key) {
260            if new_score > confidence_score(&existing.confidence) {
261                best.insert(key, ip_info);
262            }
263        } else {
264            best.insert(key, ip_info);
265        }
266    }
267
268    // Sort by confidence (highest first)
269    let mut results: Vec<FoundIp> = best.into_values().collect();
270    results.sort_by_key(|result| std::cmp::Reverse(confidence_score(&result.confidence)));
271
272    // ── Verify top 5 IPs ────────────────────────────────────────────────
273    if let Some(t) = &progress_tx {
274        let _ = t
275            .send(crate::ScanProgress {
276                module: "Cloudflare Bypass".into(),
277                percentage: 95.0,
278                message: "Verifying active status of top leaked IPs...".into(),
279                status: "Info".into(),
280            })
281            .await;
282    }
283    for i in 0..results.len().min(5) {
284        let status = verify_ip(&results[i].ip).await;
285        results[i].status = Some(status);
286    }
287    for item in results.iter_mut().skip(5) {
288        item.status = Some("unverified".into());
289    }
290
291    Ok(CloudflareBypassResult {
292        domain: clean_domain.to_string(),
293        cloudflare_protected,
294        found_ips: results,
295        scan_time_ms: start.elapsed().as_millis(),
296    })
297}
298
299// ── IP History ──────────────────────────────────────────────────────────────
300
301async fn check_ip_history(
302    client: &Client,
303    domain: &str,
304    ip_regex: &Regex,
305    progress_tx: &Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
306) -> Vec<FoundIp> {
307    let mut results = Vec::new();
308
309    for (name, url_template) in IP_HISTORY_SOURCES {
310        if let Some(t) = progress_tx {
311            let _ = t
312                .send(crate::ScanProgress {
313                    module: "Cloudflare Bypass".into(),
314                    percentage: 80.0,
315                    message: format!("Querying IP history: {}", name),
316                    status: "Info".into(),
317                })
318                .await;
319        }
320        let url = url_template.replace("{}", domain);
321        let resp = match client
322            .get(&url)
323            .header(
324                "User-Agent",
325                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
326            )
327            .header(
328                "Accept",
329                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
330            )
331            .header("Referer", "https://www.google.com/")
332            .send()
333            .await
334        {
335            Ok(r) if r.status().is_success() => r,
336            _ => continue,
337        };
338
339        let body = match resp.text().await {
340            Ok(t) => t,
341            Err(_) => continue,
342        };
343
344        let mut seen = HashSet::new();
345        for cap in ip_regex.find_iter(&body) {
346            let ip = cap.as_str().to_string();
347            if is_valid_ip(&ip)
348                && !is_cloudflare_ip(&ip)
349                && !is_private_ip(&ip)
350                && seen.insert(ip.clone())
351            {
352                results.push(FoundIp {
353                    ip,
354                    source: format!("history_{}", name),
355                    confidence: "Medium".into(),
356                    description: None,
357                    status: None,
358                });
359            }
360        }
361    }
362
363    results
364}
365
366// ── IP Verification via TCP connect ─────────────────────────────────────────
367
368async fn verify_ip(ip: &str) -> String {
369    let addr = format!("{}:80", ip);
370    match tokio::time::timeout(
371        Duration::from_secs(3),
372        tokio::net::TcpStream::connect(&addr),
373    )
374    .await
375    {
376        Ok(Ok(_)) => "active".into(),
377        _ => "inactive".into(),
378    }
379}