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