Skip to main content

rusthound_ce/objects/
computer.rs

1use serde_json::value::Value;
2use serde::{Deserialize, Serialize};
3use colored::Colorize;
4use ldap3::SearchEntry;
5use log::{info, debug, trace};
6use std::collections::HashMap;
7use std::error::Error;
8
9use crate::enums::{OBJECT_SID_RE1, SID_PART1_RE1, decode_guid_le};
10use crate::objects::common::{LdapObject, Session, AceTemplate, Member, SPNTarget, LocalGroup, Link, DCRegistryData, WebClientRunning};
11use crate::utils::date::{convert_timestamp,string_to_epoch};
12use crate::utils::crypto::convert_encryption_types;
13use crate::enums::acl::{
14    parse_embedded_security_descriptor, parse_ntsecuritydescriptor,
15};
16use crate::enums::secdesc::LdapSid;
17use crate::enums::sid::sid_maker;
18use crate::enums::uacflags::get_flag;
19
20use super::common::UserRight;
21
22/// Computer structure
23#[derive(Debug, Clone, Deserialize, Serialize, Default)]
24pub struct Computer {
25    #[serde(rename = "Properties")]
26    properties: ComputerProperties,
27    #[serde(rename = "Aces")]
28    aces: Vec<AceTemplate>,
29    #[serde(rename = "ObjectIdentifier")]
30    object_identifier: String,
31    #[serde(rename = "IsDeleted")]
32    is_deleted: bool,
33    #[serde(rename = "IsACLProtected")]
34    is_acl_protected: bool,
35    #[serde(rename = "ContainedBy")]
36    contained_by: Option<Member>,
37
38    #[serde(rename = "PrimaryGroupSID")]
39    primary_group_sid: String,
40    #[serde(rename = "AllowedToDelegate")]
41    allowed_to_delegate: Vec<Member>,
42    #[serde(rename = "AllowedToAct")]
43    allowed_to_act: Vec<Member>,
44    #[serde(rename = "HasSIDHistory")]
45    has_sid_history: Vec<Member>,
46    #[serde(rename = "DumpSMSAPassword")]
47    dump_smsa_password: Vec<Member>,
48    
49    #[serde(rename = "Sessions")]
50    sessions: Session,
51    #[serde(rename = "PrivilegedSessions")]
52    privileged_sessions: Session,
53    #[serde(rename = "RegistrySessions")]
54    registry_sessions: Session,
55    #[serde(rename = "LocalGroups")]
56    local_groups: Vec<LocalGroup>,
57    #[serde(rename = "UserRights")]
58    users_rights: Vec<UserRight>,
59    #[serde(rename = "DCRegistryData")]
60    dcregistry_data: DCRegistryData,
61    #[serde(rename = "IsWebClientRunning")]
62    is_web_client_running: WebClientRunning,
63
64    #[serde(rename = "IsDC")]
65    is_dc: bool,
66    #[serde(rename = "UnconstrainedDelegation")]
67    unconstrained_delegation: bool,
68    #[serde(rename = "DomainSID")]
69    domain_sid: String,
70
71    #[serde(rename = "Status")]
72    status: Option<String>,
73}
74
75impl Computer {
76    // New computer.
77    pub fn new() -> Self { 
78        Self { ..Default::default() } 
79    }
80
81    // Immutable access.
82    pub fn properties(&self) -> &ComputerProperties {
83        &self.properties
84    }
85    pub fn object_identifier(&self) -> &String {
86        &self.object_identifier
87    }
88    pub fn allowed_to_act(&self) -> &Vec<Member> {
89        &self.allowed_to_act
90    }
91
92    /// Active = enabled AND pwdLastSet within the expiry window
93    /// Shared by the machine-contacting modules (sessions, local-group, webclient) to skip stale/dead accounts
94    pub fn is_active(&self, expiry_days: i64) -> bool {
95        if !*self.properties().enabled() {
96            return false;
97        }
98        let pls = self.properties().pwdlastset();
99        if pls <= 0 {
100            return false;
101        }
102        let now = chrono::Utc::now().timestamp();
103        now - pls < expiry_days * 86_400
104    }
105
106    // Mutable access.
107    pub fn allowed_to_act_mut(&mut self) -> &mut Vec<Member> {
108        &mut self.allowed_to_act
109    }
110    pub fn has_sid_history_mut(&mut self) -> &mut Vec<Member> {
111        &mut self.has_sid_history
112    }
113    pub fn sessions_mut(&mut self) -> &mut Session {
114        &mut self.sessions
115    }
116    pub fn privileged_sessions_mut(&mut self) -> &mut Session {
117        &mut self.privileged_sessions
118    }
119    pub fn registry_sessions_mut(&mut self) -> &mut Session {
120        &mut self.registry_sessions
121    }
122    pub fn set_is_web_client_running(&mut self, value: WebClientRunning) {
123        self.is_web_client_running = value;
124    }
125    pub fn users_rights_mut(&mut self) -> &mut Vec<UserRight> {
126        &mut self.users_rights
127    }
128    pub fn local_groups_mut(&mut self) -> &mut Vec<LocalGroup> {
129        &mut self.local_groups
130    }
131
132    /// Function to parse and replace value for computer object.
133    /// <https://bloodhound.readthedocs.io/en/latest/further-reading/json.html#computers>
134    pub fn parse(
135        &mut self,
136        result: SearchEntry,
137        domain: &str,
138        dn_sid: &mut HashMap<String, String>,
139        sid_type: &mut HashMap<String, String>,
140        fqdn_sid: &mut HashMap<String, String>,
141        fqdn_ip: &mut HashMap<String, String>,
142        domain_sid: &str,
143        schema_guid_map: &HashMap<String, String>,
144    ) -> Result<(), Box<dyn Error>> {
145        let result_dn: String = result.dn.to_uppercase();
146        let result_attrs: HashMap<String, Vec<String>> = result.attrs;
147        let result_bin: HashMap<String, Vec<Vec<u8>>> = result.bin_attrs;
148
149        // Debug for current object
150        debug!("Parse computer: {result_dn}");
151
152        // Trace all result attributes
153        for (key, value) in &result_attrs {
154            trace!("  {key:?}:{value:?}");
155        }
156        // Trace all bin result attributes
157        for (key, value) in &result_bin {
158            trace!("  {key:?}:{value:?}");
159        }
160
161        // Change all values...
162        self.properties.domain = domain.to_uppercase();
163        self.properties.distinguishedname = result_dn;
164        self.properties.enabled = true;
165        self.domain_sid = domain_sid.to_string();
166
167        let mut sid: String = "".to_owned();
168        let mut group_id: String = "".to_owned();
169        // With a check
170        for (key, value) in &result_attrs {
171            match key.as_str() {
172                "name" => {
173                    // dNSHostName (the FQDN) is the canonical computer name and
174                    // must win. Only fall back to the `name` attribute when there
175                    // is no dNSHostName; otherwise, since both arms write
176                    // properties.name, the winner would depend on HashMap
177                    // iteration order and vary run-to-run.
178                    if !result_attrs.contains_key("dNSHostName") {
179                        let name = &value[0];
180                        let email = format!("{}.{}",name.to_owned(),domain);
181                        self.properties.name = email.to_uppercase();
182                    }
183                }
184                "sAMAccountName" => {
185                    self.properties.samaccountname = value[0].to_owned();
186                }
187                "dNSHostName" => {
188                    self.properties.name = value[0].to_uppercase();
189                }
190                "description" => {
191                    self.properties.description = Some(value[0].to_owned());
192                }
193                "adminCount" => {
194                    // A non-zero adminCount means the object is (or was) in a
195                    // protected group, so AdminSDHolder owns its DACL.
196                    let admin_count = value[0].parse::<i32>().unwrap_or(0) != 0;
197                    self.properties.admincount = admin_count;
198                    self.properties.adminsdholderprotected = admin_count;
199                }
200                "mail" => {
201                    self.properties.email = value[0].to_owned();
202                }
203                "operatingSystem" => {
204                    self.properties.operatingsystem = value[0].to_owned();
205                }
206                //"operatingSystemServicePack" => {
207                //    //operatingsystem
208                //    let mut operating_system_servicepack = "".to_owned();
209                //    //if result_attrs["operatingSystem"].len() > 0 {
210                //    //    operating_system_servicepack.push_str(&result_attrs["operatingSystem"][0]);
211                //    //}
212                //    //operating_system_servicepack.push_str(&" ");
213                //   operating_system_servicepack.push_str(&result_attrs["operatingSystemServicePack"][0]);
214                //    computer_json["Properties"]["operatingsystem"] = operating_system_servicepack.to_owned();
215                //}
216                // "member" => {
217                //     for member in value {
218                //         localadmin_json["MemberId"] = member.to_owned();
219                //         vec_localadmins.push(localadmin_json.to_owned());
220                //     }
221                //     computer_json["Members"] = vec_localadmins.to_owned();
222                // }
223                "lastLogon" => {
224                    let lastlogon = &value[0].parse::<i64>().unwrap_or(0);
225                    if lastlogon.is_positive() {
226                        let epoch = convert_timestamp(*lastlogon);
227                        self.properties.lastlogon = epoch;
228                    }
229                }
230                "lastLogonTimestamp" => {
231                    let lastlogontimestamp = &value[0].parse::<i64>().unwrap_or(0);
232                    if lastlogontimestamp.is_positive() {
233                        let epoch = convert_timestamp(*lastlogontimestamp);
234                        self.properties.lastlogontimestamp = epoch;
235                    }
236                }
237                "pwdLastSet" => {
238                    let pwdlastset = &value[0].parse::<i64>().unwrap_or(0);
239                    if pwdlastset.is_positive() {
240                        let epoch = convert_timestamp(*pwdlastset);
241                        self.properties.pwdlastset = epoch;
242                    }
243                }
244                "whenCreated" => {
245                    let epoch = string_to_epoch(&value[0])?;
246                    if epoch.is_positive() {
247                        self.properties.whencreated = epoch;
248                    }
249                }
250                "servicePrincipalName" => {
251                    //servicePrincipalName and hasspn
252                    let mut result: Vec<String> = Vec::new();
253                    for value in &result_attrs["servicePrincipalName"] {
254                        result.push(value.to_owned());
255                    }
256                    self.properties.serviceprincipalnames = result;
257                }
258                "userAccountControl" => {
259                    //userAccountControl
260                    let uac = value[0].parse::<u32>().unwrap_or(0);
261                    self.properties.useraccountcontrol = uac;
262
263                    let uac_flags = get_flag(uac);
264                    //trace!("UAC : {:?}",uac_flags);
265                    for flag in uac_flags {
266                        if flag.contains("AccountDisable") {
267                            self.properties.enabled = false;
268                        };
269                        if flag.contains("Lockout") {
270                            self.properties.lockedout = true;
271                        };
272                        if flag.contains("Script") {
273                            self.properties.logonscriptenabled = true;
274                        };
275                        if flag.contains("EncryptedTextPwdAllowed") {
276                            self.properties.encryptedtextpwdallowed = true;
277                        };
278                        if flag.contains("UseDesKeyOnly") {
279                            self.properties.usedeskeyonly = true;
280                        };
281                        if flag.contains("PasswordExpired") {
282                            self.properties.passwordexpired = true;
283                        };
284                        if flag.contains("PartialSecretsAccount") {
285                            self.properties.isreadonlydc = true;
286                        };
287                        // KUD (Kerberos Unconstrained Delegation)
288                        if flag.contains("TrustedForDelegation") {
289                            self.properties.unconstraineddelegation = true;
290                            self.unconstrained_delegation = true;
291                        };
292                        if flag.contains("TrustedToAuthForDelegation") {
293                            self.properties.trustedtoauth = true;
294                        };
295                        if flag.contains("PasswordNotRequired") {
296                            self.properties.passwordnotreqd = true;
297                        };
298                        if flag.contains("DontExpirePassword") {
299                            self.properties.pwdneverexpires = true;
300                        };
301                        if flag.contains("ServerTrustAccount") {
302                            self.properties.isdc = true;
303                            self.properties.is_dc = true;
304                            self.is_dc = true;
305                        }
306                    }
307                }
308                "msDS-AllowedToDelegateTo"  => {
309                    // KCD (Kerberos Constrained Delegation)
310                    //trace!(" AllowToDelegateTo: {:?}",&value);
311                    // AllowedToDelegate
312                    let mut vec_members2: Vec<Member> = Vec::new();
313                    for objet in value {
314                        let mut member_allowed_to_delegate = Member::new();
315                        let split = objet.split("/");
316                        let fqdn = split.collect::<Vec<&str>>()[1];
317                        let mut checker = false;
318                        for member in &vec_members2 {
319                            if member.object_identifier().contains(fqdn.to_uppercase().as_str()) {
320                                checker = true;
321                            }
322                        }
323                        if !checker {
324                            *member_allowed_to_delegate.object_identifier_mut() = fqdn.to_uppercase().to_owned().to_uppercase();
325                            *member_allowed_to_delegate.object_type_mut() = "Computer".to_owned();
326                            vec_members2.push(member_allowed_to_delegate.to_owned()); 
327                        }
328                    }
329                    // *properties.allowedtodelegate = vec_members2.to_owned();
330                    self.allowed_to_delegate = vec_members2;
331                }
332                // LAPS Legacy
333                "ms-Mcs-AdmPwd" => {
334                    // Laps is set, random password for local adminsitrator
335                    // https://github.com/BloodHoundAD/SharpHound3/blob/7615860d963ba70751e1e5a00e02bb3fbca154c6/SharpHound3/Tasks/ACLTasks.cs#L313
336                    info!(
337                        "Your user can read LAPS password on {}: {}",
338                        &result_attrs["name"][0].yellow().bold(),
339                        &result_attrs["ms-Mcs-AdmPwd"][0].yellow().bold()
340                    );
341                    self.properties.haslaps = true;
342                }
343                "ms-Mcs-AdmPwdExpirationTime" => {
344                    // LAPS is set, random password for local adminsitrator
345                    // trace!("ms-Mcs-AdmPwdExpirationTime so haslaps=true");
346                    self.properties.haslaps = true;
347                }
348                // New LAPS attributes
349                "msLAPS-Password" => {
350                    info!(
351                        "Your user can read LAPS password on {}: {:?}",
352                        &result_attrs["name"][0].yellow().bold(),
353                        &value[0].yellow().bold()
354                    );
355                    self.properties.haslaps = true;
356                }
357                "msLAPS-EncryptedPassword" => {
358                    info!(
359                        "Your user can read uncrypted LAPS password on {} please check manually to decrypt it!",
360                        &result_attrs["name"][0].yellow().bold()
361                    );
362                    self.properties.haslaps = true;
363                }
364                "msLAPS-PasswordExpirationTime" => {
365                    // LAPS is set, random password for local adminsitrator
366                    self.properties.haslaps = true;
367                }
368                "primaryGroupID" => {
369                    group_id = value[0].to_owned();
370                }
371                "isDeleted" => {
372                    self.is_deleted = true;
373                }
374                "msDS-SupportedEncryptionTypes" => {
375                    self.properties.supportedencryptiontypes = convert_encryption_types(value[0].parse::<i32>().unwrap_or(0));
376                 }
377                _ => {}
378            }
379        }
380
381        // For all, bins attributs
382        for (key, value) in &result_bin {
383            match key.as_str() {
384                "objectGUID" => {
385                    // objectGUID raw to string
386                    let guid = decode_guid_le(&value[0]);
387                    self.properties.objectguid = guid;
388                }
389                "objectSid" => {
390                    // objectSid raw to string
391                    sid = sid_maker(LdapSid::parse(&value[0]).unwrap().1, domain);
392                    self.object_identifier = sid.to_owned();
393
394                    for domain_sid in OBJECT_SID_RE1.captures_iter(&sid) {
395                        self.properties.domainsid = domain_sid[0].to_owned().to_string();
396                    }
397                }
398                "nTSecurityDescriptor" => {
399                    // nTSecurityDescriptor raw to string
400                    // trace!("Parsing nTSecurityDescriptor..");
401                    let relations_ace = parse_ntsecuritydescriptor(
402                        self,
403                        &value[0],
404                        "Computer",
405                        &result_attrs,
406                        &result_bin,
407                        domain,
408                        schema_guid_map,
409                    );
410                    self.aces = relations_ace;
411                }
412                "msDS-AllowedToActOnBehalfOfOtherIdentity" => {
413                    // RBCD (Resource-based constrained)
414                    // msDS-AllowedToActOnBehalfOfOtherIdentity parsing ACEs
415                    let relations_ace = parse_embedded_security_descriptor(
416                        self,
417                        &value[0],
418                        "Computer",
419                        &result_attrs,
420                        &result_bin,
421                        domain,
422                        schema_guid_map,
423                    );
424                    let mut vec_members_allowtoact: Vec<Member> = Vec::new();
425                    let mut allowed_to_act = Member::new();
426                    for delegated in relations_ace {
427                        //trace!("msDS-AllowedToActOnBehalfOfOtherIdentity => ACE: {:?}",delegated);
428                        // delegated["RightName"] == "Owner" => continue
429                        if *delegated.right_name() == "GenericAll" {
430                            *allowed_to_act.object_identifier_mut() = delegated.principal_sid().to_string();
431                            vec_members_allowtoact.push(allowed_to_act.to_owned()); 
432                            continue
433                        }
434                    }
435                    self.allowed_to_act = vec_members_allowtoact;
436                }
437                "sIDHistory" => {
438                    let mut list_sid_history: Vec<String> = Vec::new();
439                    let mut has_sid_history: Vec<Member> = Vec::new();
440                    for bsid in value {
441                        debug!("sIDHistory: {:?}", &bsid);
442                        let sid = sid_maker(LdapSid::parse(bsid).unwrap().1, domain);
443                        let mut member = Member::new();
444                        *member.object_identifier_mut() = sid.clone();
445                        has_sid_history.push(member);
446                        list_sid_history.push(sid);
447                    }
448                    self.properties.sidhistory = list_sid_history;
449                    self.has_sid_history = has_sid_history;
450                }
451                _ => {}
452            }
453        }
454
455        // primaryGroupID if group_id is set
456        #[allow(irrefutable_let_patterns)]
457        if let id = group_id {
458            if let Some(part1) = SID_PART1_RE1.find(&sid) {
459                self.primary_group_sid = format!("{}{}", part1.as_str(), id);
460            } else {
461                eprintln!("[!] Regex did not match any part of the SID");
462            }
463        }
464
465        // Push DN and SID in HashMap
466        dn_sid.insert(
467            self.properties.distinguishedname.to_string(),
468            self.object_identifier.to_string(),
469
470        );
471        // Push DN and Type
472        sid_type.insert(
473            self.object_identifier.to_string(),
474            "Computer".to_string(),
475        );
476
477        fqdn_sid.insert(
478            self.properties.name.to_string(),
479            self.object_identifier.to_string(),
480        );
481
482        fqdn_ip.insert(
483            self.properties.name.to_string(),
484            String::from(""),
485        );
486
487        // Trace and return Computer struct
488        // trace!("JSON OUTPUT: {:?}",serde_json::to_string(&self).unwrap());
489        Ok(())
490    }
491}
492
493impl LdapObject for Computer {
494    // To JSON
495    fn to_json(&self) -> Value {
496        serde_json::to_value(self).unwrap()
497    }
498
499    // Get values
500    fn get_object_identifier(&self) -> &String {
501        &self.object_identifier
502    }
503    fn get_is_acl_protected(&self) -> &bool {
504        &self.is_acl_protected
505    }
506    fn get_aces(&self) -> &Vec<AceTemplate> {
507        &self.aces
508    }
509    fn get_spntargets(&self) -> &Vec<SPNTarget> {
510        panic!("Not used by current object.");
511    }
512    fn get_allowed_to_delegate(&self) -> &Vec<Member> {
513        &self.allowed_to_delegate
514    }
515    fn get_links(&self) -> &Vec<Link> {
516        panic!("Not used by current object.");
517    }
518    fn get_contained_by(&self) -> &Option<Member> {
519        &self.contained_by
520    }
521    fn get_child_objects(&self) -> &Vec<Member> {
522        panic!("Not used by current object.");
523    }
524    fn get_haslaps(&self) -> &bool {
525        &self.properties.haslaps
526    }
527    
528    // Get mutable values
529    fn get_aces_mut(&mut self) -> &mut Vec<AceTemplate> {
530        &mut self.aces
531    }
532    fn get_spntargets_mut(&mut self) -> &mut Vec<SPNTarget> {
533        panic!("Not used by current object.");
534    }
535    fn get_allowed_to_delegate_mut(&mut self) -> &mut Vec<Member> {
536        &mut self.allowed_to_delegate
537    }
538  
539    // Edit values
540    fn set_is_acl_protected(&mut self, is_acl_protected: bool) {
541        self.is_acl_protected = is_acl_protected;
542        self.properties.isaclprotected = is_acl_protected;
543    }
544    fn set_aces(&mut self, aces: Vec<AceTemplate>) {
545        self.aces = aces;
546    }
547    fn set_spntargets(&mut self, _spn_targets: Vec<SPNTarget>) {
548        // Not used by current object.
549    }
550    fn set_allowed_to_delegate(&mut self, allowed_to_delegate: Vec<Member>) {
551        self.allowed_to_delegate = allowed_to_delegate;
552    }
553    fn set_links(&mut self, _links: Vec<Link>) {
554        // Not used by current object.
555    }
556    fn set_contained_by(&mut self, contained_by: Option<Member>) {
557        self.contained_by = contained_by;
558    }
559    fn set_child_objects(&mut self, _child_objects: Vec<Member>) {
560        // Not used by current object.
561    }
562    fn set_owner_rights_flags(&mut self, any: bool, any_inherited: bool) {
563        self.properties.doesanyacegrantownerrights = any;
564        self.properties.doesanyinheritedacegrantownerrights = any_inherited;
565    }
566}
567
568// Computer properties structure
569#[derive(Debug, Clone, Serialize, Deserialize, Default)]
570pub struct ComputerProperties {
571    domain: String,
572    name: String,
573    distinguishedname: String,
574    domainsid: String,
575    objectguid: String,
576    doesanyacegrantownerrights: bool,
577    doesanyinheritedacegrantownerrights: bool,
578    isaclprotected: bool,
579    highvalue: bool,
580    samaccountname: String,
581    haslaps: bool,
582    description: Option<String>,
583    whencreated: i64,
584    enabled: bool,
585    unconstraineddelegation: bool,
586    trustedtoauth: bool,  
587    lastlogon: i64,
588    lastlogontimestamp: i64,
589    pwdlastset: i64,
590    passwordnotreqd: bool,
591    pwdneverexpires: bool,
592    serviceprincipalnames: Vec<String>,
593    operatingsystem: String,
594    sidhistory: Vec<String>,
595    supportedencryptiontypes: Vec<String>,
596    useraccountcontrol: u32,
597    isdc: bool,
598    isreadonlydc: bool,
599    admincount: bool,
600    adminsdholderprotected: bool,
601    lockedout: bool,
602    passwordexpired: bool,
603    usedeskeyonly: bool,
604    encryptedtextpwdallowed: bool,
605    logonscriptenabled: bool,
606    email: String,
607    #[serde(skip_serializing)]
608    is_dc: bool
609}
610
611impl ComputerProperties {  
612    // Immutable access.
613    pub fn name(&self) -> &String {
614        &self.name
615    }
616    pub fn unconstraineddelegation(&self) -> &bool {
617        &self.unconstraineddelegation
618    }
619    pub fn enabled(&self) -> &bool {
620        &self.enabled
621    }
622    pub fn get_is_dc(&self) -> &bool {
623        &self.is_dc
624    }
625    pub fn pwdlastset(&self) -> i64 { 
626        self.pwdlastset
627    }
628    pub fn distinguishedname(&self) -> &String {
629        &self.distinguishedname
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn parse_populates_has_sid_history() {
639        let mut computer = Computer::new();
640        let result = SearchEntry {
641            dn: "CN=TESTPC,OU=Computers,DC=example,DC=local".to_string(),
642            attrs: HashMap::new(),
643            bin_attrs: HashMap::from([(
644                "sIDHistory".to_string(),
645                vec![vec![1, 2, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 0x15, 0xCD, 0x5B, 0x07]],
646            )]),
647        };
648        let mut dn_sid = HashMap::new();
649        let mut sid_type = HashMap::new();
650        let mut fqdn_sid = HashMap::new();
651        let mut fqdn_ip = HashMap::new();
652        let schema_guid_map = HashMap::new();
653
654        computer
655            .parse(
656                result,
657                "example.local",
658                &mut dn_sid,
659                &mut sid_type,
660                &mut fqdn_sid,
661                &mut fqdn_ip,
662                "S-1-5-21-1-2-3",
663                &schema_guid_map,
664            )
665            .unwrap();
666    }
667}