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_info(&mut self, _t: &str, _n: &str) -> Result<ModuleInfo> {
98            Ok(ModuleInfo::default())
99        }
100        fn run_module(
101            &mut self,
102            _t: &str,
103            _n: &str,
104            _o: &BTreeMap<String, String>,
105            _p: Option<&str>,
106        ) -> Result<ModuleRunResult> {
107            if self.run_succeeds {
108                Ok(ModuleRunResult {
109                    job_id: Some(self.job_id.clone()),
110                    success: true,
111                    message: "started".to_string(),
112                    raw: String::new(),
113                })
114            } else {
115                Ok(ModuleRunResult {
116                    job_id: None,
117                    success: false,
118                    message: "boom".to_string(),
119                    raw: String::new(),
120                })
121            }
122        }
123        fn monitor_job(&mut self, job_id: &str, _timeout: Duration) -> Result<JobStatus> {
124            Ok(JobStatus {
125                job_id: job_id.to_string(),
126                running: !self.job_finishes,
127                status: if self.job_finishes {
128                    "completed".to_string()
129                } else {
130                    "still_running".to_string()
131                },
132                error: None,
133            })
134        }
135        fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
136            Ok(vec![SessionInfo {
137                id: "1".to_string(),
138                typ: "meterpreter".to_string(),
139                target_host: "10.0.0.5".to_string(),
140                platform: "linux".to_string(),
141                info: "test".to_string(),
142            }])
143        }
144        fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
145            Ok(vec![LootInfo {
146                id: "loot-1".to_string(),
147                name: "shell".to_string(),
148                content_type: "text/plain".to_string(),
149                host: "10.0.0.5".to_string(),
150                path: "/tmp/loot".to_string(),
151            }])
152        }
153    }
154
155    fn sample_step() -> ExploitStep {
156        ExploitStep {
157            module_type: MODULE_EXPLOIT.to_string(),
158            module_name: "exploit/multi/http/apache_path_traversal".to_string(),
159            cve: Some("CVE-2021-41773".to_string()),
160            vulnerability: "Apache path traversal".to_string(),
161            rank: "excellent".to_string(),
162            asset_id: "asset-1".to_string(),
163            finding_id: "finding-1".to_string(),
164            host: "10.0.0.5".to_string(),
165            service_port: Some(80),
166            service_protocol: Some("tcp".to_string()),
167            inferred_options: BTreeMap::from([("RHOSTS".to_string(), "10.0.0.5".to_string())]),
168            missing_options: vec!["LHOST".to_string()],
169            payload_compatibility: "generic".to_string(),
170            confidence: "high".to_string(),
171            default_selected: true,
172        }
173    }
174
175    #[test]
176    fn execute_step_records_completed_outcome() {
177        let mut framework = MockFramework {
178            workspace: "default".to_string(),
179            run_succeeds: true,
180            job_id: "7".to_string(),
181            job_finishes: true,
182        };
183        let outcome =
184            execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
185        assert_eq!(outcome.status, ExploitStatus::Completed);
186        assert_eq!(outcome.job_id.as_deref(), Some("7"));
187        assert_eq!(outcome.session_id.as_deref(), Some("1"));
188        assert!(outcome.loot.iter().any(|l| l == "loot-1"));
189        assert!(outcome.error.is_none());
190    }
191
192    #[test]
193    fn execute_step_marks_failure() {
194        let mut framework = MockFramework {
195            workspace: "default".to_string(),
196            run_succeeds: false,
197            job_id: String::new(),
198            job_finishes: true,
199        };
200        let outcome =
201            execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
202        assert_eq!(outcome.status, ExploitStatus::Failed);
203        assert_eq!(outcome.error.as_deref(), Some("boom"));
204    }
205
206    #[test]
207    fn execution_status_roundtrips() {
208        for status in [
209            ExploitStatus::Planned,
210            ExploitStatus::Running,
211            ExploitStatus::Completed,
212            ExploitStatus::Failed,
213            ExploitStatus::Skipped,
214            ExploitStatus::DryRun,
215        ] {
216            assert_eq!(ExploitStatus::parse(status.as_str()), status);
217        }
218    }
219
220    #[test]
221    fn persist_execution_to_sqlite_roundtrips() {
222        let mut storage = crate::db::sqlite::SqliteStorage::open(":memory:").unwrap();
223        storage.ensure_schema().unwrap();
224
225        let mut exec =
226            ExploitExecution::new("run123", "asset-1", "finding-1", "exploit/multi/http/x");
227        exec.module_type = MODULE_EXPLOIT.to_string();
228        exec.host = "10.0.0.5".to_string();
229        exec.status = ExploitStatus::Completed.as_str().to_string();
230        exec.job_id = Some("9".to_string());
231        exec.session_id = Some("2".to_string());
232        exec.loot = vec!["loot-9".to_string()];
233        exec.selected = true;
234        storage.upsert_exploit_execution(&exec).unwrap();
235
236        let loaded = storage
237            .get_exploit_execution(&exec.execution_id)
238            .unwrap()
239            .unwrap();
240        assert_eq!(loaded.run_id, "run123");
241        assert_eq!(loaded.module, "exploit/multi/http/x");
242        assert_eq!(loaded.status, "completed");
243        assert_eq!(loaded.job_id.as_deref(), Some("9"));
244        assert_eq!(loaded.session_id.as_deref(), Some("2"));
245        assert_eq!(loaded.loot, vec!["loot-9".to_string()]);
246        assert!(loaded.selected);
247
248        let by_run = storage.list_exploit_executions_for_run("run123").unwrap();
249        assert_eq!(by_run.len(), 1);
250        let all = storage.list_exploit_executions().unwrap();
251        assert_eq!(all.len(), 1);
252    }
253}
254
255/// A framework-agnostic exploitation backend.
256///
257/// The CLI and database layers only ever talk to this trait; Metasploit is one
258/// implementation behind it. This keeps the command and its history records
259/// reusable if a different framework is added later.
260pub trait ExploitFramework: Send {
261    fn name(&self) -> &'static str;
262    fn workspace(&self) -> &str;
263    fn set_workspace(&mut self, workspace: &str);
264    /// Verify connectivity and authenticate. Should be cheap when the client
265    /// already holds a valid token.
266    fn connect(&mut self) -> Result<()>;
267    fn disconnect(&mut self);
268    fn is_connected(&self) -> bool;
269
270    /// Fetch full module metadata. Backends return an error whose variant is
271    /// `AppError::RpcModuleNotFound` when the module does not exist, so
272    /// callers can tell "missing" apart from a genuine RPC failure.
273    fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo>;
274
275    /// Launch a module with fully resolved options.
276    fn run_module(
277        &mut self,
278        module_type: &str,
279        name: &str,
280        options: &BTreeMap<String, String>,
281        payload: Option<&str>,
282    ) -> Result<ModuleRunResult>;
283
284    /// Poll a background job until it settles, `timeout` expires, or the
285    /// optional `deadline` is reached.
286    fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus>;
287
288    /// List current sessions.
289    fn list_sessions(&mut self) -> Result<Vec<SessionInfo>>;
290
291    /// List loot for the active workspace.
292    fn list_loot(&mut self) -> Result<Vec<LootInfo>>;
293}