Import upstream version 0.4.0
Kali Janitor
2 years ago
60 | 60 | * Python 3.6 |
61 | 61 | * impacket >= 0.9.22 |
62 | 62 | * ldap3 >= 2.8.1 |
63 | * gssapi (Which requires `libkrb5-dev`) | |
63 | 64 | |
64 | 65 | ## FUNCTIONALITIES |
65 | 66 | |
68 | 69 | |
69 | 70 | Here's the list of available commands: |
70 | 71 | |
71 | $ ./pywerview.py --help | |
72 | $ pywerview.py --help | |
72 | 73 | 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} | |
74 | {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 | 75 | ... |
75 | 76 | |
76 | 77 | Rewriting of some PowerView's functionalities in Python |
81 | 82 | Subcommands: |
82 | 83 | Available subcommands |
83 | 84 | |
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 | |
85 | {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} | |
86 | get-adobject Takes a domain SID, samAccountName or name, and return the associated object | |
87 | get-adserviceaccount | |
88 | Returns a list of all the gMSA of the specified domain (you need privileged account to retrieve passwords) | |
89 | get-objectacl Takes a domain SID, samAccountName or name, and return the ACL of the associated object | |
87 | 90 | 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 | |
91 | get-netgroup Get a list of all current domain groups, or a list of groups a domain user is member of | |
90 | 92 | get-netcomputer Queries informations about domain computers |
91 | 93 | get-netdomaincontroller |
92 | 94 | 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 | |
95 | get-netfileserver Return a list of file servers, extracted from the domain users' homeDirectory, scriptPath, and profilePath fields | |
96 | get-dfsshare Return a list of all fault tolerant distributed file systems for a given domain | |
98 | 97 | get-netou Get a list of all current OUs in the domain |
99 | 98 | get-netsite Get a list of all current sites in the domain |
100 | 99 | get-netsubnet Get a list of all current subnets in the domain |
100 | get-netdomaintrust Returns a list of all the trusts of the specified domain | |
101 | 101 | 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" | |
102 | get-netpso Get a list of all current PSOs in the domain | |
103 | get-domainpolicy Returns the default domain or DC policy for the queried domain or DC | |
104 | get-gpttmpl Helper to parse a GptTmpl.inf policy file path into a custom object | |
105 | get-netgpogroup Parses all GPOs in the domain that set "Restricted Group" or "Groups.xml" | |
108 | 106 | 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 | |
107 | Takes a computer (or OU) and determine who has administrative access to it via GPO | |
108 | find-gpolocation Takes a username or a group name and determine the computers it has administrative access to via GPO | |
113 | 109 | 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) | |
110 | get-netsession Queries a host to return a list of active sessions on the host (you can use local credentials instead of domain credentials) | |
111 | 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 | 112 | 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. | |
113 | get-netshare Queries a host to return a list of available shares on the host (you can use local credentials instead of domain credentials) | |
114 | get-netloggedon This function will execute the NetWkstaUserEnum RPC call to query a given host for actively logged on users | |
115 | 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 | |
116 | to resolve domain SIDs. | |
131 | 117 | 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 | |
118 | Checks if the given user has local admin access on the given host | |
119 | get-netprocess This function will execute the 'Select * from Win32_Process' WMI query to a given host for a list of executed process | |
120 | get-userevent This function will execute the 'SELECT * from Win32_NTLogEvent' WMI query to a given host for a list of executed process | |
140 | 121 | invoke-userhunter Finds which machines domain users are logged into |
141 | 122 | 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 | |
123 | Searches machines for processes with specific name, or ran by specific users | |
124 | invoke-eventhunter Searches machines for events with specific name, or ran by specific users | |
146 | 125 | |
147 | 126 | Take a look at the [wiki](https://github.com/the-useless-one/pywerview/wiki) to |
148 | 127 | see a more detailed usage of every command. |
154 | 133 | is `USELESSDOMAIN`. In every command, I must use __`uselessdomain.local`__ as |
155 | 134 | an argument, and __not__ `USELESSDOMAIN`. |
156 | 135 | |
136 | ## GLOBAL ARGUMENTS | |
137 | ||
138 | ### LOGGING | |
139 | ||
140 | You can provide a logging level to `pywerview` modules by using `-l` or `--logging-level` options. Supported levels are: | |
141 | ||
142 | * `CRITICAL`: Only critical errors are displayed **(default)** | |
143 | * `WARNING` Warnings are displayed, along with citical errors | |
144 | * `DEBUG`: Debug level (caution: **very** verbose) | |
145 | * `ULTRA`: Extreme debugging level (caution: **very very** verbose) | |
146 | ||
147 | (level names are case insensitive) | |
148 | ||
149 | ### Kerberos authentication | |
150 | ||
151 | Kerberos authentication is now (partially) supported, which means you can | |
152 | pass the ticket and other stuff. To authenticate via Kerberos: | |
153 | ||
154 | 1. Point the `KRB5CCNAME` environment variable to your cache credential file. | |
155 | 2. Use the `-k` option in your function call, or the `do_kerberos` in your | |
156 | library call. | |
157 | ||
158 | ```console | |
159 | $ klist stormtroopers.ccache | |
160 | Ticket cache: FILE:stormtroopers.ccache | |
161 | Default principal: [email protected] | |
162 | ||
163 | Valid starting Expires Service principal | |
164 | 10/03/2022 16:46:45 11/03/2022 02:46:45 ldap/[email protected] | |
165 | renew until 11/03/2022 16:43:17 | |
166 | $ KRB5CCNAME=stormtroopers.ccache python3 pywerview.py get-netcomputer -t srv-ad.contoso.com -u stormtroopers -k | |
167 | dnshostname: centos.contoso.com | |
168 | ||
169 | dnshostname: debian.contoso.com | |
170 | ||
171 | dnshostname: Windows7.contoso.com | |
172 | ||
173 | dnshostname: Windows10.contoso.com | |
174 | ||
175 | dnshostname: SRV-MAIL.contoso.com | |
176 | ||
177 | dnshostname: SRV-AD.contoso.com | |
178 | ``` | |
179 | ||
180 | If your cache credential file contains a corresponding TGS, or a TGT for your | |
181 | calling user, Kerberos authentication will be used. | |
182 | ||
183 | __SPN patching is partial__. Right now, we're in a mixed configuration where we | |
184 | use `ldap3` for LDAP commands and `impacket` for the other protocols (SMB, | |
185 | RPC). That is because `impacket`'s LDAP implementation has several problems, | |
186 | such as mismanagement of non-ASCII characters (which is problematic for us | |
187 | baguette-eaters). | |
188 | ||
189 | `ldap3` uses `gssapi` to authenticate with Kerberos, and `gssapi` needs the | |
190 | full hostname in the SPN of a ticket, otherwise it throws an error. It would | |
191 | be possible to patch an SPN with an incomplete hostname, however it's not done | |
192 | for now. | |
193 | ||
194 | For any functions that only rely on `impacket` (SMB or RPC functions), you can | |
195 | use tickets with SPNs with an incomplete hostname. In the following example, we | |
196 | use an LDAP ticket with an incomplete hostname for an SMB function, without any | |
197 | trouble. You just have to make sure that the `--computername` argument matches | |
198 | this incomplete hostname in the SPN: | |
199 | ||
200 | ```console | |
201 | $ klist skywalker.ccache | |
202 | Ticket cache: FILE:skywalker.ccache | |
203 | Default principal: [email protected] | |
204 | ||
205 | Valid starting Expires Service principal | |
206 | 13/04/2022 14:26:59 14/04/2022 00:26:58 ldap/[email protected] | |
207 | renew until 14/04/2022 14:23:29 | |
208 | $ KRB5CCNAME=skywalker.ccache python3 pywerview.py get-localdisks --computername srv-ad -u skywalker -k | |
209 | disk: A: | |
210 | ||
211 | disk: C: | |
212 | ||
213 | disk: D: | |
214 | ``` | |
215 | ||
216 | To recap: | |
217 | ||
218 | | SPN in the ticket | Can be used with LDAP functions | Can be used with SMB/RPC functions | | |
219 | | :-----------------------------------: | :-----------------------------: | :--------------------------------: | | |
220 | | `ldap/[email protected]` | ✔️ | ✔️ | | |
221 | | `cifs/[email protected]` | ✔️ | ✔️ | | |
222 | | `ldap/[email protected]` | ❌ | ✔️ | | |
223 | ||
224 | ### TLS CONNECTION | |
225 | ||
226 | You can force a connection to the LDAPS port by using the `--tls` switch. It | |
227 | can be necessary with some functions, for example when retrieving gMSA | |
228 | passwords with `get-adserviceaccount`: | |
229 | ||
230 | ```console | |
231 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids | |
232 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
233 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
234 | samaccountname: gMSA-01$ | |
235 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
236 | description: | |
237 | enabled: True | |
238 | $ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids --tls | |
239 | distinguishedname: CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com | |
240 | objectsid: S-1-5-21-863927164-4106933278-53377030-3115 | |
241 | samaccountname: gMSA-01$ | |
242 | msds-managedpassword: 69730ce3914ac6[redacted] | |
243 | msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com | |
244 | description: | |
245 | enabled: True | |
246 | ``` | |
247 | ||
248 | ### JSON OUTPUT | |
249 | ||
250 | Pywerview can print results in json format by using the `--json` switch. | |
251 | ||
157 | 252 | ## TODO |
158 | 253 | |
159 | 254 | * Many, many more PowerView functionalities to implement. I'll now focus on |
160 | 255 | forest functions, then inter-forest trust functions |
161 | 256 | * Lots of rewrite due to the last version of PowerView |
162 | * Implement a debugging mode (for easier troubleshooting) | |
163 | 257 | * Gracefully fail against Unix machines running Samba |
164 | * Support Kerberos authentication | |
165 | 258 | * Perform range cycling in `get-netgroupmember` |
166 | 259 | * Manage request to the Global Catalog |
167 | 260 | * Try to fall back to `tcp/139` for RPC communications if `tcp/445` is closed |
185 | 278 | |
186 | 279 | PywerView - A Python rewriting of PowerSploit's PowerView |
187 | 280 | |
188 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021 | |
281 | Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
189 | 282 | |
190 | 283 | This program is free software: you can redistribute it and/or modify it |
191 | 284 | under the terms of the GNU General Public License as published by the |
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(), | |
27 | attributes=list(), custom_filter=str()): | |
28 | requester = NetRequester(domain_controller, domain, user, password, | |
29 | lmhash, nthash) | |
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(), | |
28 | custom_filter=str()): | |
29 | requester = NetRequester(domain_controller, domain, user, password, | |
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 | 34 | ads_path=ads_path, attributes=attributes, custom_filter=custom_filter) |
34 | 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 | ||
35 | 47 | def get_objectacl(domain_controller, domain, user, password=str(), |
36 | lmhash=str(), nthash=str(), queried_domain=str(), queried_sid=str(), | |
37 | queried_name=str(), queried_sam_account_name=str(), ads_path=str(), | |
38 | sacl=False, rights_filter=str(), resolve_sids=False, | |
39 | resolve_guids=False, custom_filter=str()): | |
40 | requester = NetRequester(domain_controller, domain, user, password, | |
41 | lmhash, nthash) | |
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) | |
42 | 55 | return requester.get_objectacl(queried_domain=queried_domain, |
43 | 56 | queried_sid=queried_sid, queried_name=queried_name, |
44 | 57 | queried_sam_account_name=queried_sam_account_name, |
47 | 60 | custom_filter=custom_filter) |
48 | 61 | |
49 | 62 | def get_netuser(domain_controller, domain, user, password=str(), lmhash=str(), |
50 | nthash=str(), queried_username=str(), queried_domain=str(), ads_path=str(), | |
51 | admin_count=False, spn=False, unconstrained=False, allow_delegation=False, | |
52 | preauth_notreq=False, custom_filter=str(), | |
53 | attributes=[]): | |
54 | requester = NetRequester(domain_controller, domain, user, password, | |
55 | 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) | |
56 | 69 | return requester.get_netuser(queried_username=queried_username, |
57 | 70 | queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count, |
58 | 71 | spn=spn, unconstrained=unconstrained, allow_delegation=allow_delegation, |
60 | 73 | attributes=attributes) |
61 | 74 | |
62 | 75 | def get_netgroup(domain_controller, domain, user, password=str(), |
63 | lmhash=str(), nthash=str(), queried_groupname='*', queried_sid=str(), | |
64 | queried_username=str(), queried_domain=str(), ads_path=str(), | |
65 | admin_count=False, full_data=False, custom_filter=str()): | |
66 | requester = NetRequester(domain_controller, domain, user, password, | |
67 | 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) | |
68 | 82 | return requester.get_netgroup(queried_groupname=queried_groupname, |
69 | 83 | queried_sid=queried_sid, queried_username=queried_username, |
70 | 84 | queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count, |
71 | 85 | full_data=full_data, custom_filter=custom_filter) |
72 | 86 | |
73 | 87 | def get_netcomputer(domain_controller, domain, user, password=str(), |
74 | lmhash=str(), nthash=str(), queried_computername='*', queried_spn=str(), | |
75 | 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(), | |
76 | 91 | printers=False, unconstrained=False, ping=False, full_data=False, |
77 | 92 | custom_filter=str(), attributes=[]): |
78 | 93 | requester = NetRequester(domain_controller, domain, user, password, |
79 | lmhash, nthash) | |
94 | lmhash, nthash, do_kerberos, do_tls) | |
80 | 95 | return requester.get_netcomputer(queried_computername=queried_computername, |
81 | 96 | queried_spn=queried_spn, queried_os=queried_os, queried_sp=queried_sp, |
82 | 97 | queried_domain=queried_domain, ads_path=ads_path, printers=printers, |
84 | 99 | custom_filter=custom_filter, attributes=attributes) |
85 | 100 | |
86 | 101 | def get_netdomaincontroller(domain_controller, domain, user, password=str(), |
87 | lmhash=str(), nthash=str(), queried_domain=str()): | |
88 | requester = NetRequester(domain_controller, domain, user, password, | |
89 | 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) | |
90 | 106 | return requester.get_netdomaincontroller(queried_domain=queried_domain) |
91 | 107 | |
92 | 108 | def get_netfileserver(domain_controller, domain, user, password=str(), |
93 | lmhash=str(), nthash=str(), queried_domain=str(), target_users=list()): | |
94 | requester = NetRequester(domain_controller, domain, user, password, | |
95 | 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) | |
96 | 113 | return requester.get_netfileserver(queried_domain=queried_domain, |
97 | 114 | target_users=target_users) |
98 | 115 | |
99 | 116 | def get_dfsshare(domain_controller, domain, user, password=str(), |
100 | lmhash=str(), nthash=str(), version=['v1', 'v2'], queried_domain=str(), | |
101 | ads_path=str()): | |
102 | requester = NetRequester(domain_controller, domain, user, password, | |
103 | 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) | |
104 | 121 | return requester.get_dfsshare(version=version, queried_domain=queried_domain, ads_path=ads_path) |
105 | 122 | |
106 | 123 | def get_netou(domain_controller, domain, user, password=str(), lmhash=str(), |
107 | nthash=str(), queried_domain=str(), queried_ouname='*', queried_guid=str(), | |
108 | ads_path=str(), full_data=False): | |
109 | requester = NetRequester(domain_controller, domain, user, password, | |
110 | 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) | |
111 | 128 | return requester.get_netou(queried_domain=queried_domain, |
112 | 129 | queried_ouname=queried_ouname, queried_guid=queried_guid, ads_path=ads_path, |
113 | 130 | full_data=full_data) |
114 | 131 | |
115 | 132 | def get_netsite(domain_controller, domain, user, password=str(), lmhash=str(), |
116 | nthash=str(), queried_domain=str(), queried_sitename=str(), | |
117 | queried_guid=str(), ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', | |
118 | full_data=False): | |
119 | requester = NetRequester(domain_controller, domain, user, password, | |
120 | 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) | |
121 | 138 | return requester.get_netsite(queried_domain=queried_domain, |
122 | 139 | queried_sitename=queried_sitename, queried_guid=queried_guid, |
123 | 140 | ads_path=ads_path, ads_prefix=ads_prefix, full_data=full_data) |
124 | 141 | |
125 | 142 | def get_netsubnet(domain_controller, domain, user, password=str(), |
126 | lmhash=str(), nthash=str(), queried_domain=str(), queried_sitename=str(), | |
127 | ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', full_data=False): | |
128 | requester = NetRequester(domain_controller, domain, user, password, | |
129 | 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) | |
130 | 148 | return requester.get_netsubnet(queried_domain=queried_domain, |
131 | 149 | queried_sitename=queried_sitename, ads_path=ads_path, ads_prefix=ads_prefix, |
132 | 150 | full_data=full_data) |
133 | 151 | |
134 | 152 | def get_netdomaintrust(domain_controller, domain, user, password=str(), |
135 | lmhash=str(), nthash=str(), queried_domain=str()): | |
136 | requester = NetRequester(domain_controller, domain, user, password, | |
137 | 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) | |
138 | 156 | return requester.get_netdomaintrust(queried_domain=queried_domain) |
139 | 157 | |
140 | 158 | def get_netgroupmember(domain_controller, domain, user, password=str(), |
141 | lmhash=str(), nthash=str(), queried_groupname=str(), queried_sid=str(), | |
142 | 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, | |
143 | 162 | full_data=False, custom_filter=str()): |
144 | 163 | requester = NetRequester(domain_controller, domain, user, password, |
145 | lmhash, nthash) | |
164 | lmhash, nthash, do_kerberos, do_tls) | |
146 | 165 | return requester.get_netgroupmember(queried_groupname=queried_groupname, |
147 | 166 | queried_sid=queried_sid, queried_domain=queried_domain, |
148 | 167 | ads_path=ads_path, recurse=recurse, |
150 | 169 | full_data=full_data, custom_filter=custom_filter) |
151 | 170 | |
152 | 171 | def get_netsession(target_computername, domain, user, password=str(), |
153 | lmhash=str(), nthash=str()): | |
154 | requester = NetRequester(target_computername, domain, user, password, | |
155 | lmhash, nthash) | |
172 | lmhash=str(), nthash=str(), do_kerberos=False): | |
173 | requester = NetRequester(target_computername, domain, user, password, | |
174 | lmhash, nthash, do_kerberos) | |
156 | 175 | return requester.get_netsession() |
157 | 176 | |
158 | 177 | def get_netshare(target_computername, domain, user, password=str(), |
159 | lmhash=str(), nthash=str()): | |
160 | requester = NetRequester(target_computername, domain, user, password, | |
161 | lmhash, nthash) | |
178 | lmhash=str(), nthash=str(), do_kerberos=False): | |
179 | requester = NetRequester(target_computername, domain, user, password, | |
180 | lmhash, nthash, do_kerberos) | |
162 | 181 | return requester.get_netshare() |
163 | 182 | |
164 | 183 | def get_localdisks(target_computername, domain, user, password=str(), |
165 | lmhash=str(), nthash=str()): | |
166 | requester = NetRequester(target_computername, domain, user, password, | |
167 | lmhash, nthash) | |
184 | lmhash=str(), nthash=str(), do_kerberos=False): | |
185 | requester = NetRequester(target_computername, domain, user, password, | |
186 | lmhash, nthash, do_kerberos) | |
168 | 187 | return requester.get_localdisks() |
169 | 188 | |
170 | 189 | def get_netdomain(domain_controller, domain, user, password=str(), |
171 | lmhash=str(), nthash=str()): | |
172 | requester = NetRequester(domain_controller, domain, user, password, | |
173 | 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) | |
174 | 194 | return requester.get_netdomain() |
175 | 195 | |
176 | 196 | def get_netloggedon(target_computername, domain, user, password=str(), |
177 | lmhash=str(), nthash=str()): | |
178 | requester = NetRequester(target_computername, domain, user, password, | |
179 | lmhash, nthash) | |
197 | lmhash=str(), nthash=str(), do_kerberos=False): | |
198 | requester = NetRequester(target_computername, domain, user, password, | |
199 | lmhash, nthash, do_kerberos) | |
180 | 200 | return requester.get_netloggedon() |
181 | 201 | |
182 | 202 | def get_netlocalgroup(target_computername, domain_controller, domain, user, |
183 | password=str(), lmhash=str(), nthash=str(), queried_groupname=str(), | |
184 | list_groups=False, recurse=False): | |
185 | requester = NetRequester(target_computername, domain, user, password, | |
186 | 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) | |
187 | 208 | return requester.get_netlocalgroup(queried_groupname=queried_groupname, |
188 | 209 | list_groups=list_groups, recurse=recurse) |
189 | 210 | |
190 | 211 | def get_netprocess(target_computername, domain, user, password=str(), |
191 | lmhash=str(), nthash=str()): | |
192 | requester = NetRequester(target_computername, domain, user, password, | |
193 | lmhash, nthash) | |
212 | lmhash=str(), nthash=str(), do_kerberos=False): | |
213 | requester = NetRequester(target_computername, domain, user, password, | |
214 | lmhash, nthash, do_kerberos) | |
194 | 215 | return requester.get_netprocess() |
195 | 216 | |
196 | 217 | def get_userevent(target_computername, domain, user, password=str(), |
197 | lmhash=str(), nthash=str(), event_type=['logon', 'tgt'], | |
198 | date_start=5): | |
199 | requester = NetRequester(target_computername, domain, user, password, | |
200 | 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) | |
201 | 222 | return requester.get_userevent(event_type=event_type, |
202 | 223 | date_start=date_start) |
203 | 224 | |
204 | 225 | def get_netgpo(domain_controller, domain, user, password=str(), |
205 | lmhash=str(), nthash=str(), queried_gponame='*', | |
206 | queried_displayname=str(), queried_domain=str(), ads_path=str()): | |
207 | requester = GPORequester(domain_controller, domain, user, password, | |
208 | 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) | |
209 | 231 | return requester.get_netgpo(queried_gponame=queried_gponame, |
210 | 232 | queried_displayname=queried_displayname, |
211 | 233 | queried_domain=queried_domain, ads_path=ads_path) |
212 | 234 | |
213 | 235 | def get_netpso(domain_controller, domain, user, password=str(), |
214 | lmhash=str(), nthash=str(), queried_psoname='*', | |
215 | queried_displayname=str(), queried_domain=str(), ads_path=str()): | |
216 | requester = GPORequester(domain_controller, domain, user, password, | |
217 | lmhash, nthash) | |
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) | |
218 | 241 | return requester.get_netpso(queried_psoname=queried_psoname, |
219 | 242 | queried_displayname=queried_displayname, |
220 | 243 | queried_domain=queried_domain, ads_path=ads_path) |
221 | 244 | |
222 | 245 | def get_domainpolicy(domain_controller, domain, user, password=str(), |
223 | lmhash=str(), nthash=str(), source='domain', queried_domain=str(), | |
224 | resolve_sids=False): | |
225 | requester = GPORequester(domain_controller, domain, user, password, | |
226 | 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) | |
227 | 250 | |
228 | 251 | return requester.get_domainpolicy(source=source, queried_domain=queried_domain, |
229 | 252 | resolve_sids=resolve_sids) |
230 | 253 | |
231 | 254 | def get_gpttmpl(gpttmpl_path, domain_controller, domain, user, password=str(), lmhash=str(), |
232 | nthash=str()): | |
233 | requester = GPORequester(domain_controller, domain, user, password, | |
234 | 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) | |
235 | 258 | |
236 | 259 | return requester.get_gpttmpl(gpttmpl_path) |
237 | 260 | |
238 | 261 | def get_netgpogroup(domain_controller, domain, user, password=str(), lmhash=str(), |
239 | nthash=str(), queried_gponame='*', queried_displayname=str(), | |
240 | queried_domain=str(), ads_path=str(), resolve_sids=False): | |
241 | requester = GPORequester(domain_controller, domain, user, password, | |
242 | 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) | |
243 | 267 | |
244 | 268 | return requester.get_netgpogroup(queried_gponame=queried_gponame, |
245 | 269 | queried_displayname=queried_displayname, |
248 | 272 | resolve_sids=resolve_sids) |
249 | 273 | |
250 | 274 | def find_gpocomputeradmin(domain_controller, domain, user, password=str(), lmhash=str(), |
251 | nthash=str(), queried_computername=str(), | |
252 | queried_ouname=str(), queried_domain=str(), | |
253 | recurse=False): | |
254 | requester = GPORequester(domain_controller, domain, user, password, | |
255 | 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) | |
256 | 279 | |
257 | 280 | return requester.find_gpocomputeradmin(queried_computername=queried_computername, |
258 | 281 | queried_ouname=queried_ouname, |
260 | 283 | recurse=recurse) |
261 | 284 | |
262 | 285 | def find_gpolocation(domain_controller, domain, user, password=str(), lmhash=str(), |
263 | nthash=str(), queried_username=str(), queried_groupname=str(), | |
264 | queried_localgroup=str(), queried_domain=str()): | |
265 | requester = GPORequester(domain_controller, domain, user, password, | |
266 | 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) | |
267 | 291 | return requester.find_gpolocation(queried_username=queried_username, |
268 | 292 | queried_groupname=queried_groupname, |
269 | 293 | queried_localgroup=queried_localgroup, |
270 | 294 | queried_domain=queried_domain) |
271 | 295 | |
272 | 296 | def invoke_checklocaladminaccess(target_computername, domain, user, password=str(), |
273 | lmhash=str(), nthash=str()): | |
274 | 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) | |
275 | 299 | |
276 | 300 | return misc.invoke_checklocaladminaccess() |
277 | 301 | |
278 | 302 | def invoke_userhunter(domain_controller, domain, user, password=str(), |
279 | lmhash=str(), nthash=str(), queried_computername=list(), | |
280 | queried_computerfile=None, queried_computerfilter=str(), | |
281 | queried_computeradspath=str(), unconstrained=False, | |
282 | 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(), | |
283 | 307 | queried_username=str(), queried_useradspath=str(), |
284 | 308 | queried_userfilter=str(), queried_userfile=None, |
285 | 309 | threads=1, admin_count=False, allow_delegation=False, |
287 | 311 | stealth=False, stealth_source=['dfs', 'dc', 'file'], |
288 | 312 | show_all=False, foreign_users=False): |
289 | 313 | user_hunter = UserHunter(domain_controller, domain, user, password, |
290 | lmhash, nthash) | |
291 | ||
314 | lmhash, nthash, do_kerberos, do_tls) | |
292 | 315 | return user_hunter.invoke_userhunter(queried_computername=queried_computername, |
293 | 316 | queried_computerfile=queried_computerfile, |
294 | 317 | queried_computerfilter=queried_computerfilter, |
304 | 327 | foreign_users=foreign_users) |
305 | 328 | |
306 | 329 | def invoke_processhunter(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_processname=list(), | |
310 | 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(), | |
311 | 334 | queried_username=str(), queried_useradspath=str(), |
312 | 335 | queried_userfilter=str(), queried_userfile=None, threads=1, |
313 | 336 | stop_on_success=False, queried_domain=str(), show_all=False): |
314 | 337 | process_hunter = ProcessHunter(domain_controller, domain, user, password, |
315 | lmhash, nthash) | |
338 | lmhash, nthash, do_kerberos, do_tls) | |
316 | 339 | |
317 | 340 | return process_hunter.invoke_processhunter(queried_computername=queried_computername, |
318 | 341 | queried_computerfile=queried_computerfile, |
327 | 350 | queried_domain=queried_domain, show_all=show_all) |
328 | 351 | |
329 | 352 | def invoke_eventhunter(domain_controller, domain, user, password=str(), |
330 | lmhash=str(), nthash=str(), queried_computername=list(), | |
331 | queried_computerfile=None, queried_computerfilter=str(), | |
332 | queried_computeradspath=str(), queried_groupname=str(), | |
333 | 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(), | |
334 | 357 | queried_useradspath=str(), queried_userfilter=str(), |
335 | 358 | queried_userfile=None, threads=1, queried_domain=str(), |
336 | 359 | search_days=3): |
337 | 360 | event_hunter = EventHunter(domain_controller, domain, user, password, |
338 | lmhash, nthash) | |
361 | lmhash, nthash, do_kerberos, do_tls) | |
339 | 362 | |
340 | 363 | return event_hunter.invoke_eventhunter(queried_computername=queried_computername, |
341 | 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', |
94 | 120 | default=[], help='Object attributes to return') |
95 | 121 | get_adobject_parser.set_defaults(func=get_adobject) |
96 | 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 | ||
97 | 142 | # Parser for the get-objectacl command |
98 | 143 | get_objectacl_parser = subparsers.add_parser('get-objectacl', help='Takes a domain SID, '\ |
99 | 'samAccountName or name, and return the ACL of the associated object', parents=[ad_parser]) | |
144 | 'samAccountName or name, and return the ACL of the associated object', | |
145 | parents=[ad_parser, logging_parser, json_output_parser]) | |
100 | 146 | get_objectacl_parser.add_argument('--sid', dest='queried_sid', |
101 | 147 | help='SID to query (wildcards accepted)') |
102 | 148 | get_objectacl_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', |
121 | 167 | |
122 | 168 | # Parser for the get-netuser command |
123 | 169 | get_netuser_parser = subparsers.add_parser('get-netuser', help='Queries information about '\ |
124 | 'a domain user', parents=[ad_parser]) | |
170 | 'a domain user', parents=[ad_parser, logging_parser, json_output_parser]) | |
125 | 171 | get_netuser_parser.add_argument('--username', dest='queried_username', |
126 | 172 | help='Username to query (wildcards accepted)') |
127 | 173 | get_netuser_parser.add_argument('-d', '--domain', dest='queried_domain', |
146 | 192 | |
147 | 193 | # Parser for the get-netgroup command |
148 | 194 | get_netgroup_parser = subparsers.add_parser('get-netgroup', help='Get a list of all current '\ |
149 | '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]) | |
150 | 197 | get_netgroup_parser.add_argument('--groupname', dest='queried_groupname', |
151 | 198 | default='*', help='Group to query (wildcards accepted)') |
152 | 199 | get_netgroup_parser.add_argument('--sid', dest='queried_sid', |
165 | 212 | |
166 | 213 | # Parser for the get-netcomputer command |
167 | 214 | get_netcomputer_parser = subparsers.add_parser('get-netcomputer', help='Queries informations about '\ |
168 | 'domain computers', parents=[ad_parser]) | |
215 | 'domain computers', parents=[ad_parser, logging_parser, json_output_parser]) | |
169 | 216 | get_netcomputer_parser.add_argument('--computername', dest='queried_computername', |
170 | 217 | default='*', help='Computer name to query') |
171 | 218 | get_netcomputer_parser.add_argument('-os', '--operating-system', dest='queried_os', |
192 | 239 | |
193 | 240 | # Parser for the get-netdomaincontroller command |
194 | 241 | get_netdomaincontroller_parser = subparsers.add_parser('get-netdomaincontroller', help='Get a list of '\ |
195 | 'domain controllers for the given domain', parents=[ad_parser]) | |
242 | 'domain controllers for the given domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
196 | 243 | get_netdomaincontroller_parser.add_argument('-d', '--domain', dest='queried_domain', |
197 | 244 | help='Domain to query') |
198 | 245 | get_netdomaincontroller_parser.set_defaults(func=get_netdomaincontroller) |
199 | 246 | |
200 | 247 | # Parser for the get-netfileserver command |
201 | 248 | get_netfileserver_parser = subparsers.add_parser('get-netfileserver', help='Return a list of '\ |
202 | '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]) | |
203 | 251 | get_netfileserver_parser.add_argument('--target-users', nargs='+', |
204 | 252 | metavar='TARGET_USER', help='A list of users to target to find file servers (wildcards accepted)') |
205 | 253 | get_netfileserver_parser.add_argument('-d', '--domain', dest='queried_domain', |
208 | 256 | |
209 | 257 | # Parser for the get-dfsshare command |
210 | 258 | get_dfsshare_parser = subparsers.add_parser('get-dfsshare', help='Return a list of '\ |
211 | '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]) | |
212 | 260 | get_dfsshare_parser.add_argument('-d', '--domain', dest='queried_domain', |
213 | 261 | help='Domain to query') |
214 | 262 | get_dfsshare_parser.add_argument('-v', '--version', nargs='+', choices=['v1', 'v2'], |
219 | 267 | |
220 | 268 | # Parser for the get-netou command |
221 | 269 | get_netou_parser = subparsers.add_parser('get-netou', help='Get a list of all current '\ |
222 | 'OUs in the domain', parents=[ad_parser]) | |
270 | 'OUs in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
223 | 271 | get_netou_parser.add_argument('--ouname', dest='queried_ouname', |
224 | 272 | default='*', help='OU name to query (wildcards accepted)') |
225 | 273 | get_netou_parser.add_argument('--guid', dest='queried_guid', |
234 | 282 | |
235 | 283 | # Parser for the get-netsite command |
236 | 284 | get_netsite_parser = subparsers.add_parser('get-netsite', help='Get a list of all current '\ |
237 | 'sites in the domain', parents=[ad_parser]) | |
285 | 'sites in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
238 | 286 | get_netsite_parser.add_argument('--sitename', dest='queried_sitename', |
239 | 287 | help='Site name to query (wildcards accepted)') |
240 | 288 | get_netsite_parser.add_argument('--guid', dest='queried_guid', |
249 | 297 | |
250 | 298 | # Parser for the get-netsubnet command |
251 | 299 | get_netsubnet_parser = subparsers.add_parser('get-netsubnet', help='Get a list of all current '\ |
252 | 'subnets in the domain', parents=[ad_parser]) | |
300 | 'subnets in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
253 | 301 | get_netsubnet_parser.add_argument('--sitename', dest='queried_sitename', |
254 | 302 | help='Only return subnets for the specified site name (wildcards accepted)') |
255 | 303 | get_netsubnet_parser.add_argument('-d', '--domain', dest='queried_domain', |
262 | 310 | |
263 | 311 | # Parser for the get-netdomaintrust command |
264 | 312 | get_netdomaintrust_parser = subparsers.add_parser('get-netdomaintrust', help='Returns a list of all the '\ |
265 | 'trusts of the specified domain', parents=[ad_parser]) | |
313 | 'trusts of the specified domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
266 | 314 | get_netdomaintrust_parser.add_argument('-d', '--domain', dest='queried_domain', |
267 | 315 | help='Domain to query') |
268 | 316 | get_netdomaintrust_parser.set_defaults(func=get_netdomaintrust) |
269 | 317 | |
270 | 318 | # Parser for the get-netgpo command |
271 | 319 | get_netgpo_parser = subparsers.add_parser('get-netgpo', help='Get a list of all current '\ |
272 | 'GPOs in the domain', parents=[ad_parser]) | |
320 | 'GPOs in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
273 | 321 | get_netgpo_parser.add_argument('--gponame', dest='queried_gponame', |
274 | 322 | default='*', help='GPO name to query for (wildcards accepted)') |
275 | 323 | get_netgpo_parser.add_argument('--displayname', dest='queried_displayname', |
282 | 330 | |
283 | 331 | # Parser for the get-netpso command |
284 | 332 | get_netpso_parser = subparsers.add_parser('get-netpso', help='Get a list of all current '\ |
285 | 'PSOs in the domain', parents=[ad_parser]) | |
333 | 'PSOs in the domain', parents=[ad_parser, logging_parser, json_output_parser]) | |
286 | 334 | get_netpso_parser.add_argument('--psoname', dest='queried_psoname', |
287 | 335 | default='*', help='pso name to query for (wildcards accepted)') |
288 | 336 | get_netpso_parser.add_argument('--displayname', dest='queried_displayname', |
295 | 343 | |
296 | 344 | # Parser for the get-domainpolicy command |
297 | 345 | get_domainpolicy_parser = subparsers.add_parser('get-domainpolicy', help='Returns the default domain or DC '\ |
298 | '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]) | |
299 | 347 | get_domainpolicy_parser.add_argument('--source', dest='source', default='domain', |
300 | 348 | choices=['domain', 'dc'], help='Extract domain or DC policy (default: %(default)s)') |
301 | 349 | get_domainpolicy_parser.add_argument('-d', '--domain', dest='queried_domain', |
306 | 354 | |
307 | 355 | # Parser for the get-gpttmpl command |
308 | 356 | get_gpttmpl_parser = subparsers.add_parser('get-gpttmpl', help='Helper to parse a GptTmpl.inf policy '\ |
309 | 'file path into a custom object', parents=[ad_parser]) | |
357 | 'file path into a custom object', parents=[ad_parser, logging_parser, json_output_parser]) | |
310 | 358 | get_gpttmpl_parser.add_argument('--gpt-tmpl-path', type=str, required=True, |
311 | 359 | dest='gpttmpl_path', help='The GptTmpl.inf file path name to parse') |
312 | 360 | get_gpttmpl_parser.set_defaults(func=get_gpttmpl) |
313 | 361 | |
314 | 362 | # Parser for the get-netgpogroup command |
315 | 363 | get_netgpogroup_parser = subparsers.add_parser('get-netgpogroup', help='Parses all GPOs in the domain '\ |
316 | '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]) | |
317 | 365 | get_netgpogroup_parser.add_argument('--gponame', dest='queried_gponame', |
318 | 366 | default='*', help='GPO name to query for (wildcards accepted)') |
319 | 367 | get_netgpogroup_parser.add_argument('--displayname', dest='queried_displayname', |
328 | 376 | |
329 | 377 | # Parser for the find-gpocomputeradmin command |
330 | 378 | find_gpocomputeradmin_parser = subparsers.add_parser('find-gpocomputeradmin', help='Takes a computer (or OU) and determine '\ |
331 | '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]) | |
332 | 380 | find_gpocomputeradmin_parser.add_argument('--computername', dest='queried_computername', |
333 | 381 | default=str(), help='The computer to determine who has administrative access to it') |
334 | 382 | find_gpocomputeradmin_parser.add_argument('--ouname', dest='queried_ouname', |
342 | 390 | |
343 | 391 | # Parser for the find-gpolocation command |
344 | 392 | find_gpolocation_parser = subparsers.add_parser('find-gpolocation', help='Takes a username or a group name and determine '\ |
345 | '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]) | |
346 | 394 | find_gpolocation_parser.add_argument('--username', dest='queried_username', |
347 | 395 | default=str(), help='The username to query for access (no wildcard)') |
348 | 396 | find_gpolocation_parser.add_argument('--groupname', dest='queried_groupname', |
355 | 403 | find_gpolocation_parser.set_defaults(func=find_gpolocation) |
356 | 404 | |
357 | 405 | # Parser for the get-netgroup command |
358 | 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]) | |
359 | 408 | get_netgroupmember_parser.add_argument('--groupname', dest='queried_groupname', |
360 | 409 | help='Group to query, defaults to the \'Domain Admins\' group (wildcards accepted)') |
361 | 410 | get_netgroupmember_parser.add_argument('--sid', dest='queried_sid', |
375 | 424 | |
376 | 425 | # Parser for the get-netsession command |
377 | 426 | get_netsession_parser = subparsers.add_parser('get-netsession', help='Queries a host to return a '\ |
378 | '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]) | |
379 | 429 | get_netsession_parser.set_defaults(func=get_netsession) |
380 | 430 | |
381 | 431 | #Parser for the get-localdisks command |
382 | 432 | get_localdisks_parser = subparsers.add_parser('get-localdisks', help='Queries a host to return a '\ |
383 | '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]) | |
384 | 435 | get_localdisks_parser.set_defaults(func=get_localdisks) |
385 | 436 | |
386 | 437 | #Parser for the get-netdomain command |
387 | 438 | get_netdomain_parser = subparsers.add_parser('get-netdomain', help='Queries a host for available domains', |
388 | parents=[ad_parser]) | |
439 | parents=[ad_parser, logging_parser, json_output_parser]) | |
389 | 440 | get_netdomain_parser.set_defaults(func=get_netdomain) |
390 | 441 | |
391 | 442 | # Parser for the get-netshare command |
392 | 443 | get_netshare_parser = subparsers.add_parser('get-netshare', help='Queries a host to return a '\ |
393 | '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]) | |
394 | 446 | get_netshare_parser.set_defaults(func=get_netshare) |
395 | 447 | |
396 | 448 | # Parser for the get-netloggedon command |
397 | 449 | get_netloggedon_parser = subparsers.add_parser('get-netloggedon', help='This function will '\ |
398 | 450 | 'execute the NetWkstaUserEnum RPC call to query a given host for actively logged on '\ |
399 | 'users', parents=[target_parser]) | |
451 | 'users', parents=[target_parser, logging_parser, json_output_parser]) | |
400 | 452 | get_netloggedon_parser.set_defaults(func=get_netloggedon) |
401 | 453 | |
402 | 454 | # Parser for the get-netlocalgroup command |
403 | 455 | get_netlocalgroup_parser = subparsers.add_parser('get-netlocalgroup', help='Gets a list of '\ |
404 | 456 | 'members of a local group on a machine, or returns every local group. You can use local '\ |
405 | 457 | 'credentials instead of domain credentials, however, domain credentials are needed to '\ |
406 | 'resolve domain SIDs.', parents=[target_parser]) | |
458 | 'resolve domain SIDs.', parents=[target_parser, logging_parser, json_output_parser]) | |
407 | 459 | get_netlocalgroup_parser.add_argument('--groupname', dest='queried_groupname', |
408 | 460 | help='Group to list the members of (defaults to the local \'Administrators\' group') |
409 | 461 | get_netlocalgroup_parser.add_argument('--list-groups', action='store_true', |
410 | 462 | help='If set, returns a list of the local groups on the targets') |
411 | 463 | get_netlocalgroup_parser.add_argument('-t', '--dc-ip', dest='domain_controller', |
412 | 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') | |
413 | 467 | get_netlocalgroup_parser.add_argument('-r', '--recurse', action='store_true', |
414 | 468 | help='If the group member is a domain group, try to resolve its members as well') |
415 | 469 | get_netlocalgroup_parser.set_defaults(func=get_netlocalgroup) |
416 | 470 | |
417 | 471 | # Parser for the invoke-checklocaladminaccess command |
418 | 472 | invoke_checklocaladminaccess_parser = subparsers.add_parser('invoke-checklocaladminaccess', help='Checks '\ |
419 | '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]) | |
420 | 475 | invoke_checklocaladminaccess_parser.set_defaults(func=invoke_checklocaladminaccess) |
421 | 476 | |
422 | 477 | # Parser for the get-netprocess command |
423 | 478 | get_netprocess_parser = subparsers.add_parser('get-netprocess', help='This function will '\ |
424 | 479 | 'execute the \'Select * from Win32_Process\' WMI query to a given host for a list of '\ |
425 | 'executed process', parents=[target_parser]) | |
480 | 'executed process', parents=[target_parser, logging_parser, json_output_parser]) | |
426 | 481 | get_netprocess_parser.set_defaults(func=get_netprocess) |
427 | 482 | |
428 | 483 | # Parser for the get-userevent command |
429 | 484 | get_userevent_parser = subparsers.add_parser('get-userevent', help='This function will '\ |
430 | 'execute the \'Select * from Win32_Process\' WMI query to a given host for a list of '\ | |
431 | '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]) | |
432 | 487 | get_userevent_parser.add_argument('--event-type', nargs='+', choices=['logon', 'tgt'], |
433 | 488 | default=['logon', 'tgt'], help='The type of event to search for: logon, tgt, or all (default: all)') |
434 | 489 | get_userevent_parser.add_argument('--date-start', type=int, |
437 | 492 | |
438 | 493 | # Parser for the invoke-userhunter command |
439 | 494 | invoke_userhunter_parser = subparsers.add_parser('invoke-userhunter', help='Finds '\ |
440 | '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]) | |
441 | 496 | invoke_userhunter_parser.add_argument('--unconstrained', action='store_true', |
442 | 497 | help='Query only computers with unconstrained delegation') |
443 | 498 | invoke_userhunter_parser.add_argument('--admin-count', action='store_true', |
463 | 518 | |
464 | 519 | # Parser for the invoke-processhunter command |
465 | 520 | invoke_processhunter_parser = subparsers.add_parser('invoke-processhunter', help='Searches machines '\ |
466 | '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]) | |
467 | 523 | invoke_processhunter_parser.add_argument('--processname', dest='queried_processname', |
468 | 524 | nargs='+', default=list(), help='Names of the process to hunt') |
469 | 525 | invoke_processhunter_parser.add_argument('--stop-on-success', action='store_true', |
474 | 530 | |
475 | 531 | # Parser for the invoke-eventhunter command |
476 | 532 | invoke_eventhunter_parser = subparsers.add_parser('invoke-eventhunter', help='Searches machines '\ |
477 | '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]) | |
478 | 535 | invoke_eventhunter_parser.add_argument('--search-days', dest='search_days', |
479 | 536 | type=int, default=3, help='Number of days back to search logs for (default: %(default)s)') |
480 | 537 | invoke_eventhunter_parser.set_defaults(func=invoke_eventhunter) |
481 | 538 | |
482 | 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 | ||
483 | 551 | if args.hashes: |
484 | 552 | try: |
485 | 553 | args.lmhash, args.nthash = args.hashes.split(':') |
490 | 558 | else: |
491 | 559 | args.lmhash = args.nthash = str() |
492 | 560 | |
493 | 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: | |
494 | 562 | from getpass import getpass |
495 | 563 | args.password = getpass('Password:') |
496 | 564 | |
497 | 565 | parsed_args = dict() |
498 | 566 | for k, v in vars(args).items(): |
499 | if k not in ('func', 'hashes', 'submodule'): | |
567 | if k not in ('func', 'hashes', 'submodule', 'logging_level', 'json_output'): | |
500 | 568 | parsed_args[k] = v |
501 | 569 | |
502 | #try: | |
570 | starting_time = datetime.datetime.now() | |
503 | 571 | results = args.func(**parsed_args) |
504 | #except Exception, e: | |
505 | #print >>sys.stderr, repr(e) | |
506 | #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 | |
507 | 578 | |
508 | 579 | if results is not None: |
509 | try: | |
510 | for x in results: | |
511 | print(x, '\n') | |
512 | # for example, invoke_checklocaladminaccess returns a bool | |
513 | except TypeError: | |
514 | print(results) | |
515 | ||
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 |
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 | ||
17 | import logging | |
18 | import binascii | |
19 | from Cryptodome.Hash import MD4 | |
20 | from impacket.examples.ntlmrelayx.attacks.ldapattack import MSDS_MANAGEDPASSWORD_BLOB | |
21 | from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR | |
16 | 22 | |
17 | 23 | __uac_flags = {0x0000001: 'SCRIPT', |
18 | 24 | 0x0000002: 'ACCOUNTDISABLE', |
75 | 81 | try: |
76 | 82 | int_value = int(raw_value) |
77 | 83 | except ValueError: |
84 | self._logger.warning('Unable to convert raw flag value to int') | |
78 | 85 | return raw_value |
79 | 86 | |
80 | 87 | parsed_flags = list() |
87 | 94 | try: |
88 | 95 | return dictionary[int(raw_value)] |
89 | 96 | except (ValueError, KeyError): |
97 | self._logger.warning('Unable to convert raw value to int') | |
90 | 98 | return raw_value |
91 | 99 | |
92 | 100 | def format_useraccountcontrol(raw_value): |
96 | 104 | try: |
97 | 105 | int_value = int(raw_value) |
98 | 106 | except ValueError: |
107 | self._logger.warning('Unable to convert raw ace acess mask value to int') | |
99 | 108 | return raw_value |
100 | 109 | |
101 | 110 | activedirectoryrights = list() |
106 | 115 | activedirectoryrights += __format_flag(raw_value, __access_mask) |
107 | 116 | |
108 | 117 | return activedirectoryrights |
118 | ||
119 | ||
120 | def format_managedpassword(raw_value): | |
121 | blob = MSDS_MANAGEDPASSWORD_BLOB() | |
122 | blob.fromString(raw_value) | |
123 | return binascii.hexlify(MD4.new(blob['CurrentPassword'][:-2]).digest()).decode('utf8') | |
124 | ||
125 | def format_groupmsamembership(raw_value): | |
126 | sid = list() | |
127 | sr = SR_SECURITY_DESCRIPTOR(data=raw_value) | |
128 | for dacl in sr['Dacl']['Data']: | |
129 | sid.append(dacl['Ace']['Sid'].formatCanonical()) | |
130 | return sid | |
109 | 131 | |
110 | 132 | def format_ace_flags(raw_value): |
111 | 133 | return __format_flag(raw_value, __ace_flags) |
121 | 143 | |
122 | 144 | def format_trustattributes(raw_value): |
123 | 145 | return __format_flag(raw_value, __trust_attrib) |
124 | ||
146 |
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 | |
65 | 64 | file_name = '\\'.join(gpttmpl_path_split[4:]) |
66 | 65 | |
67 | 66 | smb_connection = SMBConnection(remoteName=target, remoteHost=target) |
68 | # TODO: kerberos login | |
69 | smb_connection.login(self._user, self._password, self._domain, | |
70 | self._lmhash, self._nthash) | |
71 | ||
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)) | |
72 | 75 | smb_connection.connectTree(share) |
73 | 76 | smb_connection.getFile(share, file_name, content_io.write) |
74 | 77 | try: |
75 | 78 | content = content_io.getvalue().decode('utf-16le')[1:].replace('\r', '') |
76 | 79 | except UnicodeDecodeError: |
80 | self._logger.warning('Unicode error: trying utf-8') | |
77 | 81 | content = content_io.getvalue().decode('utf-8').replace('\r', '') |
78 | 82 | |
79 | 83 | gpttmpl_final = GptTmpl(list()) |
110 | 114 | try: |
111 | 115 | privilege_rights_policy = gpttmpl.privilegerights |
112 | 116 | except AttributeError: |
117 | self._logger.critical('Could not parse privilegerights from the DC policy, SIDs will not be resolved') | |
113 | 118 | return gpttmpl |
114 | 119 | |
115 | 120 | members = inspect.getmembers(privilege_rights_policy, lambda x: not(inspect.isroutine(x))) |
116 | 121 | with NetRequester(self._domain_controller, self._domain, self._user, |
117 | self._password, self._lmhash, self._nthash) as net_requester: | |
122 | self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester: | |
118 | 123 | for attr in privilege_rights_policy._attributes_dict: |
119 | 124 | attribute = privilege_rights_policy._attributes_dict[attr] |
120 | 125 | if not isinstance(attribute, list): |
129 | 134 | try: |
130 | 135 | resolved_sid = net_requester.get_adobject(queried_sid=sid, queried_domain=self._queried_domain)[0] |
131 | 136 | except IndexError: |
137 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid)) | |
132 | 138 | resolved_sid = sid |
133 | 139 | else: |
134 | 140 | resolved_sid = resolved_sid.distinguishedname.split(',')[:2] |
135 | 141 | resolved_sid = resolved_sid[1] + '\\' + resolved_sid[0] |
136 | 142 | resolved_sid = resolved_sid.replace('CN=', '') |
143 | finally: | |
137 | 144 | resolved_sids.append(resolved_sid) |
138 | 145 | if len(resolved_sids) == 1: |
139 | 146 | resolved_sids = resolved_sids[0] |
155 | 162 | file_name = '\\'.join(groupsxml_path_split[4:]) |
156 | 163 | |
157 | 164 | smb_connection = SMBConnection(remoteName=target, remoteHost=target) |
158 | # TODO: kerberos login | |
159 | smb_connection.login(self._user, self._password, self._domain, | |
160 | self._lmhash, self._nthash) | |
161 | ||
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)) | |
162 | 173 | smb_connection.connectTree(share) |
163 | 174 | try: |
164 | 175 | smb_connection.getFile(share, file_name, content_io.write) |
165 | 176 | except SessionError: |
177 | self._logger.warning('Error while getting the file {}, skipping...'.format(file_name)) | |
166 | 178 | return list() |
167 | 179 | |
168 | 180 | content = content_io.getvalue().replace(b'\r', b'') |
170 | 182 | for group in groupsxml_soup.find_all('Group'): |
171 | 183 | members = list() |
172 | 184 | memberof = list() |
173 | ||
185 | ||
174 | 186 | raw_xml_member = group.Properties.find_all('Member') |
175 | 187 | if not raw_xml_member: |
176 | 188 | continue |
222 | 234 | return list() |
223 | 235 | |
224 | 236 | membership = group_membership._attributes_dict |
225 | ||
237 | ||
226 | 238 | for ma,mv in membership.items(): |
227 | 239 | if not mv: |
228 | 240 | continue |
274 | 286 | results += self._get_groupsgpttmpl(gpttmpl_path, gpo_display_name) |
275 | 287 | except SessionError: |
276 | 288 | # If the GptTmpl file doesn't exist, we skip this |
289 | self._logger.warning('Error while getting the file {}, skipping...'.format(gpttmpl_path,)) | |
277 | 290 | pass |
278 | 291 | |
279 | 292 | if resolve_sids: |
284 | 297 | resolved_members = list() |
285 | 298 | resolved_memberof = list() |
286 | 299 | with NetRequester(self._domain_controller, self._domain, self._user, |
287 | 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: | |
288 | 301 | for member in members: |
289 | 302 | try: |
290 | 303 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0] |
291 | 304 | resolved_member = resolved_member.distinguishedname |
292 | 305 | except IndexError: |
306 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member)) | |
293 | 307 | resolved_member = member |
294 | 308 | finally: |
295 | 309 | resolved_members.append(resolved_member) |
300 | 314 | resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0] |
301 | 315 | resolved_member = resolved_member.distinguishedname |
302 | 316 | except IndexError: |
317 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member)) | |
303 | 318 | resolved_member = member |
304 | 319 | finally: |
305 | 320 | resolved_memberof.append(resolved_member) |
315 | 330 | raise ValueError('You must specify either a computer name or an OU name') |
316 | 331 | |
317 | 332 | net_requester = NetRequester(self._domain_controller, self._domain, self._user, |
318 | self._password, self._lmhash, self._nthash) | |
333 | self._password, self._lmhash, self._nthash, self._do_kerberos, | |
334 | self._do_tls) | |
319 | 335 | if queried_computername: |
320 | 336 | computers = net_requester.get_netcomputer(queried_computername=queried_computername, |
321 | 337 | queried_domain=queried_domain, |
357 | 373 | gpo_computer_admin.add_attributes({'objectname' : obj.name}) |
358 | 374 | gpo_computer_admin.add_attributes({'objectdn' : obj.distinguishedname}) |
359 | 375 | gpo_computer_admin.add_attributes({'objectsid' : obj.objectsid}) |
360 | gpo_computer_admin.add_attributes({'isgroup' : (obj.samaccounttype != '805306368')}) | |
376 | gpo_computer_admin.add_attributes({'isgroup' : (obj.samaccounttype != 805306368)}) | |
361 | 377 | |
362 | 378 | results.append(gpo_computer_admin) |
363 | 379 | |
365 | 381 | groups_to_resolve = [gpo_computer_admin.objectsid] |
366 | 382 | while groups_to_resolve: |
367 | 383 | group_to_resolve = groups_to_resolve.pop(0) |
368 | ||
384 | ||
369 | 385 | group_members = net_requester.get_netgroupmember(queried_sid=group_to_resolve, |
370 | 386 | queried_domain=self._queried_domain, |
371 | 387 | full_data=True) |
378 | 394 | gpo_computer_admin.add_attributes({'objectname' : group_member.samaccountname}) |
379 | 395 | gpo_computer_admin.add_attributes({'objectdn' : group_member.distinguishedname}) |
380 | 396 | gpo_computer_admin.add_attributes({'objectsid' : group_member.objectsid}) |
381 | gpo_computer_admin.add_attributes({'isgroup' : (group_member != '805306368')}) | |
397 | gpo_computer_admin.add_attributes({'isgroup' : (group_member.samaccounttype != 805306368)}) | |
382 | 398 | |
383 | 399 | results.append(gpo_computer_admin) |
384 | 400 | |
391 | 407 | queried_localgroup=str(), queried_domain=str()): |
392 | 408 | results = list() |
393 | 409 | net_requester = NetRequester(self._domain_controller, self._domain, self._user, |
394 | self._password, self._lmhash, self._nthash) | |
410 | self._password, self._lmhash, self._nthash, self._do_kerberos, | |
411 | self._do_tls) | |
395 | 412 | if queried_username: |
396 | 413 | try: |
397 | 414 | user = net_requester.get_netuser(queried_username=queried_username, |
399 | 416 | except IndexError: |
400 | 417 | raise ValueError('Username \'{}\' was not found'.format(queried_username)) |
401 | 418 | else: |
402 | target_sid = [user.objectsid] | |
419 | target_sid = [user.objectsid] | |
403 | 420 | object_sam_account_name = user.samaccountname |
404 | 421 | object_distinguished_name = user.distinguishedname |
405 | 422 | elif queried_groupname: |
434 | 451 | queried_domain=self._queried_domain)[0].objectsid |
435 | 452 | except IndexError: |
436 | 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.') | |
437 | 455 | try: |
438 | 456 | object_group_sid = net_requester.get_adobject(queried_name=object_group.samaccountname, |
439 | 457 | queried_domain=self._queried_domain)[0].objectsid |
440 | 458 | except IndexError: |
441 | 459 | # Freak accident when someone is a member of a group, but |
442 | 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)) | |
443 | 464 | continue |
444 | 465 | |
445 | 466 | target_sid.append(object_group_sid) |
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 |
19 | 19 | |
20 | 20 | from pywerview.requester import LDAPRPCRequester |
21 | 21 | import pywerview.functions.net |
22 | ||
23 | import struct | |
24 | 22 | |
25 | 23 | class Misc(LDAPRPCRequester): |
26 | 24 | @LDAPRPCRequester._rpc_connection_init(r'\drsuapi') |
51 | 49 | def get_domainsid(self, queried_domain=str()): |
52 | 50 | |
53 | 51 | with pywerview.functions.net.NetRequester(self._domain_controller, self._domain, self._user, |
54 | self._password, self._lmhash, self._nthash) as r: | |
52 | self._password, self._lmhash, self._nthash, | |
53 | self._do_kerberos, self._do_tls) as r: | |
55 | 54 | domain_controllers = r.get_netdomaincontroller(queried_domain=queried_domain) |
56 | 55 | |
57 | 56 | if domain_controllers: |
58 | 57 | primary_dc = domain_controllers[0] |
59 | 58 | domain_sid = primary_dc.objectsid |
60 | ||
59 | ||
61 | 60 | # we need to retrieve the domain sid from the controller sid |
62 | 61 | domain_sid = '-'.join(domain_sid.split('-')[:-1]) |
63 | 62 | else: |
79 | 78 | |
80 | 79 | return True |
81 | 80 | |
82 | class Utils(): | |
83 | @staticmethod | |
84 | def convert_sidtostr(raw_sid): | |
85 | str_sid = 'S-{0}-{1}'.format(raw_sid[0], raw_sid[1]) | |
86 | for i in range(8, len(raw_sid), 4): | |
87 | str_sid += '-{}'.format(str(struct.unpack('<I', raw_sid[i:i+4])[0])) | |
88 | return str_sid | |
89 | ||
90 | @staticmethod | |
91 | def convert_guidtostr(raw_guid): | |
92 | str_guid = str() | |
93 | str_guid += '{}-'.format(hex(struct.unpack('<I', raw_guid[0:4])[0])[2:].zfill(8)) | |
94 | str_guid += '{}-'.format(hex(struct.unpack('<H', raw_guid[4:6])[0])[2:].zfill(4)) | |
95 | str_guid += '{}-'.format(hex(struct.unpack('<H', raw_guid[6:8])[0])[2:].zfill(4)) | |
96 | str_guid += '{}-'.format(raw_guid.hex()[16:20]) | |
97 | str_guid += raw_guid.hex()[20:] | |
98 | return str_guid |
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 | |
15 | # Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022 | |
16 | ||
18 | 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 |
49 | 48 | return self._ldap_search(object_filter, adobj.ADObject, attributes=attributes) |
50 | 49 | |
51 | 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 | |
52 | 92 | def get_objectacl(self, queried_domain=str(), queried_sid=str(), |
53 | 93 | queried_name=str(), queried_sam_account_name=str(), |
54 | 94 | ads_path=str(), sacl=False, rights_filter=str(), |
68 | 108 | base_dn = ','.join(self._base_dn.split(',')[-2:]) |
69 | 109 | guid_map = {'{00000000-0000-0000-0000-000000000000}': 'All'} |
70 | 110 | with NetRequester(self._domain_controller, self._domain, self._user, self._password, |
71 | self._lmhash, self._nthash) as net_requester: | |
111 | self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester: | |
72 | 112 | for o in net_requester.get_adobject(ads_path='CN=Schema,CN=Configuration,{}'.format(base_dn), |
73 | 113 | attributes=['name', 'schemaIDGUID'], custom_filter='(schemaIDGUID=*)'): |
74 | 114 | guid_map['{{{}}}'.format(o.schemaidguid)] = o.name |
100 | 140 | |
101 | 141 | if resolve_sids: |
102 | 142 | sid_resolver = NetRequester(self._domain_controller, self._domain, |
103 | self._user, self._password, self._lmhash, self._nthash) | |
143 | self._user, self._password, self._lmhash, self._nthash, | |
144 | self._do_kerberos, self._do_tls) | |
104 | 145 | sid_mapping = adobj.ADObject._well_known_sids.copy() |
105 | 146 | else: |
106 | 147 | sid_resolver = None |
139 | 180 | queried_domain=self._queried_domain, attributes=['distinguishedname'])[0] |
140 | 181 | resolved_sid = resolved_sid.distinguishedname |
141 | 182 | except IndexError: |
183 | self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(converted_sid)) | |
142 | 184 | resolved_sid = attributes['securityidentifier'] |
143 | 185 | finally: |
144 | 186 | sid_mapping[converted_sid] = resolved_sid |
198 | 240 | |
199 | 241 | # RFC 4515, section 3 |
200 | 242 | # However if we escape *, we can no longer use wildcard within `--groupname` |
201 | # Maybe we can raise a warning here ? | |
202 | 243 | if not '*' in queried_groupname: |
203 | 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)) | |
204 | 249 | |
205 | 250 | if queried_username: |
251 | self._logger.debug('Queried username = {}'.format(queried_username)) | |
206 | 252 | results = list() |
207 | 253 | sam_account_name_to_resolve = [queried_username] |
208 | 254 | first_run = True |
249 | 295 | group_search_filter += '(objectCategory=group)' |
250 | 296 | |
251 | 297 | if queried_sid: |
298 | self._logger.debug('Queried SID = {}'.format(queried_username)) | |
252 | 299 | group_search_filter += '(objectSid={})'.format(queried_sid) |
253 | 300 | elif queried_groupname: |
301 | self._logger.debug('Queried groupname = {}'.format(queried_groupname)) | |
254 | 302 | group_search_filter += '(name={})'.format(queried_groupname) |
255 | 303 | |
256 | 304 | if full_data: |
456 | 504 | try: |
457 | 505 | # `--groupname` option is supplied |
458 | 506 | if _groupname: |
507 | self._logger.debug('Queried groupname = {}'.format(queried_groupname)) | |
459 | 508 | groups = self.get_netgroup(queried_groupname=_groupname, |
460 | 509 | queried_domain=self._queried_domain, |
461 | 510 | full_data=True) |
462 | 511 | |
463 | 512 | # `--groupname` option is missing, falling back to the "Domain Admins" |
464 | 513 | else: |
514 | self._logger.debug('No groupname provided, falling back to the "Domain Admins"'.format(queried_groupname)) | |
465 | 515 | if _sid: |
466 | 516 | queried_sid = _sid |
467 | 517 | else: |
468 | 518 | with pywerview.functions.misc.Misc(self._domain_controller, |
469 | 519 | self._domain, self._user, |
470 | 520 | self._password, self._lmhash, |
471 | self._nthash) as misc_requester: | |
521 | self._nthash, self._do_kerberos, | |
522 | self._do_tls) as misc_requester: | |
472 | 523 | queried_sid = misc_requester.get_domainsid(queried_domain) + '-512' |
524 | self._logger.debug('Found Domains Admins SID = {}'.format(queried_sid)) | |
473 | 525 | groups = self.get_netgroup(queried_sid=queried_sid, |
474 | 526 | queried_domain=self._queried_domain, |
475 | 527 | full_data=True) |
490 | 542 | try: |
491 | 543 | for member in group.member: |
492 | 544 | # RFC 4515, section 3 |
545 | self._logger.warning('Member name = "{}" will be escaped'.format(member)) | |
493 | 546 | member = escape_filter_chars(member, encoding='utf-8') |
494 | 547 | dn_filter = '(distinguishedname={}){}'.format(member, custom_filter) |
495 | 548 | members += self.get_netuser(custom_filter=dn_filter, queried_domain=self._queried_domain) |
496 | 549 | members += self.get_netgroup(custom_filter=dn_filter, queried_domain=self._queried_domain, full_data=True) |
497 | 550 | # The group doesn't have any members |
498 | 551 | except AttributeError: |
552 | self._logger.debug('The group doesn\'t have any members') | |
499 | 553 | continue |
500 | 554 | |
501 | 555 | for member in members: |
508 | 562 | try: |
509 | 563 | member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.') |
510 | 564 | except IndexError: |
565 | self._logger.warning('Exception was raised while handling member_dn, falling back to empty string') | |
511 | 566 | member_domain = str() |
512 | 567 | is_group = (member.samaccounttype != 805306368) |
513 | 568 | |
744 | 799 | except AttributeError: |
745 | 800 | # Here, the member is a foreign security principal |
746 | 801 | # TODO: resolve it properly |
802 | self._logger.warning('The member is a foreign security principal, SID will not be resolved') | |
747 | 803 | attributes['name'] = '{}\\{}'.format(member_domain, ad_object.objectsid) |
748 | 804 | attributes['isgroup'] = 'group' in ad_object.objectclass |
749 | 805 | try: |
750 | # TODO: Now, lastlogon is raw, convert here or within rpc __str__ ? | |
751 | 806 | attributes['lastlogon'] = ad_object.lastlogon |
752 | 807 | except AttributeError: |
808 | self._logger.warning('lastlogon is not set, falling back to empty string') | |
753 | 809 | attributes['lastlogon'] = str() |
754 | 810 | except IndexError: |
755 | 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)) | |
756 | 813 | attributes['isdomain'] = False |
757 | 814 | attributes['isgroup'] = False |
758 | 815 | attributes['name'] = attributes['sid'] |
780 | 837 | domain_member_attributes['server'] = attributes['name'] |
781 | 838 | domain_member_attributes['sid'] = domain_member.objectsid |
782 | 839 | try: |
783 | # TODO : Same here, must convert the timestamp | |
784 | 840 | domain_member_attributes['lastlogin'] = ad_object.lastlogon |
785 | 841 | except AttributeError: |
842 | self._logger.warning('lastlogon is not set, falling back to empty string') | |
786 | 843 | domain_member_attributes['lastlogin'] = str() |
787 | 844 | results.append(rpcobj.RPCObject(domain_member_attributes)) |
788 | 845 |
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 inspect |
18 | import struct | |
19 | import pyasn1 | |
18 | import logging | |
19 | ||
20 | 20 | from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR |
21 | 21 | |
22 | 22 | import pywerview.functions.misc as misc |
56 | 56 | 'S-1-5': 'NT Authority'} |
57 | 57 | |
58 | 58 | def __init__(self, attributes): |
59 | logger = logging.getLogger('pywerview_main_logger.ADObject') | |
60 | logger.ULTRA = 5 | |
61 | self._logger = logger | |
62 | ||
59 | 63 | self._attributes_dict = dict() |
60 | 64 | self.add_attributes(attributes) |
61 | 65 | |
62 | 66 | def add_attributes(self, attributes): |
67 | self._logger.log(self._logger.ULTRA,'ADObject instancied with the following attributes : {}'.format(attributes)) | |
63 | 68 | for attr in attributes: |
64 | 69 | self._attributes_dict[attr.lower()] = attributes[attr] |
65 | 70 | |
83 | 88 | max_length = len(attr) |
84 | 89 | for attr in self._attributes_dict: |
85 | 90 | attribute = self._attributes_dict[attr] |
91 | self._logger.log(self._logger.ULTRA,'Trying to print : attribute name = {0} / value = {1}'.format(attr, attribute)) | |
86 | 92 | if isinstance(attribute, list): |
87 | 93 | if any(isinstance(x, bytes) for x in attribute): |
88 | 94 | attribute = ['{}...'.format(x.hex()[:97]) for x in attribute] |
105 | 111 | attribute = ('\n' + str(attribute)).replace('\n', '\n\t') |
106 | 112 | |
107 | 113 | s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute) |
108 | #if not member.startswith('_'): | |
109 | ##print(len(member[1])) | |
110 | ## print(member) | |
111 | ## ?? | |
112 | #if member in ('logonhours', 'msds-generationid'): | |
113 | #value = member[1] | |
114 | #member_value = [x for x in value] | |
115 | ||
116 | ## Attribute is a SID | |
117 | #elif member in ('objectsid', 'ms-ds-creatorsid', 'securityidentifier'): | |
118 | #init_value = member[1] | |
119 | #member_value = misc.Utils.convert_sidtostr(init_value) | |
120 | ||
114 | ||
121 | 115 | s = s[:-1] |
122 | 116 | return s |
123 | ||
117 | ||
124 | 118 | def __repr__(self): |
125 | 119 | return str(self) |
126 | 120 | |
121 | def to_json(self): | |
122 | return self._attributes_dict | |
123 | ||
127 | 124 | class ACE(ADObject): |
128 | 125 | |
129 | 126 | def __init__(self, attributes): |
159 | 156 | class Trust(ADObject): |
160 | 157 | |
161 | 158 | def __init__(self, attributes): |
159 | logger = logging.getLogger('pywerview_main_logger.Trust') | |
160 | self._logger = logger | |
162 | 161 | ADObject.__init__(self, attributes) |
163 | 162 | trust_attributes = self.trustattributes |
164 | 163 | trust_direction = self.trustdirection |
181 | 180 | max_length = len('trustattributes') |
182 | 181 | |
183 | 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 | 184 | if attr in ('trustpartner', 'trustdirection', 'trusttype', 'whenchanged', 'whencreated'): |
185 | 185 | attribute = self._attributes_dict[attr] |
186 | 186 | elif attr == 'trustattributes': |
187 | 187 | attribute = ', '.join(self._attributes_dict[attr]) |
188 | 188 | else: |
189 | self._logger.debug('Ignoring : attribute name = {0}'.format(attr, self._attributes_dict[attr])) | |
189 | 190 | continue |
190 | 191 | s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute) |
191 | 192 | |
200 | 201 | pass |
201 | 202 | |
202 | 203 | class GptTmpl(ADObject): |
203 | pass | |
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 | |
204 | 209 | |
205 | 210 | class GPOGroup(ADObject): |
206 | 211 | pass |
214 | 219 | class GPOLocation(ADObject): |
215 | 220 | pass |
216 | 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 | import logging | |
17 | 18 | import inspect |
18 | 19 | |
19 | 20 | class RPCObject: |
20 | 21 | def __init__(self, obj): |
22 | logger = logging.getLogger('pywerview_main_logger.RPCObject') | |
23 | logger.ULTRA = 5 | |
24 | self._logger = logger | |
25 | ||
21 | 26 | attributes = dict() |
22 | 27 | try: |
23 | 28 | for key in obj.fields.keys(): |
27 | 32 | self.add_attributes(attributes) |
28 | 33 | |
29 | 34 | def add_attributes(self, attributes): |
35 | self._logger.log(self._logger.ULTRA,'RPCObject instancied with the following attributes : {}'.format(attributes)) | |
30 | 36 | for key, value in attributes.items(): |
31 | 37 | key = key.lower() |
32 | 38 | if key in ('wkui1_logon_domain', 'wkui1_logon_server', |
33 | 39 | 'wkui1_oth_domains', 'wkui1_username', |
34 | 40 | 'sesi10_cname', 'sesi10_username'): |
35 | 41 | value = value.rstrip('\x00') |
36 | ||
42 | ||
37 | 43 | setattr(self, key.lower(), value) |
38 | 44 | |
39 | 45 | def __str__(self): |
45 | 51 | if len(member[0]) > max_length: |
46 | 52 | max_length = len(member[0]) |
47 | 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])) | |
48 | 55 | if not member[0].startswith('_'): |
49 | 56 | s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member[1]) |
50 | 57 | |
53 | 60 | |
54 | 61 | def __repr__(self): |
55 | 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) | |
56 | 71 | |
57 | 72 | class TargetUser(RPCObject): |
58 | 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 | |
20 | 24 | |
21 | 25 | from ldap3.protocol.formatters.formatters import * |
22 | 26 | |
23 | 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 | |
24 | 32 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY |
25 | 33 | from impacket.dcerpc.v5 import transport, wkst, srvs, samr, scmr, drsuapi, epm |
26 | 34 | from impacket.dcerpc.v5.dcom import wmi |
32 | 40 | |
33 | 41 | class LDAPRequester(): |
34 | 42 | def __init__(self, domain_controller, domain=str(), user=(), password=str(), |
35 | lmhash=str(), nthash=str()): | |
43 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False): | |
36 | 44 | self._domain_controller = domain_controller |
37 | 45 | self._domain = domain |
38 | 46 | self._user = user |
39 | 47 | self._password = password |
40 | 48 | self._lmhash = lmhash |
41 | 49 | self._nthash = nthash |
50 | self._do_kerberos = do_kerberos | |
51 | self._do_tls = do_tls | |
42 | 52 | self._queried_domain = None |
43 | 53 | self._ads_path = None |
44 | 54 | self._ads_prefix = None |
45 | 55 | self._ldap_connection = None |
46 | 56 | self._base_dn = None |
47 | 57 | |
58 | logger = logging.getLogger('pywerview_main_logger.LDAPRequester') | |
59 | self._logger = logger | |
60 | ||
48 | 61 | def _get_netfqdn(self): |
49 | 62 | try: |
50 | 63 | smb = SMBConnection(self._domain_controller, self._domain_controller) |
51 | 64 | except socket.error: |
65 | self._logger.warning('Socket error when opening the SMB connection') | |
52 | 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)) | |
53 | 72 | |
54 | 73 | smb.login(self._user, self._password, domain=self._domain, |
55 | 74 | lmhash=self._lmhash, nthash=self._nthash) |
58 | 77 | |
59 | 78 | return fqdn |
60 | 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 | ||
61 | 101 | def _create_ldap_connection(self, queried_domain=str(), ads_path=str(), |
62 | 102 | ads_prefix=str()): |
63 | 103 | if not self._domain: |
64 | 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) | |
65 | 113 | |
66 | 114 | if not queried_domain: |
67 | 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) | |
68 | 124 | self._queried_domain = queried_domain |
69 | 125 | |
70 | 126 | base_dn = str() |
85 | 141 | # base_dn is no longer used within `_create_ldap_connection()`, but I don't want to break |
86 | 142 | # the function call. So we store it in an attriute and use it in `_ldap_search()` |
87 | 143 | self._base_dn = base_dn |
88 | ||
144 | ||
89 | 145 | # Format the username and the domain |
90 | 146 | # ldap3 seems not compatible with USER@DOMAIN format |
91 | user = '{}\\{}'.format(self._domain, self._user) | |
147 | if self._do_kerberos: | |
148 | user = '{}@{}'.format(self._user, self._domain.upper()) | |
149 | else: | |
150 | user = '{}\\{}'.format(self._domain, self._user) | |
92 | 151 | |
93 | 152 | # Call custom formatters for several AD attributes |
94 | 153 | formatter = {'userAccountControl': fmt.format_useraccountcontrol, |
98 | 157 | 'msDS-MaximumPasswordAge': format_ad_timedelta, |
99 | 158 | 'msDS-MinimumPasswordAge': format_ad_timedelta, |
100 | 159 | 'msDS-LockoutDuration': format_ad_timedelta, |
101 | 'msDS-LockoutObservationWindow': format_ad_timedelta} | |
102 | ||
103 | # Choose between password or pth | |
104 | if self._lmhash and self._nthash: | |
105 | lm_nt_hash = '{}:{}'.format(self._lmhash, self._nthash) | |
106 | ||
107 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller), | |
108 | formatter=formatter) | |
109 | ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, | |
110 | authentication=ldap3.NTLM, raise_exceptions=True) | |
111 | ||
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) | |
112 | 232 | try: |
113 | 233 | ldap_connection.bind() |
114 | except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult: | |
115 | # We need to try SSL (pth version) | |
116 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller), | |
117 | formatter=formatter) | |
118 | ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, | |
119 | authentication=ldap3.NTLM, raise_exceptions=True) | |
120 | ||
121 | ldap_connection.bind() | |
122 | ||
123 | else: | |
124 | ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller), | |
125 | formatter=formatter) | |
126 | ldap_connection = ldap3.Connection(ldap_server, user, self._password, | |
127 | authentication=ldap3.NTLM, raise_exceptions=True) | |
128 | ||
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) | |
129 | 245 | try: |
130 | 246 | ldap_connection.bind() |
131 | except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult: | |
132 | # We nedd to try SSL (password version) | |
133 | ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller), | |
134 | formatter=formatter) | |
135 | ldap_connection = ldap3.Connection(ldap_server, user, self._password, | |
136 | authentication=ldap3.NTLM, raise_exceptions=True) | |
137 | ||
138 | 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) | |
139 | 252 | |
140 | 253 | self._ldap_connection = ldap_connection |
141 | 254 | |
142 | 255 | def _ldap_search(self, search_filter, class_result, attributes=list(), controls=list()): |
143 | 256 | results = list() |
144 | ||
145 | # if no attribute name specified, we return all attributes | |
257 | ||
258 | # if no attribute name specified, we return all attributes | |
146 | 259 | if not attributes: |
147 | attributes = ldap3.ALL_ATTRIBUTES | |
148 | ||
149 | try: | |
150 | # Microsoft Active Directory set an hard limit of 1000 entries returned by any search | |
151 | search_results=self._ldap_connection.extend.standard.paged_search(search_base=self._base_dn, | |
152 | search_filter=search_filter, attributes=attributes, | |
153 | controls=controls, paged_size=1000, generator=True) | |
154 | # TODO: for debug only | |
155 | except Exception as e: | |
156 | import sys | |
157 | print('Except: ', sys.exc_info()[0]) | |
158 | ||
159 | # Skip searchResRef | |
160 | for result in search_results: | |
161 | if result['type'] != 'searchResEntry': | |
162 | continue | |
163 | results.append(class_result(result['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') | |
164 | 284 | |
165 | 285 | return results |
166 | 286 | |
190 | 310 | try: |
191 | 311 | self._ldap_connection.unbind() |
192 | 312 | except AttributeError: |
313 | self._logger.warning('Error when unbinding') | |
193 | 314 | pass |
194 | 315 | self._ldap_connection = None |
195 | 316 | |
196 | 317 | class RPCRequester(): |
197 | 318 | def __init__(self, target_computer, domain=str(), user=(), password=str(), |
198 | lmhash=str(), nthash=str()): | |
319 | lmhash=str(), nthash=str(), do_kerberos=False): | |
199 | 320 | self._target_computer = target_computer |
200 | 321 | self._domain = domain |
201 | 322 | self._user = user |
202 | 323 | self._password = password |
203 | 324 | self._lmhash = lmhash |
204 | 325 | self._nthash = nthash |
326 | self._do_kerberos = do_kerberos | |
205 | 327 | self._pipe = None |
206 | 328 | self._rpc_connection = None |
207 | 329 | self._dcom = None |
208 | 330 | self._wmi_connection = None |
331 | ||
332 | logger = logging.getLogger('pywerview_main_logger.RPCRequester') | |
333 | self._logger = logger | |
209 | 334 | |
210 | 335 | def _create_rpc_connection(self, pipe): |
211 | 336 | # Here we build the DCE/RPC connection |
230 | 355 | rpctransport = transport.SMBTransport(self._target_computer, 445, self._pipe, |
231 | 356 | username=self._user, password=self._password, |
232 | 357 | domain=self._domain, lmhash=self._lmhash, |
233 | nthash=self._nthash) | |
358 | nthash=self._nthash, doKerberos=self._do_kerberos) | |
234 | 359 | |
235 | 360 | rpctransport.set_connect_timeout(10) |
236 | 361 | dce = rpctransport.get_dce_rpc() |
240 | 365 | |
241 | 366 | try: |
242 | 367 | dce.connect() |
243 | except socket.error: | |
368 | except Exception as e: | |
369 | self._logger.critical('Error when creating RPC connection') | |
370 | self._logger.critical(e) | |
244 | 371 | self._rpc_connection = None |
245 | 372 | else: |
246 | 373 | dce.bind(binding_strings[self._pipe[1:]]) |
249 | 376 | def _create_wmi_connection(self, namespace='root\\cimv2'): |
250 | 377 | try: |
251 | 378 | self._dcom = DCOMConnection(self._target_computer, self._user, self._password, |
252 | self._domain, self._lmhash, self._nthash) | |
253 | 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) | |
254 | 383 | self._dcom = None |
255 | 384 | else: |
256 | 385 | i_interface = self._dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, |
301 | 430 | |
302 | 431 | class LDAPRPCRequester(LDAPRequester, RPCRequester): |
303 | 432 | def __init__(self, target_computer, domain=str(), user=(), password=str(), |
304 | lmhash=str(), nthash=str(), domain_controller=str()): | |
433 | lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, | |
434 | domain_controller=str()): | |
305 | 435 | # If no domain controller was given, we assume that the user wants to |
306 | 436 | # target a domain controller to perform LDAP requests against |
307 | 437 | if not domain_controller: |
308 | 438 | domain_controller = target_computer |
309 | 439 | LDAPRequester.__init__(self, domain_controller, domain, user, password, |
310 | lmhash, nthash) | |
440 | lmhash, nthash, do_kerberos, do_tls) | |
311 | 441 | RPCRequester.__init__(self, target_computer, domain, user, password, |
312 | lmhash, nthash) | |
442 | lmhash, nthash, do_kerberos) | |
443 | ||
444 | logger = logging.getLogger('pywerview_main_logger.LDAPRPCRequester') | |
445 | self._logger = logger | |
446 | ||
313 | 447 | def __enter__(self): |
314 | 448 | try: |
315 | 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 |
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.cli.main import main |
20 | 20 |
4 | 4 | long_description = open('README.md').read() |
5 | 5 | |
6 | 6 | setup(name='pywerview', |
7 | version='0.3.3', | |
7 | version='0.4.0', | |
8 | 8 | description='A Python port of PowerSploit\'s PowerView', |
9 | 9 | long_description=long_description, |
10 | 10 | long_description_content_type='text/markdown', |
26 | 26 | install_requires=[ |
27 | 27 | 'impacket>=0.9.22', |
28 | 28 | 'bs4', |
29 | 'lxml' | |
29 | 'lxml', | |
30 | 'pyasn1', | |
31 | 'ldap3>=2.8.1', | |
32 | 'gssapi' | |
30 | 33 | ], |
31 | 34 | entry_points = { |
32 | 35 | 'console_scripts': ['pywerview=pywerview.cli.main:main'], |