Skip to main content

ploit_malper/exploit/
mod.rs

1pub mod cli;
2pub mod models;
3pub mod msf;
4pub mod planner;
5
6use std::collections::BTreeMap;
7use std::time::Duration;
8
9pub use models::*;
10
11use crate::db::models::now_utc;
12use crate::error::Result;
13
14/// Execute a single planned step against the framework and report the
15/// outcome. Runs the module, monitors the resulting job, then correlates any
16/// new session or loot against the outcome.
17pub fn execute_step(
18    framework: &mut dyn ExploitFramework,
19    step: &ExploitStep,
20    payload: Option<&str>,
21    run_timeout: Duration,
22) -> Result<ExploitOutcome> {
23    let start_time = now_utc();
24    let options = step.resolved_options();
25    let result = framework.run_module(&step.module_type, &step.module_name, &options, payload)?;
26
27    let mut session_id = None;
28    let mut loot = Vec::new();
29    let mut status = ExploitStatus::Failed;
30    let mut error = None;
31    let job_id = result.job_id.clone();
32
33    if !result.success {
34        error = Some(result.message.clone());
35    } else if let Some(job) = &job_id {
36        let job_status = framework.monitor_job(job, run_timeout)?;
37        if job_status.error.is_some() {
38            error = job_status.error.clone();
39        } else if !job_status.running {
40            status = ExploitStatus::Completed;
41            if let Ok(sessions) = framework.list_sessions() {
42                session_id = sessions.first().map(|s| s.id.clone());
43            }
44            if let Ok(loot_items) = framework.list_loot() {
45                loot = loot_items.iter().map(|l| l.id.clone()).collect();
46            }
47        } else {
48            status = ExploitStatus::Running;
49        }
50    } else {
51        status = ExploitStatus::Completed;
52    }
53
54    Ok(ExploitOutcome {
55        step: step.clone(),
56        status,
57        job_id,
58        session_id,
59        error,
60        loot,
61        start_time,
62        finish_time: Some(now_utc()),
63    })
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::db::models::ExploitExecution;
70    use crate::db::Storage;
71
72    /// Deterministic framework stub for unit tests.
73    struct MockFramework {
74        workspace: String,
75        run_succeeds: bool,
76        job_id: String,
77        job_finishes: bool,
78    }
79
80    impl ExploitFramework for MockFramework {
81        fn name(&self) -> &'static str {
82            "mock"
83        }
84        fn workspace(&self) -> &str {
85            &self.workspace
86        }
87        fn set_workspace(&mut self, workspace: &str) {
88            self.workspace = workspace.to_string();
89        }
90        fn connect(&mut self) -> Result<()> {
91            Ok(())
92        }
93        fn disconnect(&mut self) {}
94        fn is_connected(&self) -> bool {
95            true
96        }
97        fn module_exists(&mut self, _t: &str, _n: &str) -> Result<bool> {
98            Ok(true)
99        }
100        fn module_info(&mut self, _t: &str, _n: &str) -> Result<ModuleInfo> {
101            Ok(ModuleInfo::default())
102        }
103        fn run_module(
104            &mut self,
105            _t: &str,
106            _n: &str,
107            _o: &BTreeMap<String, String>,
108            _p: Option<&str>,
109        ) -> Result<ModuleRunResult> {
110            if self.run_succeeds {
111                Ok(ModuleRunResult {
112                    job_id: Some(self.job_id.clone()),
113                    success: true,
114                    message: "started".to_string(),
115                    raw: String::new(),
116                })
117            } else {
118                Ok(ModuleRunResult {
119                    job_id: None,
120                    success: false,
121                    message: "boom".to_string(),
122                    raw: String::new(),
123                })
124            }
125        }
126        fn monitor_job(&mut self, job_id: &str, _timeout: Duration) -> Result<JobStatus> {
127            Ok(JobStatus {
128                job_id: job_id.to_string(),
129                running: !self.job_finishes,
130                status: if self.job_finishes {
131                    "completed".to_string()
132                } else {
133                    "still_running".to_string()
134                },
135                error: None,
136            })
137        }
138        fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
139            Ok(vec![SessionInfo {
140                id: "1".to_string(),
141                typ: "meterpreter".to_string(),
142                target_host: "10.0.0.5".to_string(),
143                platform: "linux".to_string(),
144                info: "test".to_string(),
145            }])
146        }
147        fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
148            Ok(vec![LootInfo {
149                id: "loot-1".to_string(),
150                name: "shell".to_string(),
151                content_type: "text/plain".to_string(),
152                host: "10.0.0.5".to_string(),
153                path: "/tmp/loot".to_string(),
154            }])
155        }
156    }
157
158    fn sample_step() -> ExploitStep {
159        ExploitStep {
160            module_type: MODULE_EXPLOIT.to_string(),
161            module_name: "exploit/multi/http/apache_path_traversal".to_string(),
162            cve: Some("CVE-2021-41773".to_string()),
163            vulnerability: "Apache path traversal".to_string(),
164            rank: "excellent".to_string(),
165            asset_id: "asset-1".to_string(),
166            finding_id: "finding-1".to_string(),
167            host: "10.0.0.5".to_string(),
168            service_port: Some(80),
169            service_protocol: Some("tcp".to_string()),
170            inferred_options: BTreeMap::from([("RHOSTS".to_string(), "10.0.0.5".to_string())]),
171            missing_options: vec!["LHOST".to_string()],
172            payload_compatibility: "generic".to_string(),
173            confidence: "high".to_string(),
174            default_selected: true,
175        }
176    }
177
178    #[test]
179    fn execute_step_records_completed_outcome() {
180        let mut framework = MockFramework {
181            workspace: "default".to_string(),
182            run_succeeds: true,
183            job_id: "7".to_string(),
184            job_finishes: true,
185        };
186        let outcome =
187            execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
188        assert_eq!(outcome.status, ExploitStatus::Completed);
189        assert_eq!(outcome.job_id.as_deref(), Some("7"));
190        assert_eq!(outcome.session_id.as_deref(), Some("1"));
191        assert!(outcome.loot.iter().any(|l| l == "loot-1"));
192        assert!(outcome.error.is_none());
193    }
194
195    #[test]
196    fn execute_step_marks_failure() {
197        let mut framework = MockFramework {
198            workspace: "default".to_string(),
199            run_succeeds: false,
200            job_id: String::new(),
201            job_finishes: true,
202        };
203        let outcome =
204            execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
205        assert_eq!(outcome.status, ExploitStatus::Failed);
206        assert_eq!(outcome.error.as_deref(), Some("boom"));
207    }
208
209    #[test]
210    fn execution_status_roundtrips() {
211        for status in [
212            ExploitStatus::Planned,
213            ExploitStatus::Running,
214            ExploitStatus::Completed,
215            ExploitStatus::Failed,
216            ExploitStatus::Skipped,
217            ExploitStatus::DryRun,
218        ] {
219            assert_eq!(ExploitStatus::parse(status.as_str()), status);
220        }
221    }
222
223    #[test]
224    fn persist_execution_to_sqlite_roundtrips() {
225        let mut storage = crate::db::sqlite::SqliteStorage::open(":memory:").unwrap();
226        storage.ensure_schema().unwrap();
227
228        let mut exec =
229            ExploitExecution::new("run123", "asset-1", "finding-1", "exploit/multi/http/x");
230        exec.module_type = MODULE_EXPLOIT.to_string();
231        exec.host = "10.0.0.5".to_string();
232        exec.status = ExploitStatus::Completed.as_str().to_string();
233        exec.job_id = Some("9".to_string());
234        exec.session_id = Some("2".to_string());
235        exec.loot = vec!["loot-9".to_string()];
236        exec.selected = true;
237        storage.upsert_exploit_execution(&exec).unwrap();
238
239        let loaded = storage
240            .get_exploit_execution(&exec.execution_id)
241            .unwrap()
242            .unwrap();
243        assert_eq!(loaded.run_id, "run123");
244        assert_eq!(loaded.module, "exploit/multi/http/x");
245        assert_eq!(loaded.status, "completed");
246        assert_eq!(loaded.job_id.as_deref(), Some("9"));
247        assert_eq!(loaded.session_id.as_deref(), Some("2"));
248        assert_eq!(loaded.loot, vec!["loot-9".to_string()]);
249        assert!(loaded.selected);
250
251        let by_run = storage.list_exploit_executions_for_run("run123").unwrap();
252        assert_eq!(by_run.len(), 1);
253        let all = storage.list_exploit_executions().unwrap();
254        assert_eq!(all.len(), 1);
255    }
256}
257
258/// A framework-agnostic exploitation backend.
259///
260/// The CLI and database layers only ever talk to this trait; Metasploit is one
261/// implementation behind it. This keeps the command and its history records
262/// reusable if a different framework is added later.
263pub trait ExploitFramework: Send {
264    fn name(&self) -> &'static str;
265    fn workspace(&self) -> &str;
266    fn set_workspace(&mut self, workspace: &str);
267    /// Verify connectivity and authenticate. Should be cheap when the client
268    /// already holds a valid token.
269    fn connect(&mut self) -> Result<()>;
270    fn disconnect(&mut self);
271    fn is_connected(&self) -> bool;
272
273    /// True when a module of the given type exists on the backend.
274    fn module_exists(&mut self, module_type: &str, name: &str) -> Result<bool>;
275
276    /// Fetch full module metadata.
277    fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo>;
278
279    /// Launch a module with fully resolved options.
280    fn run_module(
281        &mut self,
282        module_type: &str,
283        name: &str,
284        options: &BTreeMap<String, String>,
285        payload: Option<&str>,
286    ) -> Result<ModuleRunResult>;
287
288    /// Poll a background job until it settles, `timeout` expires, or the
289    /// optional `deadline` is reached.
290    fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus>;
291
292    /// List current sessions.
293    fn list_sessions(&mut self) -> Result<Vec<SessionInfo>>;
294
295    /// List loot for the active workspace.
296    fn list_loot(&mut self) -> Result<Vec<LootInfo>>;
297}