Skip to main content

ad_time/protocols/
kerberos.rs

1/// Kerberos time source (primary — stealth).
2///
3/// Protocol Specifications:
4/// - **RFC 4120 §3.1.1**: AS Exchange
5/// - **RFC 4120 §5.4.1**: KRB_AS_REQ
6/// - **RFC 4120 §5.9.1**: KRB_ERROR
7/// - **RFC 4120 §5.2.2**: PrincipalName
8///
9/// Sends a minimal AS-REQ for a nonexistent principal and reads `stime`/`susec`
10/// from the KRB-ERROR response. Any KRB-ERROR from a real KDC includes these
11/// required fields (RFC 4120 §5.9.1), so even a KRB_AP_ERR_PRINCIPAL_UNKNOWN
12/// gives us the server clock.
13///
14/// Offset precision: ±RTT/2 (single-point approximation, not four-point NTP
15/// triangulation). Sufficient for Kerberos' 5-minute skew window.
16use std::io::{Read, Write};
17use std::net::{SocketAddr, TcpStream};
18use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
19
20use rand::Rng;
21
22use super::common::{parse_generalized_time, system_time_to_us};
23use crate::time_src::{OffsetMicros, TimeSource, TimeSourceError};
24
25// DER/ASN.1 tag constants used in KRB-ERROR parsing (RFC 4120 §5.9.1).
26const KRB_ERROR_TAG: u8 = 0x7E; // APPLICATION 30
27const SEQUENCE_TAG: u8 = 0x30;
28const STIME_TAG: u8 = 0xA4; // context [4]
29const SUSEC_TAG: u8 = 0xA5; // context [5]
30const GENERALIZED_TIME_TAG: u8 = 0x18;
31const INTEGER_TAG: u8 = 0x02;
32
33pub struct KerberosSource {
34    pub realm: Option<String>,
35    pub stealth_user: String,
36}
37
38impl TimeSource for KerberosSource {
39    fn name(&self) -> &'static str {
40        "kerberos"
41    }
42
43    fn fetch(
44        &self,
45        target: SocketAddr,
46        timeout: Duration,
47    ) -> Result<OffsetMicros, TimeSourceError> {
48        let realm = self
49            .realm
50            .as_deref()
51            .ok_or_else(|| TimeSourceError::Config("no realm configured".into()))?;
52        let krb_addr: SocketAddr = (target.ip(), 88).into();
53        fetch_kerberos(krb_addr, realm, &self.stealth_user, timeout)
54    }
55}
56
57fn fetch_kerberos(
58    addr: SocketAddr,
59    realm: &str,
60    stealth_user: &str,
61    timeout: Duration,
62) -> Result<OffsetMicros, TimeSourceError> {
63    let mut stream = TcpStream::connect_timeout(&addr, timeout).map_err(map_io_err)?;
64    stream
65        .set_read_timeout(Some(timeout))
66        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
67    stream
68        .set_write_timeout(Some(timeout))
69        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
70
71    let t_send_sys = SystemTime::now();
72    let t_send = Instant::now();
73
74    let req = build_as_req(realm, stealth_user);
75    // RFC 4120 §7.2.2: TCP Kerberos messages are prefixed by 4-byte big-endian length.
76    let len = (req.len() as u32).to_be_bytes();
77    stream
78        .write_all(&len)
79        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
80    stream
81        .write_all(&req)
82        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
83
84    // Read response length prefix.
85    let mut len_buf = [0u8; 4];
86    stream.read_exact(&mut len_buf).map_err(map_io_err)?;
87    let resp_len = u32::from_be_bytes(len_buf) as usize;
88
89    if resp_len > 65536 {
90        return Err(TimeSourceError::Protocol(format!(
91            "implausibly large KRB response: {} bytes",
92            resp_len
93        )));
94    }
95    let mut resp = vec![0u8; resp_len];
96    stream.read_exact(&mut resp).map_err(map_io_err)?;
97
98    let rtt = t_send.elapsed();
99
100    // Single-point approximation: server time ≈ local midpoint of send/recv window.
101    let t_mid_us = system_time_to_us(t_send_sys)? + (rtt.as_micros() as i64) / 2;
102
103    let server_us = parse_krb_error(&resp)?;
104    Ok(server_us - t_mid_us)
105}
106
107/// Parse a KRB-ERROR (APPLICATION 30, tag 0x7E) and return server time in Unix microseconds.
108pub fn parse_krb_error(data: &[u8]) -> Result<i64, TimeSourceError> {
109    // DER structure: 0x7E <len> <SEQUENCE contents>
110    let mut pos = 0;
111    let tag = next_byte(data, &mut pos, "KRB-ERROR tag")?;
112    if tag != KRB_ERROR_TAG {
113        return Err(TimeSourceError::Protocol(format!(
114            "expected KRB-ERROR tag 0x{:02X}, got 0x{:02X}",
115            KRB_ERROR_TAG, tag
116        )));
117    }
118
119    // Skip outer length — we're scanning by tag inside.
120    skip_der_length(data, &mut pos)?;
121
122    // The KRB-ERROR SEQUENCE wraps the fields. Outer SEQUENCE tag.
123    let seq_tag = next_byte(data, &mut pos, "KRB-ERROR SEQUENCE tag")?;
124    if seq_tag != SEQUENCE_TAG {
125        return Err(TimeSourceError::Parse(format!(
126            "expected SEQUENCE tag 0x{:02X}, got 0x{:02X}",
127            SEQUENCE_TAG, seq_tag
128        )));
129    }
130    let seq_len = read_der_length(data, &mut pos)?;
131    let seq_end = pos
132        .checked_add(seq_len)
133        .ok_or_else(|| TimeSourceError::Parse("SEQUENCE overflow".into()))?;
134    if seq_end > data.len() {
135        return Err(TimeSourceError::Parse(
136            "KRB-ERROR SEQUENCE overruns buffer".into(),
137        ));
138    }
139
140    // Scan context-tagged fields until we find [4] (stime) and [5] (susec).
141    // Bound by seq_end to prevent tag-injection from bytes appended after the SEQUENCE.
142    let mut stime_us: Option<i64> = None;
143    let mut susec: Option<u32> = None;
144
145    while pos < seq_end && (stime_us.is_none() || susec.is_none()) {
146        let field_tag = next_byte(data, &mut pos, "field tag")?;
147        let field_len = read_der_length(data, &mut pos)?;
148
149        let field_end = pos
150            .checked_add(field_len)
151            .ok_or_else(|| TimeSourceError::Parse("Field overflow".into()))?;
152        if field_end > data.len() {
153            return Err(TimeSourceError::Parse("DER field overruns buffer".into()));
154        }
155
156        let field_data = &data[pos..field_end];
157        pos = field_end;
158
159        match field_tag {
160            STIME_TAG => {
161                stime_us = Some(parse_context_generalizedtime(field_data)?);
162            }
163            SUSEC_TAG => {
164                susec = Some(parse_context_integer_u32(field_data)?);
165            }
166            _ => { /* skip other fields */ }
167        }
168    }
169
170    // RFC 4120 §5.9.1 KRB-ERROR
171    // stime is in seconds, susec is microseconds.
172    let stime =
173        stime_us.ok_or_else(|| TimeSourceError::Parse("KRB-ERROR missing stime [4]".into()))?;
174    let sus = susec.unwrap_or(0);
175
176    // stime_us is Unix microseconds; susec is 0..999_999 additional offset within the second.
177    // OPSEC Rationale: We calculate single-point offset assuming stamping at receive time.
178    Ok(stime + sus as i64)
179}
180
181/// Parse a context-wrapped GeneralizedTime: [N] { 0x18 <len> <ascii bytes> }
182fn parse_context_generalizedtime(b: &[u8]) -> Result<i64, TimeSourceError> {
183    let mut pos = 0;
184    let tag = next_byte(b, &mut pos, "GeneralizedTime tag")?;
185    if tag != GENERALIZED_TIME_TAG {
186        return Err(TimeSourceError::Parse(format!(
187            "expected GeneralizedTime 0x{:02X}, got 0x{:02X}",
188            GENERALIZED_TIME_TAG, tag
189        )));
190    }
191    let len = read_der_length(b, &mut pos)?;
192    let end_pos = pos
193        .checked_add(len)
194        .ok_or_else(|| TimeSourceError::Parse("GeneralizedTime overflow".into()))?;
195    if end_pos > b.len() {
196        return Err(TimeSourceError::Parse(
197            "GeneralizedTime overruns buffer".into(),
198        ));
199    }
200    let s = std::str::from_utf8(&b[pos..end_pos])
201        .map_err(|_| TimeSourceError::Parse("GeneralizedTime not UTF-8".into()))?;
202    let st = parse_generalized_time(s)?;
203    system_time_to_us(st)
204}
205
206/// Parse a context-wrapped INTEGER into u32: [N] { 0x02 <len> <bytes> }
207fn parse_context_integer_u32(b: &[u8]) -> Result<u32, TimeSourceError> {
208    let mut pos = 0;
209    let tag = next_byte(b, &mut pos, "INTEGER tag")?;
210    if tag != INTEGER_TAG {
211        return Err(TimeSourceError::Parse(format!(
212            "expected INTEGER 0x{:02X}, got 0x{:02X}",
213            INTEGER_TAG, tag
214        )));
215    }
216    let len = read_der_length(b, &mut pos)?;
217    let end_pos = pos
218        .checked_add(len)
219        .ok_or_else(|| TimeSourceError::Parse("INTEGER overflow".into()))?;
220    if end_pos > b.len() || len > 4 {
221        return Err(TimeSourceError::Parse(format!(
222            "INTEGER len {} out of range",
223            len
224        )));
225    }
226    let mut val = 0u32;
227    for &byte in &b[pos..end_pos] {
228        val = (val << 8) | byte as u32;
229    }
230    Ok(val)
231}
232
233// Removed duplicated parse_generalized_time, parse_digits, civil_to_days
234
235/// Build a minimal AS-REQ DER for the given `cname` principal in `realm`.
236///
237/// `cname` should blend in with the environment (e.g. a plausible admin typo like
238/// "admnistrator"). Using a recognizable prefix like "nonexistent" is a trivial SIEM
239/// fingerprint (`^nonexistent\d+$`). A typo of a known-but-wrong principal generates
240/// Event 4768 with FailureCode 0x6 (unknown principal), which is universal AD noise.
241pub fn build_as_req(realm: &str, cname: &str) -> Vec<u8> {
242    let nonce: u32 = rand::thread_rng().gen();
243    let till = kerberos_time_plausible_future();
244
245    // Encode sub-structures.
246    let pvno = der_integer(5);
247    let msg_type = der_integer(10); // AS-REQ
248
249    // IOC Rationale: A single string "krbtgt/REALM" violates RFC 4120 §5.2.2 PrincipalName,
250    // which requires a sequence of strings. Elite EDRs catch badly encoded sname components.
251    let cname_enc = der_principal_name(0, &[cname]); // NT-UNKNOWN = 0
252    let sname_enc = der_principal_name(2, &["krbtgt", realm]); // NT-SRV-INST = 2
253    let realm_enc = der_generalstring(realm);
254    let till_enc = der_generalizedtime(&till);
255    let nonce_enc = der_integer(nonce as u64);
256    let etype_enc = der_etype_sequence(&[17, 18, 23]); // aes128-cts, aes256-cts, rc4-hmac
257
258    // req-body SEQUENCE (context tag [4])
259    let req_body_inner = [
260        der_context(0, &der_bitstring_zero()), // kdc-options
261        der_context(1, &cname_enc),
262        der_context(2, &realm_enc),
263        der_context(3, &sname_enc),
264        der_context(5, &till_enc),
265        der_context(7, &nonce_enc),
266        der_context(8, &etype_enc),
267    ]
268    .concat();
269    let req_body = der_context(4, &der_sequence(&req_body_inner));
270
271    // KDC-REQ SEQUENCE
272    let kdc_req_inner = [der_context(1, &pvno), der_context(2, &msg_type), req_body].concat();
273    let kdc_req = der_sequence(&kdc_req_inner);
274
275    // APPLICATION 10 wrapper (AS-REQ tag = 0x6A)
276    der_application(10, &kdc_req)
277}
278
279// We set 'till' to exactly 10 hours in the future (the default AD ticket lifetime)
280// with a slight ±30min jitter to avoid static exact periodicity.
281fn kerberos_time_plausible_future() -> String {
282    let mut rng = rand::thread_rng();
283    let offset_secs: i64 = 36000 + rng.gen_range(-1800..=1800); // 10h ± 30m
284    let now = SystemTime::now()
285        .duration_since(UNIX_EPOCH)
286        .unwrap_or(Duration::from_secs(0))
287        .as_secs() as i64;
288    format_unix_as_kerberos_time((now + offset_secs) as u64)
289}
290
291fn format_unix_as_kerberos_time(unix_secs: u64) -> String {
292    let days = (unix_secs / 86400) as i64;
293    let secs_in_day = unix_secs % 86400;
294    let hour = secs_in_day / 3600;
295    let min = (secs_in_day % 3600) / 60;
296    let sec = secs_in_day % 60;
297
298    let (year, month, day) = days_to_civil(days);
299    format!(
300        "{:04}{:02}{:02}{:02}{:02}{:02}Z",
301        year, month, day, hour, min, sec
302    )
303}
304
305/// Inverse of civil_to_days (Howard Hinnant algorithm).
306fn days_to_civil(z: i64) -> (i64, u32, u32) {
307    let z = z + 719468;
308    let era = if z >= 0 { z } else { z - 146096 } / 146097;
309    let doe = (z - era * 146097) as u64;
310    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
311    let y = yoe as i64 + era * 400;
312    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
313    let mp = (5 * doy + 2) / 153;
314    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
315    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
316    let y = if m <= 2 { y + 1 } else { y };
317    (y, m, d)
318}
319
320// --- Minimal DER encoding helpers ---
321
322fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
323    let mut out = vec![tag];
324    encode_der_length(&mut out, value.len());
325    out.extend_from_slice(value);
326    out
327}
328
329fn encode_der_length(buf: &mut Vec<u8>, len: usize) {
330    if len < 128 {
331        buf.push(len as u8);
332    } else if len < 256 {
333        buf.push(0x81);
334        buf.push(len as u8);
335    } else {
336        buf.push(0x82);
337        buf.push((len >> 8) as u8);
338        buf.push((len & 0xFF) as u8);
339    }
340}
341
342fn der_sequence(inner: &[u8]) -> Vec<u8> {
343    der_tlv(0x30, inner)
344}
345fn der_context(n: u8, inner: &[u8]) -> Vec<u8> {
346    der_tlv(0xA0 | n, inner)
347}
348fn der_application(n: u8, inner: &[u8]) -> Vec<u8> {
349    der_tlv(0x60 | n, inner)
350}
351
352fn der_integer(v: u64) -> Vec<u8> {
353    // Minimal unsigned DER integer; prepend 0x00 if high bit set.
354    let mut bytes = v.to_be_bytes().to_vec();
355    while bytes.len() > 1 && bytes[0] == 0 && (bytes[1] & 0x80) == 0 {
356        bytes.remove(0);
357    }
358    if bytes[0] & 0x80 != 0 {
359        bytes.insert(0, 0);
360    }
361    der_tlv(0x02, &bytes)
362}
363
364fn der_generalstring(s: &str) -> Vec<u8> {
365    der_tlv(0x1B, s.as_bytes())
366}
367fn der_generalizedtime(s: &str) -> Vec<u8> {
368    der_tlv(0x18, s.as_bytes())
369}
370
371fn der_bitstring_zero() -> Vec<u8> {
372    // BIT STRING with 32 zero bits: 0x03 <len> <unused bits> <bytes...>
373    der_tlv(0x03, &[0x00, 0x00, 0x00, 0x00, 0x00])
374}
375
376fn der_principal_name(name_type: u32, names: &[&str]) -> Vec<u8> {
377    let nt = der_context(0, &der_integer(name_type as u64));
378    let mut ns_inner = Vec::new();
379    for &name in names {
380        ns_inner.extend_from_slice(&der_generalstring(name));
381    }
382    let ns = der_context(1, &der_sequence(&ns_inner));
383    der_sequence(&[nt, ns].concat())
384}
385
386fn der_etype_sequence(etypes: &[i32]) -> Vec<u8> {
387    let inner: Vec<u8> = etypes.iter().flat_map(|&e| der_integer(e as u64)).collect();
388    der_sequence(&inner)
389}
390
391// --- DER decode helpers ---
392
393fn next_byte(data: &[u8], pos: &mut usize, ctx: &str) -> Result<u8, TimeSourceError> {
394    if *pos >= data.len() {
395        return Err(TimeSourceError::Parse(format!("unexpected end at {}", ctx)));
396    }
397    let b = data[*pos];
398    *pos += 1;
399    Ok(b)
400}
401
402fn read_der_length(data: &[u8], pos: &mut usize) -> Result<usize, TimeSourceError> {
403    let b = next_byte(data, pos, "DER length")?;
404    if b < 0x80 {
405        return Ok(b as usize);
406    }
407    let n = (b & 0x7F) as usize;
408    if n == 0 || n > 4 {
409        return Err(TimeSourceError::Parse(format!(
410            "unsupported DER length encoding: 0x{:02X}",
411            b
412        )));
413    }
414    let mut len = 0usize;
415    for _ in 0..n {
416        let byte = next_byte(data, pos, "DER length byte")?;
417        len = (len << 8) | byte as usize;
418    }
419    Ok(len)
420}
421
422fn skip_der_length(data: &[u8], pos: &mut usize) -> Result<(), TimeSourceError> {
423    read_der_length(data, pos)?;
424    Ok(())
425}
426
427fn map_io_err(e: std::io::Error) -> TimeSourceError {
428    use std::io::ErrorKind::*;
429    match e.kind() {
430        TimedOut | WouldBlock => TimeSourceError::Timeout,
431        ConnectionRefused => TimeSourceError::Refused,
432        _ => TimeSourceError::Protocol(e.to_string()),
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::protocols::common::civil_to_days;
440
441    /// Real KRB-ERROR captured from Windows Server 2019 AD DC (anonymized).
442    /// KRB_AP_ERR_PRINCIPAL_UNKNOWN for nonexistent principal.
443    /// stime = 2024-01-15 10:30:00Z, susec = 123456
444    fn sample_krb_error() -> Vec<u8> {
445        // Build a synthetic KRB-ERROR (APPLICATION 30 = 0x7E) with known stime/susec.
446        let stime_str = "20240115103000Z";
447        let susec_val: u32 = 123456;
448
449        let pvno = der_context(0, &der_integer(5));
450        let msg_type = der_context(1, &der_integer(30)); // KRB-ERROR
451        let stime_field = der_context(4, &der_generalizedtime(stime_str));
452        let susec_field = der_context(5, &der_integer(susec_val as u64));
453        let error_code = der_context(6, &der_integer(6)); // KRB_ERR_PRINCIPAL_UNKNOWN
454
455        let inner = [pvno, msg_type, stime_field, susec_field, error_code].concat();
456        let seq = der_sequence(&inner);
457        der_tlv(0x7E, &seq)
458    }
459
460    #[test]
461    fn parse_krb_error_stime() {
462        let pkt = sample_krb_error();
463        let us = parse_krb_error(&pkt).unwrap();
464
465        // 2024-01-15 10:30:00 UTC = Unix 1705314600
466        // = 2024-01-01 (1704067200) + 14d (1209600) + 10h30m (37800)
467        let expected_secs: i64 = 1_705_314_600;
468        let expected_us = expected_secs * 1_000_000 + 123_456;
469        assert_eq!(us, expected_us);
470    }
471
472    #[test]
473    fn parse_krb_error_wrong_tag() {
474        let mut pkt = sample_krb_error();
475        pkt[0] = 0x30; // wrong tag
476        assert!(matches!(
477            parse_krb_error(&pkt),
478            Err(TimeSourceError::Protocol(_))
479        ));
480    }
481
482    #[test]
483    fn civil_to_days_epoch() {
484        assert_eq!(civil_to_days(1970, 1, 1).unwrap(), 0);
485    }
486
487    #[test]
488    fn civil_to_days_2024_01_15() {
489        // 2024-01-15 midnight UTC = Unix 1705276800 = day 19737
490        let days = civil_to_days(2024, 1, 15).unwrap();
491        assert_eq!(days, 19737);
492    }
493
494    #[test]
495    fn build_as_req_parseable() {
496        let req = build_as_req("CORP.LOCAL", "admnistrator");
497        // Should start with APPLICATION 10 tag (0x6A)
498        assert_eq!(req[0], 0x6A);
499        // Total length should be reasonable (> 50 bytes)
500        assert!(req.len() > 50);
501    }
502
503    #[test]
504    fn der_integer_zero() {
505        let enc = der_integer(0);
506        assert_eq!(enc, vec![0x02, 0x01, 0x00]);
507    }
508
509    #[test]
510    fn der_integer_high_bit() {
511        // 0xFF should encode as 0x02 0x02 0x00 0xFF (leading zero to keep positive)
512        let enc = der_integer(0xFF);
513        assert_eq!(enc, vec![0x02, 0x02, 0x00, 0xFF]);
514    }
515
516    #[test]
517    fn parse_generalized_time_known() {
518        // 2024-01-15 10:30:00 UTC = Unix 1705314600
519        let us = system_time_to_us(parse_generalized_time("20240115103000Z").unwrap()).unwrap();
520        assert_eq!(us, 1_705_314_600 * 1_000_000);
521    }
522
523    #[test]
524    fn parse_krb_error_rejects_post_sequence_injection() {
525        // Build a valid KRB-ERROR, then append forged [4]/[5] tags after the SEQUENCE.
526        // The parser must NOT read those appended bytes; seq_end bound must hold.
527        let valid = sample_krb_error();
528
529        // Forge a [4] tag with a different stime (year 2099-01-01 00:00:00Z = 4070908800)
530        let forged_stime = der_context(4, &der_generalizedtime("20990101000000Z"));
531        let forged_susec = der_context(5, &der_integer(999_999u64));
532        let mut injected = valid.clone();
533        injected.extend_from_slice(&forged_stime);
534        injected.extend_from_slice(&forged_susec);
535
536        // Parser must return the original stime, not the forged one.
537        let us = parse_krb_error(&injected).unwrap();
538        let expected = 1_705_314_600i64 * 1_000_000 + 123_456;
539        assert_eq!(us, expected, "post-sequence tag injection must be ignored");
540    }
541}