New upstream snapshot.
Kali Janitor
1 year, 5 months ago
0 | *~ | |
1 | TODO | |
2 | # Byte-compiled / optimized / DLL files | |
3 | __pycache__/ | |
4 | *.py[cod] | |
5 | ||
6 | # C extensions | |
7 | *.so | |
8 | ||
9 | # Distribution / packaging | |
10 | .Python | |
11 | env/ | |
12 | build/ | |
13 | develop-eggs/ | |
14 | dist/ | |
15 | downloads/ | |
16 | eggs/ | |
17 | .eggs/ | |
18 | lib/ | |
19 | lib64/ | |
20 | parts/ | |
21 | sdist/ | |
22 | var/ | |
23 | *.egg-info/ | |
24 | .installed.cfg | |
25 | *.egg | |
26 | ||
27 | # PyInstaller | |
28 | # Usually these files are written by a python script from a template | |
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. | |
30 | *.manifest | |
31 | *.spec | |
32 | ||
33 | # Installer logs | |
34 | pip-log.txt | |
35 | pip-delete-this-directory.txt | |
36 | ||
37 | # Unit test / coverage reports | |
38 | htmlcov/ | |
39 | .tox/ | |
40 | .coverage | |
41 | .coverage.* | |
42 | .cache | |
43 | nosetests.xml | |
44 | coverage.xml | |
45 | *,cover | |
46 | ||
47 | # Translations | |
48 | *.mo | |
49 | *.pot | |
50 | ||
51 | # Django stuff: | |
52 | *.log | |
53 | ||
54 | # Sphinx documentation | |
55 | docs/_build/ | |
56 | ||
57 | # PyBuilder | |
58 | target/⏎ |
0 | Metadata-Version: 2.1 | |
1 | Name: pywerview | |
2 | Version: 0.4.0 | |
3 | Summary: A Python port of PowerSploit's PowerView | |
4 | Home-page: https://github.com/the-useless-one/pywerview | |
5 | Author: Yannick Méheut | |
6 | Author-email: [email protected] | |
7 | License: GNU GPLv3 | |
8 | Keywords: python powersploit pentesting recon active directory windows | |
9 | Classifier: Environment :: Console | |
10 | Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) | |
11 | Classifier: Programming Language :: Python :: 3.6 | |
12 | Classifier: Topic :: Security | |
13 | Description-Content-Type: text/markdown | |
14 | License-File: LICENSE | |
15 | ||
16 | # PywerView | |
17 | ____ __ ___ | |
18 | | _ \ _ ___ _____ _ _\ \ / (_) _____ __ | |
19 | | |_) | | | \ \ /\ / / _ \ '__\ \ / /| |/ _ \ \ /\ / / | |
20 | | __/| |_| |\ V V / __/ | \ V / | | __/\ V V / | |
21 | |_| \__, | \_/\_/ \___|_| \_/ |_|\___| \_/\_/ | |
22 | |___/ | |
23 | ||
24 | A (partial) Python rewriting of [PowerSploit](https://github.com/PowerShellMafia/PowerSploit)'s | |
25 | [PowerView](https://github.com/PowerShellMafia/PowerSploit/tree/master/Recon). | |
26 | ||
27 | Fork me on [GitHub](https://github.com/the-useless-one/pywerview). | |
28 | ||
29 | [![License](https://img.shields.io/github/license/the-useless-one/pywerview)](https://github.com/the-useless-one/pywerview/blob/master/LICENSE) | |
30 | ![Python versions](https://img.shields.io/pypi/pyversions/pywerview) | |
31 | [![GitHub release](https://img.shields.io/github/v/release/the-useless-one/pywerview)](https://github.com/the-useless-one/pywerview/releases/latest) | |
32 | [![PyPI version](https://img.shields.io/pypi/v/pywerview)](https://pypi.org/project/pywerview/) | |
33 | ||
34 | ## HISTORY | |
35 | ||
36 | As a pentester, I love using PowerView during my assignments. It makes it so | |
37 | easy to find vulnerable machines, or list what domain users were added to the | |
38 | local Administrators group of a machine, and much more. | |
39 | ||
40 | However, running PowerView on a computer which is not connected to the domain | |
41 | is a pain: I always find myself using [mimikatz](https://github.com/gentilkiwi/mimikatz/)'s | |
42 | `sekurlsa::pth` to run a Powershell prompt with stolen domain credentials, and | |
43 | that's not easy to script. Plus, I'm a Linux guy and I've always found it a | |
44 | shame that there were no complete Windows/Active Directory enumeration tool on | |
45 | Linux. | |
46 | ||
47 | That's why I decided to rewrite some of PowerView's functionalities in Python, | |
48 | using the wonderful [impacket](https://github.com/SecureAuthCorp/impacket) | |
49 | library. | |
50 | ||
51 | *Update:* I haven't tested the last version of PowerView yet, which can run | |
52 | from a machine not connected to a domain. I don't know if it works correctly | |
53 | under Linux using Powershell. If anyone has had any experience with this at all, | |
54 | you can contact me, I'm really interested. We'll see if pywerview has become | |
55 | obsoleted ;) but I think I'll continue working on it eitherway: I'd still | |
56 | rather use Python than Powershell on Linux, and I'm learning a lot! Plus, it | |
57 | may integrated in existing Linux tools written in Python. It's still great news | |
58 | that PowerView now supports machines not connected to the domain! | |
59 | ||
60 | ## DISCLAIMER | |
61 | ||
62 | This tool is far from complete (as you'll see in the [TODO](#todo) section)! I | |
63 | still have a lot more awesome PowerView functionalities to implement (the user | |
64 | hunting functions, the GPO functions, the local process enumeration, etc.), | |
65 | but I still think it can be useful as is. | |
66 | ||
67 | It's also (very) possible that there are (many) bugs in the code: I've only | |
68 | tested the simplest test cases. If you use this tool during an assignment and | |
69 | you get an error, please, open an issue with the error and the conditions that | |
70 | triggered this error. | |
71 | ||
72 | Also, blah blah blah, don't use it for evil purposes. | |
73 | ||
74 | ## REQUIREMENTS | |
75 | ||
76 | * Python 3.6 | |
77 | * impacket >= 0.9.22 | |
78 | * ldap3 >= 2.8.1 | |
79 | * gssapi (Which requires `libkrb5-dev`) | |
80 | * pycryptodomex (or pycryptodome) | |
81 | ||
82 | ## FUNCTIONALITIES | |
83 | ||
84 | If you like living on the bleeding edge, check out the | |
85 | [development branch](https://github.com/the-useless-one/pywerview/tree/develop). | |
86 | ||
87 | Here's the list of available commands: | |
88 | ||
89 | $ pywerview.py --help | |
90 | usage: pywerview.py [-h] | |
91 | {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
92 | ... | |
93 | ||
94 | Rewriting of some PowerView's functionalities in Python | |
95 | ||
96 | optional arguments: | |
97 | -h, --help show this help message and exit | |
98 | ||
99 | Subcommands: | |
100 | Available subcommands | |
101 | ||
102 | {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
103 | get-adobject Takes a domain SID, samAccountName or name, and return the associated object | |
104 | get-adserviceaccount | |
105 | Returns a list of all the gMSA of the specified domain (you need privileged account to retrieve passwords) | |
106 | get-objectacl Takes a domain SID, samAccountName or name, and return the ACL of the associated object | |
107 | get-netuser Queries information about a domain user | |
108 | get-netgroup Get a list of all current domain groups, or a list of groups a domain user is member of | |
109 | get-netcomputer Queries informations about domain computers | |
110 | get-netdomaincontroller | |
111 | Get a list of domain controllers for the given domain | |
112 | get-netfileserver Return a list of file servers, extracted from the domain users' homeDirectory, scriptPath, and profilePath fields | |
113 | get-dfsshare Return a list of all fault tolerant distributed file systems for a given domain | |
114 | get-netou Get a list of all current OUs in the domain | |
115 | get-netsite Get a list of all current sites in the domain | |
116 | get-netsubnet Get a list of all current subnets in the domain | |
117 | get-netdomaintrust Returns a list of all the trusts of the specified domain | |
118 | get-netgpo Get a list of all current GPOs in the domain | |
119 | get-netpso Get a list of all current PSOs in the domain | |
120 | get-domainpolicy Returns the default domain or DC policy for the queried domain or DC | |
121 | get-gpttmpl Helper to parse a GptTmpl.inf policy file path into a custom object | |
122 | get-netgpogroup Parses all GPOs in the domain that set "Restricted Group" or "Groups.xml" | |
123 | find-gpocomputeradmin | |
124 | Takes a computer (or OU) and determine who has administrative access to it via GPO | |
125 | find-gpolocation Takes a username or a group name and determine the computers it has administrative access to via GPO | |
126 | get-netgroupmember Return a list of members of a domain group | |
127 | get-netsession Queries a host to return a list of active sessions on the host (you can use local credentials instead of domain credentials) | |
128 | get-localdisks Queries a host to return a list of active disks on the host (you can use local credentials instead of domain credentials) | |
129 | get-netdomain Queries a host for available domains | |
130 | get-netshare Queries a host to return a list of available shares on the host (you can use local credentials instead of domain credentials) | |
131 | get-netloggedon This function will execute the NetWkstaUserEnum RPC call to query a given host for actively logged on users | |
132 | get-netlocalgroup Gets a list of members of a local group on a machine, or returns every local group. You can use local credentials instead of domain credentials, however, domain credentials are needed | |
133 | to resolve domain SIDs. | |
134 | invoke-checklocaladminaccess | |
135 | Checks if the given user has local admin access on the given host | |
136 | get-netprocess This function will execute the 'Select * from Win32_Process' WMI query to a given host for a list of executed process | |
137 | get-userevent This function will execute the 'SELECT * from Win32_NTLogEvent' WMI query to a given host for a list of executed process | |
138 | invoke-userhunter Finds which machines domain users are logged into | |
139 | invoke-processhunter | |
140 | Searches machines for processes with specific name, or ran by specific users | |
141 | invoke-eventhunter Searches machines for events with specific name, or ran by specific users | |
142 | ||
143 | Take a look at the [wiki](https://github.com/the-useless-one/pywerview/wiki) to | |
144 | see a more detailed usage of every command. | |
145 | ||
146 | *Attention:* in every command, the used domain name must be the post-Win2k UPN, | |
147 | and not the Win2k compatible name. | |
148 | ||
149 | For example, my domain name is `uselessdomain.local`. The Win2K compatible name | |
150 | is `USELESSDOMAIN`. In every command, I must use __`uselessdomain.local`__ as | |
151 | an argument, and __not__ `USELESSDOMAIN`. | |
152 | ||
153 | ## GLOBAL ARGUMENTS | |
154 | ||
155 | ### LOGGING | |
156 | ||
157 | You can provide a logging level to `pywerview` modules by using `-l` or `--logging-level` options. Supported levels are: | |
158 | ||
159 | * `CRITICAL`: Only critical errors are displayed **(default)** | |
160 | * `WARNING` Warnings are displayed, along with citical errors | |
161 | * `DEBUG`: Debug level (caution: **very** verbose) | |
162 | * `ULTRA`: Extreme debugging level (caution: **very very** verbose) | |
163 | ||
164 | (level names are case insensitive) | |
165 | ||
166 | ### Kerberos authentication | |
167 | ||
168 | Kerberos authentication is now (partially) supported, which means you can | |
169 | pass the ticket and other stuff. To authenticate via Kerberos: | |
170 | ||
171 | 1. Point the `KRB5CCNAME` environment variable to your cache credential file. | |
172 | 2. Use the `-k` option in your function call, or the `do_kerberos` in your | |
173 | library call. | |
174 | ||
175 | ```console | |
176 | $ klist stormtroopers.ccache | |
177 | Ticket cache: FILE:stormtroopers.ccache | |
178 | Default principal: [email protected] | |
179 | ||
180 | Valid starting Expires Service principal | |
181 | 10/03/2022 16:46:45 11/03/2022 02:46:45 ldap/[email protected] | |
182 | renew until 11/03/2022 16:43:17 | |
183 | $ KRB5CCNAME=stormtroopers.ccache python3 pywerview.py get-netcomputer -t srv-ad.contoso.com -u stormtroopers -k | |
184 | dnshostname: centos.contoso.com | |
185 | ||
186 | dnshostname: debian.contoso.com | |
187 | ||
188 | dnshostname: Windows7.contoso.com | |
189 | ||
190 | dnshostname: Windows10.contoso.com | |
191 | ||
192 | dnshostname: SRV-MAIL.contoso.com | |
193 | ||
194 | dnshostname: SRV-AD.contoso.com | |
195 | ``` | |
196 | ||
197 | If your cache credential file contains a corresponding TGS, or a TGT for your | |
198 | calling user, Kerberos authentication will be used. | |
199 | ||
200 | __SPN patching is partial__. Right now, we're in a mixed configuration where we | |
201 | use `ldap3` for LDAP commands and `impacket` for the other protocols (SMB, | |
202 | RPC). That is because `impacket`'s LDAP implementation has several problems, | |
203 | such as mismanagement of non-ASCII characters (which is problematic for us | |
204 | baguette-eaters). | |
205 | ||
206 | `ldap3` uses `gssapi` to authenticate with Kerberos, and `gssapi` needs the | |
207 | full hostname in the SPN of a ticket, otherwise it throws an error. It would | |
208 | be possible to patch an SPN with an incomplete hostname, however it's not done | |
209 | for now. | |
210 | ||
211 | For any functions that only rely on `impacket` (SMB or RPC functions), you can | |
212 | use tickets with SPNs with an incomplete hostname. In the following example, we | |
213 | use an LDAP ticket with an incomplete hostname for an SMB function, without any | |
214 | trouble. You just have to make sure that the `--computername` argument matches | |
215 | this incomplete hostname in the SPN: | |
216 | ||
217 | ```console | |
218 | $ klist skywalker.ccache | |
219 | Ticket cache: FILE:skywalker.ccache | |
220 | Default principal: [email protected] | |
221 | ||
222 | Valid starting Expires Service principal | |
223 | 13/04/2022 14:26:59 14/04/2022 00:26:58 ldap/[email protected] | |
224 | renew until 14/04/2022 14:23:29 | |
225 | $ KRB5CCNAME=skywalker.ccache python3 pywerview.py get-localdisks --computername srv-ad -u skywalker -k | |
226 | disk: A: | |
227 | ||
228 | disk: C: | |
229 | ||
230 | disk: D: | |
231 | ``` | |
232 | ||
233 | To recap: | |
234 | ||
235 | | SPN in the ticket | Can be used with LDAP functions | Can be used with SMB/RPC functions | | |
236 | | :-----------------------------------: | :-----------------------------: | :--------------------------------: | | |
237 | | `ldap/[email protected]` | ✔️ | ✔️ | | |
238 | | `cifs/[email protected]` | ✔️ | ✔️ | | |
239 | | `ldap/[email protected]` | ❌ | ✔️ | | |
240 | ||
241 | ### TLS CONNECTION | |
242 | ||
243 | You can force a connection to the LDAPS port by using the `--tls` switch. It | |
244 | can be necessary with some functions, for example when retrieving gMSA | |
245 | passwords with `get-adserviceaccount`: | |
246 | ||
247 | ```console | |
248 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids | |
249 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
250 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
251 | samaccountname: gMSA-01$ | |
252 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
253 | description: | |
254 | enabled: True | |
255 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids --tls | |
256 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
257 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
258 | samaccountname: gMSA-01$ | |
259 | msds-managedpassword: 69730ce3914ac6[redacted] | |
260 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
261 | description: | |
262 | enabled: True | |
263 | ``` | |
264 | ||
265 | ### JSON OUTPUT | |
266 | ||
267 | Pywerview can print results in json format by using the `--json` switch. | |
268 | ||
269 | ## TODO | |
270 | ||
271 | * Many, many more PowerView functionalities to implement. I'll now focus on | |
272 | forest functions, then inter-forest trust functions | |
273 | * Lots of rewrite due to the last version of PowerView | |
274 | * Gracefully fail against Unix machines running Samba | |
275 | * Perform range cycling in `get-netgroupmember` | |
276 | * Manage request to the Global Catalog | |
277 | * Try to fall back to `tcp/139` for RPC communications if `tcp/445` is closed | |
278 | * Comment, document, and clean the code | |
279 | ||
280 | ## THANKS | |
281 | ||
282 | * Thanks to the [@PowerSploit](https://github.com/PowerShellMafia/PowerSploit/) | |
283 | team for an awesome tool. | |
284 | * Thanks to [@CoreSecurity](https://github.com/CoreSecurity/) for this complete | |
285 | and comprehensive library that is [impacket](https://github.com/CoreSecurity/impacket). | |
286 | * Special thanks to [@asolino](https://github.com/asolino) for his help on | |
287 | developing using impacket. | |
288 | * Thanks to [@byt3bl33d3r](https://github.com/byt3bl33d3r) for his | |
289 | contributions. | |
290 | * Thanks to [@ThePirateWhoSmellsOfSunflowers](https://github.com/ThePirateWhoSmellsOfSunflowers) | |
291 | for his debugging, love you baby :heart: | |
292 | * Thanks to [@mpgn](https://github.com/mpgn) for his python 3 contributions. | |
293 | ||
294 | ## COPYRIGHT | |
295 | ||
296 | PywerView - A Python rewriting of PowerSploit's PowerView | |
297 | ||
298 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
299 | ||
300 | This program is free software: you can redistribute it and/or modify it | |
301 | under the terms of the GNU General Public License as published by the | |
302 | Free Software Foundation, either version 3 of the License, or (at your | |
303 | option) any later version. | |
304 | ||
305 | This program is distributed in the hope that it will be useful, but | |
306 | WITHOUT ANY WARRANTY; without even the implied warranty of | |
307 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General | |
308 | Public License for more details. | |
309 | ||
310 | You should have received a copy of the GNU General Public License along | |
311 | with this program. If not, see | |
312 | [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). | |
313 |
13 | 13 | [![License](https://img.shields.io/github/license/the-useless-one/pywerview)](https://github.com/the-useless-one/pywerview/blob/master/LICENSE) |
14 | 14 | ![Python versions](https://img.shields.io/pypi/pyversions/pywerview) |
15 | 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) | |
16 | [![PyPI version](https://img.shields.io/pypi/v/pywerview)](https://pypi.org/project/pywerview/) | |
17 | 17 | |
18 | 18 | ## HISTORY |
19 | 19 | |
60 | 60 | * Python 3.6 |
61 | 61 | * impacket >= 0.9.22 |
62 | 62 | * ldap3 >= 2.8.1 |
63 | * gssapi (Which requires `libkrb5-dev`) | |
64 | * pycryptodomex (or pycryptodome) | |
63 | 65 | |
64 | 66 | ## FUNCTIONALITIES |
65 | 67 | |
68 | 70 | |
69 | 71 | Here's the list of available commands: |
70 | 72 | |
71 | $ ./pywerview.py --help | |
73 | $ pywerview.py --help | |
72 | 74 | usage: pywerview.py [-h] |
73 | {get-adobject,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netgpo,get-domainpolicy,get-gpttmpl,get-netgpogroup,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
75 | {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
74 | 76 | ... |
75 | 77 | |
76 | 78 | Rewriting of some PowerView's functionalities in Python |
81 | 83 | Subcommands: |
82 | 84 | Available subcommands |
83 | 85 | |
84 | {get-adobject,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netgpo,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
85 | get-adobject Takes a domain SID, samAccountName or name, and return | |
86 | the associated object | |
86 | {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
87 | get-adobject Takes a domain SID, samAccountName or name, and return the associated object | |
88 | get-adserviceaccount | |
89 | Returns a list of all the gMSA of the specified domain (you need privileged account to retrieve passwords) | |
90 | get-objectacl Takes a domain SID, samAccountName or name, and return the ACL of the associated object | |
87 | 91 | get-netuser Queries information about a domain user |
88 | get-netgroup Get a list of all current domain groups, or a list of | |
89 | groups a domain user is member of | |
92 | get-netgroup Get a list of all current domain groups, or a list of groups a domain user is member of | |
90 | 93 | get-netcomputer Queries informations about domain computers |
91 | 94 | get-netdomaincontroller |
92 | 95 | Get a list of domain controllers for the given domain |
93 | get-netfileserver Return a list of file servers, extracted from the | |
94 | domain users' homeDirectory, scriptPath, and | |
95 | profilePath fields | |
96 | get-dfsshare Return a list of all fault tolerant distributed file | |
97 | systems for a given domain | |
96 | get-netfileserver Return a list of file servers, extracted from the domain users' homeDirectory, scriptPath, and profilePath fields | |
97 | get-dfsshare Return a list of all fault tolerant distributed file systems for a given domain | |
98 | 98 | get-netou Get a list of all current OUs in the domain |
99 | 99 | get-netsite Get a list of all current sites in the domain |
100 | 100 | get-netsubnet Get a list of all current subnets in the domain |
101 | get-netdomaintrust Returns a list of all the trusts of the specified domain | |
101 | 102 | get-netgpo Get a list of all current GPOs in the domain |
102 | get-domainpolicy Returns the default domain or DC policy for the | |
103 | queried domain or DC | |
104 | get-gpttmpl Helper to parse a GptTmpl.inf policy file path into a | |
105 | custom object | |
106 | get-netgpogroup Parses all GPOs in the domain that set "Restricted | |
107 | Group" or "Groups.xml" | |
103 | get-netpso Get a list of all current PSOs in the domain | |
104 | get-domainpolicy Returns the default domain or DC policy for the queried domain or DC | |
105 | get-gpttmpl Helper to parse a GptTmpl.inf policy file path into a custom object | |
106 | get-netgpogroup Parses all GPOs in the domain that set "Restricted Group" or "Groups.xml" | |
108 | 107 | find-gpocomputeradmin |
109 | Takes a computer (or OU) and determine who has | |
110 | administrative access to it via GPO | |
111 | find-gpolocation Takes a username or a group name and determine the | |
112 | computers it has administrative access to via GPO | |
108 | Takes a computer (or OU) and determine who has administrative access to it via GPO | |
109 | find-gpolocation Takes a username or a group name and determine the computers it has administrative access to via GPO | |
113 | 110 | get-netgroupmember Return a list of members of a domain group |
114 | get-netsession Queries a host to return a list of active sessions on | |
115 | the host (you can use local credentials instead of | |
116 | domain credentials) | |
117 | get-localdisks Queries a host to return a list of active disks on the | |
118 | host (you can use local credentials instead of domain | |
119 | credentials) | |
111 | get-netsession Queries a host to return a list of active sessions on the host (you can use local credentials instead of domain credentials) | |
112 | get-localdisks Queries a host to return a list of active disks on the host (you can use local credentials instead of domain credentials) | |
120 | 113 | get-netdomain Queries a host for available domains |
121 | get-netshare Queries a host to return a list of available shares on | |
122 | the host (you can use local credentials instead of | |
123 | domain credentials) | |
124 | get-netloggedon This function will execute the NetWkstaUserEnum RPC | |
125 | call to query a given host for actively logged on | |
126 | users | |
127 | get-netlocalgroup Gets a list of members of a local group on a machine, | |
128 | or returns every local group. You can use local | |
129 | credentials instead of domain credentials, however, | |
130 | domain credentials are needed to resolve domain SIDs. | |
114 | get-netshare Queries a host to return a list of available shares on the host (you can use local credentials instead of domain credentials) | |
115 | get-netloggedon This function will execute the NetWkstaUserEnum RPC call to query a given host for actively logged on users | |
116 | get-netlocalgroup Gets a list of members of a local group on a machine, or returns every local group. You can use local credentials instead of domain credentials, however, domain credentials are needed | |
117 | to resolve domain SIDs. | |
131 | 118 | invoke-checklocaladminaccess |
132 | Checks if the given user has local admin access on the | |
133 | given host | |
134 | get-netprocess This function will execute the 'Select * from | |
135 | Win32_Process' WMI query to a given host for a list of | |
136 | executed process | |
137 | get-userevent This function will execute the 'Select * from | |
138 | Win32_Process' WMI query to a given host for a list of | |
139 | executed process | |
119 | Checks if the given user has local admin access on the given host | |
120 | get-netprocess This function will execute the 'Select * from Win32_Process' WMI query to a given host for a list of executed process | |
121 | get-userevent This function will execute the 'SELECT * from Win32_NTLogEvent' WMI query to a given host for a list of executed process | |
140 | 122 | invoke-userhunter Finds which machines domain users are logged into |
141 | 123 | invoke-processhunter |
142 | Searches machines for processes with specific name, or | |
143 | ran by specific users | |
144 | invoke-eventhunter Searches machines for events with specific name, or | |
145 | ran by specific users | |
124 | Searches machines for processes with specific name, or ran by specific users | |
125 | invoke-eventhunter Searches machines for events with specific name, or ran by specific users | |
146 | 126 | |
147 | 127 | Take a look at the [wiki](https://github.com/the-useless-one/pywerview/wiki) to |
148 | 128 | see a more detailed usage of every command. |
154 | 134 | is `USELESSDOMAIN`. In every command, I must use __`uselessdomain.local`__ as |
155 | 135 | an argument, and __not__ `USELESSDOMAIN`. |
156 | 136 | |
137 | ## GLOBAL ARGUMENTS | |
138 | ||
139 | ### LOGGING | |
140 | ||
141 | You can provide a logging level to `pywerview` modules by using `-l` or `--logging-level` options. Supported levels are: | |
142 | ||
143 | * `CRITICAL`: Only critical errors are displayed **(default)** | |
144 | * `WARNING` Warnings are displayed, along with citical errors | |
145 | * `DEBUG`: Debug level (caution: **very** verbose) | |
146 | * `ULTRA`: Extreme debugging level (caution: **very very** verbose) | |
147 | ||
148 | (level names are case insensitive) | |
149 | ||
150 | ### Kerberos authentication | |
151 | ||
152 | Kerberos authentication is now (partially) supported, which means you can | |
153 | pass the ticket and other stuff. To authenticate via Kerberos: | |
154 | ||
155 | 1. Point the `KRB5CCNAME` environment variable to your cache credential file. | |
156 | 2. Use the `-k` option in your function call, or the `do_kerberos` in your | |
157 | library call. | |
158 | ||
159 | ```console | |
160 | $ klist stormtroopers.ccache | |
161 | Ticket cache: FILE:stormtroopers.ccache | |
162 | Default principal: [email protected] | |
163 | ||
164 | Valid starting Expires Service principal | |
165 | 10/03/2022 16:46:45 11/03/2022 02:46:45 ldap/[email protected] | |
166 | renew until 11/03/2022 16:43:17 | |
167 | $ KRB5CCNAME=stormtroopers.ccache python3 pywerview.py get-netcomputer -t srv-ad.contoso.com -u stormtroopers -k | |
168 | dnshostname: centos.contoso.com | |
169 | ||
170 | dnshostname: debian.contoso.com | |
171 | ||
172 | dnshostname: Windows7.contoso.com | |
173 | ||
174 | dnshostname: Windows10.contoso.com | |
175 | ||
176 | dnshostname: SRV-MAIL.contoso.com | |
177 | ||
178 | dnshostname: SRV-AD.contoso.com | |
179 | ``` | |
180 | ||
181 | If your cache credential file contains a corresponding TGS, or a TGT for your | |
182 | calling user, Kerberos authentication will be used. | |
183 | ||
184 | __SPN patching is partial__. Right now, we're in a mixed configuration where we | |
185 | use `ldap3` for LDAP commands and `impacket` for the other protocols (SMB, | |
186 | RPC). That is because `impacket`'s LDAP implementation has several problems, | |
187 | such as mismanagement of non-ASCII characters (which is problematic for us | |
188 | baguette-eaters). | |
189 | ||
190 | `ldap3` uses `gssapi` to authenticate with Kerberos, and `gssapi` needs the | |
191 | full hostname in the SPN of a ticket, otherwise it throws an error. It would | |
192 | be possible to patch an SPN with an incomplete hostname, however it's not done | |
193 | for now. | |
194 | ||
195 | For any functions that only rely on `impacket` (SMB or RPC functions), you can | |
196 | use tickets with SPNs with an incomplete hostname. In the following example, we | |
197 | use an LDAP ticket with an incomplete hostname for an SMB function, without any | |
198 | trouble. You just have to make sure that the `--computername` argument matches | |
199 | this incomplete hostname in the SPN: | |
200 | ||
201 | ```console | |
202 | $ klist skywalker.ccache | |
203 | Ticket cache: FILE:skywalker.ccache | |
204 | Default principal: [email protected] | |
205 | ||
206 | Valid starting Expires Service principal | |
207 | 13/04/2022 14:26:59 14/04/2022 00:26:58 ldap/[email protected] | |
208 | renew until 14/04/2022 14:23:29 | |
209 | $ KRB5CCNAME=skywalker.ccache python3 pywerview.py get-localdisks --computername srv-ad -u skywalker -k | |
210 | disk: A: | |
211 | ||
212 | disk: C: | |
213 | ||
214 | disk: D: | |
215 | ``` | |
216 | ||
217 | To recap: | |
218 | ||
219 | | SPN in the ticket | Can be used with LDAP functions | Can be used with SMB/RPC functions | | |
220 | | :-----------------------------------: | :-----------------------------: | :--------------------------------: | | |
221 | | `ldap/[email protected]` | ✔️ | ✔️ | | |
222 | | `cifs/[email protected]` | ✔️ | ✔️ | | |
223 | | `ldap/[email protected]` | ❌ | ✔️ | | |
224 | ||
225 | ### TLS CONNECTION | |
226 | ||
227 | You can force a connection to the LDAPS port by using the `--tls` switch. It | |
228 | can be necessary with some functions, for example when retrieving gMSA | |
229 | passwords with `get-adserviceaccount`: | |
230 | ||
231 | ```console | |
232 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids | |
233 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
234 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
235 | samaccountname: gMSA-01$ | |
236 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
237 | description: | |
238 | enabled: True | |
239 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids --tls | |
240 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
241 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
242 | samaccountname: gMSA-01$ | |
243 | msds-managedpassword: 69730ce3914ac6[redacted] | |
244 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
245 | description: | |
246 | enabled: True | |
247 | ``` | |
248 | ||
249 | ### JSON OUTPUT | |
250 | ||
251 | Pywerview can print results in json format by using the `--json` switch. | |
252 | ||
157 | 253 | ## TODO |
158 | 254 | |
159 | 255 | * Many, many more PowerView functionalities to implement. I'll now focus on |
160 | 256 | forest functions, then inter-forest trust functions |
161 | 257 | * Lots of rewrite due to the last version of PowerView |
162 | * Implement a debugging mode (for easier troubleshooting) | |
163 | 258 | * Gracefully fail against Unix machines running Samba |
164 | * Support Kerberos authentication | |
165 | 259 | * Perform range cycling in `get-netgroupmember` |
166 | 260 | * Manage request to the Global Catalog |
167 | 261 | * Try to fall back to `tcp/139` for RPC communications if `tcp/445` is closed |
185 | 279 | |
186 | 280 | PywerView - A Python rewriting of PowerSploit's PowerView |
187 | 281 | |
188 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
282 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
189 | 283 | |
190 | 284 | This program is free software: you can redistribute it and/or modify it |
191 | 285 | under the terms of the GNU General Public License as published by the |
199 | 293 | |
200 | 294 | You should have received a copy of the GNU General Public License along |
201 | 295 | with this program. If not, see |
202 | [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). | |
203 | ||
296 | [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). | |
297 |
0 | pywerview (0.4.0+git20221123.1.89b3e78-0kali1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream snapshot. | |
3 | ||
4 | -- Kali Janitor <[email protected]> Thu, 15 Dec 2022 09:22:01 -0000 | |
5 | ||
0 | 6 | pywerview (0.3.2-0kali1) kali-dev; urgency=medium |
1 | 7 | |
2 | 8 | [ Ben Wilson ] |
14 | 14 | # You should have received a copy of the GNU General Public License |
15 | 15 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
18 | 18 | |
19 | 19 | from pywerview.functions.net import NetRequester |
20 | 20 | from pywerview.functions.gpo import GPORequester |
22 | 22 | from pywerview.functions.hunting import UserHunter, ProcessHunter, EventHunter |
23 | 23 | |
24 | 24 | def get_adobject(domain_controller, domain, user, password=str(), |
25 | lmhash=str(), nthash=str(), queried_domain=str(), queried_sid=str(), | |
26 | queried_name=str(), queried_sam_account_name=str(), ads_path=str(), | |
25 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
26 | queried_domain=str(), queried_sid=str(), queried_name=str(), | |
27 | queried_sam_account_name=str(), ads_path=str(), attributes=list(), | |
27 | 28 | custom_filter=str()): |
28 | 29 | requester = NetRequester(domain_controller, domain, user, password, |
29 | lmhash, nthash) | |
30 | lmhash, nthash, do_kerberos, do_tls) | |
30 | 31 | return requester.get_adobject(queried_domain=queried_domain, |
31 | 32 | queried_sid=queried_sid, queried_name=queried_name, |
32 | 33 | queried_sam_account_name=queried_sam_account_name, |
33 | ads_path=ads_path, custom_filter=custom_filter) | |
34 | ads_path=ads_path, attributes=attributes, custom_filter=custom_filter) | |
35 | ||
36 | def get_adserviceaccount(domain_controller, domain, user, password=str(), | |
37 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
38 | queried_domain=str(), queried_sid=str(), queried_name=str(), | |
39 | queried_sam_account_name=str(), ads_path=str(), resolve_sids=False): | |
40 | requester = NetRequester(domain_controller, domain, user, password, | |
41 | lmhash, nthash, do_kerberos, do_tls) | |
42 | return requester.get_adserviceaccount(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, resolve_sids=resolve_sids) | |
46 | ||
47 | def get_objectacl(domain_controller, domain, user, password=str(), | |
48 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
49 | queried_domain=str(), queried_sid=str(), queried_name=str(), | |
50 | queried_sam_account_name=str(), ads_path=str(), sacl=False, | |
51 | rights_filter=str(), resolve_sids=False, resolve_guids=False, | |
52 | custom_filter=str()): | |
53 | requester = NetRequester(domain_controller, domain, user, password, | |
54 | lmhash, nthash, do_kerberos, do_tls) | |
55 | return requester.get_objectacl(queried_domain=queried_domain, | |
56 | queried_sid=queried_sid, queried_name=queried_name, | |
57 | queried_sam_account_name=queried_sam_account_name, | |
58 | ads_path=ads_path, sacl=sacl, rights_filter=rights_filter, | |
59 | resolve_sids=resolve_sids, resolve_guids=resolve_guids, | |
60 | custom_filter=custom_filter) | |
34 | 61 | |
35 | 62 | def get_netuser(domain_controller, domain, user, password=str(), lmhash=str(), |
36 | nthash=str(), queried_username=str(), queried_domain=str(), ads_path=str(), | |
37 | admin_count=False, spn=False, unconstrained=False, allow_delegation=False, | |
38 | preauth_notreq=False, custom_filter=str(), | |
39 | attributes=[]): | |
40 | requester = NetRequester(domain_controller, domain, user, password, | |
41 | lmhash, nthash) | |
63 | nthash=str(), do_kerberos=False, do_tls=False, queried_username=str(), | |
64 | queried_domain=str(), ads_path=str(), admin_count=False, spn=False, | |
65 | unconstrained=False, allow_delegation=False, preauth_notreq=False, | |
66 | custom_filter=str(), attributes=[]): | |
67 | requester = NetRequester(domain_controller, domain, user, password, | |
68 | lmhash, nthash, do_kerberos, do_tls) | |
42 | 69 | return requester.get_netuser(queried_username=queried_username, |
43 | 70 | queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count, |
44 | 71 | spn=spn, unconstrained=unconstrained, allow_delegation=allow_delegation, |
46 | 73 | attributes=attributes) |
47 | 74 | |
48 | 75 | def get_netgroup(domain_controller, domain, user, password=str(), |
49 | lmhash=str(), nthash=str(), queried_groupname='*', queried_sid=str(), | |
50 | queried_username=str(), queried_domain=str(), ads_path=str(), | |
51 | admin_count=False, full_data=False, custom_filter=str()): | |
52 | requester = NetRequester(domain_controller, domain, user, password, | |
53 | lmhash, nthash) | |
76 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
77 | queried_groupname='*', queried_sid=str(), queried_username=str(), | |
78 | queried_domain=str(), ads_path=str(), admin_count=False, | |
79 | full_data=False, custom_filter=str()): | |
80 | requester = NetRequester(domain_controller, domain, user, password, | |
81 | lmhash, nthash, do_kerberos, do_tls) | |
54 | 82 | return requester.get_netgroup(queried_groupname=queried_groupname, |
55 | 83 | queried_sid=queried_sid, queried_username=queried_username, |
56 | 84 | queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count, |
57 | 85 | full_data=full_data, custom_filter=custom_filter) |
58 | 86 | |
59 | 87 | def get_netcomputer(domain_controller, domain, user, password=str(), |
60 | lmhash=str(), nthash=str(), queried_computername='*', queried_spn=str(), | |
61 | queried_os=str(), queried_sp=str(), queried_domain=str(), ads_path=str(), | |
88 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
89 | queried_computername='*', queried_spn=str(), queried_os=str(), | |
90 | queried_sp=str(), queried_domain=str(), ads_path=str(), | |
62 | 91 | printers=False, unconstrained=False, ping=False, full_data=False, |
63 | 92 | custom_filter=str(), attributes=[]): |
64 | 93 | requester = NetRequester(domain_controller, domain, user, password, |
65 | lmhash, nthash) | |
94 | lmhash, nthash, do_kerberos, do_tls) | |
66 | 95 | return requester.get_netcomputer(queried_computername=queried_computername, |
67 | 96 | queried_spn=queried_spn, queried_os=queried_os, queried_sp=queried_sp, |
68 | 97 | queried_domain=queried_domain, ads_path=ads_path, printers=printers, |
70 | 99 | custom_filter=custom_filter, attributes=attributes) |
71 | 100 | |
72 | 101 | def get_netdomaincontroller(domain_controller, domain, user, password=str(), |
73 | lmhash=str(), nthash=str(), queried_domain=str()): | |
74 | requester = NetRequester(domain_controller, domain, user, password, | |
75 | lmhash, nthash) | |
102 | lmhash=str(), nthash=str(), do_kerberos=False, | |
103 | do_tls=False, queried_domain=str()): | |
104 | requester = NetRequester(domain_controller, domain, user, password, | |
105 | lmhash, nthash, do_kerberos, do_tls) | |
76 | 106 | return requester.get_netdomaincontroller(queried_domain=queried_domain) |
77 | 107 | |
78 | 108 | def get_netfileserver(domain_controller, domain, user, password=str(), |
79 | lmhash=str(), nthash=str(), queried_domain=str(), target_users=list()): | |
80 | requester = NetRequester(domain_controller, domain, user, password, | |
81 | lmhash, nthash) | |
109 | lmhash=str(), nthash=str(), do_kerberos=False, | |
110 | do_tls=False, queried_domain=str(), target_users=list()): | |
111 | requester = NetRequester(domain_controller, domain, user, password, | |
112 | lmhash, nthash, do_kerberos, do_tls) | |
82 | 113 | return requester.get_netfileserver(queried_domain=queried_domain, |
83 | 114 | target_users=target_users) |
84 | 115 | |
85 | 116 | def get_dfsshare(domain_controller, domain, user, password=str(), |
86 | lmhash=str(), nthash=str(), version=['v1', 'v2'], queried_domain=str(), | |
87 | ads_path=str()): | |
88 | requester = NetRequester(domain_controller, domain, user, password, | |
89 | lmhash, nthash) | |
117 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
118 | version=['v1', 'v2'], queried_domain=str(), ads_path=str()): | |
119 | requester = NetRequester(domain_controller, domain, user, password, | |
120 | lmhash, nthash, do_kerberos, do_tls) | |
90 | 121 | return requester.get_dfsshare(version=version, queried_domain=queried_domain, ads_path=ads_path) |
91 | 122 | |
92 | 123 | def get_netou(domain_controller, domain, user, password=str(), lmhash=str(), |
93 | nthash=str(), queried_domain=str(), queried_ouname='*', queried_guid=str(), | |
94 | ads_path=str(), full_data=False): | |
95 | requester = NetRequester(domain_controller, domain, user, password, | |
96 | lmhash, nthash) | |
124 | nthash=str(), do_kerberos=False, do_tls=False, queried_domain=str(), | |
125 | queried_ouname='*', queried_guid=str(), ads_path=str(), full_data=False): | |
126 | requester = NetRequester(domain_controller, domain, user, password, | |
127 | lmhash, nthash, do_kerberos, do_tls) | |
97 | 128 | return requester.get_netou(queried_domain=queried_domain, |
98 | 129 | queried_ouname=queried_ouname, queried_guid=queried_guid, ads_path=ads_path, |
99 | 130 | full_data=full_data) |
100 | 131 | |
101 | 132 | def get_netsite(domain_controller, domain, user, password=str(), lmhash=str(), |
102 | nthash=str(), queried_domain=str(), queried_sitename=str(), | |
103 | queried_guid=str(), ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', | |
104 | full_data=False): | |
105 | requester = NetRequester(domain_controller, domain, user, password, | |
106 | lmhash, nthash) | |
133 | nthash=str(), do_kerberos=False, do_tls=False, queried_domain=str(), | |
134 | queried_sitename=str(), queried_guid=str(), ads_path=str(), | |
135 | ads_prefix='CN=Sites,CN=Configuration', full_data=False): | |
136 | requester = NetRequester(domain_controller, domain, user, password, | |
137 | lmhash, nthash, do_kerberos, do_tls) | |
107 | 138 | return requester.get_netsite(queried_domain=queried_domain, |
108 | 139 | queried_sitename=queried_sitename, queried_guid=queried_guid, |
109 | 140 | ads_path=ads_path, ads_prefix=ads_prefix, full_data=full_data) |
110 | 141 | |
111 | 142 | def get_netsubnet(domain_controller, domain, user, password=str(), |
112 | lmhash=str(), nthash=str(), queried_domain=str(), queried_sitename=str(), | |
113 | ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', full_data=False): | |
114 | requester = NetRequester(domain_controller, domain, user, password, | |
115 | lmhash, nthash) | |
143 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
144 | queried_domain=str(), queried_sitename=str(), ads_path=str(), | |
145 | ads_prefix='CN=Sites,CN=Configuration', full_data=False): | |
146 | requester = NetRequester(domain_controller, domain, user, password, | |
147 | lmhash, nthash, do_kerberos, do_tls) | |
116 | 148 | return requester.get_netsubnet(queried_domain=queried_domain, |
117 | 149 | queried_sitename=queried_sitename, ads_path=ads_path, ads_prefix=ads_prefix, |
118 | 150 | full_data=full_data) |
119 | 151 | |
120 | 152 | def get_netdomaintrust(domain_controller, domain, user, password=str(), |
121 | lmhash=str(), nthash=str(), queried_domain=str()): | |
122 | requester = NetRequester(domain_controller, domain, user, password, | |
123 | lmhash, nthash) | |
153 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, queried_domain=str()): | |
154 | requester = NetRequester(domain_controller, domain, user, password, | |
155 | lmhash, nthash, do_kerberos, do_tls) | |
124 | 156 | return requester.get_netdomaintrust(queried_domain=queried_domain) |
125 | 157 | |
126 | 158 | def get_netgroupmember(domain_controller, domain, user, password=str(), |
127 | lmhash=str(), nthash=str(), queried_groupname=str(), queried_sid=str(), | |
128 | queried_domain=str(), ads_path=str(), recurse=False, use_matching_rule=False, | |
159 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
160 | queried_groupname=str(), queried_sid=str(), queried_domain=str(), | |
161 | ads_path=str(), recurse=False, use_matching_rule=False, | |
129 | 162 | full_data=False, custom_filter=str()): |
130 | 163 | requester = NetRequester(domain_controller, domain, user, password, |
131 | lmhash, nthash) | |
164 | lmhash, nthash, do_kerberos, do_tls) | |
132 | 165 | return requester.get_netgroupmember(queried_groupname=queried_groupname, |
133 | 166 | queried_sid=queried_sid, queried_domain=queried_domain, |
134 | 167 | ads_path=ads_path, recurse=recurse, |
136 | 169 | full_data=full_data, custom_filter=custom_filter) |
137 | 170 | |
138 | 171 | def get_netsession(target_computername, domain, user, password=str(), |
139 | lmhash=str(), nthash=str()): | |
140 | requester = NetRequester(target_computername, domain, user, password, | |
141 | lmhash, nthash) | |
172 | lmhash=str(), nthash=str(), do_kerberos=False): | |
173 | requester = NetRequester(target_computername, domain, user, password, | |
174 | lmhash, nthash, do_kerberos) | |
142 | 175 | return requester.get_netsession() |
143 | 176 | |
144 | 177 | def get_netshare(target_computername, domain, user, password=str(), |
145 | lmhash=str(), nthash=str()): | |
146 | requester = NetRequester(target_computername, domain, user, password, | |
147 | lmhash, nthash) | |
178 | lmhash=str(), nthash=str(), do_kerberos=False): | |
179 | requester = NetRequester(target_computername, domain, user, password, | |
180 | lmhash, nthash, do_kerberos) | |
148 | 181 | return requester.get_netshare() |
149 | 182 | |
150 | 183 | def get_localdisks(target_computername, domain, user, password=str(), |
151 | lmhash=str(), nthash=str()): | |
152 | requester = NetRequester(target_computername, domain, user, password, | |
153 | lmhash, nthash) | |
184 | lmhash=str(), nthash=str(), do_kerberos=False): | |
185 | requester = NetRequester(target_computername, domain, user, password, | |
186 | lmhash, nthash, do_kerberos) | |
154 | 187 | return requester.get_localdisks() |
155 | 188 | |
156 | 189 | def get_netdomain(domain_controller, domain, user, password=str(), |
157 | lmhash=str(), nthash=str()): | |
158 | requester = NetRequester(domain_controller, domain, user, password, | |
159 | lmhash, nthash) | |
190 | lmhash=str(), nthash=str(), do_kerberos=False, | |
191 | do_tls=False): | |
192 | requester = NetRequester(domain_controller, domain, user, password, | |
193 | lmhash, nthash, do_kerberos, do_tls) | |
160 | 194 | return requester.get_netdomain() |
161 | 195 | |
162 | 196 | def get_netloggedon(target_computername, domain, user, password=str(), |
163 | lmhash=str(), nthash=str()): | |
164 | requester = NetRequester(target_computername, domain, user, password, | |
165 | lmhash, nthash) | |
197 | lmhash=str(), nthash=str(), do_kerberos=False): | |
198 | requester = NetRequester(target_computername, domain, user, password, | |
199 | lmhash, nthash, do_kerberos) | |
166 | 200 | return requester.get_netloggedon() |
167 | 201 | |
168 | 202 | def get_netlocalgroup(target_computername, domain_controller, domain, user, |
169 | password=str(), lmhash=str(), nthash=str(), queried_groupname=str(), | |
170 | list_groups=False, recurse=False): | |
171 | requester = NetRequester(target_computername, domain, user, password, | |
172 | lmhash, nthash, domain_controller) | |
203 | password=str(), lmhash=str(), nthash=str(), do_kerberos=False, | |
204 | do_tls=False, queried_groupname=str(), list_groups=False, | |
205 | recurse=False): | |
206 | requester = NetRequester(target_computername, domain, user, password, | |
207 | lmhash, nthash, do_kerberos, do_tls, domain_controller) | |
173 | 208 | return requester.get_netlocalgroup(queried_groupname=queried_groupname, |
174 | 209 | list_groups=list_groups, recurse=recurse) |
175 | 210 | |
176 | 211 | def get_netprocess(target_computername, domain, user, password=str(), |
177 | lmhash=str(), nthash=str()): | |
178 | requester = NetRequester(target_computername, domain, user, password, | |
179 | lmhash, nthash) | |
212 | lmhash=str(), nthash=str(), do_kerberos=False): | |
213 | requester = NetRequester(target_computername, domain, user, password, | |
214 | lmhash, nthash, do_kerberos) | |
180 | 215 | return requester.get_netprocess() |
181 | 216 | |
182 | 217 | def get_userevent(target_computername, domain, user, password=str(), |
183 | lmhash=str(), nthash=str(), event_type=['logon', 'tgt'], | |
184 | date_start=5): | |
185 | requester = NetRequester(target_computername, domain, user, password, | |
186 | lmhash, nthash) | |
218 | lmhash=str(), nthash=str(), do_kerberos=False, | |
219 | event_type=['logon', 'tgt'], date_start=5): | |
220 | requester = NetRequester(target_computername, domain, user, password, | |
221 | lmhash, nthash, do_kerberos) | |
187 | 222 | return requester.get_userevent(event_type=event_type, |
188 | 223 | date_start=date_start) |
189 | 224 | |
190 | 225 | def get_netgpo(domain_controller, domain, user, password=str(), |
191 | lmhash=str(), nthash=str(), queried_gponame='*', | |
192 | queried_displayname=str(), queried_domain=str(), ads_path=str()): | |
193 | requester = GPORequester(domain_controller, domain, user, password, | |
194 | lmhash, nthash) | |
226 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
227 | queried_gponame='*', queried_displayname=str(), queried_domain=str(), | |
228 | ads_path=str()): | |
229 | requester = GPORequester(domain_controller, domain, user, password, | |
230 | lmhash, nthash, do_kerberos, do_tls) | |
195 | 231 | return requester.get_netgpo(queried_gponame=queried_gponame, |
196 | 232 | queried_displayname=queried_displayname, |
197 | 233 | queried_domain=queried_domain, ads_path=ads_path) |
198 | 234 | |
235 | def get_netpso(domain_controller, domain, user, password=str(), | |
236 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
237 | queried_psoname='*', queried_displayname=str(), queried_domain=str(), | |
238 | ads_path=str()): | |
239 | requester = GPORequester(domain_controller, domain, user, password, | |
240 | lmhash, nthash, do_kerberos, do_tls) | |
241 | return requester.get_netpso(queried_psoname=queried_psoname, | |
242 | queried_displayname=queried_displayname, | |
243 | queried_domain=queried_domain, ads_path=ads_path) | |
244 | ||
199 | 245 | def get_domainpolicy(domain_controller, domain, user, password=str(), |
200 | lmhash=str(), nthash=str(), source='domain', queried_domain=str(), | |
201 | resolve_sids=False): | |
202 | requester = GPORequester(domain_controller, domain, user, password, | |
203 | lmhash, nthash) | |
246 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
247 | source='domain', queried_domain=str(), resolve_sids=False): | |
248 | requester = GPORequester(domain_controller, domain, user, password, | |
249 | lmhash, nthash, do_kerberos, do_tls) | |
204 | 250 | |
205 | 251 | return requester.get_domainpolicy(source=source, queried_domain=queried_domain, |
206 | 252 | resolve_sids=resolve_sids) |
207 | 253 | |
208 | 254 | def get_gpttmpl(gpttmpl_path, domain_controller, domain, user, password=str(), lmhash=str(), |
209 | nthash=str()): | |
210 | requester = GPORequester(domain_controller, domain, user, password, | |
211 | lmhash, nthash) | |
255 | nthash=str(), do_kerberos=False, do_tls=False): | |
256 | requester = GPORequester(domain_controller, domain, user, password, | |
257 | lmhash, nthash, do_kerberos, do_tls) | |
212 | 258 | |
213 | 259 | return requester.get_gpttmpl(gpttmpl_path) |
214 | 260 | |
215 | 261 | def get_netgpogroup(domain_controller, domain, user, password=str(), lmhash=str(), |
216 | nthash=str(), queried_gponame='*', queried_displayname=str(), | |
217 | queried_domain=str(), ads_path=str(), resolve_sids=False): | |
218 | requester = GPORequester(domain_controller, domain, user, password, | |
219 | lmhash, nthash) | |
262 | nthash=str(), do_kerberos=False, do_tls=False, queried_gponame='*', | |
263 | queried_displayname=str(), queried_domain=str(), ads_path=str(), | |
264 | resolve_sids=False): | |
265 | requester = GPORequester(domain_controller, domain, user, password, | |
266 | lmhash, nthash, do_kerberos, do_tls) | |
220 | 267 | |
221 | 268 | return requester.get_netgpogroup(queried_gponame=queried_gponame, |
222 | 269 | queried_displayname=queried_displayname, |
225 | 272 | resolve_sids=resolve_sids) |
226 | 273 | |
227 | 274 | def find_gpocomputeradmin(domain_controller, domain, user, password=str(), lmhash=str(), |
228 | nthash=str(), queried_computername=str(), | |
229 | queried_ouname=str(), queried_domain=str(), | |
230 | recurse=False): | |
231 | requester = GPORequester(domain_controller, domain, user, password, | |
232 | lmhash, nthash) | |
275 | nthash=str(), do_kerberos=False, do_tls=False, queried_computername=str(), | |
276 | queried_ouname=str(), queried_domain=str(), recurse=False): | |
277 | requester = GPORequester(domain_controller, domain, user, password, | |
278 | lmhash, nthash, do_kerberos, do_tls) | |
233 | 279 | |
234 | 280 | return requester.find_gpocomputeradmin(queried_computername=queried_computername, |
235 | 281 | queried_ouname=queried_ouname, |
237 | 283 | recurse=recurse) |
238 | 284 | |
239 | 285 | def find_gpolocation(domain_controller, domain, user, password=str(), lmhash=str(), |
240 | nthash=str(), queried_username=str(), queried_groupname=str(), | |
241 | queried_localgroup=str(), queried_domain=str()): | |
242 | requester = GPORequester(domain_controller, domain, user, password, | |
243 | lmhash, nthash) | |
286 | nthash=str(), do_kerberos=False, do_tls=False, queried_username=str(), | |
287 | queried_groupname=str(), queried_localgroup=str(), | |
288 | queried_domain=str()): | |
289 | requester = GPORequester(domain_controller, domain, user, password, | |
290 | lmhash, nthash, do_kerberos, do_tls) | |
244 | 291 | return requester.find_gpolocation(queried_username=queried_username, |
245 | 292 | queried_groupname=queried_groupname, |
246 | 293 | queried_localgroup=queried_localgroup, |
247 | 294 | queried_domain=queried_domain) |
248 | 295 | |
249 | 296 | def invoke_checklocaladminaccess(target_computername, domain, user, password=str(), |
250 | lmhash=str(), nthash=str()): | |
251 | misc = Misc(target_computername, domain, user, password, lmhash, nthash) | |
297 | lmhash=str(), nthash=str(), do_kerberos=False): | |
298 | misc = Misc(target_computername, domain, user, password, lmhash, nthash, do_kerberos) | |
252 | 299 | |
253 | 300 | return misc.invoke_checklocaladminaccess() |
254 | 301 | |
255 | 302 | def invoke_userhunter(domain_controller, domain, user, password=str(), |
256 | lmhash=str(), nthash=str(), queried_computername=list(), | |
257 | queried_computerfile=None, queried_computerfilter=str(), | |
258 | queried_computeradspath=str(), unconstrained=False, | |
259 | queried_groupname=str(), target_server=str(), | |
303 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
304 | queried_computername=list(), queried_computerfile=None, | |
305 | queried_computerfilter=str(), queried_computeradspath=str(), | |
306 | unconstrained=False, queried_groupname=str(), target_server=str(), | |
260 | 307 | queried_username=str(), queried_useradspath=str(), |
261 | 308 | queried_userfilter=str(), queried_userfile=None, |
262 | 309 | threads=1, admin_count=False, allow_delegation=False, |
264 | 311 | stealth=False, stealth_source=['dfs', 'dc', 'file'], |
265 | 312 | show_all=False, foreign_users=False): |
266 | 313 | user_hunter = UserHunter(domain_controller, domain, user, password, |
267 | lmhash, nthash) | |
268 | ||
314 | lmhash, nthash, do_kerberos, do_tls) | |
269 | 315 | return user_hunter.invoke_userhunter(queried_computername=queried_computername, |
270 | 316 | queried_computerfile=queried_computerfile, |
271 | 317 | queried_computerfilter=queried_computerfilter, |
281 | 327 | foreign_users=foreign_users) |
282 | 328 | |
283 | 329 | def invoke_processhunter(domain_controller, domain, user, password=str(), |
284 | lmhash=str(), nthash=str(), queried_computername=list(), | |
285 | queried_computerfile=None, queried_computerfilter=str(), | |
286 | queried_computeradspath=str(), queried_processname=list(), | |
287 | queried_groupname=str(), target_server=str(), | |
330 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
331 | queried_computername=list(), queried_computerfile=None, | |
332 | queried_computerfilter=str(), queried_computeradspath=str(), | |
333 | queried_processname=list(), queried_groupname=str(), target_server=str(), | |
288 | 334 | queried_username=str(), queried_useradspath=str(), |
289 | 335 | queried_userfilter=str(), queried_userfile=None, threads=1, |
290 | 336 | stop_on_success=False, queried_domain=str(), show_all=False): |
291 | 337 | process_hunter = ProcessHunter(domain_controller, domain, user, password, |
292 | lmhash, nthash) | |
338 | lmhash, nthash, do_kerberos, do_tls) | |
293 | 339 | |
294 | 340 | return process_hunter.invoke_processhunter(queried_computername=queried_computername, |
295 | 341 | queried_computerfile=queried_computerfile, |
304 | 350 | queried_domain=queried_domain, show_all=show_all) |
305 | 351 | |
306 | 352 | def invoke_eventhunter(domain_controller, domain, user, password=str(), |
307 | lmhash=str(), nthash=str(), queried_computername=list(), | |
308 | queried_computerfile=None, queried_computerfilter=str(), | |
309 | queried_computeradspath=str(), queried_groupname=str(), | |
310 | target_server=str(), queried_username=str(), | |
353 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
354 | queried_computername=list(), queried_computerfile=None, | |
355 | queried_computerfilter=str(), queried_computeradspath=str(), | |
356 | queried_groupname=str(), target_server=str(), queried_username=str(), | |
311 | 357 | queried_useradspath=str(), queried_userfilter=str(), |
312 | 358 | queried_userfile=None, threads=1, queried_domain=str(), |
313 | 359 | search_days=3): |
314 | 360 | event_hunter = EventHunter(domain_controller, domain, user, password, |
315 | lmhash, nthash) | |
361 | lmhash, nthash, do_kerberos, do_tls) | |
316 | 362 | |
317 | 363 | return event_hunter.invoke_eventhunter(queried_computername=queried_computername, |
318 | 364 | queried_computerfile=queried_computerfile, |
14 | 14 | # You should have received a copy of the GNU General Public License |
15 | 15 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | ||
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
18 | ||
19 | import logging | |
19 | 20 | import argparse |
21 | import json | |
22 | import datetime | |
20 | 23 | from pywerview.cli.helpers import * |
21 | 24 | from pywerview.functions.hunting import * |
22 | 25 | |
24 | 27 | # Main parser |
25 | 28 | parser = argparse.ArgumentParser(description='Rewriting of some PowerView\'s functionalities in Python') |
26 | 29 | subparsers = parser.add_subparsers(title='Subcommands', description='Available subcommands', dest='submodule') |
27 | ||
30 | ||
28 | 31 | # hack for python < 3.9 : https://stackoverflow.com/questions/23349349/argparse-with-required-subparser |
29 | 32 | subparsers.required = True |
33 | ||
34 | # Logging parser | |
35 | logging_parser = argparse.ArgumentParser(add_help=False) | |
36 | logging_parser.add_argument('-l', '--logging-level', dest='logging_level', type=str.upper, | |
37 | choices=['CRITICAL', 'WARNING', 'DEBUG', 'ULTRA'], default='CRITICAL', | |
38 | help='SDTERR logging level: ' | |
39 | 'CRITICAL: Only critical errors are displayed (default), ' | |
40 | 'WARNING: Warnings are displayed, along with citical errors, ' | |
41 | 'DEBUG: Debug level (caution: very verbose), ' | |
42 | 'ULTRA: Extreme debugging level (caution: very very verbose)') | |
43 | ||
44 | # json parser | |
45 | json_output_parser = argparse.ArgumentParser(add_help=False) | |
46 | json_output_parser.add_argument('--json', dest='json_output', action='store_true', | |
47 | help='Print results in JSON format') | |
30 | 48 | |
31 | 49 | # TODO: support keberos authentication |
32 | 50 | # Credentials parser |
33 | 51 | credentials_parser = argparse.ArgumentParser(add_help=False) |
34 | 52 | credentials_parser.add_argument('-w', '--workgroup', dest='domain', |
35 | 53 | default=str(), help='Name of the domain we authenticate with') |
36 | credentials_parser.add_argument('-u', '--user', required=True, | |
54 | credentials_parser.add_argument('-u', '--user', | |
37 | 55 | help='Username used to connect to the Domain Controller') |
38 | 56 | credentials_parser.add_argument('-p', '--password', |
39 | 57 | help='Password associated to the username') |
40 | credentials_parser.add_argument('--hashes', action='store', metavar = 'LMHASH:NTHASH', | |
58 | credentials_parser.add_argument('--hashes', action='store', metavar='LMHASH:NTHASH', | |
41 | 59 | help='NTLM hashes, format is [LMHASH:]NTHASH') |
60 | credentials_parser.add_argument('-k', action='store_true', dest='do_kerberos', | |
61 | help='Use Kerberos authentication. Grabs credentials from ccache file ' | |
62 | '(KRB5CCNAME) based on target parameters. If valid credentials ' | |
63 | 'cannot be found, it will use the ones specified in the command ' | |
64 | 'line') | |
42 | 65 | |
43 | 66 | # AD parser, used for net* functions running against a domain controller |
44 | 67 | ad_parser = argparse.ArgumentParser(add_help=False, parents=[credentials_parser]) |
45 | 68 | ad_parser.add_argument('-t', '--dc-ip', dest='domain_controller', |
46 | 69 | required=True, help='IP address of the Domain Controller to target') |
70 | ad_parser.add_argument('--tls', action='store_true', dest='do_tls', | |
71 | help='Force TLS connection to the Domain Controller') | |
47 | 72 | |
48 | 73 | # Target parser, used for net* functions running against a normal computer |
49 | 74 | target_parser = argparse.ArgumentParser(add_help=False, parents=[credentials_parser]) |
79 | 104 | |
80 | 105 | # Parser for the get-adobject command |
81 | 106 | get_adobject_parser = subparsers.add_parser('get-adobject', help='Takes a domain SID, '\ |
82 | 'samAccountName or name, and return the associated object', parents=[ad_parser]) | |
107 | 'samAccountName or name, and return the associated object', | |
108 | parents=[ad_parser, logging_parser, json_output_parser]) | |
83 | 109 | get_adobject_parser.add_argument('--sid', dest='queried_sid', |
84 | 110 | help='SID to query (wildcards accepted)') |
85 | 111 | get_adobject_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', |
90 | 116 | help='Domain to query') |
91 | 117 | get_adobject_parser.add_argument('-a', '--ads-path', |
92 | 118 | help='Additional ADS path') |
119 | get_adobject_parser.add_argument('--attributes', nargs='+', dest='attributes', | |
120 | default=[], help='Object attributes to return') | |
93 | 121 | get_adobject_parser.set_defaults(func=get_adobject) |
122 | ||
123 | # Parser for the get-adserviceaccount command | |
124 | get_adserviceaccount_parser = subparsers.add_parser('get-adserviceaccount', help='Returns a list of all the '\ | |
125 | 'gMSA of the specified domain. To retrieve passwords, you need a privileged account and '\ | |
126 | 'a TLS connection to the LDAP server (use the --tls switch).', | |
127 | parents=[ad_parser, logging_parser, json_output_parser]) | |
128 | get_adserviceaccount_parser.add_argument('--sid', dest='queried_sid', | |
129 | help='SID to query (wildcards accepted)') | |
130 | get_adserviceaccount_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', | |
131 | help='samAccountName to query (wildcards accepted)') | |
132 | get_adserviceaccount_parser.add_argument('--name', dest='queried_name', | |
133 | help='Name to query (wildcards accepted)') | |
134 | get_adserviceaccount_parser.add_argument('-d', '--domain', dest='queried_domain', | |
135 | help='Domain to query') | |
136 | get_adserviceaccount_parser.add_argument('-a', '--ads-path', | |
137 | help='Additional ADS path') | |
138 | get_adserviceaccount_parser.add_argument('--resolve-sids', dest='resolve_sids', | |
139 | action='store_true', help='Resolve SIDs when querying PrincipalsAllowedToRetrieveManagedPassword') | |
140 | get_adserviceaccount_parser.set_defaults(func=get_adserviceaccount) | |
141 | ||
142 | # Parser for the get-objectacl command | |
143 | get_objectacl_parser = subparsers.add_parser('get-objectacl', help='Takes a domain SID, '\ | |
144 | 'samAccountName or name, and return the ACL of the associated object', | |
145 | parents=[ad_parser, logging_parser, json_output_parser]) | |
146 | get_objectacl_parser.add_argument('--sid', dest='queried_sid', | |
147 | help='SID to query (wildcards accepted)') | |
148 | get_objectacl_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', | |
149 | help='samAccountName to query (wildcards accepted)') | |
150 | get_objectacl_parser.add_argument('--name', dest='queried_name', | |
151 | help='Name to query (wildcards accepted)') | |
152 | get_objectacl_parser.add_argument('-d', '--domain', dest='queried_domain', | |
153 | help='Domain to query') | |
154 | get_objectacl_parser.add_argument('-a', '--ads-path', | |
155 | help='Additional ADS path') | |
156 | get_objectacl_parser.add_argument('--sacl', action='store_true', | |
157 | help='Return the SACL instead of the DACL for the object (requires '\ | |
158 | 'a privileged account)') | |
159 | get_objectacl_parser.add_argument('--rights-filter', dest='rights_filter', | |
160 | choices=['reset-password', 'write-members', 'all'], help='A specific set of rights to return '\ | |
161 | '(reset-password, write-members, all)') | |
162 | get_objectacl_parser.add_argument('--resolve-sids', dest='resolve_sids', | |
163 | action='store_true', help='Resolve SIDs when querying an ACL') | |
164 | get_objectacl_parser.add_argument('--resolve-guids', action='store_true', | |
165 | help='Resolve GUIDs to their display names') | |
166 | get_objectacl_parser.set_defaults(func=get_objectacl) | |
94 | 167 | |
95 | 168 | # Parser for the get-netuser command |
96 | 169 | get_netuser_parser = subparsers.add_parser('get-netuser', help='Queries information about '\ |
97 | 'a domain user', parents=[ad_parser]) | |
170 | 'a domain user', parents=[ad_parser, logging_parser, json_output_parser]) | |
98 | 171 | get_netuser_parser.add_argument('--username', dest='queried_username', |
99 | 172 | help='Username to query (wildcards accepted)') |
100 | 173 | get_netuser_parser.add_argument('-d', '--domain', dest='queried_domain', |
119 | 192 | |
120 | 193 | # Parser for the get-netgroup command |
121 | 194 | get_netgroup_parser = subparsers.add_parser('get-netgroup', help='Get a list of all current '\ |
122 | 'domain groups, or a list of groups a domain user is member of', parents=[ad_parser]) | |
195 | 'domain groups, or a list of groups a domain user is member of', | |
196 | parents=[ad_parser, logging_parser, json_output_parser]) | |
123 | 197 | get_netgroup_parser.add_argument('--groupname', dest='queried_groupname', |
124 | 198 | default='*', help='Group to query (wildcards accepted)') |
125 | 199 | get_netgroup_parser.add_argument('--sid', dest='queried_sid', |
138 | 212 | |
139 | 213 | # Parser for the get-netcomputer command |
140 | 214 | get_netcomputer_parser = subparsers.add_parser('get-netcomputer', help='Queries informations about '\ |
141 | 'domain computers', parents=[ad_parser]) | |
215 | 'domain computers', parents=[ad_parser, logging_parser, json_output_parser]) | |
142 | 216 | get_netcomputer_parser.add_argument('--computername', dest='queried_computername', |
143 | 217 | default='*', help='Computer name to query') |
144 | 218 | get_netcomputer_parser.add_argument('-os', '--operating-system', dest='queried_os', |
165 | 239 | |
166 | 240 | # Parser for the get-netdomaincontroller command |
167 | 241 | get_netdomaincontroller_parser = subparsers.add_parser('get-netdomaincontroller', help='Get a list of '\ |
168 | 'domain controllers for the given domain', parents=[ad_parser]) | |
242 | 'domain controllers for the given domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
169 | 243 | get_netdomaincontroller_parser.add_argument('-d', '--domain', dest='queried_domain', |
170 | 244 | help='Domain to query') |
171 | 245 | get_netdomaincontroller_parser.set_defaults(func=get_netdomaincontroller) |
172 | 246 | |
173 | 247 | # Parser for the get-netfileserver command |
174 | 248 | get_netfileserver_parser = subparsers.add_parser('get-netfileserver', help='Return a list of '\ |
175 | 'file servers, extracted from the domain users\' homeDirectory, scriptPath, and profilePath fields', parents=[ad_parser]) | |
249 | 'file servers, extracted from the domain users\' homeDirectory, scriptPath, and profilePath fields', | |
250 | parents=[ad_parser, logging_parser, json_output_parser]) | |
176 | 251 | get_netfileserver_parser.add_argument('--target-users', nargs='+', |
177 | 252 | metavar='TARGET_USER', help='A list of users to target to find file servers (wildcards accepted)') |
178 | 253 | get_netfileserver_parser.add_argument('-d', '--domain', dest='queried_domain', |
181 | 256 | |
182 | 257 | # Parser for the get-dfsshare command |
183 | 258 | get_dfsshare_parser = subparsers.add_parser('get-dfsshare', help='Return a list of '\ |
184 | 'all fault tolerant distributed file systems for a given domain', parents=[ad_parser]) | |
259 | 'all fault tolerant distributed file systems for a given domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
185 | 260 | get_dfsshare_parser.add_argument('-d', '--domain', dest='queried_domain', |
186 | 261 | help='Domain to query') |
187 | 262 | get_dfsshare_parser.add_argument('-v', '--version', nargs='+', choices=['v1', 'v2'], |
192 | 267 | |
193 | 268 | # Parser for the get-netou command |
194 | 269 | get_netou_parser = subparsers.add_parser('get-netou', help='Get a list of all current '\ |
195 | 'OUs in the domain', parents=[ad_parser]) | |
270 | 'OUs in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
196 | 271 | get_netou_parser.add_argument('--ouname', dest='queried_ouname', |
197 | 272 | default='*', help='OU name to query (wildcards accepted)') |
198 | 273 | get_netou_parser.add_argument('--guid', dest='queried_guid', |
207 | 282 | |
208 | 283 | # Parser for the get-netsite command |
209 | 284 | get_netsite_parser = subparsers.add_parser('get-netsite', help='Get a list of all current '\ |
210 | 'sites in the domain', parents=[ad_parser]) | |
285 | 'sites in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
211 | 286 | get_netsite_parser.add_argument('--sitename', dest='queried_sitename', |
212 | 287 | help='Site name to query (wildcards accepted)') |
213 | 288 | get_netsite_parser.add_argument('--guid', dest='queried_guid', |
222 | 297 | |
223 | 298 | # Parser for the get-netsubnet command |
224 | 299 | get_netsubnet_parser = subparsers.add_parser('get-netsubnet', help='Get a list of all current '\ |
225 | 'subnets in the domain', parents=[ad_parser]) | |
300 | 'subnets in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
226 | 301 | get_netsubnet_parser.add_argument('--sitename', dest='queried_sitename', |
227 | 302 | help='Only return subnets for the specified site name (wildcards accepted)') |
228 | 303 | get_netsubnet_parser.add_argument('-d', '--domain', dest='queried_domain', |
235 | 310 | |
236 | 311 | # Parser for the get-netdomaintrust command |
237 | 312 | get_netdomaintrust_parser = subparsers.add_parser('get-netdomaintrust', help='Returns a list of all the '\ |
238 | 'trusts of the specified domain', parents=[ad_parser]) | |
313 | 'trusts of the specified domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
239 | 314 | get_netdomaintrust_parser.add_argument('-d', '--domain', dest='queried_domain', |
240 | 315 | help='Domain to query') |
241 | 316 | get_netdomaintrust_parser.set_defaults(func=get_netdomaintrust) |
242 | 317 | |
243 | 318 | # Parser for the get-netgpo command |
244 | 319 | get_netgpo_parser = subparsers.add_parser('get-netgpo', help='Get a list of all current '\ |
245 | 'GPOs in the domain', parents=[ad_parser]) | |
320 | 'GPOs in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
246 | 321 | get_netgpo_parser.add_argument('--gponame', dest='queried_gponame', |
247 | 322 | default='*', help='GPO name to query for (wildcards accepted)') |
248 | 323 | get_netgpo_parser.add_argument('--displayname', dest='queried_displayname', |
253 | 328 | help='Additional ADS path') |
254 | 329 | get_netgpo_parser.set_defaults(func=get_netgpo) |
255 | 330 | |
331 | # Parser for the get-netpso command | |
332 | get_netpso_parser = subparsers.add_parser('get-netpso', help='Get a list of all current '\ | |
333 | 'PSOs in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
334 | get_netpso_parser.add_argument('--psoname', dest='queried_psoname', | |
335 | default='*', help='pso name to query for (wildcards accepted)') | |
336 | get_netpso_parser.add_argument('--displayname', dest='queried_displayname', | |
337 | help='Display name to query for (wildcards accepted)') | |
338 | get_netpso_parser.add_argument('-d', '--domain', dest='queried_domain', | |
339 | help='Domain to query') | |
340 | get_netpso_parser.add_argument('-a', '--ads-path', | |
341 | help='Additional ADS path') | |
342 | get_netpso_parser.set_defaults(func=get_netpso) | |
343 | ||
256 | 344 | # Parser for the get-domainpolicy command |
257 | 345 | get_domainpolicy_parser = subparsers.add_parser('get-domainpolicy', help='Returns the default domain or DC '\ |
258 | 'policy for the queried domain or DC', parents=[ad_parser]) | |
346 | 'policy for the queried domain or DC', parents=[ad_parser, logging_parser, json_output_parser]) | |
259 | 347 | get_domainpolicy_parser.add_argument('--source', dest='source', default='domain', |
260 | 348 | choices=['domain', 'dc'], help='Extract domain or DC policy (default: %(default)s)') |
261 | 349 | get_domainpolicy_parser.add_argument('-d', '--domain', dest='queried_domain', |
266 | 354 | |
267 | 355 | # Parser for the get-gpttmpl command |
268 | 356 | get_gpttmpl_parser = subparsers.add_parser('get-gpttmpl', help='Helper to parse a GptTmpl.inf policy '\ |
269 | 'file path into a custom object', parents=[ad_parser]) | |
357 | 'file path into a custom object', parents=[ad_parser, logging_parser, json_output_parser]) | |
270 | 358 | get_gpttmpl_parser.add_argument('--gpt-tmpl-path', type=str, required=True, |
271 | 359 | dest='gpttmpl_path', help='The GptTmpl.inf file path name to parse') |
272 | 360 | get_gpttmpl_parser.set_defaults(func=get_gpttmpl) |
273 | 361 | |
274 | 362 | # Parser for the get-netgpogroup command |
275 | 363 | get_netgpogroup_parser = subparsers.add_parser('get-netgpogroup', help='Parses all GPOs in the domain '\ |
276 | 'that set "Restricted Group" or "Groups.xml"', parents=[ad_parser]) | |
364 | 'that set "Restricted Group" or "Groups.xml"', parents=[ad_parser, logging_parser, json_output_parser]) | |
277 | 365 | get_netgpogroup_parser.add_argument('--gponame', dest='queried_gponame', |
278 | 366 | default='*', help='GPO name to query for (wildcards accepted)') |
279 | 367 | get_netgpogroup_parser.add_argument('--displayname', dest='queried_displayname', |
288 | 376 | |
289 | 377 | # Parser for the find-gpocomputeradmin command |
290 | 378 | find_gpocomputeradmin_parser = subparsers.add_parser('find-gpocomputeradmin', help='Takes a computer (or OU) and determine '\ |
291 | 'who has administrative access to it via GPO', parents=[ad_parser]) | |
379 | 'who has administrative access to it via GPO', parents=[ad_parser, logging_parser, json_output_parser]) | |
292 | 380 | find_gpocomputeradmin_parser.add_argument('--computername', dest='queried_computername', |
293 | 381 | default=str(), help='The computer to determine who has administrative access to it') |
294 | 382 | find_gpocomputeradmin_parser.add_argument('--ouname', dest='queried_ouname', |
302 | 390 | |
303 | 391 | # Parser for the find-gpolocation command |
304 | 392 | find_gpolocation_parser = subparsers.add_parser('find-gpolocation', help='Takes a username or a group name and determine '\ |
305 | 'the computers it has administrative access to via GPO', parents=[ad_parser]) | |
393 | 'the computers it has administrative access to via GPO', parents=[ad_parser, logging_parser, json_output_parser]) | |
306 | 394 | find_gpolocation_parser.add_argument('--username', dest='queried_username', |
307 | 395 | default=str(), help='The username to query for access (no wildcard)') |
308 | 396 | find_gpolocation_parser.add_argument('--groupname', dest='queried_groupname', |
315 | 403 | find_gpolocation_parser.set_defaults(func=find_gpolocation) |
316 | 404 | |
317 | 405 | # Parser for the get-netgroup command |
318 | get_netgroupmember_parser = subparsers.add_parser('get-netgroupmember', help='Return a list of members of a domain group', parents=[ad_parser]) | |
406 | get_netgroupmember_parser = subparsers.add_parser('get-netgroupmember', help='Return a list of members of a domain group', | |
407 | parents=[ad_parser, logging_parser, json_output_parser]) | |
319 | 408 | get_netgroupmember_parser.add_argument('--groupname', dest='queried_groupname', |
320 | 409 | help='Group to query, defaults to the \'Domain Admins\' group (wildcards accepted)') |
321 | 410 | get_netgroupmember_parser.add_argument('--sid', dest='queried_sid', |
335 | 424 | |
336 | 425 | # Parser for the get-netsession command |
337 | 426 | get_netsession_parser = subparsers.add_parser('get-netsession', help='Queries a host to return a '\ |
338 | 'list of active sessions on the host (you can use local credentials instead of domain credentials)', parents=[target_parser]) | |
427 | 'list of active sessions on the host (you can use local credentials instead of domain credentials)', | |
428 | parents=[target_parser, logging_parser, json_output_parser]) | |
339 | 429 | get_netsession_parser.set_defaults(func=get_netsession) |
340 | 430 | |
341 | 431 | #Parser for the get-localdisks command |
342 | 432 | get_localdisks_parser = subparsers.add_parser('get-localdisks', help='Queries a host to return a '\ |
343 | 'list of active disks on the host (you can use local credentials instead of domain credentials)', parents=[target_parser]) | |
433 | 'list of active disks on the host (you can use local credentials instead of domain credentials)', | |
434 | parents=[target_parser, logging_parser, json_output_parser]) | |
344 | 435 | get_localdisks_parser.set_defaults(func=get_localdisks) |
345 | 436 | |
346 | 437 | #Parser for the get-netdomain command |
347 | 438 | get_netdomain_parser = subparsers.add_parser('get-netdomain', help='Queries a host for available domains', |
348 | parents=[ad_parser]) | |
439 | parents=[ad_parser, logging_parser, json_output_parser]) | |
349 | 440 | get_netdomain_parser.set_defaults(func=get_netdomain) |
350 | 441 | |
351 | 442 | # Parser for the get-netshare command |
352 | 443 | get_netshare_parser = subparsers.add_parser('get-netshare', help='Queries a host to return a '\ |
353 | 'list of available shares on the host (you can use local credentials instead of domain credentials)', parents=[target_parser]) | |
444 | 'list of available shares on the host (you can use local credentials instead of domain credentials)', | |
445 | parents=[target_parser, logging_parser, json_output_parser]) | |
354 | 446 | get_netshare_parser.set_defaults(func=get_netshare) |
355 | 447 | |
356 | 448 | # Parser for the get-netloggedon command |
357 | 449 | get_netloggedon_parser = subparsers.add_parser('get-netloggedon', help='This function will '\ |
358 | 450 | 'execute the NetWkstaUserEnum RPC call to query a given host for actively logged on '\ |
359 | 'users', parents=[target_parser]) | |
451 | 'users', parents=[target_parser, logging_parser, json_output_parser]) | |
360 | 452 | get_netloggedon_parser.set_defaults(func=get_netloggedon) |
361 | 453 | |
362 | 454 | # Parser for the get-netlocalgroup command |
363 | 455 | get_netlocalgroup_parser = subparsers.add_parser('get-netlocalgroup', help='Gets a list of '\ |
364 | 456 | 'members of a local group on a machine, or returns every local group. You can use local '\ |
365 | 457 | 'credentials instead of domain credentials, however, domain credentials are needed to '\ |
366 | 'resolve domain SIDs.', parents=[target_parser]) | |
458 | 'resolve domain SIDs.', parents=[target_parser, logging_parser, json_output_parser]) | |
367 | 459 | get_netlocalgroup_parser.add_argument('--groupname', dest='queried_groupname', |
368 | 460 | help='Group to list the members of (defaults to the local \'Administrators\' group') |
369 | 461 | get_netlocalgroup_parser.add_argument('--list-groups', action='store_true', |
370 | 462 | help='If set, returns a list of the local groups on the targets') |
371 | 463 | get_netlocalgroup_parser.add_argument('-t', '--dc-ip', dest='domain_controller', |
372 | 464 | default=str(), help='IP address of the Domain Controller (used to resolve domain SIDs)') |
465 | get_netlocalgroup_parser.add_argument('--tls', action='store_true', dest='do_tls', | |
466 | help='Force TLS connection to the Domain Controller') | |
373 | 467 | get_netlocalgroup_parser.add_argument('-r', '--recurse', action='store_true', |
374 | 468 | help='If the group member is a domain group, try to resolve its members as well') |
375 | 469 | get_netlocalgroup_parser.set_defaults(func=get_netlocalgroup) |
376 | 470 | |
377 | 471 | # Parser for the invoke-checklocaladminaccess command |
378 | 472 | invoke_checklocaladminaccess_parser = subparsers.add_parser('invoke-checklocaladminaccess', help='Checks '\ |
379 | 'if the given user has local admin access on the given host', parents=[target_parser]) | |
473 | 'if the given user has local admin access on the given host', | |
474 | parents=[target_parser, logging_parser, json_output_parser]) | |
380 | 475 | invoke_checklocaladminaccess_parser.set_defaults(func=invoke_checklocaladminaccess) |
381 | 476 | |
382 | 477 | # Parser for the get-netprocess command |
383 | 478 | get_netprocess_parser = subparsers.add_parser('get-netprocess', help='This function will '\ |
384 | 479 | 'execute the \'Select * from Win32_Process\' WMI query to a given host for a list of '\ |
385 | 'executed process', parents=[target_parser]) | |
480 | 'executed process', parents=[target_parser, logging_parser, json_output_parser]) | |
386 | 481 | get_netprocess_parser.set_defaults(func=get_netprocess) |
387 | 482 | |
388 | 483 | # Parser for the get-userevent command |
389 | 484 | get_userevent_parser = subparsers.add_parser('get-userevent', help='This function will '\ |
390 | 'execute the \'Select * from Win32_Process\' WMI query to a given host for a list of '\ | |
391 | 'executed process', parents=[target_parser]) | |
485 | 'execute the \'SELECT * from Win32_NTLogEvent\' WMI query to a given host for a list of '\ | |
486 | 'executed process', parents=[target_parser, logging_parser, json_output_parser]) | |
392 | 487 | get_userevent_parser.add_argument('--event-type', nargs='+', choices=['logon', 'tgt'], |
393 | 488 | default=['logon', 'tgt'], help='The type of event to search for: logon, tgt, or all (default: all)') |
394 | 489 | get_userevent_parser.add_argument('--date-start', type=int, |
397 | 492 | |
398 | 493 | # Parser for the invoke-userhunter command |
399 | 494 | invoke_userhunter_parser = subparsers.add_parser('invoke-userhunter', help='Finds '\ |
400 | 'which machines domain users are logged into', parents=[ad_parser, hunter_parser]) | |
495 | 'which machines domain users are logged into', parents=[ad_parser, hunter_parser, logging_parser]) | |
401 | 496 | invoke_userhunter_parser.add_argument('--unconstrained', action='store_true', |
402 | 497 | help='Query only computers with unconstrained delegation') |
403 | 498 | invoke_userhunter_parser.add_argument('--admin-count', action='store_true', |
423 | 518 | |
424 | 519 | # Parser for the invoke-processhunter command |
425 | 520 | invoke_processhunter_parser = subparsers.add_parser('invoke-processhunter', help='Searches machines '\ |
426 | 'for processes with specific name, or ran by specific users', parents=[ad_parser, hunter_parser]) | |
521 | 'for processes with specific name, or ran by specific users', | |
522 | parents=[ad_parser, hunter_parser, logging_parser]) | |
427 | 523 | invoke_processhunter_parser.add_argument('--processname', dest='queried_processname', |
428 | 524 | nargs='+', default=list(), help='Names of the process to hunt') |
429 | 525 | invoke_processhunter_parser.add_argument('--stop-on-success', action='store_true', |
434 | 530 | |
435 | 531 | # Parser for the invoke-eventhunter command |
436 | 532 | invoke_eventhunter_parser = subparsers.add_parser('invoke-eventhunter', help='Searches machines '\ |
437 | 'for events with specific name, or ran by specific users', parents=[ad_parser, hunter_parser]) | |
533 | 'for events with specific name, or ran by specific users', | |
534 | parents=[ad_parser, hunter_parser, logging_parser]) | |
438 | 535 | invoke_eventhunter_parser.add_argument('--search-days', dest='search_days', |
439 | 536 | type=int, default=3, help='Number of days back to search logs for (default: %(default)s)') |
440 | 537 | invoke_eventhunter_parser.set_defaults(func=invoke_eventhunter) |
441 | 538 | |
442 | 539 | args = parser.parse_args() |
540 | ||
541 | # setup the main logger | |
542 | logger = logging.getLogger('pywerview_main_logger') | |
543 | logging.addLevelName(5, 'ULTRA') | |
544 | logger.setLevel(args.logging_level) | |
545 | console_handler = logging.StreamHandler() | |
546 | console_handler.setLevel(args.logging_level) | |
547 | formatter = logging.Formatter('[%(levelname)s] %(name)s - %(funcName)s : %(message)s') | |
548 | console_handler.setFormatter(formatter) | |
549 | logger.addHandler(console_handler) | |
550 | ||
443 | 551 | if args.hashes: |
444 | 552 | try: |
445 | 553 | args.lmhash, args.nthash = args.hashes.split(':') |
450 | 558 | else: |
451 | 559 | args.lmhash = args.nthash = str() |
452 | 560 | |
453 | if args.password is None and not args.hashes: | |
561 | if args.password is None and args.hashes is None and not args.do_kerberos: | |
454 | 562 | from getpass import getpass |
455 | 563 | args.password = getpass('Password:') |
456 | 564 | |
457 | 565 | parsed_args = dict() |
458 | 566 | for k, v in vars(args).items(): |
459 | if k not in ('func', 'hashes', 'submodule'): | |
567 | if k not in ('func', 'hashes', 'submodule', 'logging_level', 'json_output'): | |
460 | 568 | parsed_args[k] = v |
461 | 569 | |
462 | #try: | |
570 | starting_time = datetime.datetime.now() | |
463 | 571 | results = args.func(**parsed_args) |
464 | #except Exception, e: | |
465 | #print >>sys.stderr, repr(e) | |
466 | #sys.exit(-1) | |
572 | ending_time = datetime.datetime.now() | |
573 | ||
574 | try: | |
575 | json_output = args.json_output | |
576 | except AttributeError: | |
577 | json_output = False | |
467 | 578 | |
468 | 579 | if results is not None: |
469 | try: | |
470 | for x in results: | |
471 | print(x) | |
472 | # for example, invoke_checklocaladminaccess returns a bool | |
473 | except TypeError: | |
474 | print(results) | |
475 | ||
580 | if json_output: | |
581 | results_json = {'cmd' : {'submodule' : args.submodule, 'args' : parsed_args, | |
582 | 'starting_time': starting_time, 'ending_time': ending_time}} | |
583 | try: | |
584 | objects_json = [x.to_json() for x in results] | |
585 | except TypeError: | |
586 | try: | |
587 | objects_json = [results.to_json()] | |
588 | except AttributeError: | |
589 | objects_json = results | |
590 | results_json['results'] = objects_json | |
591 | print(json.dumps(results_json, default=str)) | |
592 | else: | |
593 | try: | |
594 | print('\n\n'.join(str(x) for x in results)) | |
595 | except TypeError: | |
596 | print(results) | |
597 |
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 © 2022 | |
16 | ||
17 | import logging | |
18 | import binascii | |
19 | ||
20 | try: | |
21 | from Cryptodome.Hash import MD4 | |
22 | except ImportError: | |
23 | from Crypto.Hash import MD4 | |
24 | ||
25 | from impacket.examples.ntlmrelayx.attacks.ldapattack import MSDS_MANAGEDPASSWORD_BLOB | |
26 | from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR | |
27 | ||
28 | __uac_flags = {0x0000001: 'SCRIPT', | |
29 | 0x0000002: 'ACCOUNTDISABLE', | |
30 | 0x0000008: 'HOMEDIR_REQUIRED', | |
31 | 0x0000010: 'LOCKOUT', | |
32 | 0x0000020: 'PASSWD_NOTREQD', | |
33 | 0x0000040: 'PASSWD_CANT_CHANGE', | |
34 | 0x0000080: 'ENCRYPTED_TEXT_PWD_ALLOWED', | |
35 | 0x0000100: 'TEMP_DUPLICATE_ACCOUNT', | |
36 | 0x0000200: 'NORMAL_ACCOUNT', | |
37 | 0x0000800: 'INTERDOMAIN_TRUST_ACCOUNT', | |
38 | 0x0001000: 'WORKSTATION_TRUST_ACCOUNT', | |
39 | 0x0002000: 'SERVER_TRUST_ACCOUNT', | |
40 | 0x0010000: 'DONT_EXPIRE_PASSWORD', | |
41 | 0x0020000: 'MNS_LOGON_ACCOUNT', | |
42 | 0x0040000: 'SMARTCARD_REQUIRED', | |
43 | 0x0080000: 'TRUSTED_FOR_DELEGATION', | |
44 | 0x0100000: 'NOT_DELEGATED', | |
45 | 0x0200000: 'USE_DES_KEY_ONLY', | |
46 | 0x0400000: 'DONT_REQ_PREAUTH', | |
47 | 0x0800000: 'PASSWORD_EXPIRED', | |
48 | 0x1000000: 'TRUSTED_TO_AUTH_FOR_DELEGATION', | |
49 | 0x4000000: 'PARTIAL_SECRETS_ACCOUNT'} | |
50 | ||
51 | __ace_flags = {0x1: 'object_inherit', 0x2: 'container_inherit', | |
52 | 0x4: 'non_propagate_inherit', 0x8: 'inherit_only', | |
53 | 0x10: 'inherited_ace', 0x20: 'audit_successful_accesses', | |
54 | 0x40: 'audit_failed_access'} | |
55 | ||
56 | __object_ace_flags = {0x1: 'object_ace_type_present', 0x2: 'inherited_object_ace_type_present'} | |
57 | ||
58 | # Resources: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/990fb975-ab31-4bc1-8b75-5da132cd4584 | |
59 | __access_mask = {0x1: 'create_child', 0x2: 'delete_child', | |
60 | 0x4: 'list_children', 0x08: 'self', | |
61 | 0x10: 'read_property', 0x20: 'write_property', | |
62 | 0x40: 'delete_tree', 0x80: 'list_object', | |
63 | 0x100: 'extended_right', 0x10000: 'delete', | |
64 | 0x20000: 'read_control', 0x40000: 'write_dacl', | |
65 | 0x80000: 'write_owner'} | |
66 | ||
67 | __access_mask_generic = {0xf01ff: 'generic_all', 0x20094: 'generic_read', | |
68 | 0x20028: 'generic_write', 0x20004: 'generic_execute'} | |
69 | ||
70 | __trust_attrib = {0x1: 'non_transitive', 0x2: 'uplevel_only', | |
71 | 0x4: 'filter_sids', 0x8: 'forest_transitive', | |
72 | 0x10: 'cross_organization', 0x20: 'within_forest', | |
73 | 0x40: 'treat_as_external', | |
74 | 0x80: 'trust_uses_rc4_encryption', | |
75 | 0x100: 'trust_uses_aes_keys', | |
76 | 0X200: 'cross_organization_no_tgt_delegation', | |
77 | 0x400: 'pim_trust'} | |
78 | ||
79 | __trust_direction = {0: 'disabled', 1: 'inbound', | |
80 | 2: 'outbound', 3: 'bidirectional'} | |
81 | ||
82 | __trust_type = {1: 'windows_non_active_directory', | |
83 | 2: 'windows_active_directory', 3: 'mit'} | |
84 | ||
85 | def __format_flag(raw_value, flag_dict): | |
86 | try: | |
87 | int_value = int(raw_value) | |
88 | except ValueError: | |
89 | self._logger.warning('Unable to convert raw flag value to int') | |
90 | return raw_value | |
91 | ||
92 | parsed_flags = list() | |
93 | for flag, flag_label in flag_dict.items(): | |
94 | if (int_value & flag) == flag: | |
95 | parsed_flags.append(flag_label) | |
96 | return parsed_flags | |
97 | ||
98 | def __format_dict_lookup(raw_value, dictionary): | |
99 | try: | |
100 | return dictionary[int(raw_value)] | |
101 | except (ValueError, KeyError): | |
102 | self._logger.warning('Unable to convert raw value to int') | |
103 | return raw_value | |
104 | ||
105 | def format_useraccountcontrol(raw_value): | |
106 | return __format_flag(raw_value, __uac_flags) | |
107 | ||
108 | def format_ace_access_mask(raw_value): | |
109 | try: | |
110 | int_value = int(raw_value) | |
111 | except ValueError: | |
112 | self._logger.warning('Unable to convert raw ace acess mask value to int') | |
113 | return raw_value | |
114 | ||
115 | activedirectoryrights = list() | |
116 | for flag, flag_label in __access_mask_generic.items(): | |
117 | if (int_value & flag) == flag: | |
118 | activedirectoryrights.append(flag_label) | |
119 | int_value ^= flag | |
120 | activedirectoryrights += __format_flag(raw_value, __access_mask) | |
121 | ||
122 | return activedirectoryrights | |
123 | ||
124 | ||
125 | def format_managedpassword(raw_value): | |
126 | blob = MSDS_MANAGEDPASSWORD_BLOB() | |
127 | blob.fromString(raw_value) | |
128 | return binascii.hexlify(MD4.new(blob['CurrentPassword'][:-2]).digest()).decode('utf8') | |
129 | ||
130 | def format_groupmsamembership(raw_value): | |
131 | sid = list() | |
132 | sr = SR_SECURITY_DESCRIPTOR(data=raw_value) | |
133 | for dacl in sr['Dacl']['Data']: | |
134 | sid.append(dacl['Ace']['Sid'].formatCanonical()) | |
135 | return sid | |
136 | ||
137 | def format_ace_flags(raw_value): | |
138 | return __format_flag(raw_value, __ace_flags) | |
139 | ||
140 | def format_object_ace_flags(raw_value): | |
141 | return __format_flag(raw_value, __object_ace_flags) | |
142 | ||
143 | def format_trustdirection(raw_value): | |
144 | return __format_dict_lookup(raw_value, __trust_direction) | |
145 | ||
146 | def format_trusttype(raw_value): | |
147 | return __format_dict_lookup(raw_value, __trust_type) | |
148 | ||
149 | def format_trustattributes(raw_value): | |
150 | return __format_flag(raw_value, __trust_attrib) | |
151 |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
16 | ||
17 | import codecs | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | ||
18 | 17 | from bs4 import BeautifulSoup |
19 | 18 | from io import BytesIO |
20 | 19 | |
40 | 39 | gpo_search_filter = '(&{})'.format(gpo_search_filter) |
41 | 40 | |
42 | 41 | return self._ldap_search(gpo_search_filter, GPO) |
42 | ||
43 | @LDAPRequester._ldap_connection_init | |
44 | def get_netpso(self, queried_psoname='*', queried_displayname=str(), | |
45 | queried_domain=str(), ads_path=str()): | |
46 | ||
47 | pso_search_filter = '(objectClass=msDS-PasswordSettings)' | |
48 | ||
49 | if queried_displayname: | |
50 | pso_search_filter += '(displayname={})'.format(queried_displayname) | |
51 | else: | |
52 | pso_search_filter += '(name={})'.format(queried_psoname) | |
53 | ||
54 | pso_search_filter = '(&{})'.format(pso_search_filter) | |
55 | ||
56 | return self._ldap_search(pso_search_filter, PSO) | |
43 | 57 | |
44 | 58 | def get_gpttmpl(self, gpttmpl_path): |
45 | 59 | content_io = BytesIO() |
50 | 64 | file_name = '\\'.join(gpttmpl_path_split[4:]) |
51 | 65 | |
52 | 66 | smb_connection = SMBConnection(remoteName=target, remoteHost=target) |
53 | # TODO: kerberos login | |
54 | smb_connection.login(self._user, self._password, self._domain, | |
55 | self._lmhash, self._nthash) | |
56 | ||
67 | if self._do_kerberos: | |
68 | smb_connection.kerberosLogin(self._user, self._password, self._domain, | |
69 | self._lmhash, self._nthash) | |
70 | else: | |
71 | smb_connection.login(self._user, self._password, self._domain, | |
72 | self._lmhash, self._nthash) | |
73 | ||
74 | self._logger.debug('Get File: Share = {0}, file_name ={1}'.format(share, file_name)) | |
57 | 75 | smb_connection.connectTree(share) |
58 | 76 | smb_connection.getFile(share, file_name, content_io.write) |
59 | 77 | try: |
60 | content = codecs.decode(content_io.getvalue(), 'utf-16le')[1:].replace('\r', '') | |
78 | content = content_io.getvalue().decode('utf-16le')[1:].replace('\r', '') | |
61 | 79 | except UnicodeDecodeError: |
62 | content = str(content_io.getvalue()).replace('\r', '') | |
80 | self._logger.warning('Unicode error: trying utf-8') | |
81 | content = content_io.getvalue().decode('utf-8').replace('\r', '') | |
63 | 82 | |
64 | 83 | gpttmpl_final = GptTmpl(list()) |
65 | 84 | for l in content.split('\n'): |
66 | 85 | if l.startswith('['): |
67 | 86 | section_name = l.strip('[]').replace(' ', '').lower() |
68 | setattr(gpttmpl_final, section_name, Policy(list())) | |
87 | gpttmpl_final._attributes_dict[section_name] = Policy(list()) | |
69 | 88 | elif '=' in l: |
70 | 89 | property_name, property_values = [x.strip() for x in l.split('=')] |
71 | 90 | if ',' in property_values: |
72 | 91 | 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) | |
92 | gpttmpl_final._attributes_dict[section_name]._attributes_dict[property_name] = property_values | |
78 | 93 | |
79 | 94 | return gpttmpl_final |
80 | 95 | |
99 | 114 | try: |
100 | 115 | privilege_rights_policy = gpttmpl.privilegerights |
101 | 116 | except AttributeError: |
117 | self._logger.critical('Could not parse privilegerights from the DC policy, SIDs will not be resolved') | |
102 | 118 | return gpttmpl |
103 | 119 | |
104 | 120 | members = inspect.getmembers(privilege_rights_policy, lambda x: not(inspect.isroutine(x))) |
105 | 121 | with NetRequester(self._domain_controller, self._domain, self._user, |
106 | 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]] | |
122 | self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester: | |
123 | for attr in privilege_rights_policy._attributes_dict: | |
124 | attribute = privilege_rights_policy._attributes_dict[attr] | |
125 | if not isinstance(attribute, list): | |
126 | sids = [attribute] | |
112 | 127 | else: |
113 | sids = member[1] | |
128 | sids = attribute | |
114 | 129 | resolved_sids = list() |
115 | 130 | for sid in sids: |
116 | 131 | if not sid: |
117 | 132 | continue |
133 | sid = sid.replace('*', '') | |
118 | 134 | try: |
119 | resolved_sid = net_requester.get_adobject(queried_sid=sid, queried_domain=queried_domain)[0] | |
135 | resolved_sid = net_requester.get_adobject(queried_sid=sid, queried_domain=self._queried_domain)[0] | |
120 | 136 | except IndexError: |
137 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid)) | |
121 | 138 | resolved_sid = sid |
122 | 139 | else: |
123 | 140 | resolved_sid = resolved_sid.distinguishedname.split(',')[:2] |
124 | resolved_sid = '{}\\{}'.format(resolved_sid[1], resolved_sid[0]) | |
141 | resolved_sid = resolved_sid[1] + '\\' + resolved_sid[0] | |
125 | 142 | resolved_sid = resolved_sid.replace('CN=', '') |
143 | finally: | |
126 | 144 | resolved_sids.append(resolved_sid) |
127 | 145 | if len(resolved_sids) == 1: |
128 | 146 | resolved_sids = resolved_sids[0] |
129 | setattr(privilege_rights_policy, member[0], resolved_sids) | |
147 | privilege_rights_policy._attributes_dict[attr] = resolved_sids | |
130 | 148 | |
131 | 149 | gpttmpl.privilegerights = privilege_rights_policy |
132 | 150 | |
144 | 162 | file_name = '\\'.join(groupsxml_path_split[4:]) |
145 | 163 | |
146 | 164 | smb_connection = SMBConnection(remoteName=target, remoteHost=target) |
147 | # TODO: kerberos login | |
148 | smb_connection.login(self._user, self._password, self._domain, | |
149 | self._lmhash, self._nthash) | |
150 | ||
165 | if self._do_kerberos: | |
166 | smb_connection.kerberosLogin(self._user, self._password, self._domain, | |
167 | self._lmhash, self._nthash) | |
168 | else: | |
169 | smb_connection.login(self._user, self._password, self._domain, | |
170 | self._lmhash, self._nthash) | |
171 | ||
172 | self._logger.debug('Get File: Share = {0}, file_name ={1}'.format(share, file_name)) | |
151 | 173 | smb_connection.connectTree(share) |
152 | 174 | try: |
153 | 175 | smb_connection.getFile(share, file_name, content_io.write) |
154 | 176 | except SessionError: |
177 | self._logger.warning('Error while getting the file {}, skipping...'.format(file_name)) | |
155 | 178 | return list() |
156 | 179 | |
157 | 180 | content = content_io.getvalue().replace(b'\r', b'') |
158 | groupsxml_soup = BeautifulSoup(content, 'xml') | |
159 | ||
181 | groupsxml_soup = BeautifulSoup(content.decode('utf-8'), 'xml') | |
160 | 182 | for group in groupsxml_soup.find_all('Group'): |
161 | 183 | members = list() |
162 | 184 | memberof = list() |
185 | ||
186 | raw_xml_member = group.Properties.find_all('Member') | |
187 | if not raw_xml_member: | |
188 | continue | |
189 | ||
163 | 190 | local_sid = group.Properties.get('groupSid', str()) |
191 | ||
164 | 192 | if not local_sid: |
165 | 193 | if 'administrators' in group.Properties['groupName'].lower(): |
166 | 194 | local_sid = 'S-1-5-32-544' |
170 | 198 | local_sid = group.Properties['groupName'] |
171 | 199 | memberof.append(local_sid) |
172 | 200 | |
173 | for member in group.Properties.find_all('Member'): | |
201 | for member in raw_xml_member: | |
174 | 202 | if not member['action'].lower() == 'add': |
175 | 203 | continue |
176 | 204 | if member['sid']: |
184 | 212 | # have the barest support for filters, so ¯\_(ツ)_/¯ |
185 | 213 | |
186 | 214 | 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) | |
215 | gpo_group._attributes_dict['gpodisplayname'] = gpo_display_name | |
216 | gpo_group._attributes_dict['gponame'] = gpo_name | |
217 | gpo_group._attributes_dict['gpopath'] = groupsxml_path | |
218 | gpo_group._attributes_dict['members'] = members | |
219 | gpo_group._attributes_dict['memberof'] = memberof | |
192 | 220 | |
193 | 221 | gpo_groups.append(gpo_group) |
194 | 222 | |
195 | 223 | return gpo_groups |
196 | 224 | |
197 | 225 | def _get_groupsgpttmpl(self, gpttmpl_path, gpo_display_name): |
198 | import inspect | |
199 | 226 | gpo_groups = list() |
200 | 227 | |
201 | 228 | gpt_tmpl = self.get_gpttmpl(gpttmpl_path) |
206 | 233 | except AttributeError: |
207 | 234 | return list() |
208 | 235 | |
209 | membership = inspect.getmembers(group_membership, lambda x: not(inspect.isroutine(x))) | |
210 | for m in membership: | |
211 | if not m[1]: | |
236 | membership = group_membership._attributes_dict | |
237 | ||
238 | for ma,mv in membership.items(): | |
239 | if not mv: | |
212 | 240 | continue |
213 | 241 | members = list() |
214 | 242 | 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]] | |
243 | if ma.lower().endswith('__memberof'): | |
244 | members.append(ma.upper().lstrip('*').replace('__MEMBEROF', '')) | |
245 | if not isinstance(mv, list): | |
246 | memberof_list = [mv] | |
219 | 247 | else: |
220 | memberof_list = m[1] | |
248 | memberof_list = mv | |
221 | 249 | 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]] | |
250 | elif ma.lower().endswith('__members'): | |
251 | memberof.append(ma.upper().lstrip('*').replace('__MEMBERS', '')) | |
252 | if not isinstance(mv, list): | |
253 | members_list = [mv] | |
226 | 254 | else: |
227 | members_list = m[1] | |
255 | members_list = mv | |
228 | 256 | members += [x.lstrip('*') for x in members_list] |
229 | 257 | |
230 | 258 | if members and memberof: |
231 | 259 | 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) | |
260 | gpo_group.add_attributes({'gpodisplayname' : gpo_display_name}) | |
261 | gpo_group.add_attributes({'gponame' : gpo_name}) | |
262 | gpo_group.add_attributes({'gpopath' : gpttmpl_path}) | |
263 | gpo_group.add_attributes({'members' : members}) | |
264 | gpo_group.add_attributes({'memberof' : memberof}) | |
237 | 265 | |
238 | 266 | gpo_groups.append(gpo_group) |
239 | 267 | |
258 | 286 | results += self._get_groupsgpttmpl(gpttmpl_path, gpo_display_name) |
259 | 287 | except SessionError: |
260 | 288 | # If the GptTmpl file doesn't exist, we skip this |
289 | self._logger.warning('Error while getting the file {}, skipping...'.format(gpttmpl_path,)) | |
261 | 290 | pass |
262 | 291 | |
263 | 292 | if resolve_sids: |
268 | 297 | resolved_members = list() |
269 | 298 | resolved_memberof = list() |
270 | 299 | with NetRequester(self._domain_controller, self._domain, self._user, |
271 | self._password, self._lmhash, self._nthash) as net_requester: | |
300 | self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester: | |
272 | 301 | for member in members: |
273 | 302 | 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=', '') | |
303 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0] | |
304 | resolved_member = resolved_member.distinguishedname | |
279 | 305 | except IndexError: |
306 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member)) | |
280 | 307 | resolved_member = member |
281 | 308 | finally: |
282 | 309 | resolved_members.append(resolved_member) |
283 | gpo_group.members = resolved_members | |
310 | gpo_group._attributes_dict['members'] = resolved_members | |
284 | 311 | |
285 | 312 | for member in memberof: |
286 | 313 | 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=', '') | |
314 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0] | |
315 | resolved_member = resolved_member.distinguishedname | |
291 | 316 | except IndexError: |
317 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member)) | |
292 | 318 | resolved_member = member |
293 | 319 | finally: |
294 | 320 | resolved_memberof.append(resolved_member) |
295 | gpo_group.memberof = memberof = resolved_memberof | |
296 | ||
321 | gpo_group._attributes_dict['memberof'] = memberof = resolved_memberof | |
297 | 322 | return results |
298 | 323 | |
299 | 324 | def find_gpocomputeradmin(self, queried_computername=str(), |
305 | 330 | raise ValueError('You must specify either a computer name or an OU name') |
306 | 331 | |
307 | 332 | net_requester = NetRequester(self._domain_controller, self._domain, self._user, |
308 | self._password, self._lmhash, self._nthash) | |
333 | self._password, self._lmhash, self._nthash, self._do_kerberos, | |
334 | self._do_tls) | |
309 | 335 | if queried_computername: |
310 | 336 | computers = net_requester.get_netcomputer(queried_computername=queried_computername, |
311 | 337 | queried_domain=queried_domain, |
326 | 352 | for target_ou in target_ous: |
327 | 353 | ous = net_requester.get_netou(ads_path=target_ou, queried_domain=queried_domain, |
328 | 354 | full_data=True) |
329 | ||
330 | 355 | for ou in ous: |
331 | for gplink in ou.gplink.strip('[]').split(']['): | |
356 | try: | |
357 | gplinks = ou.gplink.strip('[]').split('][') | |
358 | except AttributeError: | |
359 | continue | |
360 | for gplink in gplinks: | |
332 | 361 | gplink = gplink.split(';')[0] |
333 | 362 | gpo_groups = self.get_netgpogroup(queried_domain=queried_domain, |
334 | 363 | ads_path=gplink) |
335 | 364 | for gpo_group in gpo_groups: |
336 | 365 | for member in gpo_group.members: |
337 | 366 | obj = net_requester.get_adobject(queried_sid=member, |
338 | queried_domain=queried_domain)[0] | |
367 | queried_domain=self._queried_domain)[0] | |
339 | 368 | 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')) | |
369 | gpo_computer_admin.add_attributes({'computername' : queried_computername}) | |
370 | gpo_computer_admin.add_attributes({'ou' : target_ou}) | |
371 | gpo_computer_admin.add_attributes({'gpodisplayname' : gpo_group.gpodisplayname}) | |
372 | gpo_computer_admin.add_attributes({'gpopath' : gpo_group.gpopath}) | |
373 | gpo_computer_admin.add_attributes({'objectname' : obj.name}) | |
374 | gpo_computer_admin.add_attributes({'objectdn' : obj.distinguishedname}) | |
375 | gpo_computer_admin.add_attributes({'objectsid' : obj.objectsid}) | |
376 | gpo_computer_admin.add_attributes({'isgroup' : (obj.samaccounttype != 805306368)}) | |
348 | 377 | |
349 | 378 | results.append(gpo_computer_admin) |
350 | 379 | |
352 | 381 | groups_to_resolve = [gpo_computer_admin.objectsid] |
353 | 382 | while groups_to_resolve: |
354 | 383 | group_to_resolve = groups_to_resolve.pop(0) |
384 | ||
355 | 385 | group_members = net_requester.get_netgroupmember(queried_sid=group_to_resolve, |
356 | queried_domain=queried_domain, | |
386 | queried_domain=self._queried_domain, | |
357 | 387 | full_data=True) |
358 | 388 | for group_member in group_members: |
359 | 389 | 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')) | |
390 | gpo_computer_admin.add_attributes({'computername' : queried_computername}) | |
391 | gpo_computer_admin.add_attributes({'ou' : target_ou}) | |
392 | gpo_computer_admin.add_attributes({'gpodisplayname' : gpo_group.gpodisplayname}) | |
393 | gpo_computer_admin.add_attributes({'gpopath' : gpo_group.gpopath}) | |
394 | gpo_computer_admin.add_attributes({'objectname' : group_member.samaccountname}) | |
395 | gpo_computer_admin.add_attributes({'objectdn' : group_member.distinguishedname}) | |
396 | gpo_computer_admin.add_attributes({'objectsid' : group_member.objectsid}) | |
397 | gpo_computer_admin.add_attributes({'isgroup' : (group_member.samaccounttype != 805306368)}) | |
368 | 398 | |
369 | 399 | results.append(gpo_computer_admin) |
370 | 400 | |
377 | 407 | queried_localgroup=str(), queried_domain=str()): |
378 | 408 | results = list() |
379 | 409 | net_requester = NetRequester(self._domain_controller, self._domain, self._user, |
380 | self._password, self._lmhash, self._nthash) | |
410 | self._password, self._lmhash, self._nthash, self._do_kerberos, | |
411 | self._do_tls) | |
381 | 412 | if queried_username: |
382 | 413 | try: |
383 | 414 | user = net_requester.get_netuser(queried_username=queried_username, |
384 | queried_domain=queried_domain)[0] | |
415 | queried_domain=self._queried_domain)[0] | |
385 | 416 | except IndexError: |
386 | 417 | raise ValueError('Username \'{}\' was not found'.format(queried_username)) |
387 | 418 | else: |
391 | 422 | elif queried_groupname: |
392 | 423 | try: |
393 | 424 | group = net_requester.get_netgroup(queried_groupname=queried_groupname, |
394 | queried_domain=queried_domain, | |
425 | queried_domain=self._queried_domain, | |
395 | 426 | full_data=True)[0] |
396 | 427 | except IndexError: |
397 | 428 | raise ValueError('Group name \'{}\' was not found'.format(queried_groupname)) |
417 | 448 | for object_group in object_groups: |
418 | 449 | try: |
419 | 450 | object_group_sid = net_requester.get_adobject(queried_sam_account_name=object_group.samaccountname, |
420 | queried_domain=queried_domain)[0].objectsid | |
451 | queried_domain=self._queried_domain)[0].objectsid | |
421 | 452 | except IndexError: |
422 | 453 | # We may have the name of the group, but not its sam account name |
454 | self._logger.warning('We may have the name of the group, but not its sam account name.') | |
423 | 455 | try: |
424 | 456 | object_group_sid = net_requester.get_adobject(queried_name=object_group.samaccountname, |
425 | queried_domain=queried_domain)[0].objectsid | |
457 | queried_domain=self._queried_domain)[0].objectsid | |
426 | 458 | except IndexError: |
427 | 459 | # Freak accident when someone is a member of a group, but |
428 | 460 | # we can't find the group in the AD |
461 | self._logger.warning('Freak accident when someone is a member of a group, but we can\'t find the group in the AD,' | |
462 | 'see DEBUG level for more info') | |
463 | self._logger.debug('Dumping the mysterious object = {}'.format(object_group)) | |
429 | 464 | continue |
430 | 465 | |
431 | 466 | target_sid.append(object_group_sid) |
434 | 469 | for gpo_group in self.get_netgpogroup(queried_domain=queried_domain): |
435 | 470 | try: |
436 | 471 | for member in gpo_group.members: |
472 | member = member | |
437 | 473 | if not member.upper().startswith('S-1-5'): |
438 | 474 | try: |
439 | 475 | member = net_requester.get_adobject(queried_sam_account_name=member, |
440 | queried_domain=queried_domain)[0].objectsid | |
476 | queried_domain=self._queried_domain)[0].objectsid | |
441 | 477 | except (IndexError, AttributeError): |
442 | 478 | continue |
443 | 479 | if (member.upper() in target_sid) or (member.lower() in target_sid): |
450 | 486 | |
451 | 487 | for gpo_group in gpo_groups: |
452 | 488 | gpo_guid = gpo_group.gponame |
453 | ous = net_requester.get_netou(queried_domain=queried_domain, | |
489 | ous = net_requester.get_netou(queried_domain=self._queried_domain, | |
454 | 490 | queried_guid=gpo_guid, full_data=True) |
455 | 491 | for ou in ous: |
492 | ou_distinguishedname = 'LDAP://{}'.format(ou.distinguishedname) | |
456 | 493 | # TODO: support filters for GPO |
457 | 494 | ou_computers = [x.dnshostname for x in \ |
458 | net_requester.get_netcomputer(queried_domain=queried_domain, | |
459 | ads_path=ou.distinguishedname)] | |
495 | net_requester.get_netcomputer(queried_domain=self._queried_domain, | |
496 | ads_path=ou_distinguishedname)] | |
460 | 497 | 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) | |
498 | gpo_location.add_attributes({'objectname' : object_distinguished_name}) | |
499 | gpo_location.add_attributes({'gponame' : gpo_group.gpodisplayname}) | |
500 | gpo_location.add_attributes({'gpoguid' : gpo_guid}) | |
501 | gpo_location.add_attributes({'containername' : ou.distinguishedname}) | |
502 | gpo_location.add_attributes({'computers' : ou_computers}) | |
466 | 503 | |
467 | 504 | results.append(gpo_location) |
468 | 505 |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | 16 | |
17 | 17 | import random |
18 | 18 | import multiprocessing |
25 | 25 | |
26 | 26 | class Hunter(NetRequester): |
27 | 27 | def __init__(self, target_computer, domain=str(), user=(), password=str(), |
28 | lmhash=str(), nthash=str(), domain_controller=str(), queried_domain=str()): | |
28 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
29 | domain_controller=str(), queried_domain=str()): | |
29 | 30 | NetRequester.__init__(self, target_computer, domain, user, password, |
30 | lmhash, nthash, domain_controller) | |
31 | lmhash, nthash, do_kerberos, do_tls, domain_controller) | |
31 | 32 | self._target_domains = list() |
32 | 33 | self._target_computers = list() |
33 | 34 | self._target_users = list() |
88 | 89 | self._target_users.append(rpcobj.TargetUser(attributes)) |
89 | 90 | elif target_server: |
90 | 91 | with NetRequester(target_server, domain, user, password, lmhash, |
91 | nthash, domain_controller) as target_server_requester: | |
92 | nthash, do_kerberos, do_tls, domain_controller) as target_server_requester: | |
92 | 93 | for x in target_server_requester.get_netlocalgroup(recurse=True): |
93 | 94 | if x.isdomain and not x.isgroup: |
94 | 95 | attributes = {'memberdomain': x.name.split('/')[0].lower(), |
138 | 139 | self._parent_pipes.append(parent_pipe) |
139 | 140 | worker = worker_class(worker_pipe, self._domain, self._user, |
140 | 141 | self._password, self._lmhash, self._nthash, |
141 | *worker_args) | |
142 | self._do_kerberos, self._do_tls, *worker_args) | |
142 | 143 | |
143 | 144 | worker.start() |
144 | 145 | self._workers.append(worker) |
154 | 155 | rlist, wlist, _ = select.select(self._parent_pipes, write_watch_list, list()) |
155 | 156 | |
156 | 157 | for readable in rlist: |
157 | jobs_done += 1 | |
158 | jobs_done += 1 | |
158 | 159 | results = readable.recv() |
159 | 160 | for result in results: |
160 | 161 | yield result |
199 | 200 | |
200 | 201 | if foreign_users: |
201 | 202 | with Misc(self._domain_controller, self._domain, self._user, |
202 | self._password, self._lmhash, self._nthash) as misc_requester: | |
203 | self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as misc_requester: | |
203 | 204 | domain_sid = misc_requester.get_domainsid(queried_domain) |
204 | 205 | domain_short_name = misc_requester.convert_sidtont4(domain_sid).split('\\')[0] |
205 | 206 | else: |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | 16 | |
17 | 17 | from impacket.dcerpc.v5.rpcrt import DCERPCException |
18 | 18 | from impacket.dcerpc.v5 import scmr, drsuapi |
49 | 49 | def get_domainsid(self, queried_domain=str()): |
50 | 50 | |
51 | 51 | with pywerview.functions.net.NetRequester(self._domain_controller, self._domain, self._user, |
52 | self._password, self._lmhash, self._nthash) as r: | |
52 | self._password, self._lmhash, self._nthash, | |
53 | self._do_kerberos, self._do_tls) as r: | |
53 | 54 | domain_controllers = r.get_netdomaincontroller(queried_domain=queried_domain) |
54 | 55 | |
55 | 56 | if domain_controllers: |
56 | 57 | primary_dc = domain_controllers[0] |
57 | domain_sid = '-'.join(primary_dc.objectsid.split('-')[:-1]) | |
58 | domain_sid = primary_dc.objectsid | |
59 | ||
60 | # we need to retrieve the domain sid from the controller sid | |
61 | domain_sid = '-'.join(domain_sid.split('-')[:-1]) | |
58 | 62 | else: |
59 | 63 | domain_sid = None |
60 | 64 |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
16 | ||
17 | import socket | |
18 | import datetime | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | ||
17 | from datetime import datetime, timedelta | |
19 | 18 | from impacket.dcerpc.v5.ndr import NULL |
20 | 19 | from impacket.dcerpc.v5 import wkst, srvs, samr |
21 | 20 | from impacket.dcerpc.v5.samr import DCERPCSessionError |
23 | 22 | from impacket.dcerpc.v5.dcom.wmi import WBEM_FLAG_FORWARD_ONLY |
24 | 23 | from bs4 import BeautifulSoup |
25 | 24 | from ldap3.utils.conv import escape_filter_chars |
25 | from ldap3.protocol.microsoft import security_descriptor_control | |
26 | from ldap3.protocol.formatters.formatters import * | |
27 | from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR | |
26 | 28 | |
27 | 29 | from pywerview.requester import LDAPRPCRequester |
28 | 30 | import pywerview.objects.adobjects as adobj |
29 | 31 | import pywerview.objects.rpcobjects as rpcobj |
30 | 32 | import pywerview.functions.misc |
33 | import pywerview.formatters as fmt | |
31 | 34 | |
32 | 35 | class NetRequester(LDAPRPCRequester): |
33 | 36 | @LDAPRPCRequester._ldap_connection_init |
34 | 37 | def get_adobject(self, queried_domain=str(), queried_sid=str(), |
35 | 38 | 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)): | |
39 | ads_path=str(), attributes=list(), custom_filter=str()): | |
40 | for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)), | |
41 | ('samAccountName', escape_filter_chars(queried_sam_account_name))): | |
40 | 42 | if attr_value: |
41 | 43 | object_filter = '(&({}={}){})'.format(attr_desc, attr_value, custom_filter) |
42 | 44 | break |
43 | ||
44 | return self._ldap_search(object_filter, adobj.ADObject) | |
45 | else: | |
46 | object_filter = '(&(name=*){})'.format(custom_filter) | |
47 | ||
48 | return self._ldap_search(object_filter, adobj.ADObject, attributes=attributes) | |
49 | ||
50 | @LDAPRPCRequester._ldap_connection_init | |
51 | def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), | |
52 | queried_name=str(), queried_sam_account_name=str(), | |
53 | ads_path=str(), resolve_sids=False): | |
54 | filter_objectclass = '(ObjectClass=msDS-GroupManagedServiceAccount)' | |
55 | attributes = ['samaccountname', 'distinguishedname', 'objectsid', 'description', | |
56 | 'msds-managedpassword', 'msds-groupmsamembership', 'useraccountcontrol'] | |
57 | ||
58 | if not self._ldap_connection.server.ssl: | |
59 | self._logger.warning('LDAP connection is not encrypted, we can\'t ask '\ | |
60 | 'for msds-managedpassword, removing from list of attributes') | |
61 | attributes.remove('msds-managedpassword') | |
62 | ||
63 | for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)), | |
64 | ('samAccountName', escape_filter_chars(queried_sam_account_name))): | |
65 | if attr_value: | |
66 | object_filter = '(&({}={}){})'.format(attr_desc, attr_value, filter_objectclass) | |
67 | break | |
68 | else: | |
69 | object_filter = '(&(name=*){})'.format(filter_objectclass) | |
70 | ||
71 | adserviceaccounts = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes) | |
72 | sid_resolver = NetRequester(self._domain_controller, self._domain, self._user, self._password, self._lmhash, self._nthash) | |
73 | ||
74 | # In this loop, we resolve SID (if true) and we populate 'enabled' attribute | |
75 | for i, adserviceaccount in enumerate(adserviceaccounts): | |
76 | if resolve_sids: | |
77 | results = list() | |
78 | for sid in getattr(adserviceaccount, 'msds-groupmsamembership'): | |
79 | try: | |
80 | resolved_sid = sid_resolver.get_adobject(queried_sid=sid, queried_domain=self._queried_domain, | |
81 | attributes=['distinguishedname'])[0].distinguishedname | |
82 | except IndexError: | |
83 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid)) | |
84 | resolved_sid = sid | |
85 | results.append(resolved_sid) | |
86 | adserviceaccounts[i].add_attributes({'msds-groupmsamembership': results}) | |
87 | adserviceaccounts[i].add_attributes({'Enabled': 'ACCOUNTDISABLE' not in adserviceaccount.useraccountcontrol}) | |
88 | adserviceaccounts[i]._attributes_dict.pop('useraccountcontrol') | |
89 | return adserviceaccounts | |
90 | ||
91 | @LDAPRPCRequester._ldap_connection_init | |
92 | def get_objectacl(self, queried_domain=str(), queried_sid=str(), | |
93 | queried_name=str(), queried_sam_account_name=str(), | |
94 | ads_path=str(), sacl=False, rights_filter=str(), | |
95 | resolve_sids=False, resolve_guids=False, custom_filter=str()): | |
96 | for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)), | |
97 | ('samAccountName', escape_filter_chars(queried_sam_account_name))): | |
98 | if attr_value: | |
99 | object_filter = '(&({}={}){})'.format(attr_desc, attr_value, custom_filter) | |
100 | break | |
101 | else: | |
102 | object_filter = '(&(name=*){})'.format(custom_filter) | |
103 | ||
104 | guid_map = dict() | |
105 | # This works on a mono-domain forest, must be tested on a more complex one | |
106 | if resolve_guids: | |
107 | # Dirty fix to get base DN even if custom ADS path was given | |
108 | base_dn = ','.join(self._base_dn.split(',')[-2:]) | |
109 | guid_map = {'{00000000-0000-0000-0000-000000000000}': 'All'} | |
110 | with NetRequester(self._domain_controller, self._domain, self._user, self._password, | |
111 | self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester: | |
112 | for o in net_requester.get_adobject(ads_path='CN=Schema,CN=Configuration,{}'.format(base_dn), | |
113 | attributes=['name', 'schemaIDGUID'], custom_filter='(schemaIDGUID=*)'): | |
114 | guid_map['{{{}}}'.format(o.schemaidguid)] = o.name | |
115 | ||
116 | for o in net_requester.get_adobject(ads_path='CN=Extended-Rights,CN=Configuration,{}'.format(base_dn), | |
117 | attributes=['name', 'rightsGuid'], custom_filter='(objectClass=controlAccessRight)'): | |
118 | guid_map['{{{}}}'.format(o.rightsguid.lower())] = o.name | |
119 | ||
120 | attributes = ['distinguishedname', 'objectsid', 'ntsecuritydescriptor'] | |
121 | if sacl: | |
122 | controls = list() | |
123 | acl_type = 'Sacl' | |
124 | else: | |
125 | # The control is used to get access to ntSecurityDescriptor with an | |
126 | # unprivileged user, see https://stackoverflow.com/questions/40771503/selecting-the-ad-ntsecuritydescriptor-attribute-as-a-non-admin/40773088 | |
127 | # /!\ May break pagination from what I've read (see Stack Overflow answer) | |
128 | controls = security_descriptor_control(criticality=True, sdflags=0x07) | |
129 | acl_type = 'Dacl' | |
130 | ||
131 | security_descriptors = self._ldap_search(object_filter, adobj.ADObject, | |
132 | attributes=attributes, controls=controls) | |
133 | ||
134 | acl = list() | |
135 | ||
136 | rights_to_guid = {'reset-password': '{00299570-246d-11d0-a768-00aa006e0529}', | |
137 | 'write-members': '{bf9679c0-0de6-11d0-a285-00aa003049e2}', | |
138 | 'all': '{00000000-0000-0000-0000-000000000000}'} | |
139 | guid_filter = rights_to_guid.get(rights_filter, None) | |
140 | ||
141 | if resolve_sids: | |
142 | sid_resolver = NetRequester(self._domain_controller, self._domain, | |
143 | self._user, self._password, self._lmhash, self._nthash, | |
144 | self._do_kerberos, self._do_tls) | |
145 | sid_mapping = adobj.ADObject._well_known_sids.copy() | |
146 | else: | |
147 | sid_resolver = None | |
148 | ||
149 | for security_descriptor in security_descriptors: | |
150 | sd = SR_SECURITY_DESCRIPTOR() | |
151 | try: | |
152 | sd.fromString(security_descriptor.ntsecuritydescriptor) | |
153 | except TypeError: | |
154 | continue | |
155 | for ace in sd[acl_type]['Data']: | |
156 | if guid_filter: | |
157 | try: | |
158 | object_type = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}' | |
159 | except KeyError: | |
160 | continue | |
161 | if object_type != guid_filter: | |
162 | continue | |
163 | attributes = dict() | |
164 | attributes['objectdn'] = security_descriptor.distinguishedname | |
165 | attributes['objectsid'] = security_descriptor.objectsid | |
166 | attributes['acetype'] = ace['TypeName'] | |
167 | attributes['binarysize'] = ace['AceSize'] | |
168 | attributes['aceflags'] = fmt.format_ace_flags(ace['AceFlags']) | |
169 | attributes['accessmask'] = ace['Ace']['Mask']['Mask'] | |
170 | attributes['activedirectoryrights'] = fmt.format_ace_access_mask(ace['Ace']['Mask']['Mask']) | |
171 | attributes['isinherited'] = bool(ace['AceFlags'] & 0x10) | |
172 | attributes['securityidentifier'] = format_sid(ace['Ace']['Sid'].getData()) | |
173 | if sid_resolver: | |
174 | converted_sid = attributes['securityidentifier'] | |
175 | try: | |
176 | resolved_sid = sid_mapping[converted_sid] | |
177 | except KeyError: | |
178 | try: | |
179 | resolved_sid = sid_resolver.get_adobject(queried_sid=converted_sid, | |
180 | queried_domain=self._queried_domain, attributes=['distinguishedname'])[0] | |
181 | resolved_sid = resolved_sid.distinguishedname | |
182 | except IndexError: | |
183 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(converted_sid)) | |
184 | resolved_sid = attributes['securityidentifier'] | |
185 | finally: | |
186 | sid_mapping[converted_sid] = resolved_sid | |
187 | attributes['securityidentifier'] = resolved_sid | |
188 | try: | |
189 | attributes['objectaceflags'] = fmt.format_object_ace_flags(ace['Ace']['Flags']) | |
190 | except KeyError: | |
191 | pass | |
192 | try: | |
193 | attributes['objectacetype'] = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}' | |
194 | attributes['objectacetype'] = guid_map[attributes['objectacetype']] | |
195 | except KeyError: | |
196 | pass | |
197 | try: | |
198 | attributes['inheritedobjectacetype'] = format_uuid_le(ace['Ace']['InheritedObjectType']) if ace['Ace']['InheritedObjectType'] else '{00000000-0000-0000-0000-000000000000}' | |
199 | attributes['inheritedobjectacetype'] = guid_map[attributes['inheritedobjectacetype']] | |
200 | except KeyError: | |
201 | pass | |
202 | ||
203 | acl.append(adobj.ACE(attributes)) | |
204 | ||
205 | return acl | |
45 | 206 | |
46 | 207 | @LDAPRPCRequester._ldap_connection_init |
47 | 208 | def get_netuser(self, queried_username=str(), queried_domain=str(), |
79 | 240 | |
80 | 241 | # RFC 4515, section 3 |
81 | 242 | # However if we escape *, we can no longer use wildcard within `--groupname` |
82 | # Maybe we can raise a warning here ? | |
83 | 243 | if not '*' in queried_groupname: |
84 | 244 | queried_groupname = escape_filter_chars(queried_groupname) |
245 | else: | |
246 | self._logger.warning('"*" detected in "{}", if it also contains "(",")" or "\\", ' | |
247 | 'script will probably crash ("invalid filter"). ' | |
248 | 'Don\'t use wildcard with these characters'.format(queried_groupname)) | |
85 | 249 | |
86 | 250 | if queried_username: |
251 | self._logger.debug('Queried username = {}'.format(queried_username)) | |
87 | 252 | results = list() |
88 | 253 | sam_account_name_to_resolve = [queried_username] |
89 | 254 | first_run = True |
119 | 284 | final_results = list() |
120 | 285 | for group_sam_account_name in results: |
121 | 286 | obj_member_of = adobj.Group(list()) |
122 | setattr(obj_member_of, 'samaccountname', group_sam_account_name) | |
287 | obj_member_of._attributes_dict['samaccountname'] = group_sam_account_name | |
123 | 288 | final_results.append(obj_member_of) |
124 | 289 | return final_results |
125 | 290 | else: |
130 | 295 | group_search_filter += '(objectCategory=group)' |
131 | 296 | |
132 | 297 | if queried_sid: |
298 | self._logger.debug('Queried SID = {}'.format(queried_username)) | |
133 | 299 | group_search_filter += '(objectSid={})'.format(queried_sid) |
134 | 300 | elif queried_groupname: |
301 | self._logger.debug('Queried groupname = {}'.format(queried_groupname)) | |
135 | 302 | group_search_filter += '(name={})'.format(queried_groupname) |
136 | 303 | |
137 | 304 | if full_data: |
208 | 375 | |
209 | 376 | final_results = list() |
210 | 377 | for file_server_name in results: |
211 | attributes = list() | |
212 | attributes.append({'type': 'dnshostname', 'vals': [file_server_name]}) | |
378 | attributes = dict() | |
379 | attributes['dnshostname'] = file_server_name | |
213 | 380 | final_results.append(adobj.FileServer(attributes)) |
214 | 381 | |
215 | 382 | return final_results |
227 | 394 | for remote_server in dfs.remoteservername: |
228 | 395 | remote_server = str(remote_server) |
229 | 396 | if '\\' in remote_server: |
230 | attributes = {'name': [dfs.name.encode('utf-8')], | |
231 | 'remoteservername': [remote_server.split('\\')[2].encode('utf-8')]} | |
397 | attributes = {'name': dfs.name, | |
398 | 'remoteservername': remote_server.split('\\')[2]} | |
232 | 399 | results.append(adobj.DFS(attributes)) |
233 | 400 | |
234 | 401 | return results |
249 | 416 | for target in soup_target_list.targets.contents: |
250 | 417 | if '\\' in target.string: |
251 | 418 | 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')]} | |
419 | attributes = {'name': '{}{}'.format(dfs_root, share_name), | |
420 | 'remoteservername': server_name} | |
254 | 421 | |
255 | 422 | results.append(adobj.DFS(attributes)) |
256 | 423 | |
337 | 504 | try: |
338 | 505 | # `--groupname` option is supplied |
339 | 506 | if _groupname: |
507 | self._logger.debug('Queried groupname = {}'.format(queried_groupname)) | |
340 | 508 | groups = self.get_netgroup(queried_groupname=_groupname, |
341 | queried_domain=queried_domain, | |
509 | queried_domain=self._queried_domain, | |
342 | 510 | full_data=True) |
343 | 511 | |
344 | 512 | # `--groupname` option is missing, falling back to the "Domain Admins" |
345 | 513 | else: |
514 | self._logger.debug('No groupname provided, falling back to the "Domain Admins"'.format(queried_groupname)) | |
346 | 515 | if _sid: |
347 | 516 | queried_sid = _sid |
348 | 517 | else: |
349 | 518 | with pywerview.functions.misc.Misc(self._domain_controller, |
350 | 519 | self._domain, self._user, |
351 | 520 | self._password, self._lmhash, |
352 | self._nthash) as misc_requester: | |
521 | self._nthash, self._do_kerberos, | |
522 | self._do_tls) as misc_requester: | |
353 | 523 | queried_sid = misc_requester.get_domainsid(queried_domain) + '-512' |
524 | self._logger.debug('Found Domains Admins SID = {}'.format(queried_sid)) | |
354 | 525 | groups = self.get_netgroup(queried_sid=queried_sid, |
355 | queried_domain=queried_domain, | |
526 | queried_domain=self._queried_domain, | |
356 | 527 | full_data=True) |
357 | 528 | except IndexError: |
358 | 529 | raise ValueError('The group {} was not found'.format(_groupname)) |
365 | 536 | group_memberof_filter = '(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:={}){})'.format(group.distinguishedname, custom_filter) |
366 | 537 | |
367 | 538 | members = self.get_netuser(custom_filter=group_memberof_filter, |
368 | queried_domain=queried_domain) | |
539 | queried_domain=self._queried_domain) | |
369 | 540 | else: |
370 | 541 | # TODO: range cycling |
371 | 542 | try: |
372 | 543 | for member in group.member: |
373 | 544 | # RFC 4515, section 3 |
545 | self._logger.warning('Member name = "{}" will be escaped'.format(member)) | |
374 | 546 | member = escape_filter_chars(member, encoding='utf-8') |
375 | 547 | 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) | |
548 | members += self.get_netuser(custom_filter=dn_filter, queried_domain=self._queried_domain) | |
549 | members += self.get_netgroup(custom_filter=dn_filter, queried_domain=self._queried_domain, full_data=True) | |
378 | 550 | # The group doesn't have any members |
379 | 551 | except AttributeError: |
552 | self._logger.debug('The group doesn\'t have any members') | |
380 | 553 | continue |
381 | 554 | |
382 | 555 | for member in members: |
389 | 562 | try: |
390 | 563 | member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.') |
391 | 564 | except IndexError: |
565 | self._logger.warning('Exception was raised while handling member_dn, falling back to empty string') | |
392 | 566 | member_domain = str() |
393 | is_group = (member.samaccounttype != '805306368') | |
567 | is_group = (member.samaccounttype != 805306368) | |
394 | 568 | |
395 | 569 | attributes = dict() |
396 | 570 | if queried_domain: |
397 | 571 | attributes['groupdomain'] = queried_domain |
398 | 572 | else: |
399 | attributes['groupdomain'] = self._domain | |
573 | attributes['groupdomain'] = self._queried_domain | |
400 | 574 | attributes['groupname'] = group.name |
401 | 575 | attributes['membername'] = member.samaccountname |
402 | 576 | attributes['memberdomain'] = member_domain |
403 | 577 | attributes['isgroup'] = is_group |
404 | 578 | attributes['memberdn'] = member_dn |
405 | attributes['membersid'] = member.objectsid | |
579 | attributes['objectsid'] = member.objectsid | |
406 | 580 | |
407 | 581 | final_member.add_attributes(attributes) |
408 | 582 | |
603 | 777 | member_handle = resp['UserHandle'] |
604 | 778 | attributes['isgroup'] = False |
605 | 779 | resp = samr.hSamrQueryInformationUser(self._rpc_connection, member_handle) |
606 | attributes['name'] = '{}/{}'.format(member_domain, resp['Buffer']['General']['UserName']) | |
780 | attributes['name'] = '{}\\{}'.format(member_domain, resp['Buffer']['General']['UserName']) | |
607 | 781 | except DCERPCSessionError: |
608 | 782 | resp = samr.hSamrOpenAlias(self._rpc_connection, domain_handle, aliasId=member_rid) |
609 | 783 | member_handle = resp['AliasHandle'] |
610 | 784 | attributes['isgroup'] = True |
611 | 785 | resp = samr.hSamrQueryInformationAlias(self._rpc_connection, member_handle) |
612 | attributes['name'] = '{}/{}'.format(member_domain, resp['Buffer']['General']['Name']) | |
613 | attributes['lastlogin'] = str() | |
786 | attributes['name'] = '{}\\{}'.format(member_domain, resp['Buffer']['General']['Name']) | |
787 | attributes['lastlogon'] = str() | |
614 | 788 | break |
615 | 789 | # It's a domain member |
616 | 790 | else: |
621 | 795 | member_dn = ad_object.distinguishedname |
622 | 796 | member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.') |
623 | 797 | try: |
624 | attributes['name'] = '{}/{}'.format(member_domain, ad_object.samaccountname) | |
798 | attributes['name'] = '{}\\{}'.format(member_domain, ad_object.samaccountname) | |
625 | 799 | except AttributeError: |
626 | 800 | # Here, the member is a foreign security principal |
627 | 801 | # TODO: resolve it properly |
628 | attributes['name'] = '{}/{}'.format(member_domain, ad_object.objectsid) | |
629 | attributes['isgroup'] = ad_object.isgroup | |
802 | self._logger.warning('The member is a foreign security principal, SID will not be resolved') | |
803 | attributes['name'] = '{}\\{}'.format(member_domain, ad_object.objectsid) | |
804 | attributes['isgroup'] = 'group' in ad_object.objectclass | |
630 | 805 | try: |
631 | attributes['lastlogin'] = ad_object.lastlogon | |
806 | attributes['lastlogon'] = ad_object.lastlogon | |
632 | 807 | except AttributeError: |
633 | attributes['lastlogin'] = str() | |
808 | self._logger.warning('lastlogon is not set, falling back to empty string') | |
809 | attributes['lastlogon'] = str() | |
634 | 810 | except IndexError: |
635 | 811 | # We did not manage to resolve this SID against the DC |
812 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member_sid)) | |
636 | 813 | attributes['isdomain'] = False |
637 | 814 | attributes['isgroup'] = False |
638 | 815 | attributes['name'] = attributes['sid'] |
639 | attributes['lastlogin'] = str() | |
816 | attributes['lastlogon'] = str() | |
640 | 817 | else: |
641 | 818 | attributes['isgroup'] = False |
642 | 819 | attributes['name'] = str() |
643 | attributes['lastlogin'] = str() | |
820 | attributes['lastlogon'] = str() | |
644 | 821 | |
645 | 822 | results.append(rpcobj.RPCObject(attributes)) |
646 | 823 | |
652 | 829 | domain_member_attributes['isdomain'] = True |
653 | 830 | member_dn = domain_member.distinguishedname |
654 | 831 | member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.') |
655 | domain_member_attributes['name'] = '{}/{}'.format(member_domain, domain_member.samaccountname) | |
832 | domain_member_attributes['name'] = '{}\\{}'.format(member_domain, domain_member.samaccountname) | |
656 | 833 | domain_member_attributes['isgroup'] = domain_member.isgroup |
657 | 834 | domain_member_attributes['isdomain'] = True |
835 | # TODO: Nope, maybe here we can call get-netdomaincontroller ? | |
836 | # Need to check in powerview | |
658 | 837 | domain_member_attributes['server'] = attributes['name'] |
659 | 838 | domain_member_attributes['sid'] = domain_member.objectsid |
660 | 839 | try: |
661 | 840 | domain_member_attributes['lastlogin'] = ad_object.lastlogon |
662 | 841 | except AttributeError: |
842 | self._logger.warning('lastlogon is not set, falling back to empty string') | |
663 | 843 | domain_member_attributes['lastlogin'] = str() |
664 | 844 | results.append(rpcobj.RPCObject(domain_member_attributes)) |
665 | 845 | |
690 | 870 | |
691 | 871 | @LDAPRPCRequester._wmi_connection_init() |
692 | 872 | 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') | |
873 | limit_date = (datetime.today() - timedelta(days=date_start)).strftime('%Y%m%d%H%M%S.%f-000') | |
694 | 874 | if event_type == ['logon']: |
695 | 875 | where_clause = 'EventCode=4624' |
696 | 876 | elif event_type == ['tgt']: |
707 | 887 | wmi_event = wmi_enum_event.Next(0xffffffff, 1)[0] |
708 | 888 | wmi_event_type = wmi_event.EventIdentifier |
709 | 889 | wmi_event_info = wmi_event.InsertionStrings |
710 | time = datetime.datetime.strptime(wmi_event.TimeGenerated, '%Y%m%d%H%M%S.%f-000') | |
890 | time = datetime.strptime(wmi_event.TimeGenerated, '%Y%m%d%H%M%S.%f-000') | |
711 | 891 | if wmi_event_type == 4624: |
712 | 892 | logon_type = int(wmi_event_info[8]) |
713 | 893 | user = wmi_event_info[5] |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
16 | ||
17 | from datetime import datetime, timedelta | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | ||
18 | 17 | import inspect |
19 | import struct | |
20 | import pyasn1 | |
21 | import codecs | |
18 | import logging | |
19 | ||
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 | logger = logging.getLogger('pywerview_main_logger.ADObject') | |
60 | logger.ULTRA = 5 | |
61 | self._logger = logger | |
62 | ||
63 | self._attributes_dict = dict() | |
48 | 64 | self.add_attributes(attributes) |
49 | 65 | |
50 | 66 | def add_attributes(self, attributes): |
67 | self._logger.log(self._logger.ULTRA,'ADObject instancied with the following attributes : {}'.format(attributes)) | |
51 | 68 | 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'): | |
69 | self._attributes_dict[attr.lower()] = attributes[attr] | |
70 | ||
71 | def __getattr__(self, attr): | |
72 | try: | |
73 | return self._attributes_dict[attr] | |
74 | except KeyError: | |
75 | if attr == 'isgroup': | |
79 | 76 | 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 | ||
77 | return 'group' in self._attributes_dict['objectclass'] | |
78 | except KeyError: | |
79 | return False | |
80 | raise AttributeError | |
81 | ||
82 | # In this method, we try to pretty print common AD attributes | |
110 | 83 | def __str__(self): |
111 | 84 | s = str() |
112 | members = inspect.getmembers(self, lambda x: not(inspect.isroutine(x))) | |
113 | 85 | 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) | |
86 | for attr in self._attributes_dict: | |
87 | if len(attr) > max_length: | |
88 | max_length = len(attr) | |
89 | for attr in self._attributes_dict: | |
90 | attribute = self._attributes_dict[attr] | |
91 | self._logger.log(self._logger.ULTRA,'Trying to print : attribute name = {0} / value = {1}'.format(attr, attribute)) | |
92 | if isinstance(attribute, list): | |
93 | if any(isinstance(x, bytes) for x in attribute): | |
94 | attribute = ['{}...'.format(x.hex()[:97]) for x in attribute] | |
95 | attribute_temp = ', '.join(str(x) for x in attribute) | |
96 | if len(attribute_temp) > 100: | |
97 | attribute_temp = str() | |
98 | line = str() | |
99 | for x in attribute: | |
100 | if len(line) + len(str(x)) <= 100: | |
101 | line += '{}, '.format(x) | |
102 | else: | |
103 | attribute_temp += line + '\n' + ' ' * (max_length + 2) | |
104 | line = str() | |
105 | line += '{}, '.format(x) | |
106 | attribute_temp += line + '\n' + ' ' * (max_length + 2) | |
107 | attribute = attribute_temp.rstrip().rstrip(',') | |
108 | elif isinstance(attribute, bytes): | |
109 | attribute = '{}...'.format(attribute.hex()[:100]) | |
110 | elif isinstance(attribute, ADObject): | |
111 | attribute = ('\n' + str(attribute)).replace('\n', '\n\t') | |
112 | ||
113 | s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute) | |
150 | 114 | |
151 | 115 | s = s[:-1] |
152 | 116 | return s |
154 | 118 | def __repr__(self): |
155 | 119 | return str(self) |
156 | 120 | |
157 | class User(ADObject): | |
121 | def to_json(self): | |
122 | return self._attributes_dict | |
123 | ||
124 | class ACE(ADObject): | |
125 | ||
158 | 126 | def __init__(self, attributes): |
159 | 127 | 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()) | |
128 | ||
129 | # We set iscallback, depending on the type of ACE | |
130 | self._attributes_dict['iscallbak'] = ('CALLBACK' in self.acetype) | |
131 | ||
132 | class User(ADObject): | |
133 | pass | |
165 | 134 | |
166 | 135 | class Group(ADObject): |
136 | pass | |
137 | ||
138 | class Computer(ADObject): | |
139 | pass | |
140 | ||
141 | class FileServer(ADObject): | |
142 | pass | |
143 | ||
144 | class DFS(ADObject): | |
145 | pass | |
146 | ||
147 | class OU(ADObject): | |
148 | pass | |
149 | ||
150 | class Site(ADObject): | |
151 | pass | |
152 | ||
153 | class Subnet(ADObject): | |
154 | pass | |
155 | ||
156 | class Trust(ADObject): | |
157 | ||
167 | 158 | def __init__(self, attributes): |
159 | logger = logging.getLogger('pywerview_main_logger.Trust') | |
160 | self._logger = logger | |
168 | 161 | 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 | ||
162 | trust_attributes = self.trustattributes | |
163 | trust_direction = self.trustdirection | |
225 | 164 | # If the filter SIDs attribute is not manually set, we check if we're |
226 | 165 | # not in a use case where SIDs are implicitly filtered |
227 | 166 | # 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): | |
167 | if 'filter_sids' not in trust_attributes: | |
168 | if not (trust_direction == 'disabled' or \ | |
169 | trust_direction == 'inbound' or \ | |
170 | 'within_forest' in trust_attributes or \ | |
171 | 'pim_trust' in trust_attributes): | |
172 | if 'forest_transitive' in trust_attributes and 'treat_as_external' not in trust_attributes: | |
173 | self._attributes_dict['trustattributes'].append('filter_sids') | |
174 | ||
175 | # Pretty printing Trust object, we don't want to print all the attributes | |
176 | # so we only print useful ones (trustattributes, trustdirection, trustpartner | |
177 | # trusttype, whenchanged, whencreated) | |
240 | 178 | def __str__(self): |
241 | 179 | 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) | |
180 | max_length = len('trustattributes') | |
181 | ||
182 | for attr in self._attributes_dict: | |
183 | self._logger.log(self._logger.ULTRA,'Trying to print : attribute name = {0} / value = {1}'.format(attr, self._attributes_dict[attr])) | |
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 | self._logger.debug('Ignoring : attribute name = {0}'.format(attr, self._attributes_dict[attr])) | |
190 | continue | |
191 | s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute) | |
249 | 192 | |
250 | 193 | s = s[:-1] |
251 | 194 | return s |
195 | pass | |
196 | ||
197 | class GPO(ADObject): | |
198 | pass | |
199 | ||
200 | class PSO(ADObject): | |
201 | pass | |
202 | ||
203 | class GptTmpl(ADObject): | |
204 | def to_json(self): | |
205 | json_dict = {} | |
206 | for k, v in self._attributes_dict.items(): | |
207 | json_dict[k] = v.to_json() | |
208 | return json_dict | |
252 | 209 | |
253 | 210 | class GPOGroup(ADObject): |
254 | 211 | pass |
262 | 219 | class GPOLocation(ADObject): |
263 | 220 | pass |
264 | 221 | |
222 | class GMSAAccount(ADObject): | |
223 | pass | |
224 |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | 16 | |
17 | from __future__ import unicode_literals | |
18 | ||
17 | import logging | |
19 | 18 | import inspect |
20 | 19 | |
21 | 20 | class RPCObject: |
22 | 21 | def __init__(self, obj): |
22 | logger = logging.getLogger('pywerview_main_logger.RPCObject') | |
23 | logger.ULTRA = 5 | |
24 | self._logger = logger | |
25 | ||
23 | 26 | attributes = dict() |
24 | 27 | try: |
25 | 28 | for key in obj.fields.keys(): |
29 | 32 | self.add_attributes(attributes) |
30 | 33 | |
31 | 34 | def add_attributes(self, attributes): |
35 | self._logger.log(self._logger.ULTRA,'RPCObject instancied with the following attributes : {}'.format(attributes)) | |
32 | 36 | for key, value in attributes.items(): |
33 | 37 | key = key.lower() |
34 | 38 | if key in ('wkui1_logon_domain', 'wkui1_logon_server', |
35 | 39 | 'wkui1_oth_domains', 'wkui1_username', |
36 | 40 | 'sesi10_cname', 'sesi10_username'): |
37 | 41 | value = value.rstrip('\x00') |
38 | ||
42 | ||
39 | 43 | setattr(self, key.lower(), value) |
40 | 44 | |
41 | 45 | def __str__(self): |
47 | 51 | if len(member[0]) > max_length: |
48 | 52 | max_length = len(member[0]) |
49 | 53 | for member in members: |
54 | self._logger.log(self._logger.ULTRA,'Trying to print : attribute name = {0} / value = {1}'.format(member[0], member[1])) | |
50 | 55 | if not member[0].startswith('_'): |
51 | 56 | s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member[1]) |
52 | 57 | |
55 | 60 | |
56 | 61 | def __repr__(self): |
57 | 62 | return str(self) |
63 | ||
64 | def to_json(self): | |
65 | members = inspect.getmembers(self, lambda x: not(inspect.isroutine(x))) | |
66 | results = dict() | |
67 | for member in members: | |
68 | if not member[0].startswith('_'): | |
69 | results[member[0]]=member[1] | |
70 | return(results) | |
58 | 71 | |
59 | 72 | class TargetUser(RPCObject): |
60 | 73 | pass |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
16 | ||
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | ||
17 | import sys | |
18 | import logging | |
17 | 19 | import socket |
18 | 20 | import ntpath |
19 | 21 | import ldap3 |
22 | import os | |
23 | import tempfile | |
24 | ||
25 | from ldap3.protocol.formatters.formatters import * | |
20 | 26 | |
21 | 27 | from impacket.smbconnection import SMBConnection |
28 | from impacket.smbconnection import SessionError | |
29 | from impacket.krb5.ccache import CCache, Credential, CountedOctetString | |
30 | from impacket.krb5 import constants | |
31 | from impacket.krb5.types import Principal | |
22 | 32 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY |
23 | 33 | from impacket.dcerpc.v5 import transport, wkst, srvs, samr, scmr, drsuapi, epm |
24 | 34 | from impacket.dcerpc.v5.dcom import wmi |
26 | 36 | from impacket.dcerpc.v5.dcomrt import DCOMConnection |
27 | 37 | from impacket.dcerpc.v5.rpcrt import DCERPCException |
28 | 38 | |
39 | import pywerview.formatters as fmt | |
40 | ||
29 | 41 | class LDAPRequester(): |
30 | 42 | def __init__(self, domain_controller, domain=str(), user=(), password=str(), |
31 | lmhash=str(), nthash=str()): | |
43 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False): | |
32 | 44 | self._domain_controller = domain_controller |
33 | 45 | self._domain = domain |
34 | 46 | self._user = user |
35 | 47 | self._password = password |
36 | 48 | self._lmhash = lmhash |
37 | 49 | self._nthash = nthash |
50 | self._do_kerberos = do_kerberos | |
51 | self._do_tls = do_tls | |
38 | 52 | self._queried_domain = None |
39 | 53 | self._ads_path = None |
40 | 54 | self._ads_prefix = None |
41 | 55 | self._ldap_connection = None |
42 | 56 | self._base_dn = None |
43 | 57 | |
58 | logger = logging.getLogger('pywerview_main_logger.LDAPRequester') | |
59 | self._logger = logger | |
60 | ||
44 | 61 | def _get_netfqdn(self): |
45 | 62 | try: |
46 | 63 | smb = SMBConnection(self._domain_controller, self._domain_controller) |
47 | 64 | except socket.error: |
65 | self._logger.warning('Socket error when opening the SMB connection') | |
48 | 66 | return str() |
67 | ||
68 | self._logger.debug('SMB loging parameters : user = {0} / password = {1} / domain = {2} ' | |
69 | '/ LM hash = {3} / NT hash = {4}'.format(self._user, self._password, | |
70 | self._domain, self._lmhash, | |
71 | self._nthash)) | |
49 | 72 | |
50 | 73 | smb.login(self._user, self._password, domain=self._domain, |
51 | 74 | lmhash=self._lmhash, nthash=self._nthash) |
54 | 77 | |
55 | 78 | return fqdn |
56 | 79 | |
80 | def _patch_spn(self, creds, principal): | |
81 | self._logger.debug('Patching principal to {}'.format(principal)) | |
82 | ||
83 | from pyasn1.codec.der import decoder, encoder | |
84 | from impacket.krb5.asn1 import TGS_REP, Ticket | |
85 | ||
86 | # Code is ~~based on~~ stolen from https://github.com/SecureAuthCorp/impacket/pull/1256 | |
87 | tgs = creds.toTGS(principal) | |
88 | decoded_st = decoder.decode(tgs['KDC_REP'], asn1Spec=TGS_REP())[0] | |
89 | decoded_st['ticket']['sname']['name-string'][0] = 'ldap' | |
90 | decoded_st['ticket']['sname']['name-string'][1] = self._domain_controller.lower() | |
91 | decoded_st['ticket']['realm'] = self._queried_domain.upper() | |
92 | ||
93 | new_creds = Credential(data=creds.getData()) | |
94 | new_creds.ticket = CountedOctetString() | |
95 | new_creds.ticket['data'] = encoder.encode(decoded_st['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True)) | |
96 | new_creds.ticket['length'] = len(new_creds.ticket['data']) | |
97 | new_creds['server'].fromPrincipal(Principal(principal, type=constants.PrincipalNameType.NT_PRINCIPAL.value)) | |
98 | ||
99 | return new_creds | |
100 | ||
57 | 101 | def _create_ldap_connection(self, queried_domain=str(), ads_path=str(), |
58 | 102 | ads_prefix=str()): |
59 | 103 | if not self._domain: |
60 | self._domain = self._get_netfqdn() | |
104 | if self._do_kerberos: | |
105 | ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) | |
106 | self._domain = ccache.principal.realm['data'].decode('utf-8') | |
107 | else: | |
108 | try: | |
109 | self._domain = self._get_netfqdn() | |
110 | except SessionError as e: | |
111 | self._logger.critical(e) | |
112 | sys.exit(-1) | |
61 | 113 | |
62 | 114 | if not queried_domain: |
63 | queried_domain = self._get_netfqdn() | |
115 | if self._do_kerberos: | |
116 | ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) | |
117 | queried_domain = ccache.principal.realm['data'].decode('utf-8') | |
118 | else: | |
119 | try: | |
120 | queried_domain = self._get_netfqdn() | |
121 | except SessionError as e: | |
122 | self._logger.critical(e) | |
123 | sys.exit(-1) | |
64 | 124 | self._queried_domain = queried_domain |
65 | 125 | |
66 | 126 | base_dn = str() |
81 | 141 | # base_dn is no longer used within `_create_ldap_connection()`, but I don't want to break |
82 | 142 | # the function call. So we store it in an attriute and use it in `_ldap_search()` |
83 | 143 | self._base_dn = base_dn |
84 | ||
144 | ||
85 | 145 | # Format the username and the domain |
86 | 146 | # 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 | ||
147 | if self._do_kerberos: | |
148 | user = '{}@{}'.format(self._user, self._domain.upper()) | |
149 | else: | |
150 | user = '{}\\{}'.format(self._domain, self._user) | |
151 | ||
152 | # Call custom formatters for several AD attributes | |
153 | formatter = {'userAccountControl': fmt.format_useraccountcontrol, | |
154 | 'trustType': fmt.format_trusttype, | |
155 | 'trustDirection': fmt.format_trustdirection, | |
156 | 'trustAttributes': fmt.format_trustattributes, | |
157 | 'msDS-MaximumPasswordAge': format_ad_timedelta, | |
158 | 'msDS-MinimumPasswordAge': format_ad_timedelta, | |
159 | 'msDS-LockoutDuration': format_ad_timedelta, | |
160 | 'msDS-LockoutObservationWindow': format_ad_timedelta, | |
161 | 'msDS-GroupMSAMembership': fmt.format_groupmsamembership, | |
162 | 'msDS-ManagedPassword': fmt.format_managedpassword} | |
163 | ||
164 | if self._do_tls: | |
165 | ldap_scheme = 'ldaps' | |
166 | self._logger.debug('LDAPS connection forced') | |
167 | else: | |
168 | ldap_scheme = 'ldap' | |
169 | ldap_server = ldap3.Server('{}://{}'.format(ldap_scheme, self._domain_controller), formatter=formatter) | |
170 | ldap_connection_kwargs = {'user': user, 'raise_exceptions': True} | |
171 | ||
172 | # We build the authentication arguments depending on auth mode | |
173 | if self._do_kerberos: | |
174 | self._logger.debug('LDAP authentication with Keberos') | |
175 | ldap_connection_kwargs['authentication'] = ldap3.SASL | |
176 | ldap_connection_kwargs['sasl_mechanism'] = ldap3.KERBEROS | |
177 | ||
178 | # Verifying if we have the correct TGS/TGT to interrogate the LDAP server | |
179 | ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) | |
180 | principal = 'ldap/{}@{}'.format(self._domain_controller.lower(), self._queried_domain.upper()) | |
181 | ||
182 | # We look for the TGS with the right SPN | |
183 | creds = ccache.getCredential(principal, anySPN=False) | |
184 | if creds: | |
185 | self._logger.debug('TGS found in KRB5CCNAME file') | |
186 | if creds['server'].prettyPrint().lower() != creds['server'].prettyPrint(): | |
187 | self._logger.debug('SPN not in lowercase, patching SPN') | |
188 | new_creds = self._patch_spn(creds, principal) | |
189 | # We build a new CCache with the new ticket | |
190 | ccache.credentials.append(new_creds) | |
191 | temp_ccache = tempfile.NamedTemporaryFile() | |
192 | ccache.saveFile(temp_ccache.name) | |
193 | cred_store = {'ccache': 'FILE:{}'.format(temp_ccache.name)} | |
194 | else: | |
195 | cred_store = dict() | |
196 | else: | |
197 | self._logger.debug('TGS not found in KRB5CCNAME, looking for ' | |
198 | 'TGS with alternative SPN') | |
199 | # If we don't find it, we search for any SPN | |
200 | creds = ccache.getCredential(principal, anySPN=True) | |
201 | if creds: | |
202 | # If we find one, we build a custom TGS | |
203 | self._logger.debug('Alternative TGS found, patching SPN') | |
204 | new_creds = self._patch_spn(creds, principal) | |
205 | # We build a new CCache with the new ticket | |
206 | ccache.credentials.append(new_creds) | |
207 | temp_ccache = tempfile.NamedTemporaryFile() | |
208 | ccache.saveFile(temp_ccache.name) | |
209 | cred_store = {'ccache': 'FILE:{}'.format(temp_ccache.name)} | |
210 | else: | |
211 | # If we don't find any, we hope for the best (TGT in cache) | |
212 | self._logger.debug('Alternative TGS not found, using KRB5CCNAME as is ' | |
213 | 'while hoping it contains a TGT') | |
214 | cred_store = dict() | |
215 | ldap_connection_kwargs['cred_store'] = cred_store | |
216 | self._logger.debug('LDAP binding parameters: server = {0} / user = {1} ' | |
217 | '/ Kerberos auth'.format(self._domain_controller, user)) | |
218 | else: | |
219 | self._logger.debug('LDAP authentication with NTLM') | |
220 | ldap_connection_kwargs['authentication'] = ldap3.NTLM | |
221 | if self._lmhash and self._nthash: | |
222 | ldap_connection_kwargs['password'] = '{}:{}'.format(self._lmhash, self._nthash) | |
223 | self._logger.debug('LDAP binding parameters: server = {0} / user = {1} ' | |
224 | '/ hash = {2}'.format(self._domain_controller, user, ldap_connection_kwargs['password'])) | |
225 | else: | |
226 | ldap_connection_kwargs['password'] = self._password | |
227 | self._logger.debug('LDAP binding parameters: server = {0} / user = {1} ' | |
228 | '/ password = {2}'.format(self._domain_controller, user, ldap_connection_kwargs['password'])) | |
229 | ||
230 | try: | |
231 | ldap_connection = ldap3.Connection(ldap_server, **ldap_connection_kwargs) | |
97 | 232 | try: |
98 | 233 | 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 | ||
234 | except ldap3.core.exceptions.LDAPSocketOpenError as e: | |
235 | self._logger.critical(e) | |
236 | if self._do_tls: | |
237 | self._logger.critical('TLS negociation failed, this error is mostly due to your host ' | |
238 | 'not supporting SHA1 as signing algorithm for certificates') | |
239 | sys.exit(-1) | |
240 | except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult: | |
241 | # We need to try TLS | |
242 | self._logger.warning('Server returns LDAPStrongerAuthRequiredResult, falling back to LDAPS') | |
243 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller), formatter=formatter) | |
244 | ldap_connection = ldap3.Connection(ldap_server, **ldap_connection_kwargs) | |
112 | 245 | try: |
113 | 246 | 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() | |
247 | except ldap3.core.exceptions.LDAPSocketOpenError as e: | |
248 | self._logger.critical(e) | |
249 | self._logger.critical('TLS negociation failed, this error is mostly due to your host ' | |
250 | 'not supporting SHA1 as signing algorithm for certificates') | |
251 | sys.exit(-1) | |
121 | 252 | |
122 | 253 | self._ldap_connection = ldap_connection |
123 | 254 | |
124 | def _ldap_search(self, search_filter, class_result, attributes=list()): | |
255 | def _ldap_search(self, search_filter, class_result, attributes=list(), controls=list()): | |
125 | 256 | results = list() |
126 | ||
127 | # if no attribute name specified, we return all attributes | |
257 | ||
258 | # if no attribute name specified, we return all attributes | |
128 | 259 | 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 | |
142 | for result in search_results: | |
143 | if result['type'] is not 'searchResEntry': | |
144 | continue | |
145 | results.append(class_result(result['raw_attributes'])) | |
260 | attributes = ldap3.ALL_ATTRIBUTES | |
261 | ||
262 | self._logger.debug('search_base = {0} / search_filter = {1} / attributes = {2}'.format(self._base_dn, | |
263 | search_filter, | |
264 | attributes)) | |
265 | ||
266 | # Microsoft Active Directory set an hard limit of 1000 entries returned by any search | |
267 | search_results=self._ldap_connection.extend.standard.paged_search(search_base=self._base_dn, | |
268 | search_filter=search_filter, attributes=attributes, | |
269 | controls=controls, paged_size=1000, generator=True) | |
270 | ||
271 | try: | |
272 | # Skip searchResRef | |
273 | for result in search_results: | |
274 | if result['type'] != 'searchResEntry': | |
275 | continue | |
276 | results.append(class_result(result['attributes'])) | |
277 | ||
278 | except ldap3.core.exceptions.LDAPAttributeError as e: | |
279 | self._logger.critical(e) | |
280 | sys.exit(-1) | |
281 | ||
282 | if not results: | |
283 | self._logger.debug('Query returned an empty result') | |
146 | 284 | |
147 | 285 | return results |
148 | 286 | |
172 | 310 | try: |
173 | 311 | self._ldap_connection.unbind() |
174 | 312 | except AttributeError: |
313 | self._logger.warning('Error when unbinding') | |
175 | 314 | pass |
176 | 315 | self._ldap_connection = None |
177 | 316 | |
178 | 317 | class RPCRequester(): |
179 | 318 | def __init__(self, target_computer, domain=str(), user=(), password=str(), |
180 | lmhash=str(), nthash=str()): | |
319 | lmhash=str(), nthash=str(), do_kerberos=False): | |
181 | 320 | self._target_computer = target_computer |
182 | 321 | self._domain = domain |
183 | 322 | self._user = user |
184 | 323 | self._password = password |
185 | 324 | self._lmhash = lmhash |
186 | 325 | self._nthash = nthash |
326 | self._do_kerberos = do_kerberos | |
187 | 327 | self._pipe = None |
188 | 328 | self._rpc_connection = None |
189 | 329 | self._dcom = None |
190 | 330 | self._wmi_connection = None |
331 | ||
332 | logger = logging.getLogger('pywerview_main_logger.RPCRequester') | |
333 | self._logger = logger | |
191 | 334 | |
192 | 335 | def _create_rpc_connection(self, pipe): |
193 | 336 | # Here we build the DCE/RPC connection |
212 | 355 | rpctransport = transport.SMBTransport(self._target_computer, 445, self._pipe, |
213 | 356 | username=self._user, password=self._password, |
214 | 357 | domain=self._domain, lmhash=self._lmhash, |
215 | nthash=self._nthash) | |
358 | nthash=self._nthash, doKerberos=self._do_kerberos) | |
216 | 359 | |
217 | 360 | rpctransport.set_connect_timeout(10) |
218 | 361 | dce = rpctransport.get_dce_rpc() |
222 | 365 | |
223 | 366 | try: |
224 | 367 | dce.connect() |
225 | except socket.error: | |
368 | except Exception as e: | |
369 | self._logger.critical('Error when creating RPC connection') | |
370 | self._logger.critical(e) | |
226 | 371 | self._rpc_connection = None |
227 | 372 | else: |
228 | 373 | dce.bind(binding_strings[self._pipe[1:]]) |
231 | 376 | def _create_wmi_connection(self, namespace='root\\cimv2'): |
232 | 377 | try: |
233 | 378 | self._dcom = DCOMConnection(self._target_computer, self._user, self._password, |
234 | self._domain, self._lmhash, self._nthash) | |
235 | except DCERPCException: | |
379 | self._domain, self._lmhash, self._nthash, doKerberos=self._do_kerberos) | |
380 | except Exception as e: | |
381 | self._logger.critical('Error when creating WMI connection') | |
382 | self._logger.critical(e) | |
236 | 383 | self._dcom = None |
237 | 384 | else: |
238 | 385 | i_interface = self._dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, |
283 | 430 | |
284 | 431 | class LDAPRPCRequester(LDAPRequester, RPCRequester): |
285 | 432 | def __init__(self, target_computer, domain=str(), user=(), password=str(), |
286 | lmhash=str(), nthash=str(), domain_controller=str()): | |
433 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
434 | domain_controller=str()): | |
287 | 435 | # If no domain controller was given, we assume that the user wants to |
288 | 436 | # target a domain controller to perform LDAP requests against |
289 | 437 | if not domain_controller: |
290 | 438 | domain_controller = target_computer |
291 | 439 | LDAPRequester.__init__(self, domain_controller, domain, user, password, |
292 | lmhash, nthash) | |
440 | lmhash, nthash, do_kerberos, do_tls) | |
293 | 441 | RPCRequester.__init__(self, target_computer, domain, user, password, |
294 | lmhash, nthash) | |
442 | lmhash, nthash, do_kerberos) | |
443 | ||
444 | logger = logging.getLogger('pywerview_main_logger.LDAPRPCRequester') | |
445 | self._logger = logger | |
446 | ||
295 | 447 | def __enter__(self): |
296 | 448 | try: |
297 | 449 | LDAPRequester.__enter__(self) |
12 | 12 | # You should have received a copy of the GNU General Public License |
13 | 13 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. |
14 | 14 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | 16 | |
17 | 17 | from multiprocessing import Process, Pipe |
18 | 18 | |
21 | 21 | import pywerview.objects.rpcobjects as rpcobj |
22 | 22 | |
23 | 23 | class HunterWorker(Process): |
24 | def __init__(self, pipe, domain, user, password, lmhash, nthash): | |
24 | def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos): | |
25 | 25 | Process.__init__(self) |
26 | 26 | self._pipe = pipe |
27 | 27 | self._domain = domain |
29 | 29 | self._password = password |
30 | 30 | self._lmhash = lmhash |
31 | 31 | self._nthash = nthash |
32 | self._do_kerberos = do_kerberos | |
32 | 33 | |
33 | 34 | def terminate(self): |
34 | 35 | self._pipe.close() |
41 | 42 | self._pipe.send(result) |
42 | 43 | |
43 | 44 | class UserHunterWorker(HunterWorker): |
44 | def __init__(self, pipe, domain, user, password, lmhash, nthash, foreign_users, | |
45 | stealth, target_users, domain_short_name, check_access): | |
46 | HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash) | |
45 | def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos, | |
46 | foreign_users, stealth, target_users, domain_short_name, check_access): | |
47 | HunterWorker.__init__(self, pipe, domain, user, password, lmhash, | |
48 | nthash, do_kerberos) | |
47 | 49 | self._foreign_users = foreign_users |
48 | 50 | self._stealth = stealth |
49 | 51 | self._target_users = target_users |
57 | 59 | # First, we get every distant session on the target computer |
58 | 60 | distant_sessions = list() |
59 | 61 | with NetRequester(target_computer, self._domain, self._user, self._password, |
60 | self._lmhash, self._nthash) as net_requester: | |
62 | self._lmhash, self._nthash, self._do_kerberos) as net_requester: | |
61 | 63 | if not self._foreign_users: |
62 | 64 | distant_sessions += net_requester.get_netsession() |
63 | 65 | if not self._stealth: |
98 | 100 | |
99 | 101 | if self._check_access: |
100 | 102 | with Misc(target_computer, self._domain, self._user, self._password, |
101 | self._lmhash, self._nthash) as misc_requester: | |
103 | self._lmhash, self._nthash, self._do_kerberos) as misc_requester: | |
102 | 104 | attributes['localadmin'] = misc_requester.invoke_checklocaladminaccess() |
103 | 105 | else: |
104 | 106 | attributes['localadmin'] = str() |
108 | 110 | return results |
109 | 111 | |
110 | 112 | class ProcessHunterWorker(HunterWorker): |
111 | def __init__(self, pipe, domain, user, password, lmhash, nthash, process_name, | |
112 | target_users): | |
113 | HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash) | |
113 | def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos, | |
114 | process_name, target_users): | |
115 | HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos) | |
114 | 116 | self._process_name = process_name |
115 | 117 | self._target_users = target_users |
116 | 118 | |
119 | 121 | |
120 | 122 | distant_processes = list() |
121 | 123 | with NetRequester(target_computer, self._domain, self._user, self._password, |
122 | self._lmhash, self._nthash) as net_requester: | |
124 | self._lmhash, self._nthash, self._do_kerberos) as net_requester: | |
123 | 125 | distant_processes = net_requester.get_netprocess() |
124 | 126 | |
125 | 127 | for process in distant_processes: |
135 | 137 | return results |
136 | 138 | |
137 | 139 | class EventHunterWorker(HunterWorker): |
138 | def __init__(self, pipe, domain, user, password, lmhash, nthash, search_days, | |
139 | target_users): | |
140 | HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash) | |
140 | def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos, | |
141 | search_days, target_users): | |
142 | HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos) | |
141 | 143 | self._target_users = target_users |
142 | 144 | self._search_days = search_days |
143 | 145 | |
146 | 148 | |
147 | 149 | distant_processes = list() |
148 | 150 | with NetRequester(target_computer, self._domain, self._user, self._password, |
149 | self._lmhash, self._nthash) as net_requester: | |
151 | self._lmhash, self._nthash, self._do_kerberos) as net_requester: | |
150 | 152 | distant_events = net_requester.get_userevent(date_start=self._search_days) |
151 | 153 | |
152 | 154 | for event in distant_events: |
156 | 158 | results.append(event) |
157 | 159 | |
158 | 160 | return results |
161 |
0 | Metadata-Version: 2.1 | |
1 | Name: pywerview | |
2 | Version: 0.4.0 | |
3 | Summary: A Python port of PowerSploit's PowerView | |
4 | Home-page: https://github.com/the-useless-one/pywerview | |
5 | Author: Yannick Méheut | |
6 | Author-email: [email protected] | |
7 | License: GNU GPLv3 | |
8 | Keywords: python powersploit pentesting recon active directory windows | |
9 | Classifier: Environment :: Console | |
10 | Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) | |
11 | Classifier: Programming Language :: Python :: 3.6 | |
12 | Classifier: Topic :: Security | |
13 | Description-Content-Type: text/markdown | |
14 | License-File: LICENSE | |
15 | ||
16 | # PywerView | |
17 | ____ __ ___ | |
18 | | _ \ _ ___ _____ _ _\ \ / (_) _____ __ | |
19 | | |_) | | | \ \ /\ / / _ \ '__\ \ / /| |/ _ \ \ /\ / / | |
20 | | __/| |_| |\ V V / __/ | \ V / | | __/\ V V / | |
21 | |_| \__, | \_/\_/ \___|_| \_/ |_|\___| \_/\_/ | |
22 | |___/ | |
23 | ||
24 | A (partial) Python rewriting of [PowerSploit](https://github.com/PowerShellMafia/PowerSploit)'s | |
25 | [PowerView](https://github.com/PowerShellMafia/PowerSploit/tree/master/Recon). | |
26 | ||
27 | Fork me on [GitHub](https://github.com/the-useless-one/pywerview). | |
28 | ||
29 | [![License](https://img.shields.io/github/license/the-useless-one/pywerview)](https://github.com/the-useless-one/pywerview/blob/master/LICENSE) | |
30 | ![Python versions](https://img.shields.io/pypi/pyversions/pywerview) | |
31 | [![GitHub release](https://img.shields.io/github/v/release/the-useless-one/pywerview)](https://github.com/the-useless-one/pywerview/releases/latest) | |
32 | [![PyPI version](https://img.shields.io/pypi/v/pywerview)](https://pypi.org/project/pywerview/) | |
33 | ||
34 | ## HISTORY | |
35 | ||
36 | As a pentester, I love using PowerView during my assignments. It makes it so | |
37 | easy to find vulnerable machines, or list what domain users were added to the | |
38 | local Administrators group of a machine, and much more. | |
39 | ||
40 | However, running PowerView on a computer which is not connected to the domain | |
41 | is a pain: I always find myself using [mimikatz](https://github.com/gentilkiwi/mimikatz/)'s | |
42 | `sekurlsa::pth` to run a Powershell prompt with stolen domain credentials, and | |
43 | that's not easy to script. Plus, I'm a Linux guy and I've always found it a | |
44 | shame that there were no complete Windows/Active Directory enumeration tool on | |
45 | Linux. | |
46 | ||
47 | That's why I decided to rewrite some of PowerView's functionalities in Python, | |
48 | using the wonderful [impacket](https://github.com/SecureAuthCorp/impacket) | |
49 | library. | |
50 | ||
51 | *Update:* I haven't tested the last version of PowerView yet, which can run | |
52 | from a machine not connected to a domain. I don't know if it works correctly | |
53 | under Linux using Powershell. If anyone has had any experience with this at all, | |
54 | you can contact me, I'm really interested. We'll see if pywerview has become | |
55 | obsoleted ;) but I think I'll continue working on it eitherway: I'd still | |
56 | rather use Python than Powershell on Linux, and I'm learning a lot! Plus, it | |
57 | may integrated in existing Linux tools written in Python. It's still great news | |
58 | that PowerView now supports machines not connected to the domain! | |
59 | ||
60 | ## DISCLAIMER | |
61 | ||
62 | This tool is far from complete (as you'll see in the [TODO](#todo) section)! I | |
63 | still have a lot more awesome PowerView functionalities to implement (the user | |
64 | hunting functions, the GPO functions, the local process enumeration, etc.), | |
65 | but I still think it can be useful as is. | |
66 | ||
67 | It's also (very) possible that there are (many) bugs in the code: I've only | |
68 | tested the simplest test cases. If you use this tool during an assignment and | |
69 | you get an error, please, open an issue with the error and the conditions that | |
70 | triggered this error. | |
71 | ||
72 | Also, blah blah blah, don't use it for evil purposes. | |
73 | ||
74 | ## REQUIREMENTS | |
75 | ||
76 | * Python 3.6 | |
77 | * impacket >= 0.9.22 | |
78 | * ldap3 >= 2.8.1 | |
79 | * gssapi (Which requires `libkrb5-dev`) | |
80 | * pycryptodomex (or pycryptodome) | |
81 | ||
82 | ## FUNCTIONALITIES | |
83 | ||
84 | If you like living on the bleeding edge, check out the | |
85 | [development branch](https://github.com/the-useless-one/pywerview/tree/develop). | |
86 | ||
87 | Here's the list of available commands: | |
88 | ||
89 | $ pywerview.py --help | |
90 | usage: pywerview.py [-h] | |
91 | {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
92 | ... | |
93 | ||
94 | Rewriting of some PowerView's functionalities in Python | |
95 | ||
96 | optional arguments: | |
97 | -h, --help show this help message and exit | |
98 | ||
99 | Subcommands: | |
100 | Available subcommands | |
101 | ||
102 | {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter} | |
103 | get-adobject Takes a domain SID, samAccountName or name, and return the associated object | |
104 | get-adserviceaccount | |
105 | Returns a list of all the gMSA of the specified domain (you need privileged account to retrieve passwords) | |
106 | get-objectacl Takes a domain SID, samAccountName or name, and return the ACL of the associated object | |
107 | get-netuser Queries information about a domain user | |
108 | get-netgroup Get a list of all current domain groups, or a list of groups a domain user is member of | |
109 | get-netcomputer Queries informations about domain computers | |
110 | get-netdomaincontroller | |
111 | Get a list of domain controllers for the given domain | |
112 | get-netfileserver Return a list of file servers, extracted from the domain users' homeDirectory, scriptPath, and profilePath fields | |
113 | get-dfsshare Return a list of all fault tolerant distributed file systems for a given domain | |
114 | get-netou Get a list of all current OUs in the domain | |
115 | get-netsite Get a list of all current sites in the domain | |
116 | get-netsubnet Get a list of all current subnets in the domain | |
117 | get-netdomaintrust Returns a list of all the trusts of the specified domain | |
118 | get-netgpo Get a list of all current GPOs in the domain | |
119 | get-netpso Get a list of all current PSOs in the domain | |
120 | get-domainpolicy Returns the default domain or DC policy for the queried domain or DC | |
121 | get-gpttmpl Helper to parse a GptTmpl.inf policy file path into a custom object | |
122 | get-netgpogroup Parses all GPOs in the domain that set "Restricted Group" or "Groups.xml" | |
123 | find-gpocomputeradmin | |
124 | Takes a computer (or OU) and determine who has administrative access to it via GPO | |
125 | find-gpolocation Takes a username or a group name and determine the computers it has administrative access to via GPO | |
126 | get-netgroupmember Return a list of members of a domain group | |
127 | get-netsession Queries a host to return a list of active sessions on the host (you can use local credentials instead of domain credentials) | |
128 | get-localdisks Queries a host to return a list of active disks on the host (you can use local credentials instead of domain credentials) | |
129 | get-netdomain Queries a host for available domains | |
130 | get-netshare Queries a host to return a list of available shares on the host (you can use local credentials instead of domain credentials) | |
131 | get-netloggedon This function will execute the NetWkstaUserEnum RPC call to query a given host for actively logged on users | |
132 | get-netlocalgroup Gets a list of members of a local group on a machine, or returns every local group. You can use local credentials instead of domain credentials, however, domain credentials are needed | |
133 | to resolve domain SIDs. | |
134 | invoke-checklocaladminaccess | |
135 | Checks if the given user has local admin access on the given host | |
136 | get-netprocess This function will execute the 'Select * from Win32_Process' WMI query to a given host for a list of executed process | |
137 | get-userevent This function will execute the 'SELECT * from Win32_NTLogEvent' WMI query to a given host for a list of executed process | |
138 | invoke-userhunter Finds which machines domain users are logged into | |
139 | invoke-processhunter | |
140 | Searches machines for processes with specific name, or ran by specific users | |
141 | invoke-eventhunter Searches machines for events with specific name, or ran by specific users | |
142 | ||
143 | Take a look at the [wiki](https://github.com/the-useless-one/pywerview/wiki) to | |
144 | see a more detailed usage of every command. | |
145 | ||
146 | *Attention:* in every command, the used domain name must be the post-Win2k UPN, | |
147 | and not the Win2k compatible name. | |
148 | ||
149 | For example, my domain name is `uselessdomain.local`. The Win2K compatible name | |
150 | is `USELESSDOMAIN`. In every command, I must use __`uselessdomain.local`__ as | |
151 | an argument, and __not__ `USELESSDOMAIN`. | |
152 | ||
153 | ## GLOBAL ARGUMENTS | |
154 | ||
155 | ### LOGGING | |
156 | ||
157 | You can provide a logging level to `pywerview` modules by using `-l` or `--logging-level` options. Supported levels are: | |
158 | ||
159 | * `CRITICAL`: Only critical errors are displayed **(default)** | |
160 | * `WARNING` Warnings are displayed, along with citical errors | |
161 | * `DEBUG`: Debug level (caution: **very** verbose) | |
162 | * `ULTRA`: Extreme debugging level (caution: **very very** verbose) | |
163 | ||
164 | (level names are case insensitive) | |
165 | ||
166 | ### Kerberos authentication | |
167 | ||
168 | Kerberos authentication is now (partially) supported, which means you can | |
169 | pass the ticket and other stuff. To authenticate via Kerberos: | |
170 | ||
171 | 1. Point the `KRB5CCNAME` environment variable to your cache credential file. | |
172 | 2. Use the `-k` option in your function call, or the `do_kerberos` in your | |
173 | library call. | |
174 | ||
175 | ```console | |
176 | $ klist stormtroopers.ccache | |
177 | Ticket cache: FILE:stormtroopers.ccache | |
178 | Default principal: [email protected] | |
179 | ||
180 | Valid starting Expires Service principal | |
181 | 10/03/2022 16:46:45 11/03/2022 02:46:45 ldap/[email protected] | |
182 | renew until 11/03/2022 16:43:17 | |
183 | $ KRB5CCNAME=stormtroopers.ccache python3 pywerview.py get-netcomputer -t srv-ad.contoso.com -u stormtroopers -k | |
184 | dnshostname: centos.contoso.com | |
185 | ||
186 | dnshostname: debian.contoso.com | |
187 | ||
188 | dnshostname: Windows7.contoso.com | |
189 | ||
190 | dnshostname: Windows10.contoso.com | |
191 | ||
192 | dnshostname: SRV-MAIL.contoso.com | |
193 | ||
194 | dnshostname: SRV-AD.contoso.com | |
195 | ``` | |
196 | ||
197 | If your cache credential file contains a corresponding TGS, or a TGT for your | |
198 | calling user, Kerberos authentication will be used. | |
199 | ||
200 | __SPN patching is partial__. Right now, we're in a mixed configuration where we | |
201 | use `ldap3` for LDAP commands and `impacket` for the other protocols (SMB, | |
202 | RPC). That is because `impacket`'s LDAP implementation has several problems, | |
203 | such as mismanagement of non-ASCII characters (which is problematic for us | |
204 | baguette-eaters). | |
205 | ||
206 | `ldap3` uses `gssapi` to authenticate with Kerberos, and `gssapi` needs the | |
207 | full hostname in the SPN of a ticket, otherwise it throws an error. It would | |
208 | be possible to patch an SPN with an incomplete hostname, however it's not done | |
209 | for now. | |
210 | ||
211 | For any functions that only rely on `impacket` (SMB or RPC functions), you can | |
212 | use tickets with SPNs with an incomplete hostname. In the following example, we | |
213 | use an LDAP ticket with an incomplete hostname for an SMB function, without any | |
214 | trouble. You just have to make sure that the `--computername` argument matches | |
215 | this incomplete hostname in the SPN: | |
216 | ||
217 | ```console | |
218 | $ klist skywalker.ccache | |
219 | Ticket cache: FILE:skywalker.ccache | |
220 | Default principal: [email protected] | |
221 | ||
222 | Valid starting Expires Service principal | |
223 | 13/04/2022 14:26:59 14/04/2022 00:26:58 ldap/[email protected] | |
224 | renew until 14/04/2022 14:23:29 | |
225 | $ KRB5CCNAME=skywalker.ccache python3 pywerview.py get-localdisks --computername srv-ad -u skywalker -k | |
226 | disk: A: | |
227 | ||
228 | disk: C: | |
229 | ||
230 | disk: D: | |
231 | ``` | |
232 | ||
233 | To recap: | |
234 | ||
235 | | SPN in the ticket | Can be used with LDAP functions | Can be used with SMB/RPC functions | | |
236 | | :-----------------------------------: | :-----------------------------: | :--------------------------------: | | |
237 | | `ldap/[email protected]` | ✔️ | ✔️ | | |
238 | | `cifs/[email protected]` | ✔️ | ✔️ | | |
239 | | `ldap/[email protected]` | ❌ | ✔️ | | |
240 | ||
241 | ### TLS CONNECTION | |
242 | ||
243 | You can force a connection to the LDAPS port by using the `--tls` switch. It | |
244 | can be necessary with some functions, for example when retrieving gMSA | |
245 | passwords with `get-adserviceaccount`: | |
246 | ||
247 | ```console | |
248 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids | |
249 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
250 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
251 | samaccountname: gMSA-01$ | |
252 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
253 | description: | |
254 | enabled: True | |
255 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids --tls | |
256 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
257 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
258 | samaccountname: gMSA-01$ | |
259 | msds-managedpassword: 69730ce3914ac6[redacted] | |
260 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
261 | description: | |
262 | enabled: True | |
263 | ``` | |
264 | ||
265 | ### JSON OUTPUT | |
266 | ||
267 | Pywerview can print results in json format by using the `--json` switch. | |
268 | ||
269 | ## TODO | |
270 | ||
271 | * Many, many more PowerView functionalities to implement. I'll now focus on | |
272 | forest functions, then inter-forest trust functions | |
273 | * Lots of rewrite due to the last version of PowerView | |
274 | * Gracefully fail against Unix machines running Samba | |
275 | * Perform range cycling in `get-netgroupmember` | |
276 | * Manage request to the Global Catalog | |
277 | * Try to fall back to `tcp/139` for RPC communications if `tcp/445` is closed | |
278 | * Comment, document, and clean the code | |
279 | ||
280 | ## THANKS | |
281 | ||
282 | * Thanks to the [@PowerSploit](https://github.com/PowerShellMafia/PowerSploit/) | |
283 | team for an awesome tool. | |
284 | * Thanks to [@CoreSecurity](https://github.com/CoreSecurity/) for this complete | |
285 | and comprehensive library that is [impacket](https://github.com/CoreSecurity/impacket). | |
286 | * Special thanks to [@asolino](https://github.com/asolino) for his help on | |
287 | developing using impacket. | |
288 | * Thanks to [@byt3bl33d3r](https://github.com/byt3bl33d3r) for his | |
289 | contributions. | |
290 | * Thanks to [@ThePirateWhoSmellsOfSunflowers](https://github.com/ThePirateWhoSmellsOfSunflowers) | |
291 | for his debugging, love you baby :heart: | |
292 | * Thanks to [@mpgn](https://github.com/mpgn) for his python 3 contributions. | |
293 | ||
294 | ## COPYRIGHT | |
295 | ||
296 | PywerView - A Python rewriting of PowerSploit's PowerView | |
297 | ||
298 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
299 | ||
300 | This program is free software: you can redistribute it and/or modify it | |
301 | under the terms of the GNU General Public License as published by the | |
302 | Free Software Foundation, either version 3 of the License, or (at your | |
303 | option) any later version. | |
304 | ||
305 | This program is distributed in the hope that it will be useful, but | |
306 | WITHOUT ANY WARRANTY; without even the implied warranty of | |
307 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General | |
308 | Public License for more details. | |
309 | ||
310 | You should have received a copy of the GNU General Public License along | |
311 | with this program. If not, see | |
312 | [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). | |
313 |
0 | LICENSE | |
1 | MANIFEST.in | |
2 | README.md | |
3 | setup.cfg | |
4 | setup.py | |
5 | pywerview/__init__.py | |
6 | pywerview/formatters.py | |
7 | pywerview/requester.py | |
8 | pywerview.egg-info/PKG-INFO | |
9 | pywerview.egg-info/SOURCES.txt | |
10 | pywerview.egg-info/dependency_links.txt | |
11 | pywerview.egg-info/entry_points.txt | |
12 | pywerview.egg-info/not-zip-safe | |
13 | pywerview.egg-info/requires.txt | |
14 | pywerview.egg-info/top_level.txt | |
15 | pywerview/cli/__init__.py | |
16 | pywerview/cli/helpers.py | |
17 | pywerview/cli/main.py | |
18 | pywerview/functions/__init__.py | |
19 | pywerview/functions/gpo.py | |
20 | pywerview/functions/hunting.py | |
21 | pywerview/functions/misc.py | |
22 | pywerview/functions/net.py | |
23 | pywerview/objects/__init__.py | |
24 | pywerview/objects/adobjects.py | |
25 | pywerview/objects/rpcobjects.py | |
26 | pywerview/worker/__init__.py | |
27 | pywerview/worker/hunting.py⏎ |
0 | https://github.com/SecureAuthCorp/impacket/tarball/master#egg=impacket-0.9.22 |
0 | pywerview |
0 | #!/usr/bin/env python3 | |
1 | # | |
2 | # This file is part of PywerView. | |
3 | ||
4 | # PywerView is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU General Public License as published by | |
6 | # the Free Software Foundation, either version 3 of the License, or | |
7 | # (at your option) any later version. | |
8 | ||
9 | # PywerView is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. | |
13 | ||
14 | # You should have received a copy of the GNU General Public License | |
15 | # along with PywerView. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
18 | ||
19 | from pywerview.cli.main import main | |
20 | ||
21 | if __name__ == '__main__': | |
22 | main() | |
23 |
0 | 0 | [metadata] |
1 | description-file = README.md | |
1 | description_file = README.md | |
2 | ||
3 | [egg_info] | |
4 | tag_build = | |
5 | tag_date = 0 | |
6 |
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.4.0', | |
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', |
29 | 26 | install_requires=[ |
30 | 27 | 'impacket>=0.9.22', |
31 | 28 | 'bs4', |
32 | 'lxml' | |
29 | 'lxml', | |
30 | 'pyasn1', | |
31 | 'ldap3>=2.8.1', | |
32 | 'gssapi', | |
33 | 'pycryptodome', | |
33 | 34 | ], |
34 | 35 | entry_points = { |
35 | 36 | 'console_scripts': ['pywerview=pywerview.cli.main:main'], |