Skip to main content

ikebuster/
lib.rs

1//! # ikebuster
2//!
3//! A small utility to scan your IKE servers for insecure ciphers
4
5#![warn(missing_docs, clippy::unwrap_used, clippy::expect_used)]
6
7use std::collections::HashMap;
8use std::collections::VecDeque;
9use std::io;
10use std::net::IpAddr;
11use std::net::SocketAddr;
12use std::sync::Arc;
13use std::time::Duration;
14
15use isakmp::v1::definitions::NotifyMessageType;
16use isakmp::v1::generator::MessageBuilder;
17use isakmp::v1::generator::Transform;
18use thiserror::Error;
19use tokio::net::UdpSocket;
20use tokio::select;
21use tokio::sync::mpsc;
22use tokio::time::interval;
23use tokio::time::sleep;
24use tracing::debug;
25use tracing::error;
26use tracing::info;
27use tracing::instrument;
28use tracing::trace;
29use tracing::warn;
30
31use crate::recv::ReceiveError;
32use crate::utils::gen_transforms::gen_v1_transforms;
33use crate::utils::payload_to_transforms::payload_to_transforms;
34
35mod recv;
36pub mod utils;
37
38/// The results of the scan
39#[derive(Debug, Clone)]
40pub struct ScanResult {
41    /// All transforms that were accepted by the target server
42    pub valid_transforms: Vec<Transform>,
43}
44
45/// Options to "configure" the scanner
46#[derive(Debug, Clone)]
47pub struct ScanOptions {
48    /// Target IP
49    pub ip: IpAddr,
50    /// Target port
51    pub port: u16,
52    /// Interval between each sent message
53    pub interval: u64,
54    /// Number of transforms to send in a single proposal
55    pub transform_no: usize,
56    /// The sleep to set when a valid transform is found.
57    ///
58    /// This may be important as some servers timeout requests when requests aren't fully closed
59    pub sleep_on_transform_found: Duration,
60}
61
62/// Scan the provided ip address
63#[instrument(skip_all)]
64pub async fn scan(opts: ScanOptions) -> Result<ScanResult, ScanError> {
65    // Initialize udp socket
66    let addr = SocketAddr::new(opts.ip, opts.port);
67
68    info!("Binding and starting to scan {addr}");
69    let socket = Arc::new(match addr.ip() {
70        IpAddr::V4(_) => UdpSocket::bind("0.0.0.0:500")
71            .await
72            .map_err(ScanError::CouldNotBind)?,
73        IpAddr::V6(_) => UdpSocket::bind("[::]:500")
74            .await
75            .map_err(ScanError::CouldNotBind)?,
76    });
77    socket.connect(&addr).await.map_err(ScanError::Receive)?;
78
79    let (tx, mut rx) = mpsc::unbounded_channel();
80    let mut interval = interval(Duration::from_millis(opts.interval));
81
82    tokio::spawn(recv::handle_receive(socket.clone(), tx));
83
84    // list of a list of transforms which should be sent in the future
85    let mut todo: VecDeque<Vec<_>> = gen_v1_transforms(opts.transform_no);
86
87    // Lookup of cookie to the transforms that were sent in the corresponding message
88    let mut open: HashMap<u64, Vec<Transform>> = HashMap::new();
89
90    // Number of cookies lost in attempts to remove them from the tracked list, used as a fallback
91    let mut lost_cookies = 0;
92
93    // The valid transforms that were found
94    let mut found: Vec<Transform> = vec![];
95
96    // If sleep is active, the sending part will pause
97    let mut do_sleep = false;
98
99    loop {
100        select! {
101            // Handle received isakmp messages or errors from receiving side
102            msg_res = rx.recv() => {
103                if let Some(res) = msg_res {
104                    match res {
105                        Ok(msg) => {
106                            trace!("Received message: {msg:?}");
107
108                            // Retrieving a security association means we got at least one transform right
109                            if !msg.security_associations.is_empty() {
110                                for sa in &msg.security_associations {
111                                    for prop in &sa.proposal_payload {
112                                        do_sleep = true;
113
114                                        let Ok(transforms) = payload_to_transforms(prop) else {
115                                            warn!("Could not retrieve transform from msg: {msg:?}");
116                                            debug!("{msg:?}");
117                                            continue;
118                                        };
119
120                                        // Add the found transform to our list
121                                        found.extend(transforms.clone());
122
123                                        let Some(all) = open.get(&msg.header.initiator_cookie) else {
124                                            warn!("Missing initiator cookie");
125                                            trace!("{} :: {:#?}", msg.header.initiator_cookie, open);
126                                            continue;
127                                        };
128
129                                        // Retrieve all transforms not returned in the message
130                                        let other: Vec<Transform> = all.clone().into_iter().filter(|x| !transforms.contains(x)).collect();
131
132                                        // Split the transforms into two new messages
133                                        let  [mut a,mut b] = [vec![], vec![]];
134                                        for x in other {
135                                            if a.len() == b.len() {
136                                                a.push(x);
137                                            } else {
138                                                b.push(x);
139                                            }
140                                        }
141
142                                        // create new todos
143                                        if !b.is_empty() {
144                                            todo.push_back(a);
145                                            todo.push_back(b);
146                                        } else if !a.is_empty() {
147                                            todo.push_back(a);
148                                        }
149                                    }
150                                }
151                                let removed = open.remove(&msg.header.initiator_cookie);
152                                if removed.is_none() {
153                                    warn!("Could not find corresponding initiator cookie: {}", msg.header.initiator_cookie);
154                                    lost_cookies += 1;
155                                }
156
157                            // A notification of type NO_PROPOSAL_CHOSEN means all transforms were invalid
158                            } else if msg.notification_payloads.iter().any(|x| x.notify_message_type == NotifyMessageType::NoProposalChosen) {
159                                let removed = open.remove(&msg.header.initiator_cookie);
160                                if removed.is_none() {
161                                    warn!("Could not find corresponding initiator cookie: {}", msg.header.initiator_cookie);
162                                    lost_cookies += 1;
163                                }
164                            } else {
165                                warn!("Unknown message: {:?}", msg)
166                            }
167
168                        }
169                        Err(err) => match err {
170                            ReceiveError::Io(err) => {
171                                error!("Error in receiving side: {err}");
172                                return Err(ScanError::Receive(err));
173                            }
174                            ReceiveError::InvalidMessage(err) => {
175                                trace!("Could not parse incoming message: {err}");
176                            }
177                        }
178                    }
179                }
180            }
181
182            // Handle the sending of messages
183            _ = interval.tick() => {
184                match todo.pop_front() {
185                    // Nothing more to do, this will be the return path
186                    None => {
187                        debug!("Nothing more to do, waiting some time for more incoming messages");
188                        interval.tick().await;
189                        if todo.is_empty() && open.len() <= lost_cookies {
190                            found.sort();
191                            found.dedup();
192
193                            return Ok(ScanResult {
194                                valid_transforms: found,
195                            })
196                        }
197                    }
198                    Some(transforms) => {
199                        let mut mb = MessageBuilder::new();
200                        for transform in &transforms {
201                            mb = mb.add_transform(transform.clone());
202                        }
203                        let (msg, initiator_cookie) = mb.build();
204                        trace!("Send ({initiator_cookie}) transforms: {transforms:?}");
205
206                        if do_sleep {
207                            info!(
208                                "Sleep {} seconds to evade running into timeout due to half-open connections",
209                                opts.sleep_on_transform_found.as_secs(),
210                            );
211                            sleep(opts.sleep_on_transform_found).await;
212                            do_sleep = false;
213                        }
214
215                        open.insert(initiator_cookie, transforms);
216                        socket.send(&msg).await.map_err(ScanError::Send)?;
217                    }
218                }
219            }
220        }
221    }
222}
223
224/// Errors that may occur while scanning
225#[derive(Debug, Error)]
226#[allow(missing_docs)]
227pub enum ScanError {
228    #[error("Could not bind: {0}")]
229    CouldNotBind(io::Error),
230    #[error("Could not recv: {0}")]
231    Receive(io::Error),
232    #[error("Could not send: {0}")]
233    Send(io::Error),
234}