Skip to main content

ploit_malper/engine/
msfrpc.rs

1use std::collections::BTreeMap;
2use std::process::{Command, Stdio};
3
4use crate::error::Result;
5use crate::models::{
6    MSFHostCheckResult, MSFHostInfo, MSFHostsResult, MSFLoginResult, MSFWorkspaceInfo,
7    MSFWorkspacesResult,
8};
9use crate::msgpack::{decode, encode, MsgValue};
10
11#[derive(Debug, Clone)]
12pub struct MsfRpcClient {
13    host: String,
14    port: u16,
15    ssl: bool,
16    username: String,
17    password: String,
18    token: Option<String>,
19}
20
21impl MsfRpcClient {
22    pub fn new(host: &str, port: u16, username: &str, password: &str, ssl: bool) -> Self {
23        Self {
24            host: host.to_string(),
25            port,
26            ssl,
27            username: username.to_string(),
28            password: password.to_string(),
29            token: None,
30        }
31    }
32
33    pub fn login(&mut self) -> Result<MSFLoginResult> {
34        let url = build_url(&self.host, self.port, self.ssl);
35        // Array format: [method, ...params]
36        let payload = encode(&msg_array(vec![
37            msg_str("auth.login"),
38            msg_str(&self.username),
39            msg_str(&self.password),
40        ]));
41
42        match send_request(&url, &payload, 10) {
43            Ok(val) => {
44                let success = val.get("result").and_then(MsgValue::as_str) == Some("success");
45                let token = val
46                    .get("token")
47                    .and_then(MsgValue::as_str)
48                    .unwrap_or("")
49                    .to_string();
50
51                if success && !token.is_empty() {
52                    self.token = Some(token.clone());
53                    Ok(MSFLoginResult {
54                        success: true,
55                        token,
56                        error: String::new(),
57                    })
58                } else {
59                    let error_msg = val
60                        .get("error_message")
61                        .or_else(|| val.get("error_string"))
62                        .and_then(MsgValue::as_str)
63                        .unwrap_or("Authentication failed");
64                    Ok(MSFLoginResult {
65                        success: false,
66                        token: String::new(),
67                        error: error_msg.to_string(),
68                    })
69                }
70            }
71            Err(error) => Ok(MSFLoginResult {
72                success: false,
73                token: String::new(),
74                error,
75            }),
76        }
77    }
78
79    pub fn logout(&mut self) {
80        if let Some(token) = self.token.clone() {
81            let url = build_url(&self.host, self.port, self.ssl);
82            // Array format: [method, token]
83            let payload = encode(&msg_array(vec![msg_str("auth.logout"), msg_str(&token)]));
84            let _ = send_request(&url, &payload, 5);
85        }
86        self.token = None;
87    }
88
89    pub fn is_authenticated(&self) -> bool {
90        self.token.is_some()
91    }
92
93    pub fn get_workspaces(&self) -> Result<MSFWorkspacesResult> {
94        let token = match self.token.as_ref() {
95            Some(token) => token.clone(),
96            None => {
97                return Ok(MSFWorkspacesResult {
98                    workspaces: vec![],
99                    error: "Not authenticated".to_string(),
100                });
101            }
102        };
103
104        let url = build_url(&self.host, self.port, self.ssl);
105        // Array format: [method, token]
106        let payload = encode(&msg_array(vec![
107            msg_str("db.workspaces"),
108            msg_str(&token),
109        ]));
110
111        match send_request(&url, &payload, 15) {
112            Ok(val) => Ok(MSFWorkspacesResult {
113                workspaces: extract_workspaces(&val),
114                error: String::new(),
115            }),
116            Err(error) => Ok(MSFWorkspacesResult {
117                workspaces: vec![],
118                error,
119            }),
120        }
121    }
122
123    pub fn get_hosts(&self, workspace: Option<&str>) -> Result<MSFHostsResult> {
124        let token = match self.token.as_ref() {
125            Some(token) => token.clone(),
126            None => {
127                return Ok(MSFHostsResult {
128                    hosts: vec![],
129                    error: "Not authenticated".to_string(),
130                });
131            }
132        };
133
134        let url = build_url(&self.host, self.port, self.ssl);
135        // Array format: [method, token, options_hash]
136        // The RPC dispatcher extracts the token from args for auth,
137        // so we need the workspace filter as the next parameter
138        let mut params = vec![msg_str("db.hosts"), msg_str(&token)];
139        if let Some(workspace) = workspace {
140            params.push(MsgValue::Map(BTreeMap::from([(
141                "workspace".to_string(),
142                msg_str(workspace),
143            )])));
144        } else {
145            params.push(MsgValue::Map(BTreeMap::new()));
146        }
147        let payload = encode(&msg_array(params));
148
149        match send_request(&url, &payload, 15) {
150            Ok(val) => Ok(MSFHostsResult {
151                hosts: extract_hosts(&val),
152                error: String::new(),
153            }),
154            Err(error) => Ok(MSFHostsResult {
155                hosts: vec![],
156                error,
157            }),
158        }
159    }
160
161    pub fn check_host_exists(
162        &self,
163        address: &str,
164        workspace: Option<&str>,
165    ) -> Result<MSFHostCheckResult> {
166        let hosts = self.get_hosts(workspace)?;
167        Ok(MSFHostCheckResult {
168            exists: hosts.hosts.iter().any(|host| host.address == address),
169            error: hosts.error,
170        })
171    }
172}
173
174fn build_url(host: &str, port: u16, ssl: bool) -> String {
175    let scheme = if ssl { "https" } else { "http" };
176    format!("{}://{}:{}/api/", scheme, host, port)
177}
178
179fn send_request(
180    url: &str,
181    payload: &[u8],
182    timeout_secs: u64,
183) -> std::result::Result<MsgValue, String> {
184    let mut child = Command::new("curl")
185        .arg("-sS")
186        .arg("--max-time")
187        .arg(timeout_secs.to_string())
188        .arg("-H")
189        .arg("Content-Type: binary/message-pack")
190        .arg("-H")
191        .arg("Accept: binary/message-pack")
192        .arg("-i")
193        .arg("--data-binary")
194        .arg("@-")
195        .arg(url)
196        .stdin(Stdio::piped())
197        .stdout(Stdio::piped())
198        .stderr(Stdio::piped())
199        .spawn()
200        .map_err(|e| format!("failed to spawn curl: {e}"))?;
201
202    if let Some(stdin) = child.stdin.as_mut() {
203        use std::io::Write;
204        stdin.write_all(payload).map_err(|e| format!("failed to write request body: {e}"))?;
205    }
206
207    let output = child.wait_with_output().map_err(|e| format!("curl wait failed: {e}"))?;
208
209    let stdout = &output.stdout;
210    if stdout.is_empty() {
211        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
212        if !stderr.is_empty() {
213            return Err(stderr);
214        }
215        return Err("empty response from server".to_string());
216    }
217
218    // Split headers from body (curl -i includes headers in stdout)
219    let body = if let Some(pos) = find_header_body_boundary(stdout) {
220        let header_part = String::from_utf8_lossy(&stdout[..pos]);
221        let status_line = header_part.lines().next().unwrap_or("unknown");
222        eprintln!("[MSF-RPC] HTTP {status_line}");
223        &stdout[pos..]
224    } else {
225        &stdout[..]
226    };
227
228    if body.is_empty() {
229        return Err("empty response body from server".to_string());
230    }
231
232    decode(body).map_err(|e| {
233        let dump_len = body.len().min(512);
234        let hex_dump: Vec<String> = body[..dump_len]
235            .iter()
236            .map(|b| format!("{:02x}", b))
237            .collect();
238        let utf8_dump = String::from_utf8_lossy(&body[..dump_len]);
239        format!(
240            "{}\n  response body hex ({} bytes): {}\n  response body utf8: {}",
241            e,
242            dump_len,
243            hex_dump.join(" "),
244            utf8_dump
245        )
246    })
247}
248
249fn find_header_body_boundary(data: &[u8]) -> Option<usize> {
250    data.windows(4)
251        .position(|w| w == b"\r\n\r\n")
252        .map(|p| p + 4)
253}
254
255fn msg_str(value: &str) -> MsgValue {
256    MsgValue::String(value.to_string())
257}
258
259fn msg_array(values: Vec<MsgValue>) -> MsgValue {
260    MsgValue::Array(values)
261}
262
263fn extract_workspaces(val: &MsgValue) -> Vec<MSFWorkspaceInfo> {
264    let mut workspaces = Vec::new();
265    if let Some(map) = val.as_map() {
266        if let Some(MsgValue::Array(ws_arr)) = map.get("workspaces") {
267            for ws in ws_arr {
268                if let Some(ws_map) = ws.as_map() {
269                    let name = ws_map
270                        .get("name")
271                        .and_then(MsgValue::as_str)
272                        .unwrap_or("")
273                        .to_string();
274                    let scope = ws_map
275                        .get("scope")
276                        .and_then(MsgValue::as_str)
277                        .unwrap_or("")
278                        .to_string();
279                    let host_count = ws_map
280                        .get("hosts_count")
281                        .or_else(|| ws_map.get("host_count"))
282                        .and_then(MsgValue::as_u64)
283                        .unwrap_or(0);
284
285                    workspaces.push(MSFWorkspaceInfo {
286                        name,
287                        scope,
288                        host_count,
289                    });
290                }
291            }
292        }
293    }
294    workspaces
295}
296
297fn extract_hosts(val: &MsgValue) -> Vec<MSFHostInfo> {
298    let mut hosts = Vec::new();
299    if let Some(map) = val.as_map() {
300        if let Some(MsgValue::Array(host_arr)) = map.get("hosts") {
301            for host in host_arr {
302                if let Some(host_map) = host.as_map() {
303                    let address = host_map
304                        .get("address")
305                        .and_then(MsgValue::as_str)
306                        .unwrap_or("")
307                        .to_string();
308                    let os_name = host_map
309                        .get("os_name")
310                        .and_then(MsgValue::as_str)
311                        .unwrap_or("")
312                        .to_string();
313                    let os_flavor = host_map
314                        .get("os_flavor")
315                        .and_then(MsgValue::as_str)
316                        .unwrap_or("")
317                        .to_string();
318                    let state = host_map
319                        .get("state")
320                        .and_then(MsgValue::as_str)
321                        .unwrap_or("")
322                        .to_string();
323                    let notes_count = host_map
324                        .get("notes_count")
325                        .and_then(MsgValue::as_u64)
326                        .unwrap_or(0);
327
328                    hosts.push(MSFHostInfo {
329                        address,
330                        os_name,
331                        os_flavor,
332                        state,
333                        notes_count,
334                    });
335                }
336            }
337        }
338    }
339    hosts
340}