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