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