use clap::Parser;
use colored::Colorize as _;
use crate::cli::probe::{lookup_path, make_client_with_hostname, make_mount_client, parse_addr_with_port};
use crate::cli::{GlobalOpts, H_BEHAVIOR, H_IDENTITY, H_STEALTH, H_TARGET};
use crate::engine::uid_sprayer::{SprayConfig, UidSprayer, access_bits};
use crate::util::stealth::StealthConfig;
#[derive(Parser)]
pub(crate) struct UidSprayArgs {
#[arg(help_heading = H_TARGET, value_name = "TARGET")]
pub target: String,
#[arg(short = 'e', long, value_name = "PATH", help_heading = H_TARGET)]
pub export: Option<String>,
#[arg(long, default_value = "0", value_name = "UID", help_heading = H_IDENTITY)]
pub uid_start: u32,
#[arg(long, default_value = "65535", value_name = "UID", help_heading = H_IDENTITY)]
pub uid_end: u32,
#[arg(long, value_name = "GID", help_heading = H_IDENTITY)]
pub gid_start: Option<u32>,
#[arg(long, value_name = "GID", help_heading = H_IDENTITY)]
pub gid_end: Option<u32>,
#[arg(long, default_value = "100000", value_name = "N", help_heading = H_BEHAVIOR)]
pub max_attempts: u64,
#[arg(long, default_value = "/", value_name = "PATH", help_heading = H_BEHAVIOR)]
pub path: String,
#[arg(long, default_value = "0", value_name = "MS", help_heading = H_STEALTH)]
pub attempt_delay: u64,
}
pub(crate) async fn run(args: UidSprayArgs, globals: &GlobalOpts) -> anyhow::Result<()> {
let target = crate::cli::target::parse(&args.target, args.export.as_deref(), None, true)?;
let host = target.host.to_string();
let export = target.export().unwrap_or("/").to_owned();
let stealth = StealthConfig::new(globals.delay, globals.jitter);
let paired_gid = args.gid_start.is_none() && args.gid_end.is_none();
let gid_range = args.gid_start.unwrap_or(0)..=args.gid_end.unwrap_or(65535);
let uid_count = u64::from(args.uid_end.saturating_sub(args.uid_start)) + 1;
let attempts = if paired_gid { uid_count } else { uid_count.saturating_mul(u64::from(gid_range.end().saturating_sub(*gid_range.start())) + 1) };
if attempts > args.max_attempts {
anyhow::bail!(
"sweep would issue {attempts} credential probes, over the {} limit.\n\
Narrow --uid-start/--uid-end or --gid-start/--gid-end, or raise --max-attempts if this is intended.",
args.max_attempts
);
}
if paired_gid {
eprintln!("{}", format!("[*] Spraying UIDs {}-{} (gid = uid) on {host}:{export} -- {attempts} probes", args.uid_start, args.uid_end).yellow());
} else {
eprintln!("{}", format!("[*] Spraying UIDs {}-{} x GIDs {}-{} on {host}:{export} -- {attempts} probes", args.uid_start, args.uid_end, gid_range.start(), gid_range.end()).yellow());
}
let addr = parse_addr_with_port(&host, globals.nfs_port)?;
let (_, circuit, client) = make_client_with_hostname(addr, &export, 0, 0, &globals.aux_gids, stealth.clone(), globals.proxy.as_deref(), globals.nfs_port, &globals.hostname);
let mount = make_mount_client(globals);
let mnt = mount.mount(addr, &export).await?;
let target_fh = if args.path == "/" { mnt.handle } else { lookup_path(&client, &mnt.handle, &args.path).await? };
let sprayer = UidSprayer::new(client, circuit, stealth.clone());
let config = SprayConfig { uid_range: args.uid_start..=args.uid_end, gid_range, paired_gid, auxiliary_gids: globals.aux_gids.clone(), _target_path: args.path, _concurrency: 1, required_access: access_bits::ALL, per_attempt_delay_ms: args.attempt_delay };
let results = sprayer.spray(&config, &target_fh).await;
eprintln!("{}", format!("[+] {} credential(s) granted access", results.len()).green());
for r in &results {
let flags = access_summary(r.access);
eprintln!(" uid={} gid={} [{flags}]", r.uid, r.gid);
}
crate::cli::emit_replay(globals);
Ok(())
}
fn access_summary(access: u32) -> String {
let mut flags = Vec::with_capacity(6);
if access & access_bits::READ != 0 {
flags.push("READ");
}
if access & access_bits::LOOKUP != 0 {
flags.push("LOOKUP");
}
if access & access_bits::MODIFY != 0 {
flags.push("MODIFY");
}
if access & access_bits::EXTEND != 0 {
flags.push("EXTEND");
}
if access & access_bits::DELETE != 0 {
flags.push("DELETE");
}
if access & access_bits::EXECUTE != 0 {
flags.push("EXECUTE");
}
flags.join("|")
}