use crate::ndr::NdrEncoder;
use crate::transport::SmbPipe;
use crate::{Result, RpcError, Syntax};
use smb2_client::SmbClient;
pub fn tsch_syntax() -> Syntax {
Syntax::new("86d35949-83c9-4044-b424-db363231fd0c", 1, 0)
}
pub mod opnum {
pub const REGISTER_TASK: u16 = 1;
pub const RUN: u16 = 12;
pub const DELETE: u16 = 13;
}
const TASK_CREATE: u32 = 0x0000_0002;
const TASK_LOGON_NONE: u32 = 0;
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
fn task_xml(command: &str) -> String {
format!(
"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\
<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\
<Principals><Principal id=\"LocalSystem\"><UserId>S-1-5-18</UserId><RunLevel>HighestAvailable</RunLevel></Principal></Principals>\
<Settings>\
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\
<AllowHardTerminate>true</AllowHardTerminate>\
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>\
<IdleSettings><StopOnIdleEnd>true</StopOnIdleEnd><RestartOnIdle>false</RestartOnIdle></IdleSettings>\
<AllowStartOnDemand>true</AllowStartOnDemand><Enabled>true</Enabled><Hidden>true</Hidden>\
<ExecutionTimeLimit>PT10M</ExecutionTimeLimit><Priority>7</Priority>\
</Settings>\
<Actions Context=\"LocalSystem\"><Exec><Command>cmd.exe</Command><Arguments>/Q /c {}</Arguments></Exec></Actions>\
</Task>",
xml_escape(command)
)
}
fn encode_register(path: &str, xml: &str) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.referent();
e.conformant_varying_wstr(path);
e.align(4);
e.conformant_varying_wstr(xml);
e.align(4);
e.u32(TASK_CREATE); e.null_ptr(); e.u32(TASK_LOGON_NONE); e.u32(0); e.null_ptr(); e.into_bytes()
}
fn encode_run(path: &str) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.referent();
e.conformant_varying_wstr(path);
e.align(4);
e.u32(0); e.null_ptr(); e.u32(0); e.u32(0); e.null_ptr(); e.into_bytes()
}
fn encode_delete(path: &str) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.referent();
e.conformant_varying_wstr(path);
e.align(4);
e.u32(0); e.into_bytes()
}
fn hresult(stub: &[u8]) -> u32 {
if stub.len() < 4 {
return 0xFFFF_FFFF;
}
u32::from_le_bytes(stub[stub.len() - 4..].try_into().unwrap())
}
pub async fn atexec(
client: &mut SmbClient,
command: &str,
domain: &str,
user: &str,
password: &str,
host: &str,
) -> Result<(String, u32)> {
let tag = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let path = format!("\\ADh{tag:08x}");
let xml = task_xml(command);
let file_id = client
.open_pipe("atsvc")
.await
.map_err(|e| RpcError::Protocol(format!("open \\atsvc: {e}")))?;
let mut pipe = SmbPipe::new(client, file_id);
pipe.bind_sealed(tsch_syntax(), domain, user, password, host)
.await?;
let reg = pipe
.call_sealed(opnum::REGISTER_TASK, &encode_register(&path, &xml))
.await?;
let reg_hr = hresult(®);
if reg_hr != 0 {
return Err(RpcError::Protocol(format!(
"SchRpcRegisterTask failed (HRESULT 0x{reg_hr:08x})"
)));
}
let run = pipe.call_sealed(opnum::RUN, &encode_run(&path)).await?;
let run_hr = hresult(&run);
let _ = pipe.call_sealed(opnum::DELETE, &encode_delete(&path)).await;
Ok((path, run_hr))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xml_escapes_redirect() {
let x = task_xml("whoami > C:\\out 2>&1");
assert!(x.contains("/Q /c whoami > C:\\out 2>&1"));
assert!(x.contains("<UserId>S-1-5-18</UserId>"));
}
#[test]
fn register_stub_inline_path_then_referent() {
let s = encode_register("\\ADh1", "<Task/>");
assert_ne!(
u32::from_le_bytes(s[0..4].try_into().unwrap()),
0,
"path referent"
);
assert_eq!(
u32::from_le_bytes(s[4..8].try_into().unwrap()),
6,
"path WSTR max_count"
);
}
}