Skip to main content

web_analyzer/
subdomain_takeover_mobile.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct DnsCheckResult {
5    pub a_records: Vec<String>,
6    pub aaaa_records: Vec<String>,
7    pub cname_records: Vec<String>,
8    pub mx_records: Vec<String>,
9    pub txt_records: Vec<String>,
10    pub ns_records: Vec<String>,
11    pub has_valid_dns: bool,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TakeoverVulnerability {
16    pub subdomain: String,
17    pub service: String,
18    pub vulnerability_type: String,
19    pub cname: Option<String>,
20    pub confidence: String,
21    pub description: String,
22    pub exploitation_difficulty: String,
23    pub mitigation: String,
24    pub dns_info: DnsCheckResult,
25    pub http_status: Option<u16>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ScanStatistics {
30    pub subdomains_scanned: usize,
31    pub vulnerable_count: usize,
32    pub high_confidence: usize,
33    pub medium_confidence: usize,
34    pub low_confidence: usize,
35    pub scan_time_secs: f64,
36    pub services_checked: usize,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct TakeoverResult {
41    pub domain: String,
42    pub statistics: ScanStatistics,
43    pub vulnerable: Vec<TakeoverVulnerability>,
44}
45
46use hickory_resolver::config::*;
47use hickory_resolver::AsyncResolver;
48use std::time::Instant;
49use tokio::task::JoinSet;
50
51pub async fn check_subdomain_takeover(
52    domain: &str,
53    subdomains: &[String],
54    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
55) -> Result<TakeoverResult, Box<dyn std::error::Error + Send + Sync>> {
56    let start_time = Instant::now();
57    let resolver = AsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default());
58
59    if let Some(t) = &progress_tx {
60        let _ = t.send(crate::ScanProgress {
61            module: "Subdomain Takeover".into(),
62            percentage: 10.0,
63            message: format!("Checking {} subdomains for dangling CNAMEs natively", subdomains.len()),
64            status: "Info".into(),
65        }).await;
66    }
67
68    let mut vulnerable_list = Vec::new();
69    let mut set = JoinSet::new();
70    let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(10));
71
72    for d in subdomains {
73        let subdomain = d.clone();
74        let res = resolver.clone();
75        let permit = semaphore.clone().acquire_owned().await.unwrap();
76        set.spawn(async move {
77            use hickory_resolver::proto::rr::RecordType;
78            let mut cname_records = vec![];
79            let mut a_records = vec![];
80
81            if let Ok(response) = res.lookup(subdomain.as_str(), RecordType::CNAME).await {
82                for record in response.iter() {
83                    if let Some(cname) = record.as_cname() {
84                        cname_records.push(cname.to_string());
85                    }
86                }
87            }
88            
89            if let Ok(response) = res.ipv4_lookup(subdomain.as_str()).await {
90                for ip in response.iter() {
91                    a_records.push(ip.to_string());
92                }
93            }
94
95            let result = if !cname_records.is_empty() && a_records.is_empty() {
96                Some(TakeoverVulnerability {
97                    subdomain: subdomain.clone(),
98                    service: "Unknown".into(),
99                    vulnerability_type: "Dangling CNAME".into(),
100                    cname: Some(cname_records[0].clone()),
101                    confidence: "High".into(),
102                    description: format!("CNAME points to {} which doesn't resolve to an IP.", cname_records[0]),
103                    exploitation_difficulty: "Medium".into(),
104                    mitigation: "Remove the DNS record or claim the external resource.".into(),
105                    dns_info: DnsCheckResult {
106                        a_records,
107                        aaaa_records: vec![],
108                        cname_records,
109                        mx_records: vec![],
110                        txt_records: vec![],
111                        ns_records: vec![],
112                        has_valid_dns: true,
113                    },
114                    http_status: None,
115                })
116            } else {
117                None
118            };
119            drop(permit);
120            result
121        });
122    }
123
124    let mut scanned = 0;
125    while let Some(vuln_opt) = set.join_next().await {
126        scanned += 1;
127        if let Ok(Some(v)) = vuln_opt {
128            vulnerable_list.push(v);
129        }
130    }
131
132    if let Some(t) = &progress_tx {
133        let _ = t.send(crate::ScanProgress {
134            module: "Subdomain Takeover".into(),
135            percentage: 100.0,
136            message: "Finished native CNAME checking".into(),
137            status: "Info".into(),
138        }).await;
139    }
140
141    Ok(TakeoverResult {
142        domain: domain.to_string(),
143        statistics: ScanStatistics {
144            subdomains_scanned: scanned,
145            vulnerable_count: vulnerable_list.len(),
146            high_confidence: vulnerable_list.len(),
147            medium_confidence: 0,
148            low_confidence: 0,
149            scan_time_secs: start_time.elapsed().as_secs_f64(),
150            services_checked: 0,
151        },
152        vulnerable: vulnerable_list,
153    })
154}