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