Skip to main content

rusthound_ce/objects/
group.rs

1use serde::{Deserialize, Serialize};
2use serde_json::value::Value;
3use ldap3::SearchEntry;
4use log::{debug, trace};
5use std::collections::HashMap;
6use std::error::Error;
7
8use crate::enums::{decode_guid_le, group_scope};
9use crate::enums::regex::OBJECT_SID_RE1;
10use crate::objects::common::{LdapObject, AceTemplate, SPNTarget, Link, Member};
11use crate::enums::acl::parse_ntsecuritydescriptor;
12use crate::enums::secdesc::LdapSid;
13use crate::enums::sid::{objectsid_to_vec8, sid_maker};
14use crate::utils::date::string_to_epoch;
15
16/// Group structure
17#[derive(Debug, Clone, Deserialize, Serialize, Default)]
18pub struct Group {
19    #[serde(rename = "ObjectIdentifier")]
20    object_identifier: String,
21    #[serde(rename = "IsDeleted")]
22    is_deleted: bool,
23    #[serde(rename = "IsACLProtected")]
24    is_acl_protected: bool,
25    #[serde(rename = "Properties")]
26    properties: GroupProperties,
27    #[serde(rename = "Members")]
28    members: Vec<Member>,
29    #[serde(rename = "HasSIDHistory")]
30    has_sid_history: Vec<String>,
31    #[serde(rename = "Aces")]
32    aces: Vec<AceTemplate>,
33    #[serde(rename = "ContainedBy")]
34    contained_by: Option<Member>,
35}
36
37impl Group {
38    // New group.
39    pub fn new() -> Self { 
40        Self { ..Default::default() } 
41    }
42
43    // Immutable access.
44    pub fn members(&self) -> &Vec<Member> {
45        &self.members
46    }
47    pub fn properties(&self) -> &GroupProperties {
48        &self.properties 
49    }
50    pub fn object_identifier(&self) -> &String {
51        &self.object_identifier
52    }
53
54    // Mutable access.
55    pub fn properties_mut(&mut self) -> &mut GroupProperties {
56        &mut self.properties
57    }
58    pub fn object_identifier_mut(&mut self) -> &mut String {
59        &mut self.object_identifier
60    }
61    pub fn members_mut(&mut self) -> &mut Vec<Member> {
62        &mut self.members
63    }
64
65    /// Function to parse and replace value for group object.
66    /// <https://bloodhound.readthedocs.io/en/latest/further-reading/json.html#groups>
67    pub fn parse(
68        &mut self,
69        result: SearchEntry,
70        domain: &str,
71        dn_sid: &mut HashMap<String, String>,
72        sid_type: &mut HashMap<String, String>,
73        domain_sid: &str,
74        schema_guid_map: &HashMap<String, String>,
75    ) -> Result<(), Box<dyn Error>> {
76        let result_dn: String = result.dn.to_uppercase();
77        let result_attrs: HashMap<String, Vec<String>> = result.attrs;
78        let result_bin: HashMap<String, Vec<Vec<u8>>> = result.bin_attrs;
79
80        debug!("Parse group: {result_dn}");
81
82        // Trace all result attributes
83        for (key, value) in &result_attrs {
84            trace!("  {key:?}:{value:?}");
85        }
86        // Trace all bin result attributes
87        for (key, value) in &result_bin {
88            trace!("  {key:?}:{value:?}");
89        }
90
91        // Change all values...
92        self.properties.domain = domain.to_uppercase();
93        self.properties.distinguishedname = result_dn;
94        self.properties.domainsid = domain_sid.to_string();
95
96        // With a check
97        for (key, value) in &result_attrs {
98            match key.as_str() {
99                "name" => {
100                    let name = &value[0];
101                    let email = format!("{}@{}", name.to_owned(), domain);
102                    self.properties.name = email.to_uppercase();
103                }
104                "description" => {
105                    self.properties.description = Some(value[0].to_owned());
106                }
107                "adminCount" => {
108                    // adminCount is not limited to 1: any non-zero value means
109                    // the object is (or was) in a protected group, so
110                    // AdminSDHolder owns its DACL.
111                    let admincount = value[0].parse::<i32>().unwrap_or(0) != 0;
112                    self.properties.admincount = admincount;
113                    self.properties.adminsdholderprotected = admincount;
114                }
115                "groupType" => {
116                    self.properties.groupscope = group_scope(value[0].parse::<i64>().unwrap_or(0));
117                }
118                "sAMAccountName" => {
119                    self.properties.samaccountname = value[0].to_owned();
120                }
121                "member" => {
122                    if !value.is_empty() {
123                        let mut _vec_members: Vec<Member> = Vec::new();
124
125                        for member in value {
126                            let _member = member.trim();
127                            if _member.is_empty() {
128                                continue;
129                            }
130                            if _member.eq_ignore_ascii_case("SID") {
131                                continue;
132                            }
133
134                            let mut m = Member::new();
135                            *m.object_identifier_mut() = _member.to_uppercase();
136                            _vec_members.push(m);
137                        }
138                        
139                        self.members = _vec_members;
140                    }
141                }
142                "objectSid" => {
143                    // objectSid to vec and raw to string
144                    let vec_sid = objectsid_to_vec8(&value[0]);
145                    let sid = sid_maker(LdapSid::parse(&vec_sid).unwrap().1, domain);
146                    self.object_identifier = sid.to_owned();
147
148                    /*let re = Regex::new(r"^S-[0-9]{1}-[0-9]{1}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}").unwrap();
149                    for domain_sid in re.captures_iter(&sid) 
150                    {
151                        group_json["Properties"]["domainsid"] = domain_sid[0].to_owned().to_string();
152                    }*/
153
154                    // highvalue
155                    if sid.ends_with("-512") 
156                        || sid.ends_with("-516") 
157                        || sid.ends_with("-519") 
158                        || sid.ends_with("-520") 
159                    {
160                        self.properties.highvalue = true;
161                    } else if sid.ends_with("S-1-5-32-544") 
162                        || sid.ends_with("S-1-5-32-548") 
163                        || sid.ends_with("S-1-5-32-549")
164                        || sid.ends_with("S-1-5-32-550") 
165                        || sid.ends_with("S-1-5-32-551")
166                    {
167                        self.properties.highvalue = true;
168                    } else {
169                        self.properties.highvalue = false;
170                    }
171                }
172                "whenCreated" => {
173                    let epoch = string_to_epoch(&value[0])?;
174                    if epoch.is_positive() {
175                        self.properties.whencreated = epoch;
176                    }
177                }
178                "isDeleted" => {
179                    self.is_deleted = true;
180                }
181                _ => {}
182            }
183        }
184
185        // For all, bins attributs
186        for (key, value) in &result_bin {
187            match key.as_str() {
188                "objectGUID" => {
189                    // objectGUID raw to string
190                    let guid = decode_guid_le(&value[0]);
191                    self.properties.objectguid = guid;
192                }
193                "objectSid" => {
194                    // objectSid raw to string
195                    let sid = sid_maker(LdapSid::parse(&value[0]).unwrap().1, domain);
196                    self.object_identifier = sid.to_owned();
197
198                    for domain_sid in OBJECT_SID_RE1.captures_iter(&sid) {
199                        self.properties.domainsid = domain_sid[0].to_owned().to_string();
200                    }
201    
202                    // highvalue
203                    if sid.ends_with("-512") 
204                        || sid.ends_with("-516") 
205                        || sid.ends_with("-519") 
206                        || sid.ends_with("-520") 
207                    {
208                        self.properties.highvalue = true;
209                    }
210                    else if sid.ends_with("S-1-5-32-544") 
211                        || sid.ends_with("S-1-5-32-548") 
212                        || sid.ends_with("S-1-5-32-549")
213                        || sid.ends_with("S-1-5-32-550") 
214                        || sid.ends_with("S-1-5-32-551") 
215                    {
216                        self.properties.highvalue = true;
217                    }
218                    else {
219                        self.properties.highvalue = false;
220                    }
221                }
222                "nTSecurityDescriptor" => {
223                    // nTSecurityDescriptor raw to string
224                    let relations_ace = parse_ntsecuritydescriptor(
225                        self,
226                        &value[0],
227                        "Group",
228                        &result_attrs,
229                        &result_bin,
230                        domain,
231                        schema_guid_map,
232                    );
233                    self.aces = relations_ace;
234                }
235                "sIDHistory" => {
236                    let mut list_sid_history: Vec<String> = Vec::new();
237                    for bsid in value {
238                        debug!("sIDHistory: {:?}", &bsid);
239                        list_sid_history.push(sid_maker(LdapSid::parse(bsid).unwrap().1, domain));
240                    }
241                    self.properties.sidhistory = list_sid_history.clone();
242                    self.has_sid_history = list_sid_history;
243                }
244                _ => {}
245            }
246        }
247
248        // Push DN and SID in HashMap
249        dn_sid.insert(
250            self.properties.distinguishedname.to_string(),
251            self.object_identifier.to_string(),
252        );
253        // Push DN and Type
254        sid_type.insert(
255            self.object_identifier.to_string(),
256            "Group".to_string(),
257        );
258
259        // Trace and return Group struct
260        // trace!("JSON OUTPUT: {:?}",serde_json::to_string(&self).unwrap());
261        Ok(())
262    }
263}
264
265impl LdapObject for Group {
266    // To JSON
267    fn to_json(&self) -> Value {
268        serde_json::to_value(self).unwrap()
269    }
270
271    // Get values
272    fn get_object_identifier(&self) -> &String {
273        &self.object_identifier
274    }
275    fn get_is_acl_protected(&self) -> &bool {
276        &self.is_acl_protected
277    }
278    fn get_aces(&self) -> &Vec<AceTemplate> {
279        &self.aces
280    }
281    fn get_spntargets(&self) -> &Vec<SPNTarget> {
282        panic!("Not used by current object.");
283    }
284    fn get_allowed_to_delegate(&self) -> &Vec<Member> {
285        panic!("Not used by current object.");
286    }
287    fn get_links(&self) -> &Vec<Link> {
288        panic!("Not used by current object.");
289    }
290    fn get_contained_by(&self) -> &Option<Member> {
291        &self.contained_by
292    }
293    fn get_child_objects(&self) -> &Vec<Member> {
294        panic!("Not used by current object.");
295    }
296    fn get_haslaps(&self) -> &bool {
297        &false
298    }
299    
300    // Get mutable values
301    fn get_aces_mut(&mut self) -> &mut Vec<AceTemplate> {
302        &mut self.aces
303    }
304    fn get_spntargets_mut(&mut self) -> &mut Vec<SPNTarget> {
305        panic!("Not used by current object.");
306    }
307    fn get_allowed_to_delegate_mut(&mut self) -> &mut Vec<Member> {
308        panic!("Not used by current object.");
309    }
310    
311    // Edit values
312    fn set_is_acl_protected(&mut self, is_acl_protected: bool) {
313        self.is_acl_protected = is_acl_protected;
314        self.properties.isaclprotected = is_acl_protected;
315    }
316    fn set_aces(&mut self, aces: Vec<AceTemplate>) {
317        self.aces = aces;
318    }
319    fn set_spntargets(&mut self, _spn_targets: Vec<SPNTarget>) {
320        // Not used by current object.
321    }
322    fn set_allowed_to_delegate(&mut self, _allowed_to_delegate: Vec<Member>) {
323        // Not used by current object.
324    }
325    fn set_links(&mut self, _links: Vec<Link>) {
326        // Not used by current object.
327    }
328    fn set_contained_by(&mut self, contained_by: Option<Member>) {
329        self.contained_by = contained_by;
330    }
331    fn set_child_objects(&mut self, _child_objects: Vec<Member>) {
332        // Not used by current object.
333    }
334    fn set_owner_rights_flags(&mut self, any: bool, any_inherited: bool) {
335        self.properties.doesanyacegrantownerrights = any;
336        self.properties.doesanyinheritedacegrantownerrights = any_inherited;
337    }
338}
339
340// Group properties structure
341#[derive(Debug, Clone, Deserialize, Serialize, Default)]
342pub struct GroupProperties {
343    domain: String,
344    name: String,
345    distinguishedname: String,
346    domainsid: String,
347    objectguid: String,
348    doesanyacegrantownerrights: bool,
349    doesanyinheritedacegrantownerrights: bool,
350    isaclprotected: bool,
351    highvalue: bool,
352    samaccountname: String,
353    description: Option<String>,
354    whencreated: i64,
355    admincount: bool,
356    adminsdholderprotected: bool,
357    groupscope: String,
358    sidhistory: Vec<String>,
359}
360
361impl GroupProperties {
362    // Get access.
363    pub fn name(&self) -> &String {
364        &self.name
365    }
366
367    // Mutable access.
368    pub fn name_mut(&mut self) -> &mut String {
369        &mut self.name
370    }
371    pub fn domain_mut(&mut self) -> &mut String {
372        &mut self.domain
373    }
374    pub fn domainsid_mut(&mut self) -> &mut String {
375        &mut self.domainsid
376    }
377    pub fn highvalue_mut(&mut self) -> &mut bool {
378        &mut self.highvalue
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn group_type_maps_to_bloodhound_scope() {
388        // groupType is a signed 32-bit value; security groups carry 0x80000000.
389        assert_eq!(group_scope(-2147483646), "Global");      // security, global
390        assert_eq!(group_scope(-2147483643), "DomainLocal"); // security, builtin local
391        assert_eq!(group_scope(-2147483644), "DomainLocal"); // security, resource
392        assert_eq!(group_scope(-2147483640), "Universal");   // security, universal
393        assert_eq!(group_scope(2), "Global");                // distribution, global
394        assert_eq!(group_scope(8), "Universal");             // distribution, universal
395        assert_eq!(group_scope(0), "");
396    }
397}