pub(crate) mod analyze;
pub(crate) mod brute_handle;
pub(crate) mod convert;
pub(crate) mod escape;
pub(crate) mod mount;
pub(crate) mod probe;
pub(crate) mod scan;
pub(crate) mod shell;
pub(crate) mod target;
pub(crate) mod uid_spray;
use clap::{Parser, Subcommand};
pub(crate) const H_TARGET: &str = "Target / Source";
pub(crate) const H_IDENTITY: &str = "Identity";
pub(crate) const H_PERMISSIONS: &str = "Permissions";
pub(crate) const H_NETWORK: &str = "Network";
pub(crate) const H_STEALTH: &str = "Stealth";
pub(crate) const H_OUTPUT: &str = "Output";
pub(crate) const H_BEHAVIOR: &str = "Behavior";
#[derive(Parser)]
#[command(
name = "nfswolf",
version,
about,
long_about = None,
// Suppress clap's flat `{subcommands}` block; the categorised listing is
// rendered by `after_help` (COMMANDS_HELP) since clap can't group subcommands.
help_template = "{about-with-newline}\n{usage-heading} {usage}\n\n{options}\n{after-help}",
after_help = COMMANDS_HELP,
)]
pub(crate) struct Cli {
#[arg(short = 'u', long, global = true, default_value = "1000", value_name = "UID", help_heading = H_IDENTITY)]
pub uid: u32,
#[arg(short = 'g', long, global = true, default_value = "1000", value_name = "GID", help_heading = H_IDENTITY)]
pub gid: u32,
#[arg(long, global = true, default_value = "localhost", value_name = "NAME", help_heading = H_IDENTITY)]
pub hostname: String,
#[arg(long, global = true, value_delimiter = ',', value_name = "G1,G2,...", help_heading = H_IDENTITY)]
pub aux_gids: Vec<u32>,
#[arg(long, global = true, help_heading = H_NETWORK)]
pub privileged_port: bool,
#[arg(long, global = true, value_name = "HOST:PORT", help_heading = H_NETWORK)]
pub proxy: Option<String>,
#[arg(short = 't', long, global = true, default_value = "3000", value_name = "MS", help_heading = H_NETWORK)]
pub timeout: u64,
#[arg(long, global = true, value_name = "PORT", help_heading = H_NETWORK)]
pub nfs_port: Option<u16>,
#[arg(long, global = true, value_name = "PORT", help_heading = H_NETWORK)]
pub mount_port: Option<u16>,
#[arg(long, global = true, default_value = "0", value_name = "MS", help_heading = H_STEALTH)]
pub delay: u64,
#[arg(long, global = true, default_value = "0", value_name = "MS", help_heading = H_STEALTH)]
pub jitter: u64,
#[arg(long, global = true, help_heading = H_OUTPUT)]
pub no_color: bool,
#[arg(short, long, global = true, action = clap::ArgAction::Count, help_heading = H_OUTPUT)]
pub verbose: u8,
#[arg(short, long, global = true, help_heading = H_OUTPUT)]
pub quiet: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub(crate) enum Command {
Scan(scan::ScanArgs),
Analyze(analyze::AnalyzeArgs),
Escape(escape::EscapeArgs),
Shell(shell::ShellArgs),
#[cfg(feature = "fuse")]
Mount(mount::MountArgs),
BruteHandle(brute_handle::BruteHandleArgs),
UidSpray(uid_spray::UidSprayArgs),
Convert(convert::ConvertArgs),
Completions(CompletionsArgs),
}
#[cfg(feature = "fuse")]
const COMMANDS_HELP: &str = concat!(
"Commands:\n",
" Recon:\n",
" scan Discover NFS servers on a network\n",
" analyze Deep security audit of an NFS server\n",
" escape Break out of an export to the filesystem root (subtree_check bypass)\n",
" Connect:\n",
" shell Interactive NFS exploration shell\n",
" mount FUSE-mount an NFS export with UID spoofing\n",
" Advanced:\n",
" brute-handle Brute-force file handles via the STALE/BADHANDLE oracle\n",
" uid-spray UID/GID spray (last-resort credential discovery)\n",
" Utilities:\n",
" convert Render an `analyze --json` dump to HTML/MD/CSV/TXT/console\n",
" completions Generate shell completions\n",
"\n",
"Run `nfswolf <COMMAND> --help` for per-command options.",
);
#[cfg(not(feature = "fuse"))]
const COMMANDS_HELP: &str = concat!(
"Commands:\n",
" Recon:\n",
" scan Discover NFS servers on a network\n",
" analyze Deep security audit of an NFS server\n",
" escape Break out of an export to the filesystem root (subtree_check bypass)\n",
" Connect:\n",
" shell Interactive NFS exploration shell\n",
" Advanced:\n",
" brute-handle Brute-force file handles via the STALE/BADHANDLE oracle\n",
" uid-spray UID/GID spray (last-resort credential discovery)\n",
" Utilities:\n",
" convert Render an `analyze --json` dump to HTML/MD/CSV/TXT/console\n",
" completions Generate shell completions\n",
"\n",
"Run `nfswolf <COMMAND> --help` for per-command options.",
);
#[derive(Parser)]
pub(crate) struct CompletionsArgs {
#[arg(value_enum)]
pub shell: clap_complete::Shell,
}
#[derive(Debug, Clone)]
pub(crate) struct GlobalOpts {
pub uid: u32,
pub gid: u32,
pub hostname: String,
pub aux_gids: Vec<u32>,
pub privileged_port: bool,
pub proxy: Option<String>,
pub timeout: u64,
pub nfs_port: Option<u16>,
pub mount_port: Option<u16>,
pub delay: u64,
pub jitter: u64,
pub no_color: bool,
pub _verbose: u8,
pub quiet: bool,
}
impl Cli {
#[must_use]
pub(crate) fn global_opts(&self) -> GlobalOpts {
GlobalOpts {
uid: self.uid,
gid: self.gid,
hostname: self.hostname.clone(),
aux_gids: self.aux_gids.clone(),
privileged_port: self.privileged_port,
proxy: self.proxy.clone(),
timeout: self.timeout,
nfs_port: self.nfs_port,
mount_port: self.mount_port,
delay: self.delay,
jitter: self.jitter,
no_color: self.no_color,
_verbose: self.verbose,
quiet: self.quiet,
}
}
}
pub(crate) fn completions(args: &CompletionsArgs) {
let mut cmd = <Cli as clap::CommandFactory>::command();
clap_complete::generate(args.shell, &mut cmd, "nfswolf", &mut std::io::stdout());
}
pub(crate) fn emit_replay(globals: &GlobalOpts) {
if globals.quiet {
return;
}
let argv: Vec<String> = std::env::args().skip(1).collect();
if argv.is_empty() {
return;
}
eprintln!("# rerun: nfswolf {}", argv.join(" "));
}