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
61            .send(crate::ScanProgress {
62                module: "Subdomain Takeover".into(),
63                percentage: 10.0,
64                message: format!(
65                    "Checking {} subdomains for dangling CNAMEs natively",
66                    subdomains.len()
67                ),
68                status: "Info".into(),
69            })
70            .await;
71    }
72
73    let mut vulnerable_list = Vec::new();
74    let mut set = JoinSet::new();
75    let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(10));
76
77    for d in subdomains {
78        let subdomain = d.clone();
79        let res = resolver.clone();
80        let permit = semaphore.clone().acquire_owned().await.unwrap();
81        set.spawn(async move {
82            use hickory_resolver::proto::rr::RecordType;
83            let mut cname_records = vec![];
84            let mut a_records = vec![];
85
86            if let Ok(response) = res.lookup(subdomain.as_str(), RecordType::CNAME).await {
87                for record in response.iter() {
88                    if let Some(cname) = record.as_cname() {
89                        cname_records.push(cname.to_string());
90                    }
91                }
92            }
93
94            if let Ok(response) = res.ipv4_lookup(subdomain.as_str()).await {
95                for ip in response.iter() {
96                    a_records.push(ip.to_string());
97                }
98            }
99
100            let result = if !cname_records.is_empty() && a_records.is_empty() {
101                Some(TakeoverVulnerability {
102                    subdomain: subdomain.clone(),
103                    service: "Unknown".into(),
104                    vulnerability_type: "Dangling CNAME".into(),
105                    cname: Some(cname_records[0].clone()),
106                    confidence: "High".into(),
107                    description: format!(
108                        "CNAME points to {} which doesn't resolve to an IP.",
109                        cname_records[0]
110                    ),
111                    exploitation_difficulty: "Medium".into(),
112                    mitigation: "Remove the DNS record or claim the external resource.".into(),
113                    dns_info: DnsCheckResult {
114                        a_records,
115                        aaaa_records: vec![],
116                        cname_records,
117                        mx_records: vec![],
118                        txt_records: vec![],
119                        ns_records: vec![],
120                        has_valid_dns: true,
121                    },
122                    http_status: None,
123                })
124            } else {
125                None
126            };
127            drop(permit);
128            result
129        });
130    }
131
132    let mut scanned = 0;
133    while let Some(vuln_opt) = set.join_next().await {
134        scanned += 1;
135        if let Ok(Some(v)) = vuln_opt {
136            vulnerable_list.push(v);
137        }
138    }
139
140    if let Some(t) = &progress_tx {
141        let _ = t
142            .send(crate::ScanProgress {
143                module: "Subdomain Takeover".into(),
144                percentage: 100.0,
145                message: "Finished native CNAME checking".into(),
146                status: "Info".into(),
147            })
148            .await;
149    }
150
151    Ok(TakeoverResult {
152        domain: domain.to_string(),
153        statistics: ScanStatistics {
154            subdomains_scanned: scanned,
155            vulnerable_count: vulnerable_list.len(),
156            high_confidence: vulnerable_list.len(),
157            medium_confidence: 0,
158            low_confidence: 0,
159            scan_time_secs: start_time.elapsed().as_secs_f64(),
160            services_checked: 0,
161        },
162        vulnerable: vulnerable_list,
163    })
164}