subscan/modules/
zonetransfer.rs

1use std::{net::SocketAddr, str::FromStr};
2
3use async_trait::async_trait;
4use flume::Sender;
5use hickory_client::{
6    client::{Client, ClientHandle},
7    proto::{
8        rr::{domain::Name, DNSClass, Record, RecordType},
9        runtime::TokioRuntimeProvider,
10        tcp::TcpClientStream,
11    },
12};
13use hickory_resolver::config::NameServerConfig;
14use tokio::sync::Mutex;
15
16use crate::{
17    enums::{
18        dispatchers::{RequesterDispatcher, SubdomainExtractorDispatcher, SubscanModuleDispatcher},
19        result::OptionalSubscanModuleResult,
20    },
21    error::ModuleErrorKind::Custom,
22    interfaces::module::SubscanModuleInterface,
23    types::{
24        core::{Result, Subdomain},
25        result::status::SubscanModuleStatus::Finished,
26    },
27    utilities::{net, regex},
28};
29
30pub const ZONETRANSFER_MODULE_NAME: &str = "zonetransfer";
31
32/// ZoneTransfer non-generic module
33///
34/// The `ZoneTransfer` module is a non-generic component designed to perform zone transfers
35/// by querying name servers using the `AXFR` (Authoritative Zone Transfer) query type.
36/// If a name server is configured without proper security measures, this module
37/// may successfully retrieve all DNS records associated with the zone, potentially exposing
38/// sensitive information such as subdomains, email servers, and other internal
39/// infrastructure details
40///
41/// | Property           | Value          |
42/// |:------------------:|:--------------:|
43/// | Module Name        | `zonetransfer` |
44/// | Requester          | [`None`]       |
45/// | Extractor          | [`None`]       |
46/// | Generic            | [`None`]       |
47pub struct ZoneTransfer {
48    pub name: String,
49    pub ns: Option<NameServerConfig>,
50}
51
52impl ZoneTransfer {
53    pub fn dispatcher() -> SubscanModuleDispatcher {
54        let zonetransfer = Self {
55            name: ZONETRANSFER_MODULE_NAME.into(),
56            ns: net::get_default_ns(),
57        };
58
59        zonetransfer.into()
60    }
61
62    pub async fn get_tcp_client(&self, server: SocketAddr) -> Result<Client> {
63        let provider = TokioRuntimeProvider::new();
64        let (stream, handler) = TcpClientStream::new(server, None, None, provider);
65        let result = Client::new(stream, handler, None).await;
66
67        result.map_err(|_| Custom("client error".into())).map(|(client, bg)| {
68            tokio::spawn(bg);
69            Ok(client)
70        })?
71    }
72
73    pub async fn get_ns_as_ip(&self, server: SocketAddr, domain: &str) -> Option<Vec<SocketAddr>> {
74        let mut ips = vec![];
75        let mut client = self.get_tcp_client(server).await.ok()?;
76
77        let name = Name::from_str(domain).ok()?;
78        let ns_response = client.query(name, DNSClass::IN, RecordType::NS);
79
80        for answer in ns_response.await.ok()?.answers() {
81            let name = Name::from_str(&answer.data().as_ns()?.to_utf8()).ok()?;
82            let a_response = client.query(name, DNSClass::IN, RecordType::A).await;
83
84            let with_port = |answer: &Record| Some(format!("{}:{}", answer.data(), server.port()));
85            let as_ip = |with_port: String| SocketAddr::from_str(&with_port).ok();
86
87            ips.extend(a_response.ok()?.answers().iter().filter_map(with_port).filter_map(as_ip));
88        }
89
90        Some(ips)
91    }
92
93    pub async fn attempt_zone_transfer(
94        &self,
95        server: SocketAddr,
96        domain: &str,
97    ) -> Result<Vec<Subdomain>> {
98        let pattern = regex::generate_subdomain_regex(domain)?;
99
100        let mut subs = Vec::new();
101        let mut client = self.get_tcp_client(server).await?;
102
103        let name = Name::from_str(domain).unwrap();
104        let axfr_response = client.query(name, DNSClass::IN, RecordType::AXFR);
105
106        if let Ok(response) = axfr_response.await {
107            for answer in response.answers() {
108                let rtype = answer.data().record_type();
109
110                if rtype == RecordType::A || rtype == RecordType::AAAA {
111                    let name = answer.name().to_string();
112                    let sub = name.strip_suffix(".").unwrap_or(&name);
113
114                    if let Some(matches) = pattern.find(sub) {
115                        subs.push(matches.as_str().to_lowercase());
116                    }
117                }
118            }
119        }
120
121        Ok(subs)
122    }
123}
124
125#[async_trait]
126impl SubscanModuleInterface for ZoneTransfer {
127    async fn name(&self) -> &str {
128        &self.name
129    }
130
131    async fn requester(&self) -> Option<&Mutex<RequesterDispatcher>> {
132        None
133    }
134
135    async fn extractor(&self) -> Option<&SubdomainExtractorDispatcher> {
136        None
137    }
138
139    async fn run(&mut self, domain: &str, results: Sender<OptionalSubscanModuleResult>) {
140        match &self.ns {
141            Some(ns) => {
142                let err = Custom("connection error".into());
143
144                match self.get_ns_as_ip(ns.socket_addr, domain).await.ok_or(err) {
145                    Ok(ips) => {
146                        for ip in ips {
147                            let subdomains = self.attempt_zone_transfer(ip, domain).await;
148
149                            for subdomain in &subdomains.unwrap_or_default() {
150                                results.send(self.item(subdomain).await).unwrap();
151                            }
152                        }
153                        results.send(self.status(Finished).await).unwrap();
154                    }
155                    Err(err) => results.send(self.status(err.into()).await).unwrap(),
156                }
157            }
158            None => results.send(self.error("no default ns").await).unwrap(),
159        };
160    }
161}