diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 4293110..eb73aed 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,10 +1,13 @@ +# This workflow is for PRs, development builds, etc. + +# Project: Mercy (https://github.com/azazelm3dj3d/mercy) +# Author(s): azazelm3dj3d (https://github.com/azazelm3dj3d) +# License: BSD 2-Clause + name: Mercy Status (Dev) on: - push: - branches: ["dev"] - pull_request: - branches: ["dev"] + push: [push] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a208a6..5bc8610 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,3 +1,9 @@ +# This workflow is for production builds, main branch builds, etc. + +# Project: Mercy (https://github.com/azazelm3dj3d/mercy) +# Author(s): azazelm3dj3d (https://github.com/azazelm3dj3d) +# License: BSD 2-Clause + name: Mercy Status (Main) on: diff --git a/.gitignore b/.gitignore index 50c8301..bf94c11 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ Cargo.lock # These are backup files generated by rustfmt -**/*.rs.bk \ No newline at end of file +**/*.rs.bk + +# macOS +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c89eebb..561dd1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mercy" description = "Mercy is an open-source Rust crate and CLI for building cybersecurity tools, assessment projects, and testing infrastructure." -version = "1.2.21" +version = "1.2.22" edition = "2021" author = "azazelm3dj3d" license = "BSD-2-Clause" @@ -21,4 +21,6 @@ reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["full"] } clap = { version = "4.0.18", features = ["derive"] } prettytable-rs = "0.10.0" -lemmeknow = "0.7" \ No newline at end of file +lemmeknow = "0.7" +project_ares = "0.9.0" +zip = "0.6.4" \ No newline at end of file diff --git a/README.md b/README.md index 0b64992..0ad52ec 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@

- +

📚 [Documentation](https://docs.rs/mercy/latest/mercy/) -![Mercy Status (Dev)](https://github.com/azazelm3dj3d/mercy/actions/workflows/dev.yml/badge.svg?branch=dev) ![Mercy Status (Main)](https://github.com/azazelm3dj3d/mercy/actions/workflows/main.yml/badge.svg?branch=main) -Mercy is an open-source Rust crate and CLI for building cybersecurity tools, assessment projects, and testing infrastructure. The goal is to create a sustainable project to make creating security tools in Rust a little easier. +Mercy is an open source Rust crate and CLI designed for building cybersecurity tools, assessment projects, and immediate testing. The goal of the project is to make creating security tools in Rust more accessible and sustainable. ## Usage Since Mercy is a standard crate, it can easily be used in any project already initialized with Cargo. Simply add the following line to your `Cargo.toml` file: ```toml -mercy = "1.2.21" +mercy = "1.2.22" ``` Once the `Cargo.toml` file is updated, you can import the crate and use the provided methods by running `cargo run`. There are lots of different examples available below. @@ -80,6 +79,9 @@ fn main() { // Attempt to identify an unknown string mercy_extra("identify", "UCrlEbqe4ppk5dVIHzdxtC7g"); + + // Attempt to crack an encrypted string + mercy_extra("crack", "YXphemVsbTNkajNk"); } ``` @@ -91,6 +93,27 @@ You can also use the following parameters, replacing the "all" keyword under `sy - os_release - proc +There's also an experimental method, which means you'll receive everything through stdout without a `println!()`: + +```rust +use mercy::mercy_experimental; + +fn main() { + // Shuffle a provided string to construct a domain name + mercy_experimental("domain_gen", "example.com"); +} +``` + +You can now also extract zip files within your script or via the CLI. This is another method that only prints to stdout: + +```rust +use mercy::mercy_experimental; + +fn main() { + mercy_experimental("zip", "/Users/name/Downloads/archive.zip"); +} +``` + ### More Info If ever in doubt, feel free to run this special function to display more information about the crate. @@ -154,10 +177,15 @@ mercy -m ip -p internal_ip ``` Quickly check if a domain is malicious. -``` +```bash mercy -m mal -p status -i "azazelm3dj3d.com" ``` +Extract a zip file. +```bash +mercy -m zip_e -p zip -i "/Users/name/Downloads/archive.zip" +``` + If you're stuck, you can use this option to learn every command at your disposal from [Mercy](https://github.com/azazelm3dj3d/mercy): ```bash mercy -e diff --git a/src/lib.rs b/src/lib.rs index a589aa9..4a6d210 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,45 @@ //! # Mercy //! -//! Mercy is an open-source Rust crate and CLI for building cybersecurity tools, assessment projects, and testing infrastructure. The goal is to create a sustainable project to make creating security tools in Rust a little easier. +//! Mercy is an open source Rust crate and CLI designed for building cybersecurity tools, assessment projects, and immediate testing. The goal of the project is to make creating security tools in Rust more accessible and sustainable. //! -//! | Function | More Info | -//! | ----------------------- | -------------------------------------- | -//! | `mercy_source` | Learn more about the crate | -//! | `mercy_decode` | Supports: base64, rot13 | -//! | `mercy_encode` | Supports: base64 | -//! | `mercy_hash` | Supports: sha2_256, md5 | -//! | `mercy_hex` | Dump hexadecimal values of a file | -//! | `mercy_malicious` | Malware detection or malicious intent | -//! | `mercy_extra` | Information about various data points | +//! | Function | More Info | +//! | ----------------------- | --------------------------------------- | +//! | `mercy_source` | Learn more about the crate | +//! | `mercy_decode` | Supports: base64, rot13 | +//! | `mercy_encode` | Supports: base64 | +//! | `mercy_hash` | Supports: sha256, md5 | +//! | `mercy_hex` | Dump hexadecimal values of a file | +//! | `mercy_malicious` | Malware detection or malicious intent | +//! | `mercy_extra` | Information about various data points | +//! | `mercy_experimental` | Experimental functions for data control | //! /* Project: Mercy (https://github.com/azazelm3dj3d/mercy) - Author: azazelm3dj3d (https://github.com/azazelm3dj3d) + Author(s): azazelm3dj3d (https://github.com/azazelm3dj3d) License: BSD 2-Clause */ -use std::{io::Write, net::TcpStream, str::from_utf8}; -use serde_json::Value; - use std::{ path::Path, - fs::{self, File}, - io::Read, - net::UdpSocket + str::from_utf8, + fs::{ + self, + File + }, + io::{ + self, + Read, + Write + }, + net::{ + UdpSocket, + TcpStream + } }; +use serde_json::Value; + use base64; use md5; use sha2::{Sha256, Digest}; @@ -44,9 +55,14 @@ use sys_info::{ use lemmeknow::Identifier; +use ares::{ + perform_cracking, + config::Config +}; + /// Learn more about the crate pub fn mercy_source() -> String { - const VERSION: &str = "1.2.21"; + const VERSION: &str = "1.2.22"; const AUTHOR: &str = "azazelm3dj3d (https://github.com/azazelm3dj3d)"; return format!("Author: {}\nVersion: {}\nDocumentation: https://docs.rs/crate/mercy/latest", AUTHOR, VERSION); } @@ -74,10 +90,10 @@ pub fn mercy_encode(mercy_call: &str, mercy_string: &str) -> String { /* Public hashing methods provided by Mercy */ -/// Supports: sha2_256, md5 +/// Supports: sha256, md5 pub fn mercy_hash(mercy_call: &str, mercy_string: &str) -> String { match mercy_call { - "sha2_256" => sha2_256_hash(mercy_string.to_string()), + "sha256" => sha256_hash(mercy_string.to_string()), "md5" => md5_hash(mercy_string.to_string()), _ => unknown_msg("Unable to hash message") } @@ -118,6 +134,10 @@ pub fn mercy_malicious(mercy_call: &str, mercy_domain: &str) -> String { /// `defang` - Returns a defanged url and/or ip address /// /// `whois` - Returns WHOIS lookup information +/// +/// `identify` - Attempt to identify an unknown string (requires a "." followed by an extension) +/// +/// `crack` - Attempt to crack an encrypted string pub fn mercy_extra(mercy_call: &str, mercy_choose: &str) -> String { match mercy_call { "internal_ip" => internal_ip(), @@ -125,10 +145,27 @@ pub fn mercy_extra(mercy_call: &str, mercy_choose: &str) -> String { "defang" => defang(mercy_choose), "whois" => whois_lookup(mercy_choose), "identify" => identify_str(mercy_choose), + "crack" => crack_str(mercy_choose), _ => unknown_msg("Unable to provide the information you requested") } } +/* Experimental methods that do not require a `prinln!()`. Instead, you just call the method within the code for stdout. */ +// Example: mercy_experimental("zip", "/Users/name/Downloads/archive.zip"); + +/// Experimental functions that only accept stdout +/// ### Methods +/// `domain_gen` - Shuffle a provided string to construct a domain name +/// +/// `zip` - Extract a zip file +pub fn mercy_experimental(mercy_call: &str, mercy_choose: &str) { + match mercy_call { + "domain_gen" => domain_gen(mercy_choose), + "zip" => zip_extract(mercy_choose), + _ => println!("Unable to provide the information you requested") + } +} + /* Decoding methods */ // Base64 decode @@ -180,7 +217,7 @@ fn base64_encode(plaintext_msg: String) -> String { /* Hashing methods */ // SHA256 hash -fn sha2_256_hash(plaintext_msg: String) -> String { +fn sha256_hash(plaintext_msg: String) -> String { let mut run_hash = Sha256::new(); run_hash.update(plaintext_msg.as_bytes()); @@ -194,6 +231,68 @@ fn md5_hash(plaintext_msg: String) -> String { return format!("{:x}", hash); } +/* Extraction methods */ + +// Zip file extraction +fn zip_extract(filename: &str) { + + // Locate zip file + let collect_file = File::open(Path::new(&*filename)).expect("Unable to locate file"); + + // Begin zip file read + let mut zip_archive = zip::ZipArchive::new(collect_file).expect("Failed to generate zip archive"); + + // Iterate over zip file data + for file in 0..zip_archive.len() { + let mut file_idx = zip_archive.by_index(file).expect("Unable to build zip index"); + + let out_path = match file_idx.enclosed_name() { + Some(path) => path.to_owned(), + None => continue + }; + + { + // Checks the status of comments + let comment = file_idx.comment(); + + if !comment.is_empty() { + println!("Comment located for file: {file}"); + println!("Comment: \n{comment}"); + } + } + + // Validates if the extracted data is a file or directory + if (*file_idx.name()).ends_with('/') { + println!("[{}] Directory path: \"{}\"", file, out_path.display()); + + // Creates internal zip directories + fs::create_dir_all(&out_path).expect("Unable to create zip directories"); + } else { + println!("[{}] File path: \"{}\" ({} bytes)", file, out_path.display(), file_idx.size()); + + if let Some(zip_path) = out_path.parent() { + if !zip_path.exists() { + fs::create_dir_all(zip_path).expect("Unable to create directories"); + } + } + + // Builds the output path + let mut out = File::create(&out_path).expect("Unable to create out_path"); + io::copy(&mut file_idx, &mut out).expect("Unable to copy contents"); + } + + // Handles permissions for Unix systems + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file_idx.unix_mode() { + fs::set_permissions(&out_path, fs::Permissions::from_mode(mode)).expect("Failure to update permissions"); + } + } + } +} + /* Hexadecimal manipulation */ // Converts file/bytes to a readable vector @@ -276,6 +375,73 @@ fn identify_str(data: &str) -> String { return Identifier::to_json(&Identifier::default().identify(data)); } +// Attempt to crack an encrypted string +fn crack_str(data: &str) -> String { + let mut config = Config::default(); + // TODO: Allow user to specify timeout + // Attempts to crack the string within 10 seconds + config.timeout = 10; + config.human_checker_on = false; + + let result = perform_cracking(data, config); + + if !result.is_none() { + match result { + Some(result) => return format!("{:?}", result.text), + _ => return "Unable to crack string".to_string() + } + } else { + return "Result is None".to_string() + } +} + +// Domain generation +fn domain_gen(url: &str) { + + let common_exts = [".com", ".io", ".co", ".ai", ".moe", ".org", ".edu", ".net", ".biz", ".ru", ".uk", ".au", ".de", ".in"]; + + for i in 0..url.len() { + let char_val = url.as_bytes()[i]; + + for bit_switch in 0..8 { + // Shuffles the character position + let shuffle: u8 = char_val ^ 1 << bit_switch; + + if shuffle.is_ascii_alphanumeric() + || shuffle as char == '-' + && shuffle.to_ascii_lowercase() + != char_val.to_ascii_lowercase() { + + let mut payload = url.as_bytes()[..i].to_vec(); + payload.push(shuffle); + + // Appends onto the Vec for parsing + payload.append(&mut url.as_bytes()[i + 1..].to_vec()); + + if let Ok(d) = String::from_utf8(payload) { + + // Iterates over the preset extensions + for e in common_exts.iter() { + + // Only returns if one of the extensions is present + if d.ends_with(e) { + println!("{}", d); + } + } + + // Handles cases where an extension is not present + if !d.contains(".") { + // Apparently, this way it doesn't return something like examplecom (when providing "example.com") + if d.contains(".") { + println!("{}", d); + } + } + } + } + } + } +} + fn unknown_msg(custom_msg: &str) -> String { return format!("{}", custom_msg); } diff --git a/src/main.rs b/src/main.rs index c4fb04c..b26a001 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ -//! # Mercy +//! # Mercy CLI //! -//! Mercy is an open-source Rust crate and CLI for building cybersecurity tools, assessment projects, and testing infrastructure. The goal is to create a sustainable project to make creating security tools in Rust a little easier. +//! Mercy is an open source Rust crate and CLI designed for building cybersecurity tools, assessment projects, and immediate testing. The goal of the project is to make creating security tools in Rust more accessible and sustainable. //! /* Project: Mercy (https://github.com/azazelm3dj3d/mercy) - Author: azazelm3dj3d (https://github.com/azazelm3dj3d) + Author(s): azazelm3dj3d (https://github.com/azazelm3dj3d) License: BSD 2-Clause */ @@ -67,7 +67,7 @@ fn main() { // Extended help section for new users if args.extended { println!("\n=== Mercy CLI ==="); - pretty_output("encode\ndecode\nhash\nhex\nsys\nip\nmal\nd\nwho\nid", "base64, rot13\nbase64, rot13\nmd5, sha2_256\nhex_dump\nsystem_info\ninternal_ip\nstatus\ndefang\nwhois\nidentify", "Method(s)", "Protocol(s)"); + pretty_output("encode\ndecode\nhash\nhex\nsys\nip\nmal\nd\nwho\nid\nc\ndg\nzip_e", "base64, rot13\nbase64, rot13\nmd5, sha2_256\nhex_dump\nsystem_info\ninternal_ip\nstatus\ndefang\nwhois\nidentify\ncrack\ndomain_gen\nzip", "Method(s)", "Protocol(s)"); println!("\n=== Mercy CLI Extended ==="); pretty_output("system_info", "hostname\ncpu_cores\ncpu_speed\nos_release\nproc\nall", "Protocol(s)", "Input(s)"); @@ -85,6 +85,12 @@ fn main() { println!("Identify an unknown string"); println!("mercy -m id -p identify -i 'UCrlEbqe4ppk5dVIHzdxtC7g'\n"); + + println!("Shuffle a provided string to construct a domain name"); + println!("mercy -m dg -p domain_gen -i 'example.com'\n"); + + println!("Extract a zip file"); + println!("mercy -m zip_e -p zip -i '/Users/name/Downloads/archive.zip'\n"); } else { match args.method.as_str() { @@ -98,6 +104,9 @@ fn main() { "d" => println!("{}", mercy::mercy_extra(&args.protocol, &args.input)), "who" => println!("{}", mercy::mercy_extra(&args.protocol, &args.input)), "id" => println!("{}", mercy::mercy_extra(&args.protocol, &args.input)), + "c" => println!("{}", mercy::mercy_extra(&args.protocol, &args.input)), + "dg" => mercy::mercy_experimental(&args.protocol, &args.input), + "zip_e" => mercy::mercy_experimental(&args.protocol, &args.input), "mal" => println!("{}", mercy::mercy_malicious(&args.protocol, &args.input)), _ => println!("Unable to parse provided arguments") }