New upstream release.
Kali Janitor
2 years ago
0 | pywerview (0.3.3-0kali1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | ||
4 | -- Kali Janitor <[email protected]> Tue, 25 Jan 2022 01:39:22 -0000 | |
5 | ||
0 | 6 | pywerview (0.3.2-0kali1) kali-dev; urgency=medium |
1 | 7 | |
2 | 8 | [ Ben Wilson ] |
24 | 24 | def get_adobject(domain_controller, domain, user, password=str(), |
25 | 25 | lmhash=str(), nthash=str(), queried_domain=str(), queried_sid=str(), |
26 | 26 | queried_name=str(), queried_sam_account_name=str(), ads_path=str(), |
27 | custom_filter=str()): | |
27 | attributes=list(), custom_filter=str()): | |
28 | 28 | requester = NetRequester(domain_controller, domain, user, password, |
29 | 29 | lmhash, nthash) |
30 | 30 | return requester.get_adobject(queried_domain=queried_domain, |
31 | 31 | queried_sid=queried_sid, queried_name=queried_name, |
32 | 32 | queried_sam_account_name=queried_sam_account_name, |
33 | ads_path=ads_path, custom_filter=custom_filter) | |
33 | ads_path=ads_path, attributes=attributes, custom_filter=custom_filter) | |
34 | ||
35 | def get_objectacl(domain_controller, domain, user, password=str(), | |
36 | lmhash=str(), nthash=str(), queried_domain=str(), queried_sid=str(), | |
37 | queried_name=str(), queried_sam_account_name=str(), ads_path=str(), | |
38 | sacl=False, rights_filter=str(), resolve_sids=False, | |
39 | resolve_guids=False, custom_filter=str()): | |
40 | requester = NetRequester(domain_controller, domain, user, password, | |
41 | lmhash, nthash) | |
42 | return requester.get_objectacl(queried_domain=queried_domain, | |
43 | queried_sid=queried_sid, queried_name=queried_name, | |
44 | queried_sam_account_name=queried_sam_account_name, | |
45 | ads_path=ads_path, sacl=sacl, rights_filter=rights_filter, | |
46 | resolve_sids=resolve_sids, resolve_guids=resolve_guids, | |
47 | custom_filter=custom_filter) | |
34 | 48 | |
35 | 49 | def get_netuser(domain_controller, domain, user, password=str(), lmhash=str(), |
36 | 50 | nthash=str(), queried_username=str(), queried_domain=str(), ads_path=str(), |
193 | 207 | requester = GPORequester(domain_controller, domain, user, password, |
194 | 208 | lmhash, nthash) |
195 | 209 | return requester.get_netgpo(queried_gponame=queried_gponame, |
210 | queried_displayname=queried_displayname, | |
211 | queried_domain=queried_domain, ads_path=ads_path) | |
212 | ||
213 | def get_netpso(domain_controller, domain, user, password=str(), | |
214 | lmhash=str(), nthash=str(), queried_psoname='*', | |
215 | queried_displayname=str(), queried_domain=str(), ads_path=str()): | |
216 | requester = GPORequester(domain_controller, domain, user, password, | |
217 | lmhash, nthash) | |
218 | return requester.get_netpso(queried_psoname=queried_psoname, | |
196 | 219 | queried_displayname=queried_displayname, |
197 | 220 | queried_domain=queried_domain, ads_path=ads_path) |
198 | 221 |
90 | 90 | help='Domain to query') |
91 | 91 | get_adobject_parser.add_argument('-a', '--ads-path', |
92 | 92 | help='Additional ADS path') |
93 | get_adobject_parser.add_argument('--attributes', nargs='+', dest='attributes', | |
94 | default=[], help='Object attributes to return') | |
93 | 95 | get_adobject_parser.set_defaults(func=get_adobject) |
96 | ||
97 | # Parser for the get-objectacl command | |
98 | get_objectacl_parser = subparsers.add_parser('get-objectacl', help='Takes a domain SID, '\ | |
99 | 'samAccountName or name, and return the ACL of the associated object', parents=[ad_parser]) | |
100 | get_objectacl_parser.add_argument('--sid', dest='queried_sid', | |
101 | help='SID to query (wildcards accepted)') | |
102 | get_objectacl_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', | |
103 | help='samAccountName to query (wildcards accepted)') | |
104 | get_objectacl_parser.add_argument('--name', dest='queried_name', | |
105 | help='Name to query (wildcards accepted)') | |
106 | get_objectacl_parser.add_argument('-d', '--domain', dest='queried_domain', | |
107 | help='Domain to query') | |
108 | get_objectacl_parser.add_argument('-a', '--ads-path', | |
109 | help='Additional ADS path') | |
110 | get_objectacl_parser.add_argument('--sacl', action='store_true', | |
111 | help='Return the SACL instead of the DACL for the object (requires '\ | |
112 | 'a privileged account)') | |
113 | get_objectacl_parser.add_argument('--rights-filter', dest='rights_filter', | |
114 | choices=['reset-password', 'write-members', 'all'], help='A specific set of rights to return '\ | |
115 | '(reset-password, write-members, all)') | |
116 | get_objectacl_parser.add_argument('--resolve-sids', dest='resolve_sids', | |
117 | action='store_true', help='Resolve SIDs when querying an ACL') | |
118 | get_objectacl_parser.add_argument('--resolve-guids', action='store_true', | |
119 | help='Resolve GUIDs to their display names') | |
120 | get_objectacl_parser.set_defaults(func=get_objectacl) | |
94 | 121 | |
95 | 122 | # Parser for the get-netuser command |
96 | 123 | get_netuser_parser = subparsers.add_parser('get-netuser', help='Queries information about '\ |
252 | 279 | get_netgpo_parser.add_argument('-a', '--ads-path', |
253 | 280 | help='Additional ADS path') |
254 | 281 | get_netgpo_parser.set_defaults(func=get_netgpo) |
282 | ||
283 | # Parser for the get-netpso command | |
284 | get_netpso_parser = subparsers.add_parser('get-netpso', help='Get a list of all current '\ | |
285 | 'PSOs in the domain', parents=[ad_parser]) | |
286 | get_netpso_parser.add_argument('--psoname', dest='queried_psoname', | |
287 | default='*', help='pso name to query for (wildcards accepted)') | |
288 | get_netpso_parser.add_argument('--displayname', dest='queried_displayname', | |
289 | help='Display name to query for (wildcards accepted)') | |
290 | get_netpso_parser.add_argument('-d', '--domain', dest='queried_domain', | |
291 | help='Domain to query') | |
292 | get_netpso_parser.add_argument('-a', '--ads-path', | |
293 | help='Additional ADS path') | |
294 | get_netpso_parser.set_defaults(func=get_netpso) | |
255 | 295 | |
256 | 296 | # Parser for the get-domainpolicy command |
257 | 297 | get_domainpolicy_parser = subparsers.add_parser('get-domainpolicy', help='Returns the default domain or DC '\ |
468 | 508 | if results is not None: |
469 | 509 | try: |
470 | 510 | for x in results: |
471 | print(x) | |
511 | print(x, '\n') | |
472 | 512 | # for example, invoke_checklocaladminaccess returns a bool |
473 | 513 | except TypeError: |
474 | 514 | print(results) |
0 | # This file is part of PywerView. | |
1 | ||
2 | # PywerView is free software: you can redistribute it and/or modify | |
3 | # it under the terms of the GNU General Public License as published by | |
4 | # the Free Software Foundation, either version 3 of the License, or | |
5 | # (at your option) any later version. | |
6 | ||
7 | # PywerView is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 | # GNU General Public License for more details. | |
11 | ||
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. | |
14 | ||
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
16 | ||
17 | __uac_flags = {0x0000001: 'SCRIPT', | |
18 | 0x0000002: 'ACCOUNTDISABLE', | |
19 | 0x0000008: 'HOMEDIR_REQUIRED', | |
20 | 0x0000010: 'LOCKOUT', | |
21 | 0x0000020: 'PASSWD_NOTREQD', | |
22 | 0x0000040: 'PASSWD_CANT_CHANGE', | |
23 | 0x0000080: 'ENCRYPTED_TEXT_PWD_ALLOWED', | |
24 | 0x0000100: 'TEMP_DUPLICATE_ACCOUNT', | |
25 | 0x0000200: 'NORMAL_ACCOUNT', | |
26 | 0x0000800: 'INTERDOMAIN_TRUST_ACCOUNT', | |
27 | 0x0001000: 'WORKSTATION_TRUST_ACCOUNT', | |
28 | 0x0002000: 'SERVER_TRUST_ACCOUNT', | |
29 | 0x0010000: 'DONT_EXPIRE_PASSWORD', | |
30 | 0x0020000: 'MNS_LOGON_ACCOUNT', | |
31 | 0x0040000: 'SMARTCARD_REQUIRED', | |
32 | 0x0080000: 'TRUSTED_FOR_DELEGATION', | |
33 | 0x0100000: 'NOT_DELEGATED', | |
34 | 0x0200000: 'USE_DES_KEY_ONLY', | |
35 | 0x0400000: 'DONT_REQ_PREAUTH', | |
36 | 0x0800000: 'PASSWORD_EXPIRED', | |
37 | 0x1000000: 'TRUSTED_TO_AUTH_FOR_DELEGATION', | |
38 | 0x4000000: 'PARTIAL_SECRETS_ACCOUNT'} | |
39 | ||
40 | __ace_flags = {0x1: 'object_inherit', 0x2: 'container_inherit', | |
41 | 0x4: 'non_propagate_inherit', 0x8: 'inherit_only', | |
42 | 0x10: 'inherited_ace', 0x20: 'audit_successful_accesses', | |
43 | 0x40: 'audit_failed_access'} | |
44 | ||
45 | __object_ace_flags = {0x1: 'object_ace_type_present', 0x2: 'inherited_object_ace_type_present'} | |
46 | ||
47 | # Resources: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/990fb975-ab31-4bc1-8b75-5da132cd4584 | |
48 | __access_mask = {0x1: 'create_child', 0x2: 'delete_child', | |
49 | 0x4: 'list_children', 0x08: 'self', | |
50 | 0x10: 'read_property', 0x20: 'write_property', | |
51 | 0x40: 'delete_tree', 0x80: 'list_object', | |
52 | 0x100: 'extended_right', 0x10000: 'delete', | |
53 | 0x20000: 'read_control', 0x40000: 'write_dacl', | |
54 | 0x80000: 'write_owner'} | |
55 | ||
56 | __access_mask_generic = {0xf01ff: 'generic_all', 0x20094: 'generic_read', | |
57 | 0x20028: 'generic_write', 0x20004: 'generic_execute'} | |
58 | ||
59 | __trust_attrib = {0x1: 'non_transitive', 0x2: 'uplevel_only', | |
60 | 0x4: 'filter_sids', 0x8: 'forest_transitive', | |
61 | 0x10: 'cross_organization', 0x20: 'within_forest', | |
62 | 0x40: 'treat_as_external', | |
63 | 0x80: 'trust_uses_rc4_encryption', | |
64 | 0x100: 'trust_uses_aes_keys', | |
65 | 0X200: 'cross_organization_no_tgt_delegation', | |
66 | 0x400: 'pim_trust'} | |
67 | ||
68 | __trust_direction = {0: 'disabled', 1: 'inbound', | |
69 | 2: 'outbound', 3: 'bidirectional'} | |
70 | ||
71 | __trust_type = {1: 'windows_non_active_directory', | |
72 | 2: 'windows_active_directory', 3: 'mit'} | |
73 | ||
74 | def __format_flag(raw_value, flag_dict): | |
75 | try: | |
76 | int_value = int(raw_value) | |
77 | except ValueError: | |
78 | return raw_value | |
79 | ||
80 | parsed_flags = list() | |
81 | for flag, flag_label in flag_dict.items(): | |
82 | if (int_value & flag) == flag: | |
83 | parsed_flags.append(flag_label) | |
84 | return parsed_flags | |
85 | ||
86 | def __format_dict_lookup(raw_value, dictionary): | |
87 | try: | |
88 | return dictionary[int(raw_value)] | |
89 | except (ValueError, KeyError): | |
90 | return raw_value | |
91 | ||
92 | def format_useraccountcontrol(raw_value): | |
93 | return __format_flag(raw_value, __uac_flags) | |
94 | ||
95 | def format_ace_access_mask(raw_value): | |
96 | try: | |
97 | int_value = int(raw_value) | |
98 | except ValueError: | |
99 | return raw_value | |
100 | ||
101 | activedirectoryrights = list() | |
102 | for flag, flag_label in __access_mask_generic.items(): | |
103 | if (int_value & flag) == flag: | |
104 | activedirectoryrights.append(flag_label) | |
105 | int_value ^= flag | |
106 | activedirectoryrights += __format_flag(raw_value, __access_mask) | |
107 | ||
108 | return activedirectoryrights | |
109 | ||
110 | def format_ace_flags(raw_value): | |
111 | return __format_flag(raw_value, __ace_flags) | |
112 | ||
113 | def format_object_ace_flags(raw_value): | |
114 | return __format_flag(raw_value, __object_ace_flags) | |
115 | ||
116 | def format_trustdirection(raw_value): | |
117 | return __format_dict_lookup(raw_value, __trust_direction) | |
118 | ||
119 | def format_trusttype(raw_value): | |
120 | return __format_dict_lookup(raw_value, __trust_type) | |
121 | ||
122 | def format_trustattributes(raw_value): | |
123 | return __format_flag(raw_value, __trust_attrib) | |
124 |
41 | 41 | |
42 | 42 | return self._ldap_search(gpo_search_filter, GPO) |
43 | 43 | |
44 | @LDAPRequester._ldap_connection_init | |
45 | def get_netpso(self, queried_psoname='*', queried_displayname=str(), | |
46 | queried_domain=str(), ads_path=str()): | |
47 | ||
48 | pso_search_filter = '(objectClass=msDS-PasswordSettings)' | |
49 | ||
50 | if queried_displayname: | |
51 | pso_search_filter += '(displayname={})'.format(queried_displayname) | |
52 | else: | |
53 | pso_search_filter += '(name={})'.format(queried_psoname) | |
54 | ||
55 | pso_search_filter = '(&{})'.format(pso_search_filter) | |
56 | ||
57 | return self._ldap_search(pso_search_filter, PSO) | |
58 | ||
44 | 59 | def get_gpttmpl(self, gpttmpl_path): |
45 | 60 | content_io = BytesIO() |
46 | 61 | |
57 | 72 | smb_connection.connectTree(share) |
58 | 73 | smb_connection.getFile(share, file_name, content_io.write) |
59 | 74 | try: |
60 | content = codecs.decode(content_io.getvalue(), 'utf-16le')[1:].replace('\r', '') | |
75 | content = content_io.getvalue().decode('utf-16le')[1:].replace('\r', '') | |
61 | 76 | except UnicodeDecodeError: |
62 | content = str(content_io.getvalue()).replace('\r', '') | |
77 | content = content_io.getvalue().decode('utf-8').replace('\r', '') | |
63 | 78 | |
64 | 79 | gpttmpl_final = GptTmpl(list()) |
65 | 80 | for l in content.split('\n'): |
66 | 81 | if l.startswith('['): |
67 | 82 | section_name = l.strip('[]').replace(' ', '').lower() |
68 | setattr(gpttmpl_final, section_name, Policy(list())) | |
83 | gpttmpl_final._attributes_dict[section_name] = Policy(list()) | |
69 | 84 | elif '=' in l: |
70 | 85 | property_name, property_values = [x.strip() for x in l.split('=')] |
71 | 86 | if ',' in property_values: |
72 | 87 | property_values = property_values.split(',') |
73 | try: | |
74 | setattr(getattr(gpttmpl_final, section_name), property_name, property_values) | |
75 | except UnicodeEncodeError: | |
76 | property_name = property_name.encode('utf-8') | |
77 | setattr(getattr(gpttmpl_final, section_name), property_name, property_values) | |
88 | gpttmpl_final._attributes_dict[section_name]._attributes_dict[property_name] = property_values | |
78 | 89 | |
79 | 90 | return gpttmpl_final |
80 | 91 | |
104 | 115 | members = inspect.getmembers(privilege_rights_policy, lambda x: not(inspect.isroutine(x))) |
105 | 116 | with NetRequester(self._domain_controller, self._domain, self._user, |
106 | 117 | self._password, self._lmhash, self._nthash) as net_requester: |
107 | for member in members: | |
108 | if member[0].startswith('_'): | |
109 | continue | |
110 | if not isinstance(member[1], list): | |
111 | sids = [member[1]] | |
118 | for attr in privilege_rights_policy._attributes_dict: | |
119 | attribute = privilege_rights_policy._attributes_dict[attr] | |
120 | if not isinstance(attribute, list): | |
121 | sids = [attribute] | |
112 | 122 | else: |
113 | sids = member[1] | |
123 | sids = attribute | |
114 | 124 | resolved_sids = list() |
115 | 125 | for sid in sids: |
116 | 126 | if not sid: |
117 | 127 | continue |
128 | sid = sid.replace('*', '') | |
118 | 129 | try: |
119 | resolved_sid = net_requester.get_adobject(queried_sid=sid, queried_domain=queried_domain)[0] | |
130 | resolved_sid = net_requester.get_adobject(queried_sid=sid, queried_domain=self._queried_domain)[0] | |
120 | 131 | except IndexError: |
121 | 132 | resolved_sid = sid |
122 | 133 | else: |
123 | 134 | resolved_sid = resolved_sid.distinguishedname.split(',')[:2] |
124 | resolved_sid = '{}\\{}'.format(resolved_sid[1], resolved_sid[0]) | |
135 | resolved_sid = resolved_sid[1] + '\\' + resolved_sid[0] | |
125 | 136 | resolved_sid = resolved_sid.replace('CN=', '') |
126 | 137 | resolved_sids.append(resolved_sid) |
127 | 138 | if len(resolved_sids) == 1: |
128 | 139 | resolved_sids = resolved_sids[0] |
129 | setattr(privilege_rights_policy, member[0], resolved_sids) | |
140 | privilege_rights_policy._attributes_dict[attr] = resolved_sids | |
130 | 141 | |
131 | 142 | gpttmpl.privilegerights = privilege_rights_policy |
132 | 143 | |
155 | 166 | return list() |
156 | 167 | |
157 | 168 | content = content_io.getvalue().replace(b'\r', b'') |
158 | groupsxml_soup = BeautifulSoup(content, 'xml') | |
159 | ||
169 | groupsxml_soup = BeautifulSoup(content.decode('utf-8'), 'xml') | |
160 | 170 | for group in groupsxml_soup.find_all('Group'): |
161 | 171 | members = list() |
162 | 172 | memberof = list() |
173 | ||
174 | raw_xml_member = group.Properties.find_all('Member') | |
175 | if not raw_xml_member: | |
176 | continue | |
177 | ||
163 | 178 | local_sid = group.Properties.get('groupSid', str()) |
179 | ||
164 | 180 | if not local_sid: |
165 | 181 | if 'administrators' in group.Properties['groupName'].lower(): |
166 | 182 | local_sid = 'S-1-5-32-544' |
170 | 186 | local_sid = group.Properties['groupName'] |
171 | 187 | memberof.append(local_sid) |
172 | 188 | |
173 | for member in group.Properties.find_all('Member'): | |
189 | for member in raw_xml_member: | |
174 | 190 | if not member['action'].lower() == 'add': |
175 | 191 | continue |
176 | 192 | if member['sid']: |
184 | 200 | # have the barest support for filters, so ¯\_(ツ)_/¯ |
185 | 201 | |
186 | 202 | gpo_group = GPOGroup(list()) |
187 | setattr(gpo_group, 'gpodisplayname', gpo_display_name) | |
188 | setattr(gpo_group, 'gponame', gpo_name) | |
189 | setattr(gpo_group, 'gpopath', groupsxml_path) | |
190 | setattr(gpo_group, 'members', members) | |
191 | setattr(gpo_group, 'memberof', memberof) | |
203 | gpo_group._attributes_dict['gpodisplayname'] = gpo_display_name | |
204 | gpo_group._attributes_dict['gponame'] = gpo_name | |
205 | gpo_group._attributes_dict['gpopath'] = groupsxml_path | |
206 | gpo_group._attributes_dict['members'] = members | |
207 | gpo_group._attributes_dict['memberof'] = memberof | |
192 | 208 | |
193 | 209 | gpo_groups.append(gpo_group) |
194 | 210 | |
195 | 211 | return gpo_groups |
196 | 212 | |
197 | 213 | def _get_groupsgpttmpl(self, gpttmpl_path, gpo_display_name): |
198 | import inspect | |
199 | 214 | gpo_groups = list() |
200 | 215 | |
201 | 216 | gpt_tmpl = self.get_gpttmpl(gpttmpl_path) |
206 | 221 | except AttributeError: |
207 | 222 | return list() |
208 | 223 | |
209 | membership = inspect.getmembers(group_membership, lambda x: not(inspect.isroutine(x))) | |
210 | for m in membership: | |
211 | if not m[1]: | |
224 | membership = group_membership._attributes_dict | |
225 | ||
226 | for ma,mv in membership.items(): | |
227 | if not mv: | |
212 | 228 | continue |
213 | 229 | members = list() |
214 | 230 | memberof = list() |
215 | if m[0].lower().endswith('__memberof'): | |
216 | members.append(m[0].upper().lstrip('*').replace('__MEMBEROF', '')) | |
217 | if not isinstance(m[1], list): | |
218 | memberof_list = [m[1]] | |
219 | else: | |
220 | memberof_list = m[1] | |
231 | if ma.lower().endswith('__memberof'): | |
232 | members.append(ma.upper().lstrip('*').replace('__MEMBEROF', '')) | |
233 | if not isinstance(mv, list): | |
234 | memberof_list = [mv] | |
235 | else: | |
236 | memberof_list = mv | |
221 | 237 | memberof += [x.lstrip('*') for x in memberof_list] |
222 | elif m[0].lower().endswith('__members'): | |
223 | memberof.append(m[0].upper().lstrip('*').replace('__MEMBERS', '')) | |
224 | if not isinstance(m[1], list): | |
225 | members_list = [m[1]] | |
226 | else: | |
227 | members_list = m[1] | |
238 | elif ma.lower().endswith('__members'): | |
239 | memberof.append(ma.upper().lstrip('*').replace('__MEMBERS', '')) | |
240 | if not isinstance(mv, list): | |
241 | members_list = [mv] | |
242 | else: | |
243 | members_list = mv | |
228 | 244 | members += [x.lstrip('*') for x in members_list] |
229 | 245 | |
230 | 246 | if members and memberof: |
231 | 247 | gpo_group = GPOGroup(list()) |
232 | setattr(gpo_group, 'gpodisplayname', gpo_display_name) | |
233 | setattr(gpo_group, 'gponame', gpo_name) | |
234 | setattr(gpo_group, 'gpopath', gpttmpl_path) | |
235 | setattr(gpo_group, 'members', members) | |
236 | setattr(gpo_group, 'memberof', memberof) | |
248 | gpo_group.add_attributes({'gpodisplayname' : gpo_display_name}) | |
249 | gpo_group.add_attributes({'gponame' : gpo_name}) | |
250 | gpo_group.add_attributes({'gpopath' : gpttmpl_path}) | |
251 | gpo_group.add_attributes({'members' : members}) | |
252 | gpo_group.add_attributes({'memberof' : memberof}) | |
237 | 253 | |
238 | 254 | gpo_groups.append(gpo_group) |
239 | 255 | |
271 | 287 | self._password, self._lmhash, self._nthash) as net_requester: |
272 | 288 | for member in members: |
273 | 289 | try: |
274 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=queried_domain)[0] | |
275 | resolved_member = resolved_member.distinguishedname.split(',') | |
276 | resolved_member_domain = '.'.join(resolved_member[1:]) | |
277 | resolved_member = '{}\\{}'.format(resolved_member_domain, resolved_member[0]) | |
278 | resolved_member = resolved_member.replace('CN=', '').replace('DC=', '') | |
290 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0] | |
291 | resolved_member = resolved_member.distinguishedname | |
279 | 292 | except IndexError: |
280 | 293 | resolved_member = member |
281 | 294 | finally: |
282 | 295 | resolved_members.append(resolved_member) |
283 | gpo_group.members = resolved_members | |
296 | gpo_group._attributes_dict['members'] = resolved_members | |
284 | 297 | |
285 | 298 | for member in memberof: |
286 | 299 | try: |
287 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=queried_domain)[0] | |
288 | resolved_member = resolved_member.distinguishedname.split(',')[:2] | |
289 | resolved_member = '{}\\{}'.format(resolved_member[1], resolved_member[0]) | |
290 | resolved_member = resolved_member.replace('CN=', '').replace('DC=', '') | |
300 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0] | |
301 | resolved_member = resolved_member.distinguishedname | |
291 | 302 | except IndexError: |
292 | 303 | resolved_member = member |
293 | 304 | finally: |
294 | 305 | resolved_memberof.append(resolved_member) |
295 | gpo_group.memberof = memberof = resolved_memberof | |
296 | ||
306 | gpo_group._attributes_dict['memberof'] = memberof = resolved_memberof | |
297 | 307 | return results |
298 | 308 | |
299 | 309 | def find_gpocomputeradmin(self, queried_computername=str(), |
326 | 336 | for target_ou in target_ous: |
327 | 337 | ous = net_requester.get_netou(ads_path=target_ou, queried_domain=queried_domain, |
328 | 338 | full_data=True) |
329 | ||
330 | 339 | for ou in ous: |
331 | for gplink in ou.gplink.strip('[]').split(']['): | |
340 | try: | |
341 | gplinks = ou.gplink.strip('[]').split('][') | |
342 | except AttributeError: | |
343 | continue | |
344 | for gplink in gplinks: | |
332 | 345 | gplink = gplink.split(';')[0] |
333 | 346 | gpo_groups = self.get_netgpogroup(queried_domain=queried_domain, |
334 | 347 | ads_path=gplink) |
335 | 348 | for gpo_group in gpo_groups: |
336 | 349 | for member in gpo_group.members: |
337 | 350 | obj = net_requester.get_adobject(queried_sid=member, |
338 | queried_domain=queried_domain)[0] | |
351 | queried_domain=self._queried_domain)[0] | |
339 | 352 | gpo_computer_admin = GPOComputerAdmin(list()) |
340 | setattr(gpo_computer_admin, 'computername', queried_computername) | |
341 | setattr(gpo_computer_admin, 'ou', target_ou) | |
342 | setattr(gpo_computer_admin, 'gpodisplayname', gpo_group.gpodisplayname) | |
343 | setattr(gpo_computer_admin, 'gpopath', gpo_group.gpopath) | |
344 | setattr(gpo_computer_admin, 'objectname', obj.name) | |
345 | setattr(gpo_computer_admin, 'objectdn', obj.distinguishedname) | |
346 | setattr(gpo_computer_admin, 'objectsid', member) | |
347 | setattr(gpo_computer_admin, 'isgroup', (obj.samaccounttype != '805306368')) | |
353 | gpo_computer_admin.add_attributes({'computername' : queried_computername}) | |
354 | gpo_computer_admin.add_attributes({'ou' : target_ou}) | |
355 | gpo_computer_admin.add_attributes({'gpodisplayname' : gpo_group.gpodisplayname}) | |
356 | gpo_computer_admin.add_attributes({'gpopath' : gpo_group.gpopath}) | |
357 | gpo_computer_admin.add_attributes({'objectname' : obj.name}) | |
358 | gpo_computer_admin.add_attributes({'objectdn' : obj.distinguishedname}) | |
359 | gpo_computer_admin.add_attributes({'objectsid' : obj.objectsid}) | |
360 | gpo_computer_admin.add_attributes({'isgroup' : (obj.samaccounttype != '805306368')}) | |
348 | 361 | |
349 | 362 | results.append(gpo_computer_admin) |
350 | 363 | |
352 | 365 | groups_to_resolve = [gpo_computer_admin.objectsid] |
353 | 366 | while groups_to_resolve: |
354 | 367 | group_to_resolve = groups_to_resolve.pop(0) |
368 | ||
355 | 369 | group_members = net_requester.get_netgroupmember(queried_sid=group_to_resolve, |
356 | queried_domain=queried_domain, | |
370 | queried_domain=self._queried_domain, | |
357 | 371 | full_data=True) |
358 | 372 | for group_member in group_members: |
359 | 373 | gpo_computer_admin = GPOComputerAdmin(list()) |
360 | setattr(gpo_computer_admin, 'computername', queried_computername) | |
361 | setattr(gpo_computer_admin, 'ou', target_ou) | |
362 | setattr(gpo_computer_admin, 'gpodisplayname', gpo_group.gpodisplayname) | |
363 | setattr(gpo_computer_admin, 'gpopath', gpo_group.gpopath) | |
364 | setattr(gpo_computer_admin, 'objectname', group_member.samaccountname) | |
365 | setattr(gpo_computer_admin, 'objectdn', group_member.distinguishedname) | |
366 | setattr(gpo_computer_admin, 'objectsid', member) | |
367 | setattr(gpo_computer_admin, 'isgroup', (group_member.samaccounttype != '805306368')) | |
374 | gpo_computer_admin.add_attributes({'computername' : queried_computername}) | |
375 | gpo_computer_admin.add_attributes({'ou' : target_ou}) | |
376 | gpo_computer_admin.add_attributes({'gpodisplayname' : gpo_group.gpodisplayname}) | |
377 | gpo_computer_admin.add_attributes({'gpopath' : gpo_group.gpopath}) | |
378 | gpo_computer_admin.add_attributes({'objectname' : group_member.samaccountname}) | |
379 | gpo_computer_admin.add_attributes({'objectdn' : group_member.distinguishedname}) | |
380 | gpo_computer_admin.add_attributes({'objectsid' : group_member.objectsid}) | |
381 | gpo_computer_admin.add_attributes({'isgroup' : (group_member != '805306368')}) | |
368 | 382 | |
369 | 383 | results.append(gpo_computer_admin) |
370 | 384 | |
381 | 395 | if queried_username: |
382 | 396 | try: |
383 | 397 | user = net_requester.get_netuser(queried_username=queried_username, |
384 | queried_domain=queried_domain)[0] | |
398 | queried_domain=self._queried_domain)[0] | |
385 | 399 | except IndexError: |
386 | 400 | raise ValueError('Username \'{}\' was not found'.format(queried_username)) |
387 | 401 | else: |
388 | target_sid = [user.objectsid] | |
402 | target_sid = [user.objectsid] | |
389 | 403 | object_sam_account_name = user.samaccountname |
390 | 404 | object_distinguished_name = user.distinguishedname |
391 | 405 | elif queried_groupname: |
392 | 406 | try: |
393 | 407 | group = net_requester.get_netgroup(queried_groupname=queried_groupname, |
394 | queried_domain=queried_domain, | |
408 | queried_domain=self._queried_domain, | |
395 | 409 | full_data=True)[0] |
396 | 410 | except IndexError: |
397 | 411 | raise ValueError('Group name \'{}\' was not found'.format(queried_groupname)) |
417 | 431 | for object_group in object_groups: |
418 | 432 | try: |
419 | 433 | object_group_sid = net_requester.get_adobject(queried_sam_account_name=object_group.samaccountname, |
420 | queried_domain=queried_domain)[0].objectsid | |
434 | queried_domain=self._queried_domain)[0].objectsid | |
421 | 435 | except IndexError: |
422 | 436 | # We may have the name of the group, but not its sam account name |
423 | 437 | try: |
424 | 438 | object_group_sid = net_requester.get_adobject(queried_name=object_group.samaccountname, |
425 | queried_domain=queried_domain)[0].objectsid | |
439 | queried_domain=self._queried_domain)[0].objectsid | |
426 | 440 | except IndexError: |
427 | 441 | # Freak accident when someone is a member of a group, but |
428 | 442 | # we can't find the group in the AD |
434 | 448 | for gpo_group in self.get_netgpogroup(queried_domain=queried_domain): |
435 | 449 | try: |
436 | 450 | for member in gpo_group.members: |
451 | member = member | |
437 | 452 | if not member.upper().startswith('S-1-5'): |
438 | 453 | try: |
439 | 454 | member = net_requester.get_adobject(queried_sam_account_name=member, |
440 | queried_domain=queried_domain)[0].objectsid | |
455 | queried_domain=self._queried_domain)[0].objectsid | |
441 | 456 | except (IndexError, AttributeError): |
442 | 457 | continue |
443 | 458 | if (member.upper() in target_sid) or (member.lower() in target_sid): |
450 | 465 | |
451 | 466 | for gpo_group in gpo_groups: |
452 | 467 | gpo_guid = gpo_group.gponame |
453 | ous = net_requester.get_netou(queried_domain=queried_domain, | |
468 | ous = net_requester.get_netou(queried_domain=self._queried_domain, | |
454 | 469 | queried_guid=gpo_guid, full_data=True) |
455 | 470 | for ou in ous: |
471 | ou_distinguishedname = 'LDAP://{}'.format(ou.distinguishedname) | |
456 | 472 | # TODO: support filters for GPO |
457 | 473 | ou_computers = [x.dnshostname for x in \ |
458 | net_requester.get_netcomputer(queried_domain=queried_domain, | |
459 | ads_path=ou.distinguishedname)] | |
474 | net_requester.get_netcomputer(queried_domain=self._queried_domain, | |
475 | ads_path=ou_distinguishedname)] | |
460 | 476 | gpo_location = GPOLocation(list()) |
461 | setattr(gpo_location, 'objectname', object_distinguished_name) | |
462 | setattr(gpo_location, 'gponame', gpo_group.gpodisplayname) | |
463 | setattr(gpo_location, 'gpoguid', gpo_guid) | |
464 | setattr(gpo_location, 'containername', ou.distinguishedname) | |
465 | setattr(gpo_location, 'computers', ou_computers) | |
477 | gpo_location.add_attributes({'objectname' : object_distinguished_name}) | |
478 | gpo_location.add_attributes({'gponame' : gpo_group.gpodisplayname}) | |
479 | gpo_location.add_attributes({'gpoguid' : gpo_guid}) | |
480 | gpo_location.add_attributes({'containername' : ou.distinguishedname}) | |
481 | gpo_location.add_attributes({'computers' : ou_computers}) | |
466 | 482 | |
467 | 483 | results.append(gpo_location) |
468 | 484 |
19 | 19 | |
20 | 20 | from pywerview.requester import LDAPRPCRequester |
21 | 21 | import pywerview.functions.net |
22 | ||
23 | import struct | |
22 | 24 | |
23 | 25 | class Misc(LDAPRPCRequester): |
24 | 26 | @LDAPRPCRequester._rpc_connection_init(r'\drsuapi') |
54 | 56 | |
55 | 57 | if domain_controllers: |
56 | 58 | primary_dc = domain_controllers[0] |
57 | domain_sid = '-'.join(primary_dc.objectsid.split('-')[:-1]) | |
59 | domain_sid = primary_dc.objectsid | |
60 | ||
61 | # we need to retrieve the domain sid from the controller sid | |
62 | domain_sid = '-'.join(domain_sid.split('-')[:-1]) | |
58 | 63 | else: |
59 | 64 | domain_sid = None |
60 | 65 | |
74 | 79 | |
75 | 80 | return True |
76 | 81 | |
82 | class Utils(): | |
83 | @staticmethod | |
84 | def convert_sidtostr(raw_sid): | |
85 | str_sid = 'S-{0}-{1}'.format(raw_sid[0], raw_sid[1]) | |
86 | for i in range(8, len(raw_sid), 4): | |
87 | str_sid += '-{}'.format(str(struct.unpack('<I', raw_sid[i:i+4])[0])) | |
88 | return str_sid | |
89 | ||
90 | @staticmethod | |
91 | def convert_guidtostr(raw_guid): | |
92 | str_guid = str() | |
93 | str_guid += '{}-'.format(hex(struct.unpack('<I', raw_guid[0:4])[0])[2:].zfill(8)) | |
94 | str_guid += '{}-'.format(hex(struct.unpack('<H', raw_guid[4:6])[0])[2:].zfill(4)) | |
95 | str_guid += '{}-'.format(hex(struct.unpack('<H', raw_guid[6:8])[0])[2:].zfill(4)) | |
96 | str_guid += '{}-'.format(raw_guid.hex()[16:20]) | |
97 | str_guid += raw_guid.hex()[20:] | |
98 | return str_guid |
15 | 15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 |
16 | 16 | |
17 | 17 | import socket |
18 | import datetime | |
18 | from datetime import datetime, timedelta | |
19 | 19 | from impacket.dcerpc.v5.ndr import NULL |
20 | 20 | from impacket.dcerpc.v5 import wkst, srvs, samr |
21 | 21 | from impacket.dcerpc.v5.samr import DCERPCSessionError |
23 | 23 | from impacket.dcerpc.v5.dcom.wmi import WBEM_FLAG_FORWARD_ONLY |
24 | 24 | from bs4 import BeautifulSoup |
25 | 25 | from ldap3.utils.conv import escape_filter_chars |
26 | from ldap3.protocol.microsoft import security_descriptor_control | |
27 | from ldap3.protocol.formatters.formatters import * | |
28 | from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR | |
26 | 29 | |
27 | 30 | from pywerview.requester import LDAPRPCRequester |
28 | 31 | import pywerview.objects.adobjects as adobj |
29 | 32 | import pywerview.objects.rpcobjects as rpcobj |
30 | 33 | import pywerview.functions.misc |
34 | import pywerview.formatters as fmt | |
31 | 35 | |
32 | 36 | class NetRequester(LDAPRPCRequester): |
33 | 37 | @LDAPRPCRequester._ldap_connection_init |
34 | 38 | def get_adobject(self, queried_domain=str(), queried_sid=str(), |
35 | 39 | queried_name=str(), queried_sam_account_name=str(), |
36 | ads_path=str(), custom_filter=str()): | |
37 | ||
38 | for attr_desc, attr_value in (('objectSid', queried_sid), ('name', queried_name), | |
39 | ('samAccountName', queried_sam_account_name)): | |
40 | ads_path=str(), attributes=list(), custom_filter=str()): | |
41 | for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)), | |
42 | ('samAccountName', escape_filter_chars(queried_sam_account_name))): | |
40 | 43 | if attr_value: |
41 | 44 | object_filter = '(&({}={}){})'.format(attr_desc, attr_value, custom_filter) |
42 | 45 | break |
43 | ||
44 | return self._ldap_search(object_filter, adobj.ADObject) | |
46 | else: | |
47 | object_filter = '(&(name=*){})'.format(custom_filter) | |
48 | ||
49 | return self._ldap_search(object_filter, adobj.ADObject, attributes=attributes) | |
50 | ||
51 | @LDAPRPCRequester._ldap_connection_init | |
52 | def get_objectacl(self, queried_domain=str(), queried_sid=str(), | |
53 | queried_name=str(), queried_sam_account_name=str(), | |
54 | ads_path=str(), sacl=False, rights_filter=str(), | |
55 | resolve_sids=False, resolve_guids=False, custom_filter=str()): | |
56 | for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)), | |
57 | ('samAccountName', escape_filter_chars(queried_sam_account_name))): | |
58 | if attr_value: | |
59 | object_filter = '(&({}={}){})'.format(attr_desc, attr_value, custom_filter) | |
60 | break | |
61 | else: | |
62 | object_filter = '(&(name=*){})'.format(custom_filter) | |
63 | ||
64 | guid_map = dict() | |
65 | # This works on a mono-domain forest, must be tested on a more complex one | |
66 | if resolve_guids: | |
67 | # Dirty fix to get base DN even if custom ADS path was given | |
68 | base_dn = ','.join(self._base_dn.split(',')[-2:]) | |
69 | guid_map = {'{00000000-0000-0000-0000-000000000000}': 'All'} | |
70 | with NetRequester(self._domain_controller, self._domain, self._user, self._password, | |
71 | self._lmhash, self._nthash) as net_requester: | |
72 | for o in net_requester.get_adobject(ads_path='CN=Schema,CN=Configuration,{}'.format(base_dn), | |
73 | attributes=['name', 'schemaIDGUID'], custom_filter='(schemaIDGUID=*)'): | |
74 | guid_map['{{{}}}'.format(o.schemaidguid)] = o.name | |
75 | ||
76 | for o in net_requester.get_adobject(ads_path='CN=Extended-Rights,CN=Configuration,{}'.format(base_dn), | |
77 | attributes=['name', 'rightsGuid'], custom_filter='(objectClass=controlAccessRight)'): | |
78 | guid_map['{{{}}}'.format(o.rightsguid.lower())] = o.name | |
79 | ||
80 | attributes = ['distinguishedname', 'objectsid', 'ntsecuritydescriptor'] | |
81 | if sacl: | |
82 | controls = list() | |
83 | acl_type = 'Sacl' | |
84 | else: | |
85 | # The control is used to get access to ntSecurityDescriptor with an | |
86 | # unprivileged user, see https://stackoverflow.com/questions/40771503/selecting-the-ad-ntsecuritydescriptor-attribute-as-a-non-admin/40773088 | |
87 | # /!\ May break pagination from what I've read (see Stack Overflow answer) | |
88 | controls = security_descriptor_control(criticality=True, sdflags=0x07) | |
89 | acl_type = 'Dacl' | |
90 | ||
91 | security_descriptors = self._ldap_search(object_filter, adobj.ADObject, | |
92 | attributes=attributes, controls=controls) | |
93 | ||
94 | acl = list() | |
95 | ||
96 | rights_to_guid = {'reset-password': '{00299570-246d-11d0-a768-00aa006e0529}', | |
97 | 'write-members': '{bf9679c0-0de6-11d0-a285-00aa003049e2}', | |
98 | 'all': '{00000000-0000-0000-0000-000000000000}'} | |
99 | guid_filter = rights_to_guid.get(rights_filter, None) | |
100 | ||
101 | if resolve_sids: | |
102 | sid_resolver = NetRequester(self._domain_controller, self._domain, | |
103 | self._user, self._password, self._lmhash, self._nthash) | |
104 | sid_mapping = adobj.ADObject._well_known_sids.copy() | |
105 | else: | |
106 | sid_resolver = None | |
107 | ||
108 | for security_descriptor in security_descriptors: | |
109 | sd = SR_SECURITY_DESCRIPTOR() | |
110 | try: | |
111 | sd.fromString(security_descriptor.ntsecuritydescriptor) | |
112 | except TypeError: | |
113 | continue | |
114 | for ace in sd[acl_type]['Data']: | |
115 | if guid_filter: | |
116 | try: | |
117 | object_type = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}' | |
118 | except KeyError: | |
119 | continue | |
120 | if object_type != guid_filter: | |
121 | continue | |
122 | attributes = dict() | |
123 | attributes['objectdn'] = security_descriptor.distinguishedname | |
124 | attributes['objectsid'] = security_descriptor.objectsid | |
125 | attributes['acetype'] = ace['TypeName'] | |
126 | attributes['binarysize'] = ace['AceSize'] | |
127 | attributes['aceflags'] = fmt.format_ace_flags(ace['AceFlags']) | |
128 | attributes['accessmask'] = ace['Ace']['Mask']['Mask'] | |
129 | attributes['activedirectoryrights'] = fmt.format_ace_access_mask(ace['Ace']['Mask']['Mask']) | |
130 | attributes['isinherited'] = bool(ace['AceFlags'] & 0x10) | |
131 | attributes['securityidentifier'] = format_sid(ace['Ace']['Sid'].getData()) | |
132 | if sid_resolver: | |
133 | converted_sid = attributes['securityidentifier'] | |
134 | try: | |
135 | resolved_sid = sid_mapping[converted_sid] | |
136 | except KeyError: | |
137 | try: | |
138 | resolved_sid = sid_resolver.get_adobject(queried_sid=converted_sid, | |
139 | queried_domain=self._queried_domain, attributes=['distinguishedname'])[0] | |
140 | resolved_sid = resolved_sid.distinguishedname | |
141 | except IndexError: | |
142 | resolved_sid = attributes['securityidentifier'] | |
143 | finally: | |
144 | sid_mapping[converted_sid] = resolved_sid | |
145 | attributes['securityidentifier'] = resolved_sid | |
146 | try: | |
147 | attributes['objectaceflags'] = fmt.format_object_ace_flags(ace['Ace']['Flags']) | |
148 | except KeyError: | |
149 | pass | |
150 | try: | |
151 | attributes['objectacetype'] = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}' | |
152 | attributes['objectacetype'] = guid_map[attributes['objectacetype']] | |
153 | except KeyError: | |
154 | pass | |
155 | try: | |
156 | attributes['inheritedobjectacetype'] = format_uuid_le(ace['Ace']['InheritedObjectType']) if ace['Ace']['InheritedObjectType'] else '{00000000-0000-0000-0000-000000000000}' | |
157 | attributes['inheritedobjectacetype'] = guid_map[attributes['inheritedobjectacetype']] | |
158 | except KeyError: | |
159 | pass | |
160 | ||
161 | acl.append(adobj.ACE(attributes)) | |
162 | ||
163 | return acl | |
45 | 164 | |
46 | 165 | @LDAPRPCRequester._ldap_connection_init |
47 | 166 | def get_netuser(self, queried_username=str(), queried_domain=str(), |
79 | 198 | |
80 | 199 | # RFC 4515, section 3 |
81 | 200 | # However if we escape *, we can no longer use wildcard within `--groupname` |
82 | # Maybe we can raise a warning here ? | |
201 | # Maybe we can raise a warning here ? | |
83 | 202 | if not '*' in queried_groupname: |
84 | 203 | queried_groupname = escape_filter_chars(queried_groupname) |
85 | 204 | |
119 | 238 | final_results = list() |
120 | 239 | for group_sam_account_name in results: |
121 | 240 | obj_member_of = adobj.Group(list()) |
122 | setattr(obj_member_of, 'samaccountname', group_sam_account_name) | |
241 | obj_member_of._attributes_dict['samaccountname'] = group_sam_account_name | |
123 | 242 | final_results.append(obj_member_of) |
124 | 243 | return final_results |
125 | 244 | else: |
208 | 327 | |
209 | 328 | final_results = list() |
210 | 329 | for file_server_name in results: |
211 | attributes = list() | |
212 | attributes.append({'type': 'dnshostname', 'vals': [file_server_name]}) | |
330 | attributes = dict() | |
331 | attributes['dnshostname'] = file_server_name | |
213 | 332 | final_results.append(adobj.FileServer(attributes)) |
214 | 333 | |
215 | 334 | return final_results |
227 | 346 | for remote_server in dfs.remoteservername: |
228 | 347 | remote_server = str(remote_server) |
229 | 348 | if '\\' in remote_server: |
230 | attributes = {'name': [dfs.name.encode('utf-8')], | |
231 | 'remoteservername': [remote_server.split('\\')[2].encode('utf-8')]} | |
349 | attributes = {'name': dfs.name, | |
350 | 'remoteservername': remote_server.split('\\')[2]} | |
232 | 351 | results.append(adobj.DFS(attributes)) |
233 | 352 | |
234 | 353 | return results |
249 | 368 | for target in soup_target_list.targets.contents: |
250 | 369 | if '\\' in target.string: |
251 | 370 | server_name, dfs_root = target.string.split('\\')[2:4] |
252 | attributes = {'name': ['{}{}'.format(dfs_root, share_name).encode('utf-8')], | |
253 | 'remoteservername': [server_name.encode('utf-8')]} | |
371 | attributes = {'name': '{}{}'.format(dfs_root, share_name), | |
372 | 'remoteservername': server_name} | |
254 | 373 | |
255 | 374 | results.append(adobj.DFS(attributes)) |
256 | 375 | |
338 | 457 | # `--groupname` option is supplied |
339 | 458 | if _groupname: |
340 | 459 | groups = self.get_netgroup(queried_groupname=_groupname, |
341 | queried_domain=queried_domain, | |
460 | queried_domain=self._queried_domain, | |
342 | 461 | full_data=True) |
343 | 462 | |
344 | 463 | # `--groupname` option is missing, falling back to the "Domain Admins" |
352 | 471 | self._nthash) as misc_requester: |
353 | 472 | queried_sid = misc_requester.get_domainsid(queried_domain) + '-512' |
354 | 473 | groups = self.get_netgroup(queried_sid=queried_sid, |
355 | queried_domain=queried_domain, | |
474 | queried_domain=self._queried_domain, | |
356 | 475 | full_data=True) |
357 | 476 | except IndexError: |
358 | 477 | raise ValueError('The group {} was not found'.format(_groupname)) |
365 | 484 | group_memberof_filter = '(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:={}){})'.format(group.distinguishedname, custom_filter) |
366 | 485 | |
367 | 486 | members = self.get_netuser(custom_filter=group_memberof_filter, |
368 | queried_domain=queried_domain) | |
487 | queried_domain=self._queried_domain) | |
369 | 488 | else: |
370 | 489 | # TODO: range cycling |
371 | 490 | try: |
373 | 492 | # RFC 4515, section 3 |
374 | 493 | member = escape_filter_chars(member, encoding='utf-8') |
375 | 494 | dn_filter = '(distinguishedname={}){}'.format(member, custom_filter) |
376 | members += self.get_netuser(custom_filter=dn_filter, queried_domain=queried_domain) | |
377 | members += self.get_netgroup(custom_filter=dn_filter, queried_domain=queried_domain, full_data=True) | |
495 | members += self.get_netuser(custom_filter=dn_filter, queried_domain=self._queried_domain) | |
496 | members += self.get_netgroup(custom_filter=dn_filter, queried_domain=self._queried_domain, full_data=True) | |
378 | 497 | # The group doesn't have any members |
379 | 498 | except AttributeError: |
380 | 499 | continue |
390 | 509 | member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.') |
391 | 510 | except IndexError: |
392 | 511 | member_domain = str() |
393 | is_group = (member.samaccounttype != '805306368') | |
512 | is_group = (member.samaccounttype != 805306368) | |
394 | 513 | |
395 | 514 | attributes = dict() |
396 | 515 | if queried_domain: |
397 | 516 | attributes['groupdomain'] = queried_domain |
398 | 517 | else: |
399 | attributes['groupdomain'] = self._domain | |
518 | attributes['groupdomain'] = self._queried_domain | |
400 | 519 | attributes['groupname'] = group.name |
401 | 520 | attributes['membername'] = member.samaccountname |
402 | 521 | attributes['memberdomain'] = member_domain |
403 | 522 | attributes['isgroup'] = is_group |
404 | 523 | attributes['memberdn'] = member_dn |
405 | attributes['membersid'] = member.objectsid | |
524 | attributes['objectsid'] = member.objectsid | |
406 | 525 | |
407 | 526 | final_member.add_attributes(attributes) |
408 | 527 | |
603 | 722 | member_handle = resp['UserHandle'] |
604 | 723 | attributes['isgroup'] = False |
605 | 724 | resp = samr.hSamrQueryInformationUser(self._rpc_connection, member_handle) |
606 | attributes['name'] = '{}/{}'.format(member_domain, resp['Buffer']['General']['UserName']) | |
725 | attributes['name'] = '{}\\{}'.format(member_domain, resp['Buffer']['General']['UserName']) | |
607 | 726 | except DCERPCSessionError: |
608 | 727 | resp = samr.hSamrOpenAlias(self._rpc_connection, domain_handle, aliasId=member_rid) |
609 | 728 | member_handle = resp['AliasHandle'] |
610 | 729 | attributes['isgroup'] = True |
611 | 730 | resp = samr.hSamrQueryInformationAlias(self._rpc_connection, member_handle) |
612 | attributes['name'] = '{}/{}'.format(member_domain, resp['Buffer']['General']['Name']) | |
613 | attributes['lastlogin'] = str() | |
731 | attributes['name'] = '{}\\{}'.format(member_domain, resp['Buffer']['General']['Name']) | |
732 | attributes['lastlogon'] = str() | |
614 | 733 | break |
615 | 734 | # It's a domain member |
616 | 735 | else: |
621 | 740 | member_dn = ad_object.distinguishedname |
622 | 741 | member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.') |
623 | 742 | try: |
624 | attributes['name'] = '{}/{}'.format(member_domain, ad_object.samaccountname) | |
743 | attributes['name'] = '{}\\{}'.format(member_domain, ad_object.samaccountname) | |
625 | 744 | except AttributeError: |
626 | 745 | # Here, the member is a foreign security principal |
627 | 746 | # TODO: resolve it properly |
628 | attributes['name'] = '{}/{}'.format(member_domain, ad_object.objectsid) | |
629 | attributes['isgroup'] = ad_object.isgroup | |
747 | attributes['name'] = '{}\\{}'.format(member_domain, ad_object.objectsid) | |
748 | attributes['isgroup'] = 'group' in ad_object.objectclass | |
630 | 749 | try: |
631 | attributes['lastlogin'] = ad_object.lastlogon | |
750 | # TODO: Now, lastlogon is raw, convert here or within rpc __str__ ? | |
751 | attributes['lastlogon'] = ad_object.lastlogon | |
632 | 752 | except AttributeError: |
633 | attributes['lastlogin'] = str() | |
753 | attributes['lastlogon'] = str() | |
634 | 754 | except IndexError: |
635 | 755 | # We did not manage to resolve this SID against the DC |
636 | 756 | attributes['isdomain'] = False |
637 | 757 | attributes['isgroup'] = False |
638 | 758 | attributes['name'] = attributes['sid'] |
639 | attributes['lastlogin'] = str() | |
759 | attributes['lastlogon'] = str() | |
640 | 760 | else: |
641 | 761 | attributes['isgroup'] = False |
642 | 762 | attributes['name'] = str() |
643 | attributes['lastlogin'] = str() | |
763 | attributes['lastlogon'] = str() | |
644 | 764 | |
645 | 765 | results.append(rpcobj.RPCObject(attributes)) |
646 | 766 | |
652 | 772 | domain_member_attributes['isdomain'] = True |
653 | 773 | member_dn = domain_member.distinguishedname |
654 | 774 | member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.') |
655 | domain_member_attributes['name'] = '{}/{}'.format(member_domain, domain_member.samaccountname) | |
775 | domain_member_attributes['name'] = '{}\\{}'.format(member_domain, domain_member.samaccountname) | |
656 | 776 | domain_member_attributes['isgroup'] = domain_member.isgroup |
657 | 777 | domain_member_attributes['isdomain'] = True |
778 | # TODO: Nope, maybe here we can call get-netdomaincontroller ? | |
779 | # Need to check in powerview | |
658 | 780 | domain_member_attributes['server'] = attributes['name'] |
659 | 781 | domain_member_attributes['sid'] = domain_member.objectsid |
660 | 782 | try: |
783 | # TODO : Same here, must convert the timestamp | |
661 | 784 | domain_member_attributes['lastlogin'] = ad_object.lastlogon |
662 | 785 | except AttributeError: |
663 | 786 | domain_member_attributes['lastlogin'] = str() |
690 | 813 | |
691 | 814 | @LDAPRPCRequester._wmi_connection_init() |
692 | 815 | def get_userevent(self, event_type=['logon', 'tgt'], date_start=5): |
693 | limit_date = (datetime.datetime.today() - datetime.timedelta(days=date_start)).strftime('%Y%m%d%H%M%S.%f-000') | |
816 | limit_date = (datetime.today() - timedelta(days=date_start)).strftime('%Y%m%d%H%M%S.%f-000') | |
694 | 817 | if event_type == ['logon']: |
695 | 818 | where_clause = 'EventCode=4624' |
696 | 819 | elif event_type == ['tgt']: |
707 | 830 | wmi_event = wmi_enum_event.Next(0xffffffff, 1)[0] |
708 | 831 | wmi_event_type = wmi_event.EventIdentifier |
709 | 832 | wmi_event_info = wmi_event.InsertionStrings |
710 | time = datetime.datetime.strptime(wmi_event.TimeGenerated, '%Y%m%d%H%M%S.%f-000') | |
833 | time = datetime.strptime(wmi_event.TimeGenerated, '%Y%m%d%H%M%S.%f-000') | |
711 | 834 | if wmi_event_type == 4624: |
712 | 835 | logon_type = int(wmi_event_info[8]) |
713 | 836 | user = wmi_event_info[5] |
14 | 14 | |
15 | 15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 |
16 | 16 | |
17 | from datetime import datetime, timedelta | |
18 | 17 | import inspect |
19 | 18 | import struct |
20 | 19 | import pyasn1 |
21 | import codecs | |
20 | from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR | |
21 | ||
22 | import pywerview.functions.misc as misc | |
22 | 23 | |
23 | 24 | class ADObject: |
24 | __uac_flags = {0x0000001: 'SCRIPT', | |
25 | 0x0000002: 'ACCOUNTDISABLE', | |
26 | 0x0000008: 'HOMEDIR_REQUIRED', | |
27 | 0x0000010: 'LOCKOUT', | |
28 | 0x0000020: 'PASSWD_NOTREQD', | |
29 | 0x0000040: 'PASSWD_CANT_CHANGE', | |
30 | 0x0000080: 'ENCRYPTED_TEXT_PWD_ALLOWED', | |
31 | 0x0000100: 'TEMP_DUPLICATE_ACCOUNT', | |
32 | 0x0000200: 'NORMAL_ACCOUNT', | |
33 | 0x0000800: 'INTERDOMAIN_TRUST_ACCOUNT', | |
34 | 0x0001000: 'WORKSTATION_TRUST_ACCOUNT', | |
35 | 0x0002000: 'SERVER_TRUST_ACCOUNT', | |
36 | 0x0010000: 'DONT_EXPIRE_PASSWORD', | |
37 | 0x0020000: 'MNS_LOGON_ACCOUNT', | |
38 | 0x0040000: 'SMARTCARD_REQUIRED', | |
39 | 0x0080000: 'TRUSTED_FOR_DELEGATION', | |
40 | 0x0100000: 'NOT_DELEGATED', | |
41 | 0x0200000: 'USE_DES_KEY_ONLY', | |
42 | 0x0400000: 'DONT_REQ_PREAUTH', | |
43 | 0x0800000: 'PASSWORD_EXPIRED', | |
44 | 0x1000000: 'TRUSTED_TO_AUTH_FOR_DELEGATION', | |
45 | 0x4000000: 'PARTIAL_SECRETS_ACCOUNT'} | |
25 | _well_known_sids = {'S-1-0-0': 'Nobody', 'S-1-0': 'Null Authority', 'S-1-1-0': 'Everyone', | |
26 | 'S-1-1': 'World Authority', 'S-1-2-0': 'Local', 'S-1-2-1': 'Console Logon', | |
27 | 'S-1-2': 'Local Authority', 'S-1-3-0': 'Creator Owner', 'S-1-3-1': 'Creator Group', | |
28 | 'S-1-3-2': 'Creator Owner Server', 'S-1-3-3': 'Creator Group Server', 'S-1-3-4': 'Owner Rights', | |
29 | 'S-1-3': 'Creator Authority', 'S-1-4': 'Non-unique Authority', 'S-1-5-10': 'Principal Self', | |
30 | 'S-1-5-11': 'Authenticated Users', 'S-1-5-12': 'Restricted Code', 'S-1-5-13': 'Terminal Server Users', | |
31 | 'S-1-5-14': 'Remote Interactive Logon', 'S-1-5-17': 'This Organization', 'S-1-5-18': 'Local System', | |
32 | 'S-1-5-19': 'NT Authority', 'S-1-5-1': 'Dialup', 'S-1-5-20': 'NT Authority', | |
33 | 'S-1-5-2': 'Network', 'S-1-5-32-546': 'Guests', 'S-1-5-32-547': 'Power Users', | |
34 | 'S-1-5-32-551': 'Backup Operators', 'S-1-5-32-555': 'Builtin\\Remote Desktop Users', | |
35 | 'S-1-5-32-556': 'Builtin\\Network Configuration Operators', | |
36 | 'S-1-5-32-557': 'Builtin\\Incoming Forest Trust Builders', | |
37 | 'S-1-5-32-558': 'Builtin\\Performance Monitor Users', | |
38 | 'S-1-5-32-559': 'Builtin\\Performance Log Users', | |
39 | 'S-1-5-32-560': 'Builtin\\Windows Authorization Access Group', | |
40 | 'S-1-5-32-561': 'Builtin\\Terminal Server License Servers', | |
41 | 'S-1-5-32-562': 'Builtin\\Distributed COM Users', | |
42 | 'S-1-5-32-569': 'Builtin\\Cryptographic Operators', | |
43 | 'S-1-5-32-573': 'Builtin\\Event Log Readers', | |
44 | 'S-1-5-32-574': 'Builtin\\Certificate Service DCOM Access', | |
45 | 'S-1-5-32-575': 'Builtin\\RDS Remote Access Servers', | |
46 | 'S-1-5-32-576': 'Builtin\\RDS Endpoint Servers', | |
47 | 'S-1-5-32-577': 'Builtin\\RDS Management Servers', | |
48 | 'S-1-5-32-578': 'Builtin\\Hyper-V Administrators', | |
49 | 'S-1-5-32-579': 'Builtin\\Access Control Assistance Operators', | |
50 | 'S-1-5-32-580': 'Builtin\\Remote Management Users', | |
51 | 'S-1-5-32-582': 'Storage Replica Administrators', | |
52 | 'S-1-5-3': 'Batch', 'S-1-5-4': 'Interactive', 'S-1-5-64-10': 'NTLM Authentication', | |
53 | 'S-1-5-64-14': 'SChannel Authentication', 'S-1-5-64-21': 'Digest Authentication', | |
54 | 'S-1-5-6': 'Service', 'S-1-5-7': 'Anonymous', 'S-1-5-80-0': 'NT Services\\All Services', | |
55 | 'S-1-5-80': 'NT Service', 'S-1-5-8': 'Proxy', 'S-1-5-9': 'Enterprise Domain Controllers', | |
56 | 'S-1-5': 'NT Authority'} | |
46 | 57 | |
47 | 58 | def __init__(self, attributes): |
59 | self._attributes_dict = dict() | |
48 | 60 | self.add_attributes(attributes) |
49 | 61 | |
50 | 62 | def add_attributes(self, attributes): |
51 | 63 | for attr in attributes: |
52 | #print(attr) | |
53 | #print(attributes[attr], attr) | |
54 | t = str(attr).lower() | |
55 | if t in ('logonhours', 'msds-generationid'): | |
56 | value = bytes(attributes[attr][0]) | |
57 | value = [x for x in value] | |
58 | elif t in ('trustattributes', 'trustdirection', 'trusttype'): | |
59 | value = int(attributes[attr][0]) | |
60 | elif t in ('objectsid', 'ms-ds-creatorsid'): | |
61 | value = codecs.encode(bytes(attributes[attr][0]),'hex') | |
62 | init_value = bytes(attributes[attr][0]) | |
63 | value = 'S-{0}-{1}'.format(init_value[0], init_value[1]) | |
64 | for i in range(8, len(init_value), 4): | |
65 | value += '-{}'.format(str(struct.unpack('<I', init_value[i:i+4])[0])) | |
66 | elif t == 'objectguid': | |
67 | init_value = bytes(attributes[attr][0]) | |
68 | value = str() | |
69 | value += '{}-'.format(hex(struct.unpack('<I', init_value[0:4])[0])[2:].zfill(8)) | |
70 | value += '{}-'.format(hex(struct.unpack('<H', init_value[4:6])[0])[2:].zfill(4)) | |
71 | value += '{}-'.format(hex(struct.unpack('<H', init_value[6:8])[0])[2:].zfill(4)) | |
72 | value += '{}-'.format((codecs.encode(init_value,'hex')[16:20]).decode('utf-8')) | |
73 | value += init_value.hex()[20:] | |
74 | elif t in ('dscorepropagationdata', 'whenchanged', 'whencreated'): | |
75 | value = list() | |
76 | for val in attributes[attr]: | |
77 | value.append(str(datetime.strptime(str(val.decode('utf-8')), '%Y%m%d%H%M%S.0Z'))) | |
78 | elif t in ('accountexpires', 'pwdlastset', 'badpasswordtime', 'lastlogontimestamp', 'lastlogon', 'lastlogoff'): | |
64 | self._attributes_dict[attr.lower()] = attributes[attr] | |
65 | ||
66 | def __getattr__(self, attr): | |
67 | try: | |
68 | return self._attributes_dict[attr] | |
69 | except KeyError: | |
70 | if attr == 'isgroup': | |
79 | 71 | try: |
80 | filetimestamp = int(attributes[attr][0].decode('utf-8')) | |
81 | if filetimestamp != 9223372036854775807: | |
82 | timestamp = (filetimestamp - 116444736000000000)/10000000 | |
83 | value = datetime.fromtimestamp(0) + timedelta(seconds=timestamp) | |
84 | else: | |
85 | value = 'never' | |
86 | except IndexError: | |
87 | value = 'empty' | |
88 | elif t == 'isgroup': | |
89 | value = attributes[attr] | |
90 | elif t == 'objectclass': | |
91 | value = [x.decode('utf-8') for x in attributes[attr]] | |
92 | setattr(self, 'isgroup', ('group' in value)) | |
93 | elif len(attributes[attr]) > 1: | |
94 | try: | |
95 | value = [x.decode('utf-8') for x in attributes[attr]] | |
96 | except (UnicodeDecodeError): | |
97 | value = [x for x in attributes[attr]] | |
98 | except (AttributeError): | |
99 | value = attributes[attr] | |
100 | else: | |
101 | try: | |
102 | value = attributes[attr][0].decode('utf-8') | |
103 | except (IndexError): | |
104 | value = str() | |
105 | except (UnicodeDecodeError): | |
106 | value = attributes[attr][0] | |
107 | ||
108 | setattr(self, t, value) | |
109 | ||
72 | return 'group' in self._attributes_dict['objectclass'] | |
73 | except KeyError: | |
74 | return False | |
75 | raise AttributeError | |
76 | ||
77 | # In this method, we try to pretty print common AD attributes | |
110 | 78 | def __str__(self): |
111 | 79 | s = str() |
112 | members = inspect.getmembers(self, lambda x: not(inspect.isroutine(x))) | |
113 | 80 | max_length = 0 |
114 | for member in members: | |
115 | if not member[0].startswith('_'): | |
116 | if len(member[0]) > max_length: | |
117 | max_length = len(member[0]) | |
118 | for member in members: | |
119 | if not member[0].startswith('_'): | |
120 | if member[0] == 'msmqdigests': | |
121 | member_value = (',\n' + ' ' * (max_length + 2)).join(x.hex() for x in member[1]) | |
122 | elif member[0] == 'useraccountcontrol': | |
123 | member_value = list() | |
124 | for uac_flag, uac_label in ADObject.__uac_flags.items(): | |
125 | if int(member[1]) & uac_flag == uac_flag: | |
126 | member_value.append(uac_label) | |
127 | elif isinstance(member[1], list): | |
128 | if member[0] in ('logonhours',): | |
129 | member_value = member[1] | |
130 | elif member[0] in ('usercertificate', | |
131 | 'protocom-sso-entries', 'protocom-sso-security-prefs',): | |
132 | member_value = (',\n' + ' ' * (max_length + 2)).join( | |
133 | '{}...'.format(x.hex()[:100]) for x in member[1]) | |
134 | else: | |
135 | member_value = (',\n' + ' ' * (max_length + 2)).join(str(x) for x in member[1]) | |
136 | elif member[0] in('msmqsigncertificates', 'userparameters', | |
137 | 'jpegphoto', 'thumbnailphoto', 'usercertificate', | |
138 | 'msexchmailboxguid', 'msexchmailboxsecuritydescriptor', | |
139 | 'msrtcsip-userroutinggroupid', 'msexchumpinchecksum', | |
140 | 'protocom-sso-auth-data', 'protocom-sso-entries-checksum', | |
141 | 'protocom-sso-security-prefs-checksum', ): | |
142 | # Attribut exists but it is empty | |
143 | try: | |
144 | member_value = '{}...'.format(member[1].hex()[:100]) | |
145 | except AttributeError: | |
146 | member_value = '' | |
147 | else: | |
148 | member_value = member[1] | |
149 | s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member_value) | |
150 | ||
81 | for attr in self._attributes_dict: | |
82 | if len(attr) > max_length: | |
83 | max_length = len(attr) | |
84 | for attr in self._attributes_dict: | |
85 | attribute = self._attributes_dict[attr] | |
86 | if isinstance(attribute, list): | |
87 | if any(isinstance(x, bytes) for x in attribute): | |
88 | attribute = ['{}...'.format(x.hex()[:97]) for x in attribute] | |
89 | attribute_temp = ', '.join(str(x) for x in attribute) | |
90 | if len(attribute_temp) > 100: | |
91 | attribute_temp = str() | |
92 | line = str() | |
93 | for x in attribute: | |
94 | if len(line) + len(str(x)) <= 100: | |
95 | line += '{}, '.format(x) | |
96 | else: | |
97 | attribute_temp += line + '\n' + ' ' * (max_length + 2) | |
98 | line = str() | |
99 | line += '{}, '.format(x) | |
100 | attribute_temp += line + '\n' + ' ' * (max_length + 2) | |
101 | attribute = attribute_temp.rstrip().rstrip(',') | |
102 | elif isinstance(attribute, bytes): | |
103 | attribute = '{}...'.format(attribute.hex()[:100]) | |
104 | elif isinstance(attribute, ADObject): | |
105 | attribute = ('\n' + str(attribute)).replace('\n', '\n\t') | |
106 | ||
107 | s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute) | |
108 | #if not member.startswith('_'): | |
109 | ##print(len(member[1])) | |
110 | ## print(member) | |
111 | ## ?? | |
112 | #if member in ('logonhours', 'msds-generationid'): | |
113 | #value = member[1] | |
114 | #member_value = [x for x in value] | |
115 | ||
116 | ## Attribute is a SID | |
117 | #elif member in ('objectsid', 'ms-ds-creatorsid', 'securityidentifier'): | |
118 | #init_value = member[1] | |
119 | #member_value = misc.Utils.convert_sidtostr(init_value) | |
120 | ||
151 | 121 | s = s[:-1] |
152 | 122 | return s |
153 | ||
123 | ||
154 | 124 | def __repr__(self): |
155 | 125 | return str(self) |
156 | 126 | |
157 | class User(ADObject): | |
127 | class ACE(ADObject): | |
128 | ||
158 | 129 | def __init__(self, attributes): |
159 | 130 | ADObject.__init__(self, attributes) |
160 | for attr in filter(lambda _: _ in attributes, ('homedirectory', | |
161 | 'scriptpath', | |
162 | 'profilepath')): | |
163 | if not hasattr(self, attr): | |
164 | setattr(self, attr, str()) | |
131 | ||
132 | # We set iscallback, depending on the type of ACE | |
133 | self._attributes_dict['iscallbak'] = ('CALLBACK' in self.acetype) | |
134 | ||
135 | class User(ADObject): | |
136 | pass | |
165 | 137 | |
166 | 138 | class Group(ADObject): |
139 | pass | |
140 | ||
141 | class Computer(ADObject): | |
142 | pass | |
143 | ||
144 | class FileServer(ADObject): | |
145 | pass | |
146 | ||
147 | class DFS(ADObject): | |
148 | pass | |
149 | ||
150 | class OU(ADObject): | |
151 | pass | |
152 | ||
153 | class Site(ADObject): | |
154 | pass | |
155 | ||
156 | class Subnet(ADObject): | |
157 | pass | |
158 | ||
159 | class Trust(ADObject): | |
160 | ||
167 | 161 | def __init__(self, attributes): |
168 | 162 | ADObject.__init__(self, attributes) |
169 | try: | |
170 | if not isinstance(self.member, list): | |
171 | self.member = [self.member] | |
172 | except AttributeError: | |
173 | pass | |
174 | ||
175 | class Computer(ADObject): | |
176 | pass | |
177 | ||
178 | class FileServer(ADObject): | |
179 | pass | |
180 | ||
181 | class DFS(ADObject): | |
182 | pass | |
183 | ||
184 | class OU(ADObject): | |
185 | def __init__(self, attributes): | |
186 | ADObject.__init__(self, attributes) | |
187 | self.distinguishedname = 'LDAP://{}'.format(self.distinguishedname) | |
188 | ||
189 | class Site(ADObject): | |
190 | pass | |
191 | ||
192 | class Subnet(ADObject): | |
193 | pass | |
194 | ||
195 | class Trust(ADObject): | |
196 | __trust_attrib = {0x1: 'non_transitive', 0x2: 'uplevel_only', | |
197 | 0x4: 'filter_sids', 0x8: 'forest_transitive', | |
198 | 0x10: 'cross_organization', 0x20: 'within_forest', | |
199 | 0x40: 'treat_as_external', | |
200 | 0x80: 'trust_uses_rc4_encryption', | |
201 | 0x100: 'trust_uses_aes_keys', | |
202 | 0X200: 'cross_organization_no_tgt_delegation', | |
203 | 0x400: 'pim_trust'} | |
204 | ||
205 | __trust_direction = {0: 'disabled', 1: 'inbound', | |
206 | 2: 'outbound', 3: 'bidirectional'} | |
207 | ||
208 | __trust_type = {1: 'windows_non_active_directory', | |
209 | 2: 'windows_active_directory', 3: 'mit'} | |
210 | ||
211 | def __init__(self, attributes): | |
212 | ad_obj = ADObject(attributes) | |
213 | self.targetname = ad_obj.name | |
214 | ||
215 | self.trustdirection = Trust.__trust_direction.get(ad_obj.trustdirection, 'unknown') | |
216 | self.trusttype = Trust.__trust_type.get(ad_obj.trusttype, 'unknown') | |
217 | self.whencreated = ad_obj.whencreated | |
218 | self.whenchanged = ad_obj.whenchanged | |
219 | ||
220 | self.trustattributes = list() | |
221 | for attrib_flag, attrib_label in Trust.__trust_attrib.items(): | |
222 | if ad_obj.trustattributes & attrib_flag: | |
223 | self.trustattributes.append(attrib_label) | |
224 | ||
163 | trust_attributes = self.trustattributes | |
164 | trust_direction = self.trustdirection | |
225 | 165 | # If the filter SIDs attribute is not manually set, we check if we're |
226 | 166 | # not in a use case where SIDs are implicitly filtered |
227 | 167 | # Based on https://github.com/vletoux/pingcastle/blob/master/Healthcheck/TrustAnalyzer.cs |
228 | if 'filter_sids' not in self.trustattributes: | |
229 | if not (self.trustdirection == 'disabled' or \ | |
230 | self.trustdirection == 'inbound' or \ | |
231 | 'within_forest' in self.trustattributes or \ | |
232 | 'pim_trust' in self.trustattributes): | |
233 | if 'forest_transitive' in self.trustattributes and 'treat_as_external' not in self.trustattributes: | |
234 | self.trustattributes.append('filter_sids') | |
235 | ||
236 | class GPO(ADObject): | |
237 | pass | |
238 | ||
239 | class GptTmpl(ADObject): | |
168 | if 'filter_sids' not in trust_attributes: | |
169 | if not (trust_direction == 'disabled' or \ | |
170 | trust_direction == 'inbound' or \ | |
171 | 'within_forest' in trust_attributes or \ | |
172 | 'pim_trust' in trust_attributes): | |
173 | if 'forest_transitive' in trust_attributes and 'treat_as_external' not in trust_attributes: | |
174 | self._attributes_dict['trustattributes'].append('filter_sids') | |
175 | ||
176 | # Pretty printing Trust object, we don't want to print all the attributes | |
177 | # so we only print useful ones (trustattributes, trustdirection, trustpartner | |
178 | # trusttype, whenchanged, whencreated) | |
240 | 179 | def __str__(self): |
241 | 180 | s = str() |
242 | members = inspect.getmembers(self, lambda x: not(inspect.isroutine(x))) | |
243 | for member in members: | |
244 | if not member[0].startswith('_'): | |
245 | s += '{}:\n'.format(member[0]) | |
246 | member_value_str = str(member[1]) | |
247 | for line in member_value_str.split('\n'): | |
248 | s += '\t{}\n'.format(line) | |
181 | max_length = len('trustattributes') | |
182 | ||
183 | for attr in self._attributes_dict: | |
184 | if attr in ('trustpartner', 'trustdirection', 'trusttype', 'whenchanged', 'whencreated'): | |
185 | attribute = self._attributes_dict[attr] | |
186 | elif attr == 'trustattributes': | |
187 | attribute = ', '.join(self._attributes_dict[attr]) | |
188 | else: | |
189 | continue | |
190 | s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute) | |
249 | 191 | |
250 | 192 | s = s[:-1] |
251 | 193 | return s |
194 | pass | |
195 | ||
196 | class GPO(ADObject): | |
197 | pass | |
198 | ||
199 | class PSO(ADObject): | |
200 | pass | |
201 | ||
202 | class GptTmpl(ADObject): | |
203 | pass | |
252 | 204 | |
253 | 205 | class GPOGroup(ADObject): |
254 | 206 | pass |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | 15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 |
16 | ||
17 | from __future__ import unicode_literals | |
18 | 16 | |
19 | 17 | import inspect |
20 | 18 |
17 | 17 | import socket |
18 | 18 | import ntpath |
19 | 19 | import ldap3 |
20 | ||
21 | from ldap3.protocol.formatters.formatters import * | |
20 | 22 | |
21 | 23 | from impacket.smbconnection import SMBConnection |
22 | 24 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY |
25 | 27 | from impacket.dcerpc.v5.dtypes import NULL |
26 | 28 | from impacket.dcerpc.v5.dcomrt import DCOMConnection |
27 | 29 | from impacket.dcerpc.v5.rpcrt import DCERPCException |
30 | ||
31 | import pywerview.formatters as fmt | |
28 | 32 | |
29 | 33 | class LDAPRequester(): |
30 | 34 | def __init__(self, domain_controller, domain=str(), user=(), password=str(), |
85 | 89 | # Format the username and the domain |
86 | 90 | # ldap3 seems not compatible with USER@DOMAIN format |
87 | 91 | user = '{}\\{}'.format(self._domain, self._user) |
92 | ||
93 | # Call custom formatters for several AD attributes | |
94 | formatter = {'userAccountControl': fmt.format_useraccountcontrol, | |
95 | 'trustType': fmt.format_trusttype, | |
96 | 'trustDirection': fmt.format_trustdirection, | |
97 | 'trustAttributes': fmt.format_trustattributes, | |
98 | 'msDS-MaximumPasswordAge': format_ad_timedelta, | |
99 | 'msDS-MinimumPasswordAge': format_ad_timedelta, | |
100 | 'msDS-LockoutDuration': format_ad_timedelta, | |
101 | 'msDS-LockoutObservationWindow': format_ad_timedelta} | |
88 | 102 | |
89 | 103 | # Choose between password or pth |
90 | 104 | if self._lmhash and self._nthash: |
91 | 105 | lm_nt_hash = '{}:{}'.format(self._lmhash, self._nthash) |
92 | 106 | |
93 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller)) | |
107 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller), | |
108 | formatter=formatter) | |
94 | 109 | ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, |
95 | 110 | authentication=ldap3.NTLM, raise_exceptions=True) |
96 | 111 | |
98 | 113 | ldap_connection.bind() |
99 | 114 | except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult: |
100 | 115 | # We need to try SSL (pth version) |
101 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller)) | |
116 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller), | |
117 | formatter=formatter) | |
102 | 118 | ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, |
103 | 119 | authentication=ldap3.NTLM, raise_exceptions=True) |
104 | 120 | |
105 | 121 | ldap_connection.bind() |
106 | 122 | |
107 | 123 | else: |
108 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller)) | |
124 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller), | |
125 | formatter=formatter) | |
109 | 126 | ldap_connection = ldap3.Connection(ldap_server, user, self._password, |
110 | 127 | authentication=ldap3.NTLM, raise_exceptions=True) |
111 | 128 | |
113 | 130 | ldap_connection.bind() |
114 | 131 | except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult: |
115 | 132 | # We nedd to try SSL (password version) |
116 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller)) | |
133 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller), | |
134 | formatter=formatter) | |
117 | 135 | ldap_connection = ldap3.Connection(ldap_server, user, self._password, |
118 | 136 | authentication=ldap3.NTLM, raise_exceptions=True) |
119 | 137 | |
121 | 139 | |
122 | 140 | self._ldap_connection = ldap_connection |
123 | 141 | |
124 | def _ldap_search(self, search_filter, class_result, attributes=list()): | |
142 | def _ldap_search(self, search_filter, class_result, attributes=list(), controls=list()): | |
125 | 143 | results = list() |
126 | 144 | |
127 | 145 | # if no attribute name specified, we return all attributes |
132 | 150 | # Microsoft Active Directory set an hard limit of 1000 entries returned by any search |
133 | 151 | search_results=self._ldap_connection.extend.standard.paged_search(search_base=self._base_dn, |
134 | 152 | search_filter=search_filter, attributes=attributes, |
135 | paged_size=1000, generator=True) | |
153 | controls=controls, paged_size=1000, generator=True) | |
136 | 154 | # TODO: for debug only |
137 | 155 | except Exception as e: |
138 | 156 | import sys |
140 | 158 | |
141 | 159 | # Skip searchResRef |
142 | 160 | for result in search_results: |
143 | if result['type'] is not 'searchResEntry': | |
161 | if result['type'] != 'searchResEntry': | |
144 | 162 | continue |
145 | results.append(class_result(result['raw_attributes'])) | |
163 | results.append(class_result(result['attributes'])) | |
146 | 164 | |
147 | 165 | return results |
148 | 166 |
1 | 1 | |
2 | 2 | from setuptools import setup, find_packages |
3 | 3 | |
4 | try: | |
5 | import pypandoc | |
6 | long_description = pypandoc.convert_file('README.md', 'rst') | |
7 | except(IOError, ImportError): | |
8 | long_description = open('README.md').read() | |
4 | long_description = open('README.md').read() | |
9 | 5 | |
10 | 6 | setup(name='pywerview', |
11 | version='0.3.2', | |
7 | version='0.3.3', | |
12 | 8 | description='A Python port of PowerSploit\'s PowerView', |
13 | 9 | long_description=long_description, |
10 | long_description_content_type='text/markdown', | |
14 | 11 | dependency_links = ['https://github.com/SecureAuthCorp/impacket/tarball/master#egg=impacket-0.9.22'], |
15 | 12 | classifiers=[ |
16 | 13 | 'Environment :: Console', |