1use reqwest::Client;
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4use tokio::process::Command;
5
6struct VulnService {
9 name: &'static str,
10 cname_pattern: &'static str,
11 error_pattern: &'static str,
12 additional: &'static str,
13}
14
15const VULNERABLE_SERVICES: &[VulnService] = &[
16 VulnService {
17 name: "AWS S3 Bucket",
18 cname_pattern: "s3.amazonaws.com",
19 error_pattern: "NoSuchBucket",
20 additional: "The specified bucket does not exist",
21 },
22 VulnService {
23 name: "AWS CloudFront",
24 cname_pattern: "cloudfront.net",
25 error_pattern: "The request could not be satisfied",
26 additional: "Bad request",
27 },
28 VulnService {
29 name: "GitHub Pages",
30 cname_pattern: "github.io",
31 error_pattern: "There isn't a GitHub Pages site here",
32 additional: "404: Not Found",
33 },
34 VulnService {
35 name: "Heroku",
36 cname_pattern: "herokuapp.com",
37 error_pattern: "No such app",
38 additional: "heroku",
39 },
40 VulnService {
41 name: "Vercel",
42 cname_pattern: "vercel.app",
43 error_pattern: "404: Not Found",
44 additional: "The deployment could not be found",
45 },
46 VulnService {
47 name: "Netlify",
48 cname_pattern: "netlify.app",
49 error_pattern: "Not found",
50 additional: "netlify",
51 },
52 VulnService {
53 name: "Azure App Service",
54 cname_pattern: "azurewebsites.net",
55 error_pattern: "Microsoft Azure App Service",
56 additional: "404 Not Found",
57 },
58 VulnService {
59 name: "Azure TrafficManager",
60 cname_pattern: "trafficmanager.net",
61 error_pattern: "Page not found",
62 additional: "Not found",
63 },
64 VulnService {
65 name: "Zendesk",
66 cname_pattern: "zendesk.com",
67 error_pattern: "Help Center Closed",
68 additional: "Zendesk",
69 },
70 VulnService {
71 name: "Shopify",
72 cname_pattern: "myshopify.com",
73 error_pattern: "Sorry, this shop is currently unavailable",
74 additional: "Shopify",
75 },
76 VulnService {
77 name: "Fastly",
78 cname_pattern: "fastly.net",
79 error_pattern: "Fastly error: unknown domain",
80 additional: "Fastly",
81 },
82 VulnService {
83 name: "Pantheon",
84 cname_pattern: "pantheonsite.io",
85 error_pattern: "The gods are wise",
86 additional: "404 Not Found",
87 },
88 VulnService {
89 name: "Tumblr",
90 cname_pattern: "tumblr.com",
91 error_pattern: "There's nothing here",
92 additional: "Tumblr",
93 },
94 VulnService {
95 name: "WordPress",
96 cname_pattern: "wordpress.com",
97 error_pattern: "Do you want to register",
98 additional: "WordPress",
99 },
100 VulnService {
101 name: "Acquia",
102 cname_pattern: "acquia-sites.com",
103 error_pattern: "No site found",
104 additional: "The requested URL was not found",
105 },
106 VulnService {
107 name: "Ghost",
108 cname_pattern: "ghost.io",
109 error_pattern: "The thing you were looking for is no longer here",
110 additional: "Ghost",
111 },
112 VulnService {
113 name: "Cargo",
114 cname_pattern: "cargocollective.com",
115 error_pattern: "404 Not Found",
116 additional: "Cargo",
117 },
118 VulnService {
119 name: "Webflow",
120 cname_pattern: "webflow.io",
121 error_pattern: "The page you are looking for doesn't exist",
122 additional: "Webflow",
123 },
124 VulnService {
125 name: "Surge.sh",
126 cname_pattern: "surge.sh",
127 error_pattern: "404 Not Found",
128 additional: "Surge",
129 },
130 VulnService {
131 name: "Squarespace",
132 cname_pattern: "squarespace.com",
133 error_pattern: "Website Expired",
134 additional: "Squarespace",
135 },
136 VulnService {
137 name: "Fly.io",
138 cname_pattern: "fly.dev",
139 error_pattern: "404 Not Found",
140 additional: "Fly.io",
141 },
142 VulnService {
143 name: "Brightcove",
144 cname_pattern: "bcvp0rtal.com",
145 error_pattern: "Brightcove Error",
146 additional: "Brightcove",
147 },
148 VulnService {
149 name: "Unbounce",
150 cname_pattern: "unbounce.com",
151 error_pattern: "The requested URL was not found",
152 additional: "Unbounce",
153 },
154 VulnService {
155 name: "Strikingly",
156 cname_pattern: "strikinglydns.com",
157 error_pattern: "404 Not Found",
158 additional: "Strikingly",
159 },
160 VulnService {
161 name: "UptimeRobot",
162 cname_pattern: "stats.uptimerobot.com",
163 error_pattern: "404 Not Found",
164 additional: "UptimeRobot",
165 },
166 VulnService {
167 name: "UserVoice",
168 cname_pattern: "uservoice.com",
169 error_pattern: "This UserVoice is currently being set up",
170 additional: "UserVoice",
171 },
172 VulnService {
173 name: "Pingdom",
174 cname_pattern: "stats.pingdom.com",
175 error_pattern: "404 Not Found",
176 additional: "Pingdom",
177 },
178 VulnService {
179 name: "Desk",
180 cname_pattern: "desk.com",
181 error_pattern: "Please try again",
182 additional: "Desk",
183 },
184 VulnService {
185 name: "Tilda",
186 cname_pattern: "tilda.ws",
187 error_pattern: "404 Not Found",
188 additional: "Tilda",
189 },
190 VulnService {
191 name: "Helpjuice",
192 cname_pattern: "helpjuice.com",
193 error_pattern: "404 Not Found",
194 additional: "Helpjuice",
195 },
196 VulnService {
197 name: "HelpScout",
198 cname_pattern: "helpscoutdocs.com",
199 error_pattern: "No settings were found",
200 additional: "HelpScout",
201 },
202 VulnService {
203 name: "Campaign Monitor",
204 cname_pattern: "createsend.com",
205 error_pattern: "404 Not Found",
206 additional: "Campaign Monitor",
207 },
208 VulnService {
209 name: "Digital Ocean",
210 cname_pattern: "digitalocean.app",
211 error_pattern: "404 Not Found",
212 additional: "Digital Ocean",
213 },
214 VulnService {
215 name: "AWS Elastic Beanstalk",
216 cname_pattern: "elasticbeanstalk.com",
217 error_pattern: "404 Not Found",
218 additional: "Elastic Beanstalk",
219 },
220 VulnService {
221 name: "Readthedocs",
222 cname_pattern: "readthedocs.io",
223 error_pattern: "Not Found",
224 additional: "readthedocs",
225 },
226 VulnService {
227 name: "Firebase",
228 cname_pattern: "firebaseapp.com",
229 error_pattern: "404 Not Found",
230 additional: "Firebase",
231 },
232];
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct DnsCheckResult {
238 pub a_records: Vec<String>,
239 pub aaaa_records: Vec<String>,
240 pub cname_records: Vec<String>,
241 pub mx_records: Vec<String>,
242 pub txt_records: Vec<String>,
243 pub ns_records: Vec<String>,
244 pub has_valid_dns: bool,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct TakeoverVulnerability {
249 pub subdomain: String,
250 pub service: String,
251 pub vulnerability_type: String,
252 pub cname: Option<String>,
253 pub confidence: String,
254 pub description: String,
255 pub exploitation_difficulty: String,
256 pub mitigation: String,
257 pub dns_info: DnsCheckResult,
258 pub http_status: Option<u16>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ScanStatistics {
263 pub subdomains_scanned: usize,
264 pub vulnerable_count: usize,
265 pub high_confidence: usize,
266 pub medium_confidence: usize,
267 pub low_confidence: usize,
268 pub scan_time_secs: f64,
269 pub services_checked: usize,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct TakeoverResult {
274 pub domain: String,
275 pub statistics: ScanStatistics,
276 pub vulnerable: Vec<TakeoverVulnerability>,
277}
278
279pub async fn check_subdomain_takeover(
282 domain: &str,
283 subdomains: &[String],
284 progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
285) -> Result<TakeoverResult, Box<dyn std::error::Error + Send + Sync>> {
286 let client = Client::builder()
287 .timeout(Duration::from_secs(10))
288 .danger_accept_invalid_certs(true)
289 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
290 .build()?;
291
292 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Subdomain Takeover".into(), percentage: 5.0, message: "Initializing subdomain takeover checks...".into(), status: "Info".into() }).await; }
293 let start = std::time::Instant::now();
294 let mut vulnerable = Vec::new();
295
296 for (i, sub) in subdomains.iter().enumerate() {
297 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Subdomain Takeover".into(), percentage: 5.0 + (90.0 * (i as f32 / subdomains.len().max(1) as f32)), message: format!("Checking {} for dangling records...", sub), status: "Info".into() }).await; }
298 if let Some(vuln) = check_single_subdomain(&client, sub).await {
299 vulnerable.push(vuln);
300 }
301 }
302
303 if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Subdomain Takeover".into(), percentage: 95.0, message: "Sorting and finalizing vulnerability results...".into(), status: "Info".into() }).await; }
304 vulnerable.sort_by(|a, b| {
306 let order = |c: &str| -> u8 {
307 match c {
308 "High" => 0,
309 "Medium" => 1,
310 _ => 2,
311 }
312 };
313 order(&a.confidence).cmp(&order(&b.confidence))
314 });
315
316 let high = vulnerable.iter().filter(|v| v.confidence == "High").count();
317 let medium = vulnerable
318 .iter()
319 .filter(|v| v.confidence == "Medium")
320 .count();
321 let low = vulnerable.iter().filter(|v| v.confidence == "Low").count();
322
323 Ok(TakeoverResult {
324 domain: domain.to_string(),
325 statistics: ScanStatistics {
326 subdomains_scanned: subdomains.len(),
327 vulnerable_count: vulnerable.len(),
328 high_confidence: high,
329 medium_confidence: medium,
330 low_confidence: low,
331 scan_time_secs: start.elapsed().as_secs_f64(),
332 services_checked: VULNERABLE_SERVICES.len(),
333 },
334 vulnerable,
335 })
336}
337
338async fn check_single_subdomain(client: &Client, subdomain: &str) -> Option<TakeoverVulnerability> {
341 let dns = check_dns(subdomain).await;
343
344 let (http_status, body) = fetch_http(client, subdomain).await;
346
347 let body_lower = body.to_lowercase();
348
349 for cname in &dns.cname_records {
351 let cname_lower = cname.to_lowercase();
352 for svc in VULNERABLE_SERVICES {
353 if cname_lower.contains(svc.cname_pattern) {
354 let has_error = body_lower.contains(&svc.error_pattern.to_lowercase())
355 || body_lower.contains(&svc.additional.to_lowercase());
356
357 if has_error {
358 return Some(TakeoverVulnerability {
359 subdomain: subdomain.to_string(),
360 service: svc.name.to_string(),
361 vulnerability_type: "CNAME Error Pattern".into(),
362 cname: Some(cname.clone()),
363 confidence: "High".into(),
364 description: format!("CNAME points to {} ({}) and returns error indicating resource doesn't exist.", svc.name, cname),
365 exploitation_difficulty: assess_difficulty("CNAME Error Pattern", svc.name),
366 mitigation: suggest_mitigation("CNAME Error Pattern", svc.name),
367 dns_info: dns,
368 http_status,
369 });
370 }
371 }
372 }
373 }
374
375 if !dns.cname_records.is_empty() && dns.a_records.is_empty() && http_status.is_none() {
377 for cname in &dns.cname_records {
378 let resolves = resolve_a(cname).await;
379 if !resolves {
380 let mut service = "Unknown".to_string();
382 let cname_lower = cname.to_lowercase();
383 for svc in VULNERABLE_SERVICES {
384 if cname_lower.contains(svc.cname_pattern) {
385 service = svc.name.to_string();
386 break;
387 }
388 }
389 let conf = if service != "Unknown" {
390 "High"
391 } else {
392 "Medium"
393 };
394
395 return Some(TakeoverVulnerability {
396 subdomain: subdomain.to_string(),
397 service: service.clone(),
398 vulnerability_type: "Dangling CNAME".into(),
399 cname: Some(cname.clone()),
400 confidence: conf.into(),
401 description: format!(
402 "CNAME points to {} which doesn't resolve to an IP.",
403 cname
404 ),
405 exploitation_difficulty: assess_difficulty("Dangling CNAME", &service),
406 mitigation: suggest_mitigation("Dangling CNAME", &service),
407 dns_info: dns,
408 http_status,
409 });
410 }
411 }
412 }
413
414 for ns in &dns.ns_records {
416 let resolves = resolve_a(ns).await;
417 if !resolves {
418 return Some(TakeoverVulnerability {
419 subdomain: subdomain.to_string(),
420 service: "Unknown".into(),
421 vulnerability_type: "Dangling NS".into(),
422 cname: None,
423 confidence: "Medium".into(),
424 description: format!("NS record points to {} which doesn't resolve.", ns),
425 exploitation_difficulty: "Medium".into(),
426 mitigation: suggest_mitigation("Dangling NS", "Unknown"),
427 dns_info: dns,
428 http_status,
429 });
430 }
431 }
432
433 if dns.has_valid_dns {
435 if let Some(status) = http_status {
436 if [404, 500, 502, 503].contains(&status) {
437 let dns_str = format!("{:?}", dns).to_lowercase();
438 let third_party = ["aws", "amazon", "azure", "heroku", "github", "vercel"];
439 let is_3rd = third_party.iter().any(|p| dns_str.contains(p));
440
441 let conf = if is_3rd { "Medium" } else { "Low" };
442 return Some(TakeoverVulnerability {
443 subdomain: subdomain.to_string(),
444 service: "Unknown".into(),
445 vulnerability_type: "Third-Party Service Error".into(),
446 cname: dns.cname_records.first().cloned(),
447 confidence: conf.into(),
448 description: format!("Valid DNS but returns HTTP {} error.", status),
449 exploitation_difficulty: "Hard".into(),
450 mitigation: suggest_mitigation("Third-Party Service Error", "Unknown"),
451 dns_info: dns,
452 http_status: Some(status),
453 });
454 }
455 }
456 }
457
458 if !dns.mx_records.is_empty() {
460 let has_spf = dns.txt_records.iter().any(|t| t.contains("v=spf1"));
461 if !has_spf {
462 return Some(TakeoverVulnerability {
463 subdomain: subdomain.to_string(),
464 service: "Unknown".into(),
465 vulnerability_type: "Missing SPF".into(),
466 cname: None,
467 confidence: "Low".into(),
468 description: "Has MX records but no SPF record — potential email spoofing risk."
469 .into(),
470 exploitation_difficulty: "Medium".into(),
471 mitigation: suggest_mitigation("Missing SPF", "Unknown"),
472 dns_info: dns,
473 http_status,
474 });
475 }
476 }
477
478 None
479}
480
481async fn check_dns(subdomain: &str) -> DnsCheckResult {
484 let (a, aaaa, cname, mx, txt, ns) = tokio::join!(
485 dig_query(subdomain, "A"),
486 dig_query(subdomain, "AAAA"),
487 dig_query(subdomain, "CNAME"),
488 dig_query(subdomain, "MX"),
489 dig_query(subdomain, "TXT"),
490 dig_query(subdomain, "NS"),
491 );
492
493 let has_valid = !a.is_empty()
494 || !aaaa.is_empty()
495 || !cname.is_empty()
496 || !mx.is_empty()
497 || !txt.is_empty()
498 || !ns.is_empty();
499
500 DnsCheckResult {
501 a_records: a,
502 aaaa_records: aaaa,
503 cname_records: cname,
504 mx_records: mx,
505 txt_records: txt,
506 ns_records: ns,
507 has_valid_dns: has_valid,
508 }
509}
510
511async fn dig_query(domain: &str, rtype: &str) -> Vec<String> {
512 let output = match Command::new("dig")
513 .args(["+short", rtype, domain])
514 .output()
515 .await
516 {
517 Ok(o) => o,
518 Err(_) => return vec![],
519 };
520 String::from_utf8_lossy(&output.stdout)
521 .lines()
522 .map(|s| s.trim().trim_end_matches('.').to_string())
523 .filter(|s| !s.is_empty())
524 .collect()
525}
526
527async fn resolve_a(host: &str) -> bool {
528 let output = match Command::new("dig")
529 .args(["+short", "A", host])
530 .output()
531 .await
532 {
533 Ok(o) => o,
534 Err(_) => return false,
535 };
536 let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
537 !result.is_empty()
538}
539
540async fn fetch_http(client: &Client, subdomain: &str) -> (Option<u16>, String) {
543 if let Ok(resp) = client.get(format!("https://{}", subdomain)).send().await {
545 let status = resp.status().as_u16();
546 let body = resp.text().await.unwrap_or_default();
547 return (Some(status), body.chars().take(1000).collect());
548 }
549 if let Ok(resp) = client.get(format!("http://{}", subdomain)).send().await {
551 let status = resp.status().as_u16();
552 let body = resp.text().await.unwrap_or_default();
553 return (Some(status), body.chars().take(1000).collect());
554 }
555 (None, String::new())
556}
557
558fn assess_difficulty(vuln_type: &str, service: &str) -> String {
561 match vuln_type {
562 "CNAME Error Pattern" => {
563 let easy = ["GitHub Pages", "Heroku", "Vercel", "Netlify", "Surge.sh"];
564 let medium = ["AWS S3 Bucket", "Firebase", "Ghost", "WordPress"];
565 if easy.contains(&service) {
566 "Easy".into()
567 } else if medium.contains(&service) {
568 "Medium".into()
569 } else {
570 "Hard".into()
571 }
572 }
573 "Dangling CNAME" => {
574 if service != "Unknown" {
575 "Medium".into()
576 } else {
577 "Hard".into()
578 }
579 }
580 "Dangling NS" => "Medium".into(),
581 _ => "Hard".into(),
582 }
583}
584
585fn suggest_mitigation(vuln_type: &str, service: &str) -> String {
588 match vuln_type {
589 "CNAME Error Pattern" => format!("Remove the CNAME record or reclaim the resource on {}. Ensure you've properly set up the service before pointing DNS records to it.", service),
590 "Dangling CNAME" => "Remove the CNAME record pointing to a non-existent endpoint. If the service is still needed, recreate the resource at the target.".into(),
591 "Dangling NS" => "Update NS records to point to valid nameservers. Remove delegations to nameservers that no longer exist.".into(),
592 "Third-Party Service Error" => "Verify the resource exists on the target service. If no longer used, remove the DNS record.".into(),
593 "Missing SPF" => "Add an SPF record to protect against email spoofing. Example: 'v=spf1 mx -all'".into(),
594 _ => "Review DNS configuration and remove references to services or resources no longer in use.".into(),
595 }
596}