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