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_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo> {
87        let client = self.client()?;
88        let raw = client.module_info(module_type, name)?;
89        let mut info = parse_module_info(module_type, name, &raw)?;
90        // `module.info` returns metadata only; the real datastore options come
91        // from `module.options`. Merge them so required options and defaults
92        // reflect the connected instance rather than offline hints.
93        if let Ok(options_raw) = client.module_options(module_type, name) {
94            let options = parse_options(&options_raw);
95            info.options = options.clone();
96            let required: Vec<String> = options
97                .iter()
98                .filter(|opt| opt.required)
99                .map(|opt| opt.name.clone())
100                .collect();
101            if !required.is_empty() {
102                info.required_options = required;
103            }
104        }
105        Ok(info)
106    }
107
108    fn run_module(
109        &mut self,
110        module_type: &str,
111        name: &str,
112        options: &BTreeMap<String, String>,
113        payload: Option<&str>,
114    ) -> Result<ModuleRunResult> {
115        let client = self.client()?;
116        let raw = client.module_execute(module_type, name, options, payload)?;
117        parse_run_result(&raw)
118    }
119
120    fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus> {
121        let client = self.client()?;
122        let started = std::time::Instant::now();
123        loop {
124            match client.job_list() {
125                Ok(list) => {
126                    let present = job_present(&list, job_id);
127                    if !present {
128                        return Ok(JobStatus {
129                            job_id: job_id.to_string(),
130                            running: false,
131                            status: "completed".to_string(),
132                            error: None,
133                        });
134                    }
135                }
136                Err(e) => {
137                    return Ok(JobStatus {
138                        job_id: job_id.to_string(),
139                        running: false,
140                        status: "failed".to_string(),
141                        error: Some(e.to_string()),
142                    });
143                }
144            }
145            if started.elapsed() >= timeout {
146                return Ok(JobStatus {
147                    job_id: job_id.to_string(),
148                    running: true,
149                    status: "still_running".to_string(),
150                    error: None,
151                });
152            }
153            std::thread::sleep(Duration::from_millis(1000));
154        }
155    }
156
157    fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
158        let client = self.client()?;
159        let raw = client
160            .session_list()
161            .map_err(|e| AppError::Network(e.to_string()))?;
162        Ok(parse_sessions(&raw))
163    }
164
165    fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
166        let workspace = self.workspace.clone();
167        let client = self.client()?;
168        let raw = client
169            .db_loots(&workspace)
170            .map_err(|e| AppError::Network(e.to_string()))?;
171        Ok(parse_loot(&raw))
172    }
173}
174
175impl MsfFramework {
176    fn client(&mut self) -> Result<&mut MsfRpcClient> {
177        self.client.as_mut().ok_or_else(|| {
178            AppError::Message("MSF-RPC not connected; call connect() first".to_string())
179        })
180    }
181}
182
183fn parse_module_info(module_type: &str, name: &str, raw: &MsgValue) -> Result<ModuleInfo> {
184    let rank = raw
185        .get("rank")
186        .and_then(MsgValue::as_str)
187        .map(str::to_string)
188        .or_else(|| {
189            // Modern servers report rank as a numeric value; translate it back
190            // to the familiar label so the plan stays readable.
191            raw.get("rank").and_then(MsgValue::as_u64).map(rank_label)
192        })
193        .unwrap_or_default();
194    let description = raw
195        .get("description")
196        .and_then(MsgValue::as_str)
197        .unwrap_or("")
198        .to_string();
199
200    let mut platforms = parse_strings(raw, "platforms");
201    if platforms.is_empty() {
202        if let Some(single) = raw.get("platform").and_then(MsgValue::as_str) {
203            platforms.push(single.to_string());
204        }
205    }
206    let targets = parse_strings(raw, "targets");
207
208    let options = parse_options(raw);
209    let mut required_options = parse_strings(raw, "required_options");
210    if required_options.is_empty() {
211        required_options = options
212            .iter()
213            .filter(|opt| opt.required && opt.default.is_none())
214            .map(|opt| opt.name.clone())
215            .collect();
216    }
217
218    Ok(ModuleInfo {
219        name: name.to_string(),
220        module_type: module_type.to_string(),
221        rank,
222        description,
223        platforms,
224        targets,
225        options,
226        required_options,
227    })
228}
229
230/// Translate Metasploit's numeric rank value to the familiar label.
231fn rank_label(rank: u64) -> String {
232    match rank {
233        0 => "manual".to_string(),
234        100 => "low".to_string(),
235        200 => "average".to_string(),
236        300 => "normal".to_string(),
237        400 => "good".to_string(),
238        500 => "great".to_string(),
239        600 => "excellent".to_string(),
240        other => format!("rank-{other}"),
241    }
242}
243
244fn parse_options(raw: &MsgValue) -> Vec<ModuleOption> {
245    let mut options = Vec::new();
246    if let Some(map) = raw.get("options").and_then(MsgValue::as_map) {
247        for (name, opt_val) in map {
248            let required = opt_val.get("required").is_some_and(|v| match v {
249                MsgValue::Bool(b) => *b,
250                MsgValue::String(s) => s == "true",
251                MsgValue::UInt(n) => *n != 0,
252                MsgValue::Int(n) => *n != 0,
253                _ => false,
254            });
255            let default = opt_val
256                .get("default")
257                .and_then(MsgValue::as_str)
258                .map(str::to_string);
259            let description = opt_val
260                .get("desc")
261                .and_then(MsgValue::as_str)
262                .unwrap_or("")
263                .to_string();
264            options.push(ModuleOption {
265                name: name.clone(),
266                required,
267                default,
268                description,
269            });
270        }
271    }
272    options
273}
274
275fn parse_run_result(raw: &MsgValue) -> Result<ModuleRunResult> {
276    let raw_text = format!("{raw:?}");
277
278    let error_flag = match raw.get("error") {
279        Some(MsgValue::Bool(b)) => *b,
280        Some(MsgValue::String(s)) => s == "true",
281        Some(MsgValue::UInt(n)) => *n != 0,
282        _ => false,
283    };
284    if error_flag {
285        let message = raw
286            .get("error_message")
287            .or_else(|| raw.get("error_string"))
288            .and_then(MsgValue::as_str)
289            .unwrap_or("module execution failed")
290            .to_string();
291        return Ok(ModuleRunResult {
292            job_id: None,
293            success: false,
294            message,
295            raw: raw_text,
296        });
297    }
298
299    let job_id = raw
300        .get("job_id")
301        .and_then(MsgValue::as_u64)
302        .map(|id| id.to_string());
303    let message = raw
304        .get("message")
305        .and_then(MsgValue::as_str)
306        .unwrap_or("module started")
307        .to_string();
308    Ok(ModuleRunResult {
309        job_id,
310        success: true,
311        message,
312        raw: raw_text,
313    })
314}
315
316fn job_present(list: &MsgValue, job_id: &str) -> bool {
317    if let Some(map) = list.get("jobs").and_then(MsgValue::as_map) {
318        map.keys().any(|id| id == job_id)
319    } else if let Some(map) = list.as_map() {
320        map.get("jobs").is_none() && map.keys().any(|id| id == job_id)
321    } else {
322        false
323    }
324}
325
326fn parse_sessions(raw: &MsgValue) -> Vec<SessionInfo> {
327    let mut sessions = Vec::new();
328    if let Some(map) = raw.get("sessions").and_then(MsgValue::as_map) {
329        for (id, val) in map {
330            let typ = val
331                .get("type")
332                .or_else(|| val.get("session_type"))
333                .and_then(MsgValue::as_str)
334                .unwrap_or("")
335                .to_string();
336            let target_host = val
337                .get("target_host")
338                .or_else(|| val.get("host"))
339                .and_then(MsgValue::as_str)
340                .unwrap_or("")
341                .to_string();
342            let platform = val
343                .get("platform")
344                .and_then(MsgValue::as_str)
345                .unwrap_or("")
346                .to_string();
347            let info = val
348                .get("info")
349                .and_then(MsgValue::as_str)
350                .unwrap_or("")
351                .to_string();
352            sessions.push(SessionInfo {
353                id: id.clone(),
354                typ,
355                target_host,
356                platform,
357                info,
358            });
359        }
360    }
361    sessions
362}
363
364fn parse_loot(raw: &MsgValue) -> Vec<LootInfo> {
365    let mut loot = Vec::new();
366    if let Some(array) = raw.get("loot").and_then(MsgValue::as_array) {
367        for val in array {
368            let id = val
369                .get("loot_id")
370                .or_else(|| val.get("id"))
371                .and_then(MsgValue::as_str)
372                .unwrap_or("")
373                .to_string();
374            let name = val
375                .get("name")
376                .and_then(MsgValue::as_str)
377                .unwrap_or("")
378                .to_string();
379            let content_type = val
380                .get("content_type")
381                .and_then(MsgValue::as_str)
382                .unwrap_or("")
383                .to_string();
384            let host = val
385                .get("host")
386                .and_then(MsgValue::as_str)
387                .unwrap_or("")
388                .to_string();
389            let path = val
390                .get("path")
391                .and_then(MsgValue::as_str)
392                .unwrap_or("")
393                .to_string();
394            loot.push(LootInfo {
395                id,
396                name,
397                content_type,
398                host,
399                path,
400            });
401        }
402    }
403    loot
404}
405
406fn parse_strings(raw: &MsgValue, key: &str) -> Vec<String> {
407    match raw.get(key) {
408        Some(MsgValue::Array(arr)) => arr
409            .iter()
410            .filter_map(MsgValue::as_str)
411            .map(str::to_string)
412            .collect(),
413        _ => Vec::new(),
414    }
415}