New upstream version 0.3.2
Sophie Brun
2 years ago
10 | 10 | |
11 | 11 | Fork me on [GitHub](https://github.com/the-useless-one/pywerview). |
12 | 12 | |
13 | ![License](https://img.shields.io/github/license/the-useless-one/pywerview.svg?maxAge=2592000) | |
14 | ![Python versions](https://img.shields.io/pypi/pyversions/pywerview.svg?maxAge=2592000) | |
15 | [![GitHub release](https://img.shields.io/github/release/the-useless-one/pywerview.svg?maxAge=2592001&label=GitHub%20release)](https://github.com/the-useless-one/pywerview/releases/latest) | |
16 | [![PyPI version](https://img.shields.io/pypi/v/pywerview.svg?maxAge=2592000)](https://pypi.python.org/pypi/pywerview) | |
13 | [![License](https://img.shields.io/github/license/the-useless-one/pywerview)](https://github.com/the-useless-one/pywerview/blob/master/LICENSE) | |
14 | ![Python versions](https://img.shields.io/pypi/pyversions/pywerview) | |
15 | [![GitHub release](https://img.shields.io/github/v/release/the-useless-one/pywerview)](https://github.com/the-useless-one/pywerview/releases/latest) | |
16 | [![PyPI version](https://img.shields.io/pypi/v/pywerview)](https://pypi.python.org/pypi/pywerview) | |
17 | 17 | |
18 | 18 | ## HISTORY |
19 | 19 | |
29 | 29 | Linux. |
30 | 30 | |
31 | 31 | That's why I decided to rewrite some of PowerView's functionalities in Python, |
32 | using the wonderful [impacket](https://github.com/CoreSecurity/impacket/) | |
32 | using the wonderful [impacket](https://github.com/SecureAuthCorp/impacket) | |
33 | 33 | library. |
34 | 34 | |
35 | 35 | *Update:* I haven't tested the last version of PowerView yet, which can run |
57 | 57 | |
58 | 58 | ## REQUIREMENTS |
59 | 59 | |
60 | * Python 2.7 | |
61 | * impacket >= 0.9.16-dev | |
60 | * Python 3.6 | |
61 | * impacket >= 0.9.22 | |
62 | * ldap3 >= 2.8.1 | |
62 | 63 | |
63 | 64 | ## FUNCTIONALITIES |
64 | 65 | |
178 | 179 | contributions. |
179 | 180 | * Thanks to [@ThePirateWhoSmellsOfSunflowers](https://github.com/ThePirateWhoSmellsOfSunflowers) |
180 | 181 | for his debugging, love you baby :heart: |
182 | * Thanks to [@mpgn](https://github.com/mpgn) for his python 3 contributions. | |
181 | 183 | |
182 | 184 | ## COPYRIGHT |
183 | 185 | |
184 | 186 | PywerView - A Python rewriting of PowerSploit's PowerView |
185 | 187 | |
186 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
188 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
187 | 189 | |
188 | 190 | This program is free software: you can redistribute it and/or modify it |
189 | 191 | under the terms of the GNU General Public License as published by the |
0 | 0 | #!/usr/bin/env python |
1 | # -*- coding: utf8 -*- | |
2 | 1 | # |
3 | 2 | # This file is part of PywerView. |
4 | 3 | |
15 | 14 | # You should have received a copy of the GNU General Public License |
16 | 15 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
17 | 16 | |
18 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
19 | 18 | |
20 | 19 | from pywerview.functions.net import NetRequester |
21 | 20 | from pywerview.functions.gpo import GPORequester |
36 | 35 | def get_netuser(domain_controller, domain, user, password=str(), lmhash=str(), |
37 | 36 | nthash=str(), queried_username=str(), queried_domain=str(), ads_path=str(), |
38 | 37 | admin_count=False, spn=False, unconstrained=False, allow_delegation=False, |
39 | preauth_notreq=False, custom_filter=str()): | |
40 | requester = NetRequester(domain_controller, domain, user, password, | |
41 | lmhash, nthash) | |
38 | preauth_notreq=False, custom_filter=str(), | |
39 | attributes=[]): | |
40 | requester = NetRequester(domain_controller, domain, user, password, | |
41 | lmhash, nthash) | |
42 | 42 | return requester.get_netuser(queried_username=queried_username, |
43 | 43 | queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count, |
44 | 44 | spn=spn, unconstrained=unconstrained, allow_delegation=allow_delegation, |
45 | preauth_notreq=preauth_notreq, custom_filter=custom_filter) | |
45 | preauth_notreq=preauth_notreq, custom_filter=custom_filter, | |
46 | attributes=attributes) | |
46 | 47 | |
47 | 48 | def get_netgroup(domain_controller, domain, user, password=str(), |
48 | 49 | lmhash=str(), nthash=str(), queried_groupname='*', queried_sid=str(), |
59 | 60 | lmhash=str(), nthash=str(), queried_computername='*', queried_spn=str(), |
60 | 61 | queried_os=str(), queried_sp=str(), queried_domain=str(), ads_path=str(), |
61 | 62 | printers=False, unconstrained=False, ping=False, full_data=False, |
62 | custom_filter=str()): | |
63 | custom_filter=str(), attributes=[]): | |
63 | 64 | requester = NetRequester(domain_controller, domain, user, password, |
64 | 65 | lmhash, nthash) |
65 | 66 | return requester.get_netcomputer(queried_computername=queried_computername, |
66 | 67 | queried_spn=queried_spn, queried_os=queried_os, queried_sp=queried_sp, |
67 | 68 | queried_domain=queried_domain, ads_path=ads_path, printers=printers, |
68 | 69 | unconstrained=unconstrained, ping=ping, full_data=full_data, |
69 | custom_filter=custom_filter) | |
70 | custom_filter=custom_filter, attributes=attributes) | |
70 | 71 | |
71 | 72 | def get_netdomaincontroller(domain_controller, domain, user, password=str(), |
72 | 73 | lmhash=str(), nthash=str(), queried_domain=str()): |
99 | 100 | |
100 | 101 | def get_netsite(domain_controller, domain, user, password=str(), lmhash=str(), |
101 | 102 | nthash=str(), queried_domain=str(), queried_sitename=str(), |
102 | queried_guid=str(), ads_path=str(), full_data=False): | |
103 | queried_guid=str(), ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', | |
104 | full_data=False): | |
103 | 105 | requester = NetRequester(domain_controller, domain, user, password, |
104 | 106 | lmhash, nthash) |
105 | 107 | return requester.get_netsite(queried_domain=queried_domain, |
106 | 108 | queried_sitename=queried_sitename, queried_guid=queried_guid, |
107 | ads_path=ads_path, full_data=full_data) | |
109 | ads_path=ads_path, ads_prefix=ads_prefix, full_data=full_data) | |
108 | 110 | |
109 | 111 | def get_netsubnet(domain_controller, domain, user, password=str(), |
110 | 112 | lmhash=str(), nthash=str(), queried_domain=str(), queried_sitename=str(), |
111 | ads_path=str(), full_data=False): | |
113 | ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', full_data=False): | |
112 | 114 | requester = NetRequester(domain_controller, domain, user, password, |
113 | 115 | lmhash, nthash) |
114 | 116 | return requester.get_netsubnet(queried_domain=queried_domain, |
115 | queried_sitename=queried_sitename, ads_path=ads_path, full_data=full_data) | |
117 | queried_sitename=queried_sitename, ads_path=ads_path, ads_prefix=ads_prefix, | |
118 | full_data=full_data) | |
116 | 119 | |
117 | 120 | def get_netdomaintrust(domain_controller, domain, user, password=str(), |
118 | 121 | lmhash=str(), nthash=str(), queried_domain=str()): |
0 | #!/usr/bin/env python | |
1 | # -*- coding: utf8 -*- | |
0 | #!/usr/bin/env python3 | |
2 | 1 | # |
3 | 2 | # This file is part of PywerView. |
4 | 3 | |
15 | 14 | # You should have received a copy of the GNU General Public License |
16 | 15 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
17 | 16 | |
18 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
19 | 18 | |
20 | 19 | import argparse |
21 | 20 | from pywerview.cli.helpers import * |
24 | 23 | def main(): |
25 | 24 | # Main parser |
26 | 25 | parser = argparse.ArgumentParser(description='Rewriting of some PowerView\'s functionalities in Python') |
27 | subparsers = parser.add_subparsers(title='Subcommands', description='Available subcommands') | |
26 | subparsers = parser.add_subparsers(title='Subcommands', description='Available subcommands', dest='submodule') | |
27 | ||
28 | # hack for python < 3.9 : https://stackoverflow.com/questions/23349349/argparse-with-required-subparser | |
29 | subparsers.required = True | |
28 | 30 | |
29 | 31 | # TODO: support keberos authentication |
30 | 32 | # Credentials parser |
111 | 113 | help='Query only users with not-null Service Principal Names') |
112 | 114 | get_netuser_parser.add_argument('--custom-filter', dest='custom_filter', |
113 | 115 | default=str(), help='Custom filter') |
116 | get_netuser_parser.add_argument('--attributes', nargs='+', dest='attributes', | |
117 | default=[], help='Object attributes to return') | |
114 | 118 | get_netuser_parser.set_defaults(func=get_netuser) |
115 | 119 | |
116 | 120 | # Parser for the get-netgroup command |
155 | 159 | help='Ping computers (will only return up computers)') |
156 | 160 | get_netcomputer_parser.add_argument('--full-data', action='store_true', |
157 | 161 | help='If set, returns full information on the groups, otherwise, just the dnsHostName') |
162 | get_netcomputer_parser.add_argument('--attributes', nargs='+', dest='attributes', | |
163 | default=[], help='Object attributes to return') | |
158 | 164 | get_netcomputer_parser.set_defaults(func=get_netcomputer) |
159 | 165 | |
160 | 166 | # Parser for the get-netdomaincontroller command |
434 | 440 | invoke_eventhunter_parser.set_defaults(func=invoke_eventhunter) |
435 | 441 | |
436 | 442 | args = parser.parse_args() |
437 | if hasattr(args,'queried_groupname'): | |
438 | args.queried_groupname = args.queried_groupname.encode('utf-8').decode('latin1') | |
439 | 443 | if args.hashes: |
440 | 444 | try: |
441 | 445 | args.lmhash, args.nthash = args.hashes.split(':') |
452 | 456 | |
453 | 457 | parsed_args = dict() |
454 | 458 | for k, v in vars(args).items(): |
455 | if k not in ('func', 'hashes'): | |
459 | if k not in ('func', 'hashes', 'submodule'): | |
456 | 460 | parsed_args[k] = v |
457 | 461 | |
458 | 462 | #try: |
464 | 468 | if results is not None: |
465 | 469 | try: |
466 | 470 | for x in results: |
467 | x = str(x).encode('latin1').decode('utf-8') | |
468 | print(x) | |
469 | if '\n' in x: | |
470 | print('') | |
471 | print(x) | |
472 | # for example, invoke_checklocaladminaccess returns a bool | |
471 | 473 | except TypeError: |
472 | 474 | print(results) |
473 | 475 |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | 16 | |
19 | 17 | import codecs |
20 | 18 | from bs4 import BeautifulSoup |
72 | 70 | property_name, property_values = [x.strip() for x in l.split('=')] |
73 | 71 | if ',' in property_values: |
74 | 72 | property_values = property_values.split(',') |
75 | setattr(getattr(gpttmpl_final, section_name), property_name, property_values) | |
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) | |
76 | 78 | |
77 | 79 | return gpttmpl_final |
78 | 80 | |
133 | 135 | def _get_groupsxml(self, groupsxml_path, gpo_display_name): |
134 | 136 | gpo_groups = list() |
135 | 137 | |
136 | content_io = StringIO() | |
138 | content_io = BytesIO() | |
137 | 139 | |
138 | 140 | groupsxml_path_split = groupsxml_path.split('\\') |
139 | 141 | gpo_name = groupsxml_path_split[6] |
152 | 154 | except SessionError: |
153 | 155 | return list() |
154 | 156 | |
155 | content = content_io.getvalue().replace('\r', '') | |
157 | content = content_io.getvalue().replace(b'\r', b'') | |
156 | 158 | groupsxml_soup = BeautifulSoup(content, 'xml') |
157 | 159 | |
158 | 160 | for group in groupsxml_soup.find_all('Group'): |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | 16 | |
19 | 17 | import random |
20 | 18 | import multiprocessing |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | 16 | |
19 | 17 | from impacket.dcerpc.v5.rpcrt import DCERPCException |
20 | 18 | from impacket.dcerpc.v5 import scmr, drsuapi |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | 16 | |
19 | 17 | import socket |
20 | 18 | import datetime |
24 | 22 | from impacket.dcerpc.v5.rpcrt import DCERPCException |
25 | 23 | from impacket.dcerpc.v5.dcom.wmi import WBEM_FLAG_FORWARD_ONLY |
26 | 24 | from bs4 import BeautifulSoup |
25 | from ldap3.utils.conv import escape_filter_chars | |
27 | 26 | |
28 | 27 | from pywerview.requester import LDAPRPCRequester |
29 | 28 | import pywerview.objects.adobjects as adobj |
49 | 48 | ads_path=str(), admin_count=False, spn=False, |
50 | 49 | unconstrained=False, allow_delegation=False, |
51 | 50 | preauth_notreq=False, |
52 | custom_filter=str()): | |
51 | custom_filter=str(), attributes=[]): | |
53 | 52 | |
54 | 53 | if unconstrained: |
55 | 54 | custom_filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)' |
70 | 69 | |
71 | 70 | user_search_filter = '(&{})'.format(user_search_filter) |
72 | 71 | |
73 | return self._ldap_search(user_search_filter, adobj.User) | |
72 | return self._ldap_search(user_search_filter, adobj.User, attributes=attributes) | |
74 | 73 | |
75 | 74 | @LDAPRPCRequester._ldap_connection_init |
76 | 75 | def get_netgroup(self, queried_groupname='*', queried_sid=str(), |
77 | 76 | queried_username=str(), queried_domain=str(), |
78 | 77 | ads_path=str(), admin_count=False, full_data=False, |
79 | 78 | custom_filter=str()): |
79 | ||
80 | # RFC 4515, section 3 | |
81 | # However if we escape *, we can no longer use wildcard within `--groupname` | |
82 | # Maybe we can raise a warning here ? | |
83 | if not '*' in queried_groupname: | |
84 | queried_groupname = escape_filter_chars(queried_groupname) | |
80 | 85 | |
81 | 86 | if queried_username: |
82 | 87 | results = list() |
83 | 88 | sam_account_name_to_resolve = [queried_username] |
84 | 89 | first_run = True |
85 | 90 | while sam_account_name_to_resolve: |
86 | sam_account_name = sam_account_name_to_resolve.pop(0) | |
91 | sam_account_name = escape_filter_chars(sam_account_name_to_resolve.pop(0)) | |
87 | 92 | if first_run: |
88 | 93 | first_run = False |
89 | 94 | if admin_count: |
135 | 140 | attributes=['samaccountname'] |
136 | 141 | |
137 | 142 | group_search_filter = '(&{})'.format(group_search_filter) |
138 | ||
139 | 143 | return self._ldap_search(group_search_filter, adobj.Group, attributes=attributes) |
140 | 144 | |
141 | 145 | @LDAPRPCRequester._ldap_connection_init |
142 | 146 | def get_netcomputer(self, queried_computername='*', queried_spn=str(), |
143 | 147 | queried_os=str(), queried_sp=str(), queried_domain=str(), |
144 | 148 | ads_path=str(), printers=False, unconstrained=False, |
145 | ping=False, full_data=False, custom_filter=str()): | |
149 | ping=False, full_data=False, custom_filter=str(), attributes=[]): | |
146 | 150 | |
147 | 151 | if unconstrained: |
148 | 152 | custom_filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)' |
160 | 164 | if full_data: |
161 | 165 | attributes=list() |
162 | 166 | else: |
163 | attributes=['dnsHostName'] | |
167 | if not attributes: | |
168 | attributes=['dnsHostName'] | |
164 | 169 | |
165 | 170 | computer_search_filter = '(&{})'.format(computer_search_filter) |
166 | 171 | |
182 | 187 | if len(split_path) >= 3: |
183 | 188 | return split_path[2] |
184 | 189 | |
190 | file_server_attributes = ['homedirectory', 'scriptpath', 'profilepath'] | |
185 | 191 | results = set() |
186 | 192 | if target_users: |
187 | 193 | users = list() |
188 | 194 | for target_user in target_users: |
189 | users += self.get_netuser(target_user, queried_domain) | |
195 | users += self.get_netuser(target_user, queried_domain, | |
196 | attributes=file_server_attributes) | |
190 | 197 | else: |
191 | users = self.get_netuser(queried_domain=queried_domain) | |
198 | users = self.get_netuser(queried_domain=queried_domain, | |
199 | attributes=file_server_attributes) | |
192 | 200 | |
193 | 201 | for user in users: |
194 | 202 | for full_path in (user.homedirectory, user.scriptpath, user.profilepath): |
219 | 227 | for remote_server in dfs.remoteservername: |
220 | 228 | remote_server = str(remote_server) |
221 | 229 | if '\\' in remote_server: |
222 | attributes = list() | |
223 | attributes.append({'type': 'name', 'vals': [dfs.name]}) | |
224 | attributes.append({'type': 'remoteservername', 'vals': [remote_server.split('\\')[2]]}) | |
230 | attributes = {'name': [dfs.name.encode('utf-8')], | |
231 | 'remoteservername': [remote_server.split('\\')[2].encode('utf-8')]} | |
225 | 232 | results.append(adobj.DFS(attributes)) |
226 | 233 | |
227 | 234 | return results |
242 | 249 | for target in soup_target_list.targets.contents: |
243 | 250 | if '\\' in target.string: |
244 | 251 | server_name, dfs_root = target.string.split('\\')[2:4] |
245 | attributes.append({'type': 'remoteservername', | |
246 | 'vals': [server_name]}) | |
247 | attributes.append({'type': 'name', | |
248 | 'vals': ['{}{}'.format(dfs_root, share_name)]}) | |
252 | attributes = {'name': ['{}{}'.format(dfs_root, share_name).encode('utf-8')], | |
253 | 'remoteservername': [server_name.encode('utf-8')]} | |
249 | 254 | |
250 | 255 | results.append(adobj.DFS(attributes)) |
251 | 256 | |
282 | 287 | |
283 | 288 | @LDAPRPCRequester._ldap_connection_init |
284 | 289 | def get_netsite(self, queried_domain=str(), queried_sitename=str(), |
285 | queried_guid=str(), ads_path=str(), full_data=False): | |
290 | queried_guid=str(), ads_path=str(), ads_prefix=str(), | |
291 | full_data=False): | |
286 | 292 | |
287 | 293 | site_search_filter = '(objectCategory=site)' |
288 | 294 | |
303 | 309 | |
304 | 310 | @LDAPRPCRequester._ldap_connection_init |
305 | 311 | def get_netsubnet(self, queried_domain=str(), queried_sitename=str(), |
306 | ads_path=str(), full_data=False): | |
312 | ads_path=str(), ads_prefix=str(), full_data=False): | |
307 | 313 | |
308 | 314 | subnet_search_filter = '(objectCategory=subnet)' |
309 | 315 | |
329 | 335 | |
330 | 336 | def _get_members(_groupname=str(), _sid=str()): |
331 | 337 | try: |
338 | # `--groupname` option is supplied | |
332 | 339 | if _groupname: |
333 | 340 | groups = self.get_netgroup(queried_groupname=_groupname, |
334 | 341 | queried_domain=queried_domain, |
335 | 342 | full_data=True) |
343 | ||
344 | # `--groupname` option is missing, falling back to the "Domain Admins" | |
336 | 345 | else: |
337 | 346 | if _sid: |
338 | 347 | queried_sid = _sid |
361 | 370 | # TODO: range cycling |
362 | 371 | try: |
363 | 372 | for member in group.member: |
373 | # RFC 4515, section 3 | |
374 | member = escape_filter_chars(member, encoding='utf-8') | |
364 | 375 | dn_filter = '(distinguishedname={}){}'.format(member, custom_filter) |
365 | 376 | members += self.get_netuser(custom_filter=dn_filter, queried_domain=queried_domain) |
366 | 377 | members += self.get_netgroup(custom_filter=dn_filter, queried_domain=queried_domain, full_data=True) |
381 | 392 | member_domain = str() |
382 | 393 | is_group = (member.samaccounttype != '805306368') |
383 | 394 | |
384 | attributes = list() | |
395 | attributes = dict() | |
385 | 396 | if queried_domain: |
386 | attributes.append({'type': 'groupdomain', 'vals': [queried_domain]}) | |
397 | attributes['groupdomain'] = queried_domain | |
387 | 398 | else: |
388 | attributes.append({'type': 'groupdomain', 'vals': [self._domain]}) | |
389 | attributes.append({'type': 'groupname', 'vals': [group.name]}) | |
390 | attributes.append({'type': 'membername', 'vals': [member.samaccountname]}) | |
391 | attributes.append({'type': 'memberdomain', 'vals': [member_domain]}) | |
392 | attributes.append({'type': 'isgroup', 'vals': [is_group]}) | |
393 | attributes.append({'type': 'memberdn', 'vals': [member_dn]}) | |
394 | attributes.append({'type': 'membersid', 'vals': [member.objectsid]}) | |
399 | attributes['groupdomain'] = self._domain | |
400 | attributes['groupname'] = group.name | |
401 | attributes['membername'] = member.samaccountname | |
402 | attributes['memberdomain'] = member_domain | |
403 | attributes['isgroup'] = is_group | |
404 | attributes['memberdn'] = member_dn | |
405 | attributes['membersid'] = member.objectsid | |
395 | 406 | |
396 | 407 | final_member.add_attributes(attributes) |
397 | 408 |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
18 | ||
19 | from datetime import datetime | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
16 | ||
17 | from datetime import datetime, timedelta | |
20 | 18 | import inspect |
21 | 19 | import struct |
22 | 20 | import pyasn1 |
51 | 49 | |
52 | 50 | def add_attributes(self, attributes): |
53 | 51 | for attr in attributes: |
54 | t = str(attr['type']).lower() | |
52 | #print(attr) | |
53 | #print(attributes[attr], attr) | |
54 | t = str(attr).lower() | |
55 | 55 | if t in ('logonhours', 'msds-generationid'): |
56 | value = str(attr['vals'][0]) | |
57 | value = [ord(x) for x in value] | |
56 | value = bytes(attributes[attr][0]) | |
57 | value = [x for x in value] | |
58 | 58 | elif t in ('trustattributes', 'trustdirection', 'trusttype'): |
59 | value = int(attr['vals'][0]) | |
59 | value = int(attributes[attr][0]) | |
60 | 60 | elif t in ('objectsid', 'ms-ds-creatorsid'): |
61 | value = codecs.encode(bytes(attr['vals'][0]),'hex') | |
62 | init_value = bytes(attr['vals'][0]) | |
63 | value = 'S-1-5' | |
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 | 64 | for i in range(8, len(init_value), 4): |
65 | 65 | value += '-{}'.format(str(struct.unpack('<I', init_value[i:i+4])[0])) |
66 | 66 | elif t == 'objectguid': |
67 | init_value = bytes(attr['vals'][0]) | |
67 | init_value = bytes(attributes[attr][0]) | |
68 | 68 | value = str() |
69 | 69 | value += '{}-'.format(hex(struct.unpack('<I', init_value[0:4])[0])[2:].zfill(8)) |
70 | 70 | value += '{}-'.format(hex(struct.unpack('<H', init_value[4:6])[0])[2:].zfill(4)) |
71 | 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]) | |
72 | value += '{}-'.format((codecs.encode(init_value,'hex')[16:20]).decode('utf-8')) | |
73 | 73 | value += init_value.hex()[20:] |
74 | 74 | elif t in ('dscorepropagationdata', 'whenchanged', 'whencreated'): |
75 | 75 | value = list() |
76 | for val in attr['vals']: | |
77 | value.append(str(datetime.strptime(str(val), '%Y%m%d%H%M%S.0Z'))) | |
78 | elif t in ('pwdlastset', 'badpasswordtime', 'lastlogon', 'lastlogoff'): | |
79 | timestamp = (int(str(attr['vals'][0])) - 116444736000000000)/10000000 | |
80 | value = datetime.fromtimestamp(timestamp) | |
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'): | |
79 | 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' | |
81 | 88 | elif t == 'isgroup': |
82 | value = attr['vals'][0] | |
89 | value = attributes[attr] | |
83 | 90 | elif t == 'objectclass': |
84 | value = [str(x) for x in attr['vals']] | |
91 | value = [x.decode('utf-8') for x in attributes[attr]] | |
85 | 92 | setattr(self, 'isgroup', ('group' in value)) |
86 | elif len(attr['vals']) > 1: | |
87 | value = [str(x) for x in attr['vals']] | |
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] | |
88 | 100 | else: |
89 | 101 | try: |
90 | value = str(attr['vals'][0]) | |
91 | except (IndexError, pyasn1.error.PyAsn1Error): | |
102 | value = attributes[attr][0].decode('utf-8') | |
103 | except (IndexError): | |
92 | 104 | value = str() |
105 | except (UnicodeDecodeError): | |
106 | value = attributes[attr][0] | |
93 | 107 | |
94 | 108 | setattr(self, t, value) |
95 | 109 | |
104 | 118 | for member in members: |
105 | 119 | if not member[0].startswith('_'): |
106 | 120 | if member[0] == 'msmqdigests': |
107 | member_value = (',\n' + ' ' * (max_length + 2)).join(x.encode('utf-8').hex() for x in member[1]) | |
121 | member_value = (',\n' + ' ' * (max_length + 2)).join(x.hex() for x in member[1]) | |
108 | 122 | elif member[0] == 'useraccountcontrol': |
109 | 123 | member_value = list() |
110 | 124 | for uac_flag, uac_label in ADObject.__uac_flags.items(): |
116 | 130 | elif member[0] in ('usercertificate', |
117 | 131 | 'protocom-sso-entries', 'protocom-sso-security-prefs',): |
118 | 132 | member_value = (',\n' + ' ' * (max_length + 2)).join( |
119 | '{}...'.format(x.encode('utf-8').hex()[:100]) for x in member[1]) | |
133 | '{}...'.format(x.hex()[:100]) for x in member[1]) | |
120 | 134 | else: |
121 | 135 | member_value = (',\n' + ' ' * (max_length + 2)).join(str(x) for x in member[1]) |
122 | 136 | elif member[0] in('msmqsigncertificates', 'userparameters', |
125 | 139 | 'msrtcsip-userroutinggroupid', 'msexchumpinchecksum', |
126 | 140 | 'protocom-sso-auth-data', 'protocom-sso-entries-checksum', |
127 | 141 | 'protocom-sso-security-prefs-checksum', ): |
128 | member_value = '{}...'.format(member[1].encode('utf-8').hex()[:100]) | |
142 | # Attribut exists but it is empty | |
143 | try: | |
144 | member_value = '{}...'.format(member[1].hex()[:100]) | |
145 | except AttributeError: | |
146 | member_value = '' | |
129 | 147 | else: |
130 | 148 | member_value = member[1] |
131 | 149 | s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member_value) |
139 | 157 | class User(ADObject): |
140 | 158 | def __init__(self, attributes): |
141 | 159 | ADObject.__init__(self, attributes) |
142 | for attr in ('homedirectory', 'scriptpath', 'profilepath'): | |
160 | for attr in filter(lambda _: _ in attributes, ('homedirectory', | |
161 | 'scriptpath', | |
162 | 'profilepath')): | |
143 | 163 | if not hasattr(self, attr): |
144 | 164 | setattr(self, attr, str()) |
145 | 165 | |
192 | 212 | ad_obj = ADObject(attributes) |
193 | 213 | self.targetname = ad_obj.name |
194 | 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 | ||
195 | 220 | self.trustattributes = list() |
196 | 221 | for attrib_flag, attrib_label in Trust.__trust_attrib.items(): |
197 | 222 | if ad_obj.trustattributes & attrib_flag: |
198 | 223 | self.trustattributes.append(attrib_label) |
199 | 224 | |
200 | self.trustdirection = Trust.__trust_direction.get(ad_obj.trustdirection, 'unknown') | |
201 | self.trusttype = Trust.__trust_type.get(ad_obj.trusttype, 'unknown') | |
202 | self.whencreated = ad_obj.whencreated | |
203 | self.whenchanged = ad_obj.whenchanged | |
225 | # If the filter SIDs attribute is not manually set, we check if we're | |
226 | # not in a use case where SIDs are implicitly filtered | |
227 | # 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') | |
204 | 235 | |
205 | 236 | class GPO(ADObject): |
206 | 237 | pass |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | 16 | |
19 | 17 | from __future__ import unicode_literals |
20 | 18 | |
37 | 35 | 'wkui1_oth_domains', 'wkui1_username', |
38 | 36 | 'sesi10_cname', 'sesi10_username'): |
39 | 37 | value = value.rstrip('\x00') |
40 | if isinstance(value, str): | |
41 | try: | |
42 | value = value | |
43 | except UnicodeDecodeError: | |
44 | pass | |
45 | ||
38 | ||
46 | 39 | setattr(self, key.lower(), value) |
47 | 40 | |
48 | 41 | def __str__(self): |
57 | 50 | if not member[0].startswith('_'): |
58 | 51 | s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member[1]) |
59 | 52 | |
60 | s = s[:-1].encode('utf-8') | |
53 | s = s[:-1] | |
61 | 54 | return s |
62 | 55 | |
63 | 56 | def __repr__(self): |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | 16 | |
19 | 17 | import socket |
20 | 18 | import ntpath |
21 | from impacket.ldap import ldap, ldapasn1 | |
19 | import ldap3 | |
20 | ||
22 | 21 | from impacket.smbconnection import SMBConnection |
23 | 22 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY |
24 | 23 | from impacket.dcerpc.v5 import transport, wkst, srvs, samr, scmr, drsuapi, epm |
40 | 39 | self._ads_path = None |
41 | 40 | self._ads_prefix = None |
42 | 41 | self._ldap_connection = None |
42 | self._base_dn = None | |
43 | 43 | |
44 | 44 | def _get_netfqdn(self): |
45 | 45 | try: |
78 | 78 | else: |
79 | 79 | base_dn += ','.join('dc={}'.format(x) for x in self._queried_domain.split('.')) |
80 | 80 | |
81 | try: | |
82 | ldap_connection = ldap.LDAPConnection('ldap://{}'.format(self._domain_controller), | |
83 | base_dn, self._domain_controller) | |
84 | ldap_connection.login(self._user, self._password, self._domain, | |
85 | self._lmhash, self._nthash) | |
86 | except ldap.LDAPSessionError as e: | |
87 | if str(e).find('strongerAuthRequired') >= 0: | |
88 | # We need to try SSL | |
89 | ldap_connection = ldap.LDAPConnection('ldaps://{}'.format(self._domain_controller), | |
90 | base_dn, self._domain_controller) | |
91 | ldap_connection.login(self._user, self._password, self._domain, | |
92 | self._lmhash, self._nthash) | |
93 | else: | |
94 | raise e | |
95 | except socket.error as e: | |
96 | return | |
81 | # base_dn is no longer used within `_create_ldap_connection()`, but I don't want to break | |
82 | # the function call. So we store it in an attriute and use it in `_ldap_search()` | |
83 | self._base_dn = base_dn | |
84 | ||
85 | # Format the username and the domain | |
86 | # ldap3 seems not compatible with USER@DOMAIN format | |
87 | user = '{}\\{}'.format(self._domain, self._user) | |
88 | ||
89 | # Choose between password or pth | |
90 | if self._lmhash and self._nthash: | |
91 | lm_nt_hash = '{}:{}'.format(self._lmhash, self._nthash) | |
92 | ||
93 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller)) | |
94 | ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, | |
95 | authentication=ldap3.NTLM, raise_exceptions=True) | |
96 | ||
97 | try: | |
98 | ldap_connection.bind() | |
99 | except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult: | |
100 | # We need to try SSL (pth version) | |
101 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller)) | |
102 | ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, | |
103 | authentication=ldap3.NTLM, raise_exceptions=True) | |
104 | ||
105 | ldap_connection.bind() | |
106 | ||
107 | else: | |
108 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller)) | |
109 | ldap_connection = ldap3.Connection(ldap_server, user, self._password, | |
110 | authentication=ldap3.NTLM, raise_exceptions=True) | |
111 | ||
112 | try: | |
113 | ldap_connection.bind() | |
114 | except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult: | |
115 | # We nedd to try SSL (password version) | |
116 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller)) | |
117 | ldap_connection = ldap3.Connection(ldap_server, user, self._password, | |
118 | authentication=ldap3.NTLM, raise_exceptions=True) | |
119 | ||
120 | ldap_connection.bind() | |
97 | 121 | |
98 | 122 | self._ldap_connection = ldap_connection |
99 | 123 | |
100 | 124 | def _ldap_search(self, search_filter, class_result, attributes=list()): |
101 | 125 | results = list() |
102 | paged_search_control = ldapasn1.SimplePagedResultsControl(criticality=True, | |
103 | size=1000) | |
104 | try: | |
105 | search_results = self._ldap_connection.search(searchFilter=search_filter, | |
106 | searchControls=[paged_search_control], | |
107 | attributes=attributes) | |
108 | except ldap.LDAPSearchError as e: | |
109 | # If we got a "size exceeded" error, we get the partial results | |
110 | if e.error == 4: | |
111 | search_results = e.answers | |
112 | else: | |
113 | raise e | |
114 | # TODO: Filter parenthesis in LDAP filter | |
115 | except ldap.LDAPFilterSyntaxError as e: | |
116 | return list() | |
117 | ||
126 | ||
127 | # if no attribute name specified, we return all attributes | |
128 | if not attributes: | |
129 | attributes = ldap3.ALL_ATTRIBUTES | |
130 | ||
131 | try: | |
132 | # Microsoft Active Directory set an hard limit of 1000 entries returned by any search | |
133 | search_results=self._ldap_connection.extend.standard.paged_search(search_base=self._base_dn, | |
134 | search_filter=search_filter, attributes=attributes, | |
135 | paged_size=1000, generator=True) | |
136 | # TODO: for debug only | |
137 | except Exception as e: | |
138 | import sys | |
139 | print('Except: ', sys.exc_info()[0]) | |
140 | ||
141 | # Skip searchResRef | |
118 | 142 | for result in search_results: |
119 | if not isinstance(result, ldapasn1.SearchResultEntry): | |
143 | if result['type'] is not 'searchResEntry': | |
120 | 144 | continue |
121 | ||
122 | results.append(class_result(result['attributes'])) | |
145 | results.append(class_result(result['raw_attributes'])) | |
123 | 146 | |
124 | 147 | return results |
125 | 148 | |
135 | 158 | (ads_path != instance._ads_path) or \ |
136 | 159 | (ads_prefix != instance._ads_prefix): |
137 | 160 | if instance._ldap_connection: |
138 | instance._ldap_connection.close() | |
161 | instance._ldap_connection.unbind() | |
139 | 162 | instance._create_ldap_connection(queried_domain=queried_domain, |
140 | 163 | ads_path=ads_path, ads_prefix=ads_prefix) |
141 | 164 | return f(*args, **kwargs) |
147 | 170 | |
148 | 171 | def __exit__(self, type, value, traceback): |
149 | 172 | try: |
150 | self._ldap_connection.close() | |
173 | self._ldap_connection.unbind() | |
151 | 174 | except AttributeError: |
152 | 175 | pass |
153 | 176 | self._ldap_connection = None |
0 | # -*- coding: utf8 -*- | |
1 | ||
2 | 0 | # This file is part of PywerView. |
3 | 1 | |
4 | 2 | # PywerView is free software: you can redistribute it and/or modify |
14 | 12 | # You should have received a copy of the GNU General Public License |
15 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 14 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | 16 | |
19 | 17 | from multiprocessing import Process, Pipe |
20 | 18 |
0 | #!/usr/bin/env python | |
1 | # -*- coding: utf8 -*- | |
0 | #!/usr/bin/env python3 | |
2 | 1 | # |
3 | 2 | # This file is part of PywerView. |
4 | 3 | |
15 | 14 | # You should have received a copy of the GNU General Public License |
16 | 15 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
17 | 16 | |
18 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
19 | 18 | |
20 | 19 | from pywerview.cli.main import main |
21 | 20 |
0 | # -*- coding: utf8 -*- | |
0 | #!/usr/bin/env python3 | |
1 | 1 | |
2 | 2 | from setuptools import setup, find_packages |
3 | 3 | |
4 | 4 | try: |
5 | 5 | import pypandoc |
6 | long_description = pypandoc.convert('README.md', 'rst') | |
6 | long_description = pypandoc.convert_file('README.md', 'rst') | |
7 | 7 | except(IOError, ImportError): |
8 | 8 | long_description = open('README.md').read() |
9 | 9 | |
10 | 10 | setup(name='pywerview', |
11 | version='0.2.0', | |
11 | version='0.3.2', | |
12 | 12 | description='A Python port of PowerSploit\'s PowerView', |
13 | 13 | long_description=long_description, |
14 | dependency_links = ['https://github.com/CoreSecurity/impacket/tarball/master#egg=impacket-0.9.16dev'], | |
14 | dependency_links = ['https://github.com/SecureAuthCorp/impacket/tarball/master#egg=impacket-0.9.22'], | |
15 | 15 | classifiers=[ |
16 | 16 | 'Environment :: Console', |
17 | 17 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', |
18 | 'Programming Language :: Python :: 2.7', | |
18 | 'Programming Language :: Python :: 3.6', | |
19 | 19 | 'Topic :: Security', |
20 | 20 | ], |
21 | 21 | keywords='python powersploit pentesting recon active directory windows', |
27 | 27 | "pywerview", "pywerview.*" |
28 | 28 | ]), |
29 | 29 | install_requires=[ |
30 | 'impacket>=0.9.16dev', | |
31 | 'pyasn1', | |
32 | 'pycrypto', | |
33 | 'pyopenssl', | |
34 | 'bs4' | |
30 | 'impacket>=0.9.22', | |
31 | 'bs4', | |
32 | 'lxml' | |
35 | 33 | ], |
36 | 34 | entry_points = { |
37 | 35 | 'console_scripts': ['pywerview=pywerview.cli.main:main'], |