Skip to main content

mythic/
agent.rs

1//! High-level agent facade — build, send, and parse Mythic protocol messages.
2
3use std::string::String;
4use std::vec::Vec;
5use uuid::Uuid;
6
7#[cfg(feature = "rsa-staging")]
8use crate::protocol::checkin::RespCheckin;
9use crate::protocol::checkin::{self, DirectResult};
10use crate::protocol::codec::{
11    Aes256HmacCrypto, decode_message, decode_message_plain, encode_message, encode_message_plain,
12};
13use crate::protocol::{
14    AgentExtras, AgentMessageExtras, ReqCheckin, ReqGetTasking, ReqPostResponse, RespGetTasking,
15    RespPostResponse,
16};
17use crate::transport::C2Transport;
18use crate::{MythicError, MythicResult};
19
20/// Post-checkin phase — holds the callback UUID assigned by Mythic.
21///
22/// Encryption state is kept on the [`C2Transport`] via
23/// [`get_aes_psk`](C2Transport::get_aes_psk) /
24/// [`set_aes_psk`](C2Transport::set_aes_psk) so the same agent
25/// can switch transports without duplicating key state.
26///
27/// # Examples
28///
29/// ```no_run
30/// use mythic::{C2Transport, MythicAgent, MythicError};
31/// use uuid::Uuid;
32///
33/// # struct HttpC2;
34/// # impl C2Transport for HttpC2 {
35/// #     fn checkin(&self, p: &str) -> Result<String, MythicError> { Ok(String::new()) }
36/// #     fn get_tasking(&self, p: &str) -> Result<String, MythicError> { Ok(String::new()) }
37/// #     fn post_response(&self, p: &str) -> Result<String, MythicError> { Ok(String::new()) }
38/// # }
39/// let mut c2 = HttpC2;
40/// let payload_uuid = Uuid::parse_str("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee").unwrap();
41///
42/// let agent = MythicAgent::easy_checkin(
43///     payload_uuid,
44///     &mut c2,
45///     vec!["10.0.0.1".into()],
46///     Some("linux".into()),
47///     Some("root".into()),
48///     Some("web01".into()),
49///     Some(1337),
50///     Some("x86_64".into()),
51///     None, None, None, None, None, None,
52/// )
53/// .unwrap();
54/// println!("callback UUID: {}", agent.callback_uuid());
55/// ```
56#[derive(Debug)]
57pub struct MythicAgent {
58    pub callback_uuid: Uuid,
59}
60
61impl MythicAgent {
62    pub fn new(payload_uuid: Uuid) -> Self {
63        Self {
64            callback_uuid: payload_uuid,
65        }
66    }
67
68    pub fn callback_uuid(&self) -> Uuid {
69        self.callback_uuid
70    }
71
72    // ── Core message flow ──────────────────────────────────────
73
74    /// One-shot checkin — create an agent and check it in, no `new()` needed.
75    ///
76    /// For full control use [`checkin`](Self::checkin) with a pre-built
77    /// [`ReqCheckin`].
78    #[allow(clippy::too_many_arguments)]
79    pub fn easy_checkin<C: C2Transport>(
80        payload_uuid: Uuid,
81        c2: &mut C,
82        ips: Vec<String>,
83        os: Option<String>,
84        user: Option<String>,
85        host: Option<String>,
86        pid: Option<u32>,
87        architecture: Option<String>,
88        domain: Option<String>,
89        integrity_level: Option<u32>,
90        external_ip: Option<String>,
91        encryption_key: Option<String>,
92        decryption_key: Option<String>,
93        process_name: Option<String>,
94    ) -> MythicResult<Self> {
95        let req = ReqCheckin::new(
96            payload_uuid,
97            ips,
98            os,
99            user,
100            host,
101            pid,
102            architecture,
103            domain,
104            integrity_level,
105            external_ip,
106            encryption_key,
107            decryption_key,
108            process_name,
109        );
110        Self::new(payload_uuid).checkin(req, c2)
111    }
112
113    /// Perform a direct checkin (plaintext or static-key PSK).
114    ///
115    /// The mode is determined automatically from the transport
116    /// via [`C2Transport::get_aes_psk`].  `req.uuid` must be the payload
117    /// UUID; it is used both in the JSON body and the wire framing.
118    ///
119    /// This method takes `&mut C` because RSA/translation staging may
120    /// negotiate a new session key that must be stored back into the transport.
121    pub fn checkin<C: C2Transport>(mut self, req: ReqCheckin, c2: &mut C) -> MythicResult<Self> {
122        let payload_uuid = req.uuid;
123
124        if c2.encrypted_exchange_check() {
125            #[cfg(feature = "rsa-staging")]
126            return self.rsa_checkin(req, c2);
127            #[cfg(not(feature = "rsa-staging"))]
128            return Err(MythicError::KeyExchangeFailed);
129        }
130
131        let needs_crypto = c2.get_aes_psk().is_some();
132        let iv = if needs_crypto {
133            c2.random_iv()?
134        } else {
135            [0u8; 16]
136        };
137
138        let DirectResult { callback_uuid, .. } =
139            checkin::direct_checkin(c2, &req, payload_uuid, &iv)?;
140
141        self.callback_uuid = callback_uuid;
142
143        Ok(self)
144    }
145
146    /// Perform an RSA encrypted key exchange checkin.
147    ///
148    /// This is used when the transport reports
149    /// [`C2Transport::encrypted_exchange_check`] as `true`. It executes the
150    /// full `staging_rsa` → temp key → normal checkin flow.
151    #[cfg(feature = "rsa-staging")]
152    pub fn rsa_checkin<C: C2Transport>(
153        mut self,
154        req: ReqCheckin,
155        c2: &mut C,
156    ) -> MythicResult<Self> {
157        use crate::protocol::checkin::{RsaStagingResult, rsa_staging_checkin};
158        use crate::protocol::codec::encode_message;
159        use crate::protocol::crypto::random_iv;
160
161        let payload_uuid = req.uuid;
162        let RsaStagingResult {
163            temp_uuid, crypto, ..
164        } = rsa_staging_checkin(&*c2, payload_uuid)?;
165
166        // Persist the negotiated session key back into the transport so that
167        // subsequent get_tasking/post_response calls use it.
168        c2.set_aes_psk(&crypto.key_b64());
169
170        let iv = random_iv()?;
171        let packed = encode_message(&req, temp_uuid, &crypto, &iv)?;
172        let response = c2.checkin(&packed)?;
173        let (_, resp): (Uuid, RespCheckin) =
174            crate::protocol::codec::decode_message(&response, Some(temp_uuid), &crypto)?;
175
176        if resp.status != "success" {
177            return Err(MythicError::protocol(format!(
178                "checkin rejected after RSA staging: status={}",
179                resp.status
180            )));
181        }
182
183        self.callback_uuid = resp.id;
184        Ok(self)
185    }
186
187    /// Poll for new tasks from the Mythic server (no extras).
188    ///
189    /// `tasking_size` of `-1` asks Mythic for all available tasks.
190    pub fn get_tasking<C: C2Transport>(
191        &self,
192        tasking_size: i32,
193        c2: &C,
194    ) -> MythicResult<RespGetTasking> {
195        self.get_tasking_with(tasking_size, c2, AgentMessageExtras::default())
196    }
197
198    /// Poll for new tasks, carrying delegates, SOCKS, RPFWD, interactive data,
199    /// edges, alerts, and/or responses alongside the request.
200    pub fn get_tasking_with<C: C2Transport>(
201        &self,
202        tasking_size: i32,
203        c2: &C,
204        extras: AgentMessageExtras,
205    ) -> MythicResult<RespGetTasking> {
206        let req = ReqGetTasking::with_extras(tasking_size, extras);
207
208        if let Some(key_b64) = c2.get_aes_psk() {
209            let crypto = Aes256HmacCrypto::from_base64_key(&key_b64)?;
210            let iv = c2.random_iv()?;
211            let packed = encode_message(&req, self.callback_uuid, &crypto, &iv)?;
212            let response = c2.get_tasking(&packed)?;
213            decode_message(&response, Some(self.callback_uuid), &crypto).map(|(_, r)| r)
214        } else {
215            let packed = encode_message_plain(&req, self.callback_uuid)?;
216            let response = c2.get_tasking(&packed)?;
217            decode_message_plain(&response, Some(self.callback_uuid)).map(|(_, r)| r)
218        }
219    }
220
221    /// Send task responses back to the Mythic server (no extras).
222    ///
223    /// The `responses` vector contains the output of completed (or in-progress)
224    /// tasks.  Use [`crate::protocol::TaskResponse`] builders like
225    /// [`crate::protocol::TaskResponse::completed`] or construct custom
226    /// responses with hooking-feature data.
227    pub fn post_response<C: C2Transport>(
228        &self,
229        responses: Vec<crate::protocol::TaskResponse>,
230        c2: &C,
231    ) -> MythicResult<RespPostResponse> {
232        self.post_response_with(responses, c2, AgentExtras::default())
233    }
234
235    /// Send task responses, carrying delegates, SOCKS, RPFWD, interactive data,
236    /// edges, and/or alerts alongside the response.
237    ///
238    /// `shared` is the [`AgentExtras`] portion — it does **not** contain
239    /// `responses` (those are the first argument).
240    pub fn post_response_with<C: C2Transport>(
241        &self,
242        responses: Vec<crate::protocol::TaskResponse>,
243        c2: &C,
244        shared: AgentExtras,
245    ) -> MythicResult<RespPostResponse> {
246        let extras = AgentMessageExtras { responses, shared };
247        let req = ReqPostResponse::from_extras(extras);
248
249        if let Some(key_b64) = c2.get_aes_psk() {
250            let crypto = Aes256HmacCrypto::from_base64_key(&key_b64)?;
251            let iv = c2.random_iv()?;
252            let packed = encode_message(&req, self.callback_uuid, &crypto, &iv)?;
253            let response = c2.post_response(&packed)?;
254            decode_message(&response, Some(self.callback_uuid), &crypto).map(|(_, r)| r)
255        } else {
256            let packed = encode_message_plain(&req, self.callback_uuid)?;
257            let response = c2.post_response(&packed)?;
258            decode_message_plain(&response, Some(self.callback_uuid)).map(|(_, r)| r)
259        }
260    }
261}