Skip to main content

rusthound_ce/objects/
user.rs

1use serde_json::value::Value;
2use serde::{Deserialize, Serialize};
3use ldap3::SearchEntry;
4use log::{debug, error, trace};
5use std::collections::HashMap;
6use std::error::Error;
7use std::collections::HashSet;
8use x509_parser::prelude::*;
9
10use crate::enums::decode_guid_le;
11use crate::enums::regex::{OBJECT_SID_RE1, SID_PART1_RE1};
12use crate::objects::common::{LdapObject, AceTemplate, SPNTarget, Link, Member};
13use crate::utils::date::{convert_timestamp, string_to_epoch};
14use crate::utils::crypto::convert_encryption_types;
15use crate::enums::acl::{
16    parse_embedded_security_descriptor, parse_gmsa, parse_ntsecuritydescriptor,
17};
18use crate::enums::secdesc::LdapSid;
19use crate::enums::sid::sid_maker;
20use crate::enums::spntasks::check_spn;
21use crate::enums::uacflags::get_flag;
22
23/// User structure
24#[derive(Debug, Clone, Deserialize, Serialize, Default)]
25pub struct User {
26    #[serde(rename ="ObjectIdentifier")]
27    object_identifier: String,
28    #[serde(rename ="IsDeleted")]
29    is_deleted: bool,
30    #[serde(rename ="IsACLProtected")]
31    is_acl_protected: bool,
32    #[serde(rename ="Properties")]
33    properties: UserProperties,
34    #[serde(rename ="PrimaryGroupSID")]
35    primary_group_sid: String,
36    #[serde(rename ="SPNTargets")]
37    spn_targets: Vec<SPNTarget>,
38    #[serde(rename ="UnconstrainedDelegation")]
39    unconstrained_delegation: bool,
40    #[serde(rename ="DomainSID")]
41    domain_sid: String,
42    #[serde(rename ="Aces")]
43    aces: Vec<AceTemplate>,
44    #[serde(rename ="AllowedToDelegate")]
45    allowed_to_delegate: Vec<Member>,
46    #[serde(rename ="HasSIDHistory")]
47    has_sid_history: Vec<Member>,
48    #[serde(rename ="ContainedBy")]
49    contained_by: Option<Member>,
50}
51
52impl User {
53    // New User
54    pub fn new() -> Self { 
55        Self { ..Default::default()} 
56    }
57
58    // Immutable access.
59    pub fn properties(&self) -> &UserProperties {
60        &self.properties
61    }
62    pub fn aces(&self) -> &Vec<AceTemplate> {
63        &self.aces
64    }
65    pub fn object_identifier(&self) -> &String {
66        &self.object_identifier
67    }
68
69    // Mutable access.
70    pub fn properties_mut(&mut self) -> &mut UserProperties {
71        &mut self.properties
72    }
73    pub fn aces_mut(&mut self) -> &mut Vec<AceTemplate> {
74        &mut self.aces
75    }
76    pub fn object_identifier_mut(&mut self) -> &mut String {
77        &mut self.object_identifier
78    }
79    pub fn has_sid_history_mut(&mut self) -> &mut Vec<Member> {
80        &mut self.has_sid_history
81    }
82
83    /// Function to parse and replace value for user object.
84    /// <https://bloodhound.readthedocs.io/en/latest/further-reading/json.html#users>
85    pub fn parse(
86        &mut self,
87        result: SearchEntry,
88        domain: &str,
89        dn_sid: &mut HashMap<String, String>,
90        sid_type: &mut HashMap<String, String>,
91        domain_sid: &str,
92        schema_guid_map: &HashMap<String, String>,
93    ) -> Result<(), Box<dyn Error>> {
94        let result_dn: String = result.dn.to_uppercase();
95        let result_attrs: HashMap<String, Vec<String>> = result.attrs;
96        let result_bin: HashMap<String, Vec<Vec<u8>>> = result.bin_attrs;
97
98        // Debug for current object
99        debug!("Parse user: {result_dn}");
100
101        // Trace all result attributes
102        for (key, value) in &result_attrs {
103            trace!("  {key:?}:{value:?}");
104        }
105        // Trace all bin result attributes
106        for (key, value) in &result_bin {
107            trace!("  {key:?}:{value:?}");
108        }
109
110        // Change all values...
111        self.properties.domain = domain.to_uppercase();
112        self.properties.distinguishedname = result_dn;
113        self.properties.enabled = true;
114        self.domain_sid = domain_sid.to_string();
115
116        // With a check
117        let mut group_id: String ="".to_owned();
118        for (key, value) in &result_attrs {
119            match key.as_str() {
120                "sAMAccountName" => {
121                    let name = &value[0];
122                    let email = format!("{}@{}",name.to_owned(),domain);
123                    self.properties.name = email.to_uppercase();
124                    self.properties.samaccountname = name.to_string();
125                }
126                "description" => {
127                    self.properties.description = Some(value[0].to_owned());
128                }
129                "mail" => {
130                    self.properties.email = value[0].to_owned();
131                }
132                "title" => {
133                    self.properties.title = value[0].to_owned();
134                }
135                "userPassword" => {
136                    self.properties.userpassword = value[0].to_owned();
137                }
138                "unixUserPassword" => {
139                    self.properties.unixpassword = value[0].to_owned();
140                }
141                "unicodePwd" => {
142                    self.properties.unicodepassword = value[0].to_owned();
143                }
144                "msSFU30Password" => {
145                    //self.properties.sfupassword = value[0].to_owned();
146                }
147                "displayName" => {
148                    self.properties.displayname = value[0].to_owned();
149                }
150                "adminCount" => {
151                    let admincount = value[0].parse::<i32>().unwrap_or(0) != 0;
152                    self.properties.admincount = admincount;
153                    self.properties.adminsdholderprotected = admincount;
154                }
155                "homeDirectory" => {
156                    self.properties.homedirectory = value[0].to_owned();
157                }
158                "scriptPath" => {
159                    self.properties.logonscript = value[0].to_owned();
160                }
161                "profilePath" | "profilepath" => {
162                    if let Some(profile_path) = value.first() {
163                        self.properties.profilepath = profile_path.to_owned();
164                    }
165                }
166                "userAccountControl" => {
167                    let uac = &value[0].parse::<u32>().unwrap_or(0);
168                    self.properties.useraccountcontrol = *uac;
169                    let uac_flags = get_flag(*uac);
170                    //trace!("UAC : {:?}",uac_flags);
171                    for flag in uac_flags {
172                        if flag.contains("AccountDisable") {
173                            self.properties.enabled = false;
174                        };
175                        //if flag.contains("Lockout") { let enabled = true; user_json["Properties"]["enabled"] = enabled;};
176                        if flag.contains("PasswordNotRequired") {
177                            self.properties.passwordnotreqd = true;
178                        };
179                        if flag.contains("DontExpirePassword") {
180                            self.properties.pwdneverexpires = true;
181                        };
182                        if flag.contains("DontReqPreauth") {
183                            self.properties.dontreqpreauth = true;
184                        };
185                        // KUD (Kerberos Unconstrained Delegation)
186                        if flag.contains("TrustedForDelegation") {
187                            self.properties.unconstraineddelegation = true;
188                            self.unconstrained_delegation = true;
189                        };
190                        if flag.contains("NotDelegated") {
191                            self.properties.sensitive = true;
192                        };
193                        //if flag.contains("PasswordExpired") { let password_expired = true; user_json["Properties"]["pwdneverexpires"] = password_expired;};
194                        if flag.contains("TrustedToAuthForDelegation") {
195                            self.properties.trustedtoauth = true;
196                        };
197                        if flag.contains("SmartcardRequired") {
198                            self.properties.smartcardrequired = true;
199                        };
200                        if flag.contains("UseDesKeyOnly") {
201                            self.properties.usedeskeyonly = true;
202                        };
203                        if flag.contains("EncryptedTextPwdAllowed") {
204                            self.properties.encryptedtextpwdallowed = true;
205                        };
206                        if flag.contains("Script") {
207                            self.properties.logonscriptenabled = true;
208                        };
209                    }
210                }
211                "msDS-User-Account-Control-Computed" => {
212                    // Constructed attribute: UF_LOCKOUT and UF_PASSWORD_EXPIRED live
213                    // here, not in userAccountControl. Computed by the DC we query,
214                    // so lockedout reflects that DC's view only.
215                    const UF_LOCKOUT: u32 = 0x0000_0010;
216                    const UF_PASSWORD_EXPIRED: u32 = 0x0080_0000;
217                    let computed = value[0].parse::<u32>().unwrap_or(0);
218                    self.properties.lockedout = computed & UF_LOCKOUT != 0;
219                    self.properties.passwordexpired = computed & UF_PASSWORD_EXPIRED != 0;
220                }
221                "msDS-AllowedToDelegateTo" => {
222                    let mut vec_members2: Vec<Member> = Vec::new();
223                    let mut seen = HashSet::<String>::new();
224
225                    for spn_raw in value {
226                        // Normalize: trim, replace '\' with '/', case-insensitive
227                        let spn = spn_raw.trim().replace('\\', "/");
228                        // SPN need to be: service/host[:port][/...]
229                        let host_part = spn
230                            .split_once('/')   // Split to get hostname and service
231                            .map(|(_, rest)| rest)
232                            .unwrap_or(spn.as_str());
233
234                        // If the SPN got a port like mssql/sql01:1443 split to remove it
235                        let host = host_part.split(':').next().unwrap_or(host_part);
236
237                        // If empty ignore it (ex: "service/")
238                        let fqdn_upper = host.trim().to_ascii_uppercase();
239                        if fqdn_upper.is_empty() {
240                            error!("Skipping empty host in SPN: {:?}", spn_raw);
241                            continue;
242                        }
243                        
244                        // Save it 
245                        if seen.insert(fqdn_upper.clone()) {
246                            let mut m = Member::new();
247                            *m.object_identifier_mut() = fqdn_upper; // already uppercase
248                            *m.object_type_mut() = "Computer".to_string();
249                            vec_members2.push(m);
250                        }
251                    }
252
253                    self.allowed_to_delegate = vec_members2;
254                }
255                "lastLogon" => {
256                    let lastlogon = &value[0].parse::<i64>().unwrap_or(0);
257                    if lastlogon.is_positive() {
258                        let epoch = convert_timestamp(*lastlogon);
259                        self.properties.lastlogon = epoch;
260                    }
261                }
262                "lastLogonTimestamp" => {
263                    let lastlogontimestamp = &value[0].parse::<i64>().unwrap_or(0);
264                    if lastlogontimestamp.is_positive() {
265                        let epoch = convert_timestamp(*lastlogontimestamp);
266                        self.properties.lastlogontimestamp = epoch;
267                    }
268                }
269                "pwdLastSet" => {
270                    let pwdlastset = &value[0].parse::<i64>().unwrap_or(0);
271                    if pwdlastset.is_positive() {
272                        let epoch = convert_timestamp(*pwdlastset);
273                        self.properties.pwdlastset = epoch;
274                    }
275                }
276                "whenCreated" => {
277                    let epoch = string_to_epoch(&value[0])?;
278                    if epoch.is_positive() {
279                        self.properties.whencreated = epoch;
280                    }
281                }
282                "servicePrincipalName" => {
283                    // SPNTargets values
284                    let mut targets: Vec<SPNTarget> = Vec::new();
285                    let mut result: Vec<String> = Vec::new();
286                    let mut added: bool = false;
287                    for v in value {
288                        result.push(v.to_owned());
289                        // Checking the spn for service-account (mssql?)
290                        let _target = match check_spn(v).to_owned() {
291                            Some(_target) => {
292                                if !added {
293                                   targets.push(_target.to_owned());
294                                   added = true;
295                                }
296                            },
297                            None => {}
298                        };
299                    }
300                    self.properties.serviceprincipalnames = result;
301                    self.properties.hasspn = true;
302                    self.spn_targets = targets;
303                }
304                "primaryGroupID" => {
305                    group_id = value[0].to_owned();
306                }
307                "isDeleted" => {
308                    self.is_deleted = true;
309                }
310                "msDS-SupportedEncryptionTypes" => {
311                    self.properties.supportedencryptiontypes = convert_encryption_types(value[0].parse::<i32>().unwrap_or(0));
312                }
313                 _ => {}
314            }
315        }
316
317        // For all, bins attributs
318        let mut sid: String = "".to_owned();
319        for (key, value) in &result_bin {
320            match key.as_str() {
321                "objectGUID" => {
322                    // objectGUID raw to string
323                    let guid = decode_guid_le(&value[0]);
324                    self.properties.objectguid = guid;
325                }
326                "objectSid" => {
327                    sid = sid_maker(LdapSid::parse(&value[0]).unwrap().1, domain);
328                    self.object_identifier = sid.to_owned();
329
330                    for domain_sid in OBJECT_SID_RE1.captures_iter(&sid) {
331                        self.properties.domainsid = domain_sid[0].to_owned().to_string();
332                    }
333                }
334                "nTSecurityDescriptor" => {
335                    // nTSecurityDescriptor raw to string
336                    let relations_ace = parse_ntsecuritydescriptor(
337                        self,
338                        &value[0],
339                        "User",
340                        &result_attrs,
341                        &result_bin,
342                        domain,
343                        schema_guid_map,
344                    );
345                    self.aces_mut().extend(relations_ace);
346                }
347                "sIDHistory" => {
348                    let mut list_sid_history: Vec<String> = Vec::new();
349                    let mut has_sid_history: Vec<Member> = Vec::new();
350                    for bsid in value {
351                        debug!("sIDHistory: {:?}", &bsid);
352                        let sid = sid_maker(LdapSid::parse(bsid).unwrap().1, domain);
353                        let mut member = Member::new();
354                        *member.object_identifier_mut() = sid.clone();
355                        has_sid_history.push(member);
356                        list_sid_history.push(sid);
357                    }
358                    self.properties.sidhistory = list_sid_history;
359                    self.has_sid_history = has_sid_history;
360                }
361                "msDS-GroupMSAMembership" => {
362                    // Embedded security descriptor granting gMSA password readers.
363                    let mut relations_ace = parse_embedded_security_descriptor(
364                        self,
365                        &value[0],
366                        "User",
367                        &result_attrs,
368                        &result_bin,
369                        domain,
370                        schema_guid_map,
371                    );
372                    // Now add the new ACE wich who can read GMSA password
373                    // trace!("User ACES before GMSA: {:?}", self.aces());
374                    parse_gmsa(&mut relations_ace, self);
375                    // trace!("User ACES after GMSA: {:?}", self.aces());
376                }
377                "userCertificate" => {
378                    // <https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adls/d66d1662-0b4f-44ab-a4c8-e788f3ae39cf>
379                    // <https://docs.rs/x509-parser/latest/x509_parser/certificate/struct.X509Certificate.html>
380                    let res = X509Certificate::from_der(&value[0]);
381                    match res {
382                        Ok((_rem, _cert)) => {},
383                        _ => error!("CA x509 certificate parsing failed: {:?}", res),
384                    }
385                }
386                _ => {}
387            }
388        }
389
390        // primaryGroupID if group_id is set
391        #[allow(irrefutable_let_patterns)]
392        if let id = group_id {
393            if let Some(part1) = SID_PART1_RE1.find(&sid) {
394                self.primary_group_sid = format!("{}{}", part1.as_str(), id);
395            } else {
396                eprintln!("[!] Regex did not match any part of the SID");
397            }
398        }
399
400        // Push DN and SID in HashMap
401        dn_sid.insert(
402            self.properties.distinguishedname.to_owned(),
403            self.object_identifier.to_owned(),
404        );
405        // Push DN and Type
406        sid_type.insert(
407            self.object_identifier.to_owned(),
408            "User".to_string(),
409        );
410
411        // Trace and return User struct
412        // trace!("JSON OUTPUT: {:?}",serde_json::to_string(&self).unwrap());
413        Ok(())
414    }
415}
416
417/// Function to change some values from LdapObject trait for User
418impl LdapObject for User {
419    // To JSON
420    fn to_json(&self) -> Value {
421        serde_json::to_value(self).unwrap()
422    }
423
424    // Get values
425    fn get_object_identifier(&self) -> &String {
426        &self.object_identifier
427    }
428    fn get_is_acl_protected(&self) -> &bool {
429        &self.is_acl_protected
430    }
431    fn get_aces(&self) -> &Vec<AceTemplate> {
432        &self.aces
433    }
434    fn get_spntargets(&self) -> &Vec<SPNTarget> {
435        &self.spn_targets
436    }
437    fn get_allowed_to_delegate(&self) -> &Vec<Member> {
438        &self.allowed_to_delegate
439    }
440    fn get_links(&self) -> &Vec<Link> {
441        panic!("Not used by current object.");
442    }
443    fn get_contained_by(&self) -> &Option<Member> {
444        &self.contained_by
445    }
446    fn get_child_objects(&self) -> &Vec<Member> {
447        panic!("Not used by current object.");
448    }
449    fn get_haslaps(&self) -> &bool {
450        &false
451    }
452
453    // Get mutable values
454    fn get_aces_mut(&mut self) -> &mut Vec<AceTemplate> {
455        &mut self.aces
456    }
457    fn get_spntargets_mut(&mut self) -> &mut Vec<SPNTarget> {
458        &mut self.spn_targets
459    }
460    fn get_allowed_to_delegate_mut(&mut self) -> &mut Vec<Member> {
461        &mut self.allowed_to_delegate
462    }
463
464    // Edit values
465    fn set_is_acl_protected(&mut self, is_acl_protected: bool) {
466        self.is_acl_protected = is_acl_protected;
467        self.properties.isaclprotected = is_acl_protected;
468    }
469    fn set_aces(&mut self, aces: Vec<AceTemplate>) {
470        self.aces = aces;
471    }
472    fn set_spntargets(&mut self, spn_targets: Vec<SPNTarget>) {
473        self.spn_targets = spn_targets;
474    }
475    fn set_allowed_to_delegate(&mut self, allowed_to_delegate: Vec<Member>) {
476        self.allowed_to_delegate = allowed_to_delegate;
477    }
478    fn set_links(&mut self, _links: Vec<Link>) {
479        // Not used by current object.
480    }
481    fn set_contained_by(&mut self, contained_by: Option<Member>) {
482        self.contained_by = contained_by;
483    }
484    fn set_child_objects(&mut self, _child_objects: Vec<Member>) {
485        // Not used by current object.
486    }
487    fn set_owner_rights_flags(&mut self, any: bool, any_inherited: bool) {
488        self.properties.doesanyacegrantownerrights = any;
489        self.properties.doesanyinheritedacegrantownerrights = any_inherited;
490    }
491}
492
493/// User properties structure
494#[derive(Debug, Clone, Deserialize, Serialize, Default)]
495pub struct UserProperties {
496    domain: String,
497    name: String,
498    domainsid: String,
499    objectguid: String,
500    doesanyacegrantownerrights: bool,
501    doesanyinheritedacegrantownerrights: bool,
502    isaclprotected: bool,
503    distinguishedname: String,
504    highvalue: bool,
505    description: Option<String>,
506    whencreated: i64,
507    sensitive: bool,
508    dontreqpreauth: bool,
509    passwordnotreqd: bool,
510    unconstraineddelegation: bool,
511    pwdneverexpires: bool,
512    enabled: bool,
513    trustedtoauth: bool,
514    lastlogon: i64,
515    lastlogontimestamp: i64,
516    pwdlastset: i64,
517    serviceprincipalnames: Vec<String>,
518    hasspn: bool,
519    displayname: String,
520    email: String,
521    title: String,
522    homedirectory: String,
523    logonscript: String,
524    useraccountcontrol: u32,
525    samaccountname: String,
526    userpassword: String,
527    unixpassword: String,
528    unicodepassword: String,
529    sfupassword: String,
530    profilepath: String,
531    admincount: bool,
532    adminsdholderprotected: bool,
533    smartcardrequired: bool,
534    usedeskeyonly: bool,
535    encryptedtextpwdallowed: bool,
536    logonscriptenabled: bool,
537    lockedout: bool,
538    passwordexpired: bool,
539    supportedencryptiontypes: Vec<String>,
540    sidhistory: Vec<String>,
541    allowedtodelegate: Vec<String>,
542}
543
544impl UserProperties {
545    // Immutable access.
546    pub fn name(&self) -> &String {
547        &self.name
548    }
549    pub fn domainsid(&self) -> &String {
550        &self.domainsid
551    }
552    pub fn isaclprotected(&self) -> &bool {
553        &self.isaclprotected
554    }
555
556    // Mutable access.
557    pub fn name_mut(&mut self) -> &mut String {
558        &mut self.name
559    }
560    pub fn domainsid_mut(&mut self) -> &mut String {
561        &mut self.domainsid
562    }
563    pub fn isaclprotected_mut(&mut self) -> &mut bool {
564        &mut self.isaclprotected
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571
572    fn parse_user_with_attrs(attrs: HashMap<String, Vec<String>>) -> User {
573        let mut user = User::new();
574        let result = SearchEntry {
575            dn: "CN=Test User,OU=Users,DC=example,DC=local".to_string(),
576            attrs,
577            bin_attrs: HashMap::new(),
578        };
579        let mut dn_sid = HashMap::new();
580        let mut sid_type = HashMap::new();
581        let schema_guid_map = HashMap::new();
582
583        user.parse(
584            result,
585            "example.local",
586            &mut dn_sid,
587            &mut sid_type,
588            "S-1-5-21-1-2-3",
589            &schema_guid_map,
590        )
591        .unwrap();
592
593        user
594    }
595
596    #[test]
597    fn parse_sets_profilepath_from_ldap_profile_path() {
598        let mut attrs = HashMap::new();
599        attrs.insert(
600            "sAMAccountName".to_string(),
601            vec!["rh.profilepath".to_string()],
602        );
603        attrs.insert(
604            "profilePath".to_string(),
605            vec![r"\\FILE01\Profiles\rh.profilepath".to_string()],
606        );
607
608        let user = parse_user_with_attrs(attrs);
609
610        assert_eq!(
611            user.properties.profilepath,
612            r"\\FILE01\Profiles\rh.profilepath"
613        );
614        assert_eq!(
615            user.to_json()["Properties"]["profilepath"],
616            r"\\FILE01\Profiles\rh.profilepath"
617        );
618    }
619
620    #[test]
621    fn parse_defaults_profilepath_to_empty_string_when_absent() {
622        let mut attrs = HashMap::new();
623        attrs.insert(
624            "sAMAccountName".to_string(),
625            vec!["rh.profilepath.control".to_string()],
626        );
627
628        let user = parse_user_with_attrs(attrs);
629
630        assert_eq!(user.properties.profilepath, "");
631        assert_eq!(user.to_json()["Properties"]["profilepath"], "");
632    }
633
634    #[test]
635    fn parse_populates_has_sid_history() {
636        let mut user = User::new();
637        let result = SearchEntry {
638            dn: "CN=Test User,OU=Users,DC=example,DC=local".to_string(),
639            attrs: HashMap::new(),
640            bin_attrs: HashMap::from([(
641                "sIDHistory".to_string(),
642                vec![vec![1, 2, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 0x15, 0xCD, 0x5B, 0x07]],
643            )]),
644        };
645        let mut dn_sid = HashMap::new();
646        let mut sid_type = HashMap::new();
647        let schema_guid_map = HashMap::new();
648
649        user.parse(
650            result,
651            "example.local",
652            &mut dn_sid,
653            &mut sid_type,
654            "S-1-5-21-1-2-3",
655            &schema_guid_map,
656        )
657        .unwrap();
658    }
659}