Skip to main content

ploit_malper/exploit/
msf.rs

1use std::collections::BTreeMap;
2use std::time::Duration;
3
4use crate::engine::msfrpc::MsfRpcClient;
5use crate::error::{AppError, Result};
6use crate::msgpack::MsgValue;
7
8use super::models::*;
9use super::ExploitFramework;
10
11/// Metasploit implementation of [`ExploitFramework`].
12///
13/// All framework communication is delegated to the existing `MsfRpcClient`,
14/// keeping Metasploit specifics isolated behind the trait.
15pub struct MsfFramework {
16    client: Option<MsfRpcClient>,
17    host: String,
18    port: u16,
19    ssl: bool,
20    username: String,
21    password: String,
22    workspace: String,
23}
24
25impl MsfFramework {
26    pub fn new(host: &str, port: u16, username: &str, password: &str, ssl: bool) -> Self {
27        Self {
28            client: None,
29            host: host.to_string(),
30            port,
31            ssl,
32            username: username.to_string(),
33            password: password.to_string(),
34            workspace: "default".to_string(),
35        }
36    }
37}
38
39impl ExploitFramework for MsfFramework {
40    fn name(&self) -> &'static str {
41        "metasploit"
42    }
43
44    fn workspace(&self) -> &str {
45        &self.workspace
46    }
47
48    fn set_workspace(&mut self, workspace: &str) {
49        self.workspace = workspace.to_string();
50    }
51
52    fn connect(&mut self) -> Result<()> {
53        let mut client = MsfRpcClient::new(
54            &self.host,
55            self.port,
56            &self.username,
57            &self.password,
58            self.ssl,
59        );
60        let login = client
61            .login()
62            .map_err(|e| AppError::Network(format!("MSF-RPC login transport failure: {e}")))?;
63        if !login.success {
64            return Err(AppError::Message(format!(
65                "MSF-RPC authentication failed: {}",
66                login.error
67            )));
68        }
69        self.client = Some(client);
70        Ok(())
71    }
72
73    fn disconnect(&mut self) {
74        if let Some(client) = self.client.as_mut() {
75            client.logout();
76        }
77        self.client = None;
78    }
79
80    fn is_connected(&self) -> bool {
81        self.client
82            .as_ref()
83            .is_some_and(MsfRpcClient::is_authenticated)
84    }
85
86    fn module_exists(&mut self, module_type: &str, name: &str) -> Result<bool> {
87        let client = self.client()?;
88        client
89            .module_exists(module_type, name)
90            .map_err(|e| AppError::Network(e.to_string()))
91    }
92
93    fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo> {
94        let client = self.client()?;
95        let raw = client
96            .module_info(module_type, name)
97            .map_err(|e| AppError::Network(e.to_string()))?;
98        parse_module_info(module_type, name, &raw)
99    }
100
101    fn run_module(
102        &mut self,
103        module_type: &str,
104        name: &str,
105        options: &BTreeMap<String, String>,
106        payload: Option<&str>,
107    ) -> Result<ModuleRunResult> {
108        let client = self.client()?;
109        let raw = client
110            .module_execute(module_type, name, options, payload)
111            .map_err(|e| AppError::Network(e.to_string()))?;
112        parse_run_result(&raw)
113    }
114
115    fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus> {
116        let client = self.client()?;
117        let started = std::time::Instant::now();
118        loop {
119            match client.job_list() {
120                Ok(list) => {
121                    let present = job_present(&list, job_id);
122                    if !present {
123                        return Ok(JobStatus {
124                            job_id: job_id.to_string(),
125                            running: false,
126                            status: "completed".to_string(),
127                            error: None,
128                        });
129                    }
130                }
131                Err(e) => {
132                    return Ok(JobStatus {
133                        job_id: job_id.to_string(),
134                        running: false,
135                        status: "failed".to_string(),
136                        error: Some(e.to_string()),
137                    });
138                }
139            }
140            if started.elapsed() >= timeout {
141                return Ok(JobStatus {
142                    job_id: job_id.to_string(),
143                    running: true,
144                    status: "still_running".to_string(),
145                    error: None,
146                });
147            }
148            std::thread::sleep(Duration::from_millis(1000));
149        }
150    }
151
152    fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
153        let client = self.client()?;
154        let raw = client
155            .session_list()
156            .map_err(|e| AppError::Network(e.to_string()))?;
157        Ok(parse_sessions(&raw))
158    }
159
160    fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
161        let workspace = self.workspace.clone();
162        let client = self.client()?;
163        let raw = client
164            .db_loots(&workspace)
165            .map_err(|e| AppError::Network(e.to_string()))?;
166        Ok(parse_loot(&raw))
167    }
168}
169
170impl MsfFramework {
171    fn client(&mut self) -> Result<&mut MsfRpcClient> {
172        self.client.as_mut().ok_or_else(|| {
173            AppError::Message("MSF-RPC not connected; call connect() first".to_string())
174        })
175    }
176}
177
178fn parse_module_info(module_type: &str, name: &str, raw: &MsgValue) -> Result<ModuleInfo> {
179    if raw.get("error").and_then(MsgValue::as_str) == Some("success") {
180        return Err(AppError::Message(format!(
181            "module not found on the connected instance: {module_type}/{name}"
182        )));
183    }
184    let rank = raw
185        .get("rank")
186        .and_then(MsgValue::as_str)
187        .unwrap_or("")
188        .to_string();
189    let description = raw
190        .get("description")
191        .and_then(MsgValue::as_str)
192        .unwrap_or("")
193        .to_string();
194
195    let mut platforms = parse_strings(raw, "platforms");
196    if platforms.is_empty() {
197        if let Some(single) = raw.get("platform").and_then(MsgValue::as_str) {
198            platforms.push(single.to_string());
199        }
200    }
201    let targets = parse_strings(raw, "targets");
202
203    let options = parse_options(raw);
204    let mut required_options = parse_strings(raw, "required_options");
205    if required_options.is_empty() {
206        required_options = options
207            .iter()
208            .filter(|opt| opt.required && opt.default.is_none())
209            .map(|opt| opt.name.clone())
210            .collect();
211    }
212
213    Ok(ModuleInfo {
214        name: name.to_string(),
215        module_type: module_type.to_string(),
216        rank,
217        description,
218        platforms,
219        targets,
220        options,
221        required_options,
222    })
223}
224
225fn parse_options(raw: &MsgValue) -> Vec<ModuleOption> {
226    let mut options = Vec::new();
227    if let Some(map) = raw.get("options").and_then(MsgValue::as_map) {
228        for (name, opt_val) in map {
229            let required = opt_val.get("required").is_some_and(|v| match v {
230                MsgValue::Bool(b) => *b,
231                MsgValue::String(s) => s == "true",
232                MsgValue::UInt(n) => *n != 0,
233                MsgValue::Int(n) => *n != 0,
234                _ => false,
235            });
236            let default = opt_val
237                .get("default")
238                .and_then(MsgValue::as_str)
239                .map(str::to_string);
240            let description = opt_val
241                .get("desc")
242                .and_then(MsgValue::as_str)
243                .unwrap_or("")
244                .to_string();
245            options.push(ModuleOption {
246                name: name.clone(),
247                required,
248                default,
249                description,
250            });
251        }
252    }
253    options
254}
255
256fn parse_run_result(raw: &MsgValue) -> Result<ModuleRunResult> {
257    let raw_text = format!("{raw:?}");
258
259    let error_flag = match raw.get("error") {
260        Some(MsgValue::Bool(b)) => *b,
261        Some(MsgValue::String(s)) => s == "true",
262        Some(MsgValue::UInt(n)) => *n != 0,
263        _ => false,
264    };
265    if error_flag {
266        let message = raw
267            .get("error_message")
268            .or_else(|| raw.get("error_string"))
269            .and_then(MsgValue::as_str)
270            .unwrap_or("module execution failed")
271            .to_string();
272        return Ok(ModuleRunResult {
273            job_id: None,
274            success: false,
275            message,
276            raw: raw_text,
277        });
278    }
279
280    let job_id = raw
281        .get("job_id")
282        .and_then(MsgValue::as_u64)
283        .map(|id| id.to_string());
284    let message = raw
285        .get("message")
286        .and_then(MsgValue::as_str)
287        .unwrap_or("module started")
288        .to_string();
289    Ok(ModuleRunResult {
290        job_id,
291        success: true,
292        message,
293        raw: raw_text,
294    })
295}
296
297fn job_present(list: &MsgValue, job_id: &str) -> bool {
298    if let Some(map) = list.get("jobs").and_then(MsgValue::as_map) {
299        map.keys().any(|id| id == job_id)
300    } else if let Some(map) = list.as_map() {
301        map.get("jobs").is_none() && map.keys().any(|id| id == job_id)
302    } else {
303        false
304    }
305}
306
307fn parse_sessions(raw: &MsgValue) -> Vec<SessionInfo> {
308    let mut sessions = Vec::new();
309    if let Some(map) = raw.get("sessions").and_then(MsgValue::as_map) {
310        for (id, val) in map {
311            let typ = val
312                .get("type")
313                .or_else(|| val.get("session_type"))
314                .and_then(MsgValue::as_str)
315                .unwrap_or("")
316                .to_string();
317            let target_host = val
318                .get("target_host")
319                .or_else(|| val.get("host"))
320                .and_then(MsgValue::as_str)
321                .unwrap_or("")
322                .to_string();
323            let platform = val
324                .get("platform")
325                .and_then(MsgValue::as_str)
326                .unwrap_or("")
327                .to_string();
328            let info = val
329                .get("info")
330                .and_then(MsgValue::as_str)
331                .unwrap_or("")
332                .to_string();
333            sessions.push(SessionInfo {
334                id: id.clone(),
335                typ,
336                target_host,
337                platform,
338                info,
339            });
340        }
341    }
342    sessions
343}
344
345fn parse_loot(raw: &MsgValue) -> Vec<LootInfo> {
346    let mut loot = Vec::new();
347    if let Some(array) = raw.get("loot").and_then(MsgValue::as_array) {
348        for val in array {
349            let id = val
350                .get("loot_id")
351                .or_else(|| val.get("id"))
352                .and_then(MsgValue::as_str)
353                .unwrap_or("")
354                .to_string();
355            let name = val
356                .get("name")
357                .and_then(MsgValue::as_str)
358                .unwrap_or("")
359                .to_string();
360            let content_type = val
361                .get("content_type")
362                .and_then(MsgValue::as_str)
363                .unwrap_or("")
364                .to_string();
365            let host = val
366                .get("host")
367                .and_then(MsgValue::as_str)
368                .unwrap_or("")
369                .to_string();
370            let path = val
371                .get("path")
372                .and_then(MsgValue::as_str)
373                .unwrap_or("")
374                .to_string();
375            loot.push(LootInfo {
376                id,
377                name,
378                content_type,
379                host,
380                path,
381            });
382        }
383    }
384    loot
385}
386
387fn parse_strings(raw: &MsgValue, key: &str) -> Vec<String> {
388    match raw.get(key) {
389        Some(MsgValue::Array(arr)) => arr
390            .iter()
391            .filter_map(MsgValue::as_str)
392            .map(str::to_string)
393            .collect(),
394        _ => Vec::new(),
395    }
396}