mod args;
use std::os::windows::process::ExitStatusExt;
use std::path::Path;
use std::process::ExitStatus;
use windows::{
Win32::{
Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0, WAIT_TIMEOUT},
System::Threading::{GetExitCodeProcess, INFINITE, WaitForSingleObject},
UI::Shell::{SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, ShellExecuteExW},
},
core::{HSTRING, PCWSTR},
};
use crate::PrivilegedOutput;
use crate::error::{PrivescError, Result};
use args::{escape_arguments, escape_bat_arguments};
pub struct PrivilegedChildInner {
handle: HANDLE,
}
unsafe impl Send for PrivilegedChildInner {}
unsafe impl Sync for PrivilegedChildInner {}
impl PrivilegedChildInner {
pub fn wait(self) -> Result<PrivilegedOutput> {
let wait_result = unsafe { WaitForSingleObject(self.handle, INFINITE) };
if wait_result != WAIT_OBJECT_0 {
return Err(PrivescError::PrivilegeEscalationFailed(
"Failed to wait for process".to_string(),
));
}
let mut exit_code: u32 = 0;
let exit_code_result = unsafe { GetExitCodeProcess(self.handle, &mut exit_code) };
if let Err(e) = exit_code_result {
return Err(PrivescError::PrivilegeEscalationFailed(format!(
"Failed to get exit code: {}",
e
)));
}
Ok(PrivilegedOutput {
status: ExitStatus::from_raw(exit_code),
stdout: None,
stderr: None,
})
}
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
let wait_result = unsafe { WaitForSingleObject(self.handle, 0) };
if wait_result == WAIT_TIMEOUT {
return Ok(None);
}
if wait_result != WAIT_OBJECT_0 {
return Err(PrivescError::PrivilegeEscalationFailed(
"Failed to check process status".to_string(),
));
}
let mut exit_code: u32 = 0;
let exit_code_result = unsafe { GetExitCodeProcess(self.handle, &mut exit_code) };
if let Err(e) = exit_code_result {
return Err(PrivescError::PrivilegeEscalationFailed(format!(
"Failed to get exit code: {}",
e
)));
}
Ok(Some(ExitStatus::from_raw(exit_code)))
}
pub fn id(&self) -> Option<u32> {
None
}
}
impl Drop for PrivilegedChildInner {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.handle) };
}
}
fn is_batch_file(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("bat") || ext.eq_ignore_ascii_case("cmd"))
}
fn spawn_gui(program: &Path, args: &[&str]) -> Result<PrivilegedChildInner> {
let verb = HSTRING::from("runas");
let file = HSTRING::from(program);
let parameters = if is_batch_file(program) {
HSTRING::from(escape_bat_arguments(args))
} else {
HSTRING::from(escape_arguments(args))
};
let mut info = SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
fMask: SEE_MASK_NOCLOSEPROCESS,
lpVerb: PCWSTR(verb.as_ptr()),
lpFile: PCWSTR(file.as_ptr()),
lpParameters: PCWSTR(parameters.as_ptr()),
nShow: windows::Win32::UI::WindowsAndMessaging::SW_HIDE.0,
..Default::default()
};
let result = unsafe { ShellExecuteExW(&mut info) };
if let Err(e) = result {
return Err(PrivescError::PrivilegeEscalationFailed(format!(
"ShellExecuteExW failed: {}",
e
)));
}
let handle = info.hProcess;
if handle.is_invalid() {
return Err(PrivescError::PrivilegeEscalationFailed(
"Failed to get process handle".to_string(),
));
}
Ok(PrivilegedChildInner { handle })
}
pub fn spawn(
program: &Path,
args: &[&str],
_gui: bool,
_prompt: Option<&str>,
) -> Result<PrivilegedChildInner> {
spawn_gui(program, args)
}
pub fn run(
program: &Path,
args: &[&str],
gui: bool,
prompt: Option<&str>,
) -> Result<PrivilegedOutput> {
spawn(program, args, gui, prompt)?.wait()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_batch_file() {
assert!(is_batch_file(Path::new("script.bat")));
assert!(is_batch_file(Path::new("script.cmd")));
assert!(is_batch_file(Path::new(r"C:\Windows\script.bat")));
assert!(is_batch_file(Path::new(r"C:\Windows\script.cmd")));
assert!(is_batch_file(Path::new("script.BAT")));
assert!(is_batch_file(Path::new("script.CMD")));
assert!(is_batch_file(Path::new("script.Bat")));
assert!(is_batch_file(Path::new("script.Cmd")));
assert!(!is_batch_file(Path::new("program.exe")));
assert!(!is_batch_file(Path::new("script.ps1")));
assert!(!is_batch_file(Path::new("script.sh")));
assert!(!is_batch_file(Path::new(r"C:\Windows\System32\cmd.exe")));
assert!(!is_batch_file(Path::new("nobatch")));
assert!(!is_batch_file(Path::new("")));
assert!(!is_batch_file(Path::new("batch.bat.exe")));
assert!(!is_batch_file(Path::new("cmd.cmd.txt")));
}
}