Skip to main content

mythic/
agent.rs

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