Update upstream source from tag 'upstream/0.3.30'
Update to upstream version '0.3.30'
with Debian dir ea37ae6de6ad936302b085ad6af314342010ab7f
Sophie Brun
2 years ago
0 | This projects contains two other project written by a 3rd party. | |
1 | All code is licensed under MIT. | |
2 | ||
3 | License for MSLDAP: | |
0 | 4 | MIT License |
1 | 5 | |
2 | Copyright (c) 2018 | |
6 | Copyright (c) 2018 Tamas Jos | |
3 | 7 | |
4 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy |
5 | 9 | of this software and associated documentation files (the "Software"), to deal |
18 | 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
19 | 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
20 | 24 | SOFTWARE. |
25 | ||
26 | ||
27 | License for "asciitree": | |
28 | ||
29 | Copyright (c) 2015 Marc Brinkmann | |
30 | ||
31 | Permission is hereby granted, free of charge, to any person obtaining a | |
32 | copy of this software and associated documentation files (the "Software"), | |
33 | to deal in the Software without restriction, including without limitation | |
34 | the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
35 | and/or sell copies of the Software, and to permit persons to whom the | |
36 | Software is furnished to do so, subject to the following conditions: | |
37 | ||
38 | The above copyright notice and this permission notice shall be included in | |
39 | all copies or substantial portions of the Software. | |
40 | ||
41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
46 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
47 | DEALINGS IN THE SOFTWARE. | |
48 | ||
49 | ||
50 | License for "aiocmd": | |
51 | ||
52 | MIT License | |
53 | ||
54 | Copyright (c) 2019 Dor Green | |
55 | ||
56 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
57 | of this software and associated documentation files (the "Software"), to deal | |
58 | in the Software without restriction, including without limitation the rights | |
59 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
60 | copies of the Software, and to permit persons to whom the Software is | |
61 | furnished to do so, subject to the following conditions: | |
62 | ||
63 | The above copyright notice and this permission notice shall be included in all | |
64 | copies or substantial portions of the Software. | |
65 | ||
66 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
67 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
68 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
69 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
70 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
71 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
72 | SOFTWARE. |
0 | 0 | Metadata-Version: 1.2 |
1 | 1 | Name: msldap |
2 | Version: 0.2.10 | |
2 | Version: 0.3.30 | |
3 | 3 | Summary: Python library to play with MS LDAP |
4 | 4 | Home-page: https://github.com/skelsec/msldap |
5 | 5 | Author: Tamas Jos |
6 | Author-email: [email protected] | |
6 | Author-email: [email protected] | |
7 | 7 | License: UNKNOWN |
8 | 8 | Description: Python library to play with MS LDAP |
9 | 9 | Platform: UNKNOWN |
10 | Classifier: Programming Language :: Python :: 3.6 | |
10 | Classifier: Programming Language :: Python :: 3.7 | |
11 | Classifier: Programming Language :: Python :: 3.8 | |
11 | 12 | Classifier: License :: OSI Approved :: MIT License |
12 | 13 | Classifier: Operating System :: OS Independent |
13 | Requires-Python: >=3.6 | |
14 | Requires-Python: >=3.7 |
0 | [![Documentation Status](https://readthedocs.org/projects/msldap/badge/?version=latest)](https://msldap.readthedocs.io/en/latest/?badge=latest) | |
1 | ||
2 | # msldap client | |
3 | ![Documentation Status](https://user-images.githubusercontent.com/19204702/81515211-3761e880-9333-11ea-837f-bcbe2a67ee48.gif ) | |
4 | ||
0 | 5 | # msldap |
1 | 6 | LDAP library for MS AD |
7 | ||
8 | # Documentation | |
9 | [Awesome documentation here!](https://msldap.readthedocs.io/en/latest/) | |
2 | 10 | |
3 | 11 | # Features |
4 | 12 | - Comes with a built-in console LDAP client |
5 | 13 | - All parameters can be conrolled via a conveinent URL (see below) |
6 | - Supports integrated windows authentication | |
14 | - Supports integrated windows authentication (SSPI) both with NTLM and with KERBEROS | |
15 | - Supports channel binding (for ntlm and kerberos not SSPI) | |
16 | - Supports encryption (for NTLM/KERBEROS/SSPI) | |
17 | - Supports LDAPS (TODO: actually verify certificate) | |
7 | 18 | - Supports SOCKS5 proxy withot the need of extra proxifyer |
8 | 19 | - Minimal footprint |
9 | 20 | - A lot of pre-built queries for convenient information polling |
10 | 21 | - Easy to integrate to your project |
11 | - Completely missing documentation | |
12 | 22 | - No testing suite |
13 | 23 | |
14 | 24 | # Installation |
18 | 28 | `pip install msldap` |
19 | 29 | |
20 | 30 | # Prerequisites |
21 | - `ldap3` module. It's pure python so you dont have to compile anything. | |
22 | 31 | - `winsspi` module. For windows only. This supports SSPI based authentication. |
23 | 32 | - `asn1crypto` module. Some LDAP queries incorporate ASN1 strucutres to be sent on top of the ASN1 transport XD |
24 | - `socks5line` module. To support socks5 proxying. | |
33 | - `asysocks` module. To support socks proxying. | |
25 | 34 | - `aiocmd` For the interactive client |
26 | 35 | - `asciitree` For plotting nice trees in the interactive client |
27 | 36 | |
34 | 43 | The new connection string is composed in the following manner: |
35 | 44 | `<protocol>+<auth_method>://<domain>\<username>:<password>@<ip>:<port>/?<param>=<value>&<param>=<value>&...` |
36 | 45 | Detailed explanation with examples: |
37 | ``` | |
38 | MSLDAP URL Format: <protocol>+<auth>://<username>:<password>@<ip_or_host>:<port>/<tree>/?<param>=<value> | |
46 | ``` | |
47 | <protocol>+<auth>://<username>:<password>@<ip_or_host>:<port>/<tree>/?<param>=<value> | |
48 | ||
49 | ||
39 | 50 | <protocol> sets the ldap protocol following values supported: |
40 | 51 | - ldap |
41 | - ldaps (ldap over SSL) << known to be problematic because of the underlying library (ldap3) | |
42 | <auth> can be omitted if plaintext authentication is to be performed, otherwise: | |
43 | - ntlm | |
44 | - sspi (windows only!) | |
52 | - ldaps | |
53 | ||
54 | <auth> can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: | |
55 | - ntlm-password | |
56 | - ntlm-nt | |
57 | - kerberos-password (dc option param must be used) | |
58 | - kerberos-rc4 / kerberos-nt (dc option param must be used) | |
59 | - kerberos-aes (dc option param must be used) | |
60 | - kerberos-keytab (dc option param must be used) | |
61 | - kerberos-ccache (dc option param must be used) | |
62 | - sspi-ntlm (windows only!) | |
63 | - sspi-kerberos (windows only!) | |
45 | 64 | - anonymous |
46 | 65 | - plain |
66 | - simple | |
67 | - sicily (same format as ntlm-nt but using the SICILY authentication) | |
68 | ||
69 | <tree>: | |
70 | OPTIONAL. Specifies the root tree of all queries | |
71 | ||
47 | 72 | <param> can be: |
48 | 73 | - timeout : connction timeout in seconds |
49 | 74 | - proxytype: currently only socks5 proxy is supported |
50 | 75 | - proxyhost: Ip or hostname of the proxy server |
51 | 76 | - proxyport: port of the proxy server |
52 | 77 | - proxytimeout: timeout ins ecodns for the proxy connection |
78 | - dc: the IP address of the domain controller, MUST be used for kerberos authentication | |
53 | 79 | |
54 | 80 | Examples: |
55 | ldap://10.10.10.2 | |
56 | ldaps://test.corp | |
57 | ldap+sspi:///test.corp | |
58 | ldap+ntlm://TEST\\victim:[email protected] | |
81 | ldap://10.10.10.2 (anonymous bind) | |
82 | ldaps://test.corp (anonymous bind) | |
83 | ldap+sspi-ntlm://test.corp | |
84 | ldap+sspi-kerberos://test.corp | |
85 | ldap://TEST\\victim:<password>@10.10.10.2 (defaults to SASL GSSAPI NTLM) | |
86 | ldap+simple://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) | |
87 | ldap+plain://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) | |
88 | ldap+ntlm-password://TEST\\victim:<password>@10.10.10.2 | |
89 | ldap+ntlm-nt://TEST\\victim:<nthash>@10.10.10.2 | |
90 | ldap+kerberos-password://TEST\\victim:<password>@10.10.10.2 | |
91 | ldap+kerberos-rc4://TEST\\victim:<rc4key>@10.10.10.2 | |
92 | ldap+kerberos-aes://TEST\\victim:<aes>@10.10.10.2 | |
59 | 93 | ldap://TEST\\victim:[email protected]/DC=test,DC=corp/ |
60 | 94 | ldap://TEST\\victim:[email protected]/DC=test,DC=corp/?timeout=99&proxytype=socks5&proxyhost=127.0.0.1&proxyport=1080&proxytimeout=44 |
61 | 95 | ``` |
62 | 96 | |
63 | 97 | # Kudos |
64 | This project is built on top of the [ldap3](https://github.com/cannatag/ldap3) project. | |
98 |
65 | 65 | |
66 | 66 | elif args.command == 'spn': |
67 | 67 | connection.connect() |
68 | adinfo = connection.get_ad_info() | |
68 | adinfo, err = connection.get_ad_info() | |
69 | 69 | with open(args.outfile, 'w', newline='', encoding = 'utf8') as f: |
70 | 70 | for user in connection.get_all_service_user_objects(): |
71 | 71 | f.write(user.sAMAccountName + '\r\n') |
0 | import enum | |
1 | import io | |
2 | import os | |
3 | ||
4 | from asn1crypto.core import ObjectIdentifier | |
5 | ||
6 | from minikerberos.protocol.constants import EncryptionType | |
7 | from minikerberos.protocol import encryption | |
8 | from minikerberos.crypto.hashing import md5, hmac_md5 | |
9 | from minikerberos.crypto.RC4 import RC4 | |
10 | ||
11 | #TODO: RC4 support! | |
12 | ||
13 | # https://tools.ietf.org/html/draft-raeburn-krb-rijndael-krb-05 | |
14 | # https://tools.ietf.org/html/rfc2478 | |
15 | # https://tools.ietf.org/html/draft-ietf-krb-wg-gssapi-cfx-02 | |
16 | # https://tools.ietf.org/html/rfc4757 | |
17 | # https://www.rfc-editor.org/errata/rfc4757 | |
18 | ||
19 | GSS_WRAP_HEADER = b'\x60\x2b\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' | |
20 | GSS_WRAP_HEADER_OID = b'\x60\x2b\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' | |
21 | ||
22 | class KRB5_MECH_INDEP_TOKEN: | |
23 | # https://tools.ietf.org/html/rfc2743#page-81 | |
24 | # Mechanism-Independent Token Format | |
25 | ||
26 | def __init__(self, data, oid, remlen = None): | |
27 | self.oid = oid | |
28 | self.data = data | |
29 | ||
30 | #dont set this | |
31 | self.length = remlen | |
32 | ||
33 | @staticmethod | |
34 | def from_bytes(data): | |
35 | return KRB5_MECH_INDEP_TOKEN.from_buffer(io.BytesIO(data)) | |
36 | ||
37 | @staticmethod | |
38 | def from_buffer(buff): | |
39 | ||
40 | start = buff.read(1) | |
41 | if start != b'\x60': | |
42 | raise Exception('Incorrect token data!') | |
43 | remaining_length = KRB5_MECH_INDEP_TOKEN.decode_length_buffer(buff) | |
44 | token_data = buff.read(remaining_length) | |
45 | ||
46 | buff = io.BytesIO(token_data) | |
47 | pos = buff.tell() | |
48 | buff.read(1) | |
49 | oid_length = KRB5_MECH_INDEP_TOKEN.decode_length_buffer(buff) | |
50 | buff.seek(pos) | |
51 | token_oid = ObjectIdentifier.load(buff.read(oid_length+2)) | |
52 | ||
53 | return KRB5_MECH_INDEP_TOKEN(buff.read(), str(token_oid), remlen = remaining_length) | |
54 | ||
55 | @staticmethod | |
56 | def decode_length_buffer(buff): | |
57 | lf = buff.read(1)[0] | |
58 | if lf <= 127: | |
59 | length = lf | |
60 | else: | |
61 | bcount = lf - 128 | |
62 | length = int.from_bytes(buff.read(bcount), byteorder = 'big', signed = False) | |
63 | return length | |
64 | ||
65 | @staticmethod | |
66 | def encode_length(length): | |
67 | if length <= 127: | |
68 | return length.to_bytes(1, byteorder = 'big', signed = False) | |
69 | else: | |
70 | lb = length.to_bytes((length.bit_length() + 7) // 8, 'big') | |
71 | return (128+len(lb)).to_bytes(1, byteorder = 'big', signed = False) + lb | |
72 | ||
73 | ||
74 | def to_bytes(self): | |
75 | t = ObjectIdentifier(self.oid).dump() + self.data | |
76 | t = b'\x60' + KRB5_MECH_INDEP_TOKEN.encode_length(len(t)) + t | |
77 | return t[:-len(self.data)] , self.data | |
78 | ||
79 | ||
80 | class GSSAPIFlags(enum.IntFlag): | |
81 | GSS_C_DCE_STYLE = 0x1000 | |
82 | GSS_C_DELEG_FLAG = 1 | |
83 | GSS_C_MUTUAL_FLAG = 2 | |
84 | GSS_C_REPLAY_FLAG = 4 | |
85 | GSS_C_SEQUENCE_FLAG = 8 | |
86 | GSS_C_CONF_FLAG = 0x10 | |
87 | GSS_C_INTEG_FLAG = 0x20 | |
88 | ||
89 | class KG_USAGE(enum.Enum): | |
90 | ACCEPTOR_SEAL = 22 | |
91 | ACCEPTOR_SIGN = 23 | |
92 | INITIATOR_SEAL = 24 | |
93 | INITIATOR_SIGN = 25 | |
94 | ||
95 | class FlagsField(enum.IntFlag): | |
96 | SentByAcceptor = 0 | |
97 | Sealed = 2 | |
98 | AcceptorSubkey = 4 | |
99 | ||
100 | # https://tools.ietf.org/html/rfc4757 (7.2) | |
101 | class GSSMIC_RC4: | |
102 | def __init__(self): | |
103 | self.TOK_ID = b'\x01\x01' | |
104 | self.SGN_ALG = b'\x11\x00' #HMAC | |
105 | self.Filler = b'\xff'*4 | |
106 | self.SND_SEQ = None | |
107 | self.SGN_CKSUM = None | |
108 | ||
109 | @staticmethod | |
110 | def from_bytes(data): | |
111 | return GSSMIC_RC4.from_buffer(io.BytesIO(data)) | |
112 | ||
113 | @staticmethod | |
114 | def from_buffer(buff): | |
115 | mic = GSSMIC_RC4() | |
116 | mic.TOK_ID = buff.read(2) | |
117 | mic.SGN_ALG = buff.read(2) | |
118 | mic.Filler = buff.read(4) | |
119 | mic.SND_SEQ = buff.read(8) | |
120 | mic.SGN_CKSUM = buff.read(8) | |
121 | ||
122 | return mic | |
123 | ||
124 | def to_bytes(self): | |
125 | t = self.TOK_ID | |
126 | t += self.SGN_ALG | |
127 | t += self.Filler | |
128 | t += self.SND_SEQ | |
129 | if self.SGN_CKSUM is not None: | |
130 | t += self.SGN_CKSUM | |
131 | ||
132 | return t | |
133 | ||
134 | class GSSWRAP_RC4: | |
135 | def __init__(self): | |
136 | self.TOK_ID = b'\x02\x01' | |
137 | self.SGN_ALG = b'\x11\x00' #HMAC | |
138 | self.SEAL_ALG = None | |
139 | self.Filler = b'\xFF' * 2 | |
140 | self.SND_SEQ = None | |
141 | self.SGN_CKSUM = None | |
142 | self.Confounder = None | |
143 | ||
144 | def __str__(self): | |
145 | t = 'GSSWRAP_RC4\r\n' | |
146 | t += 'TOK_ID : %s\r\n' % self.TOK_ID.hex() | |
147 | t += 'SGN_ALG : %s\r\n' % self.SGN_ALG.hex() | |
148 | t += 'SEAL_ALG : %s\r\n' % self.SEAL_ALG.hex() | |
149 | t += 'Filler : %s\r\n' % self.Filler.hex() | |
150 | t += 'SND_SEQ : %s\r\n' % self.SND_SEQ.hex() | |
151 | t += 'SGN_CKSUM : %s\r\n' % self.SGN_CKSUM.hex() | |
152 | t += 'Confounder : %s\r\n' % self.Confounder.hex() | |
153 | return t | |
154 | ||
155 | @staticmethod | |
156 | def from_bytes(data): | |
157 | return GSSWRAP_RC4.from_buffer(io.BytesIO(data)) | |
158 | ||
159 | @staticmethod | |
160 | def from_buffer(buff): | |
161 | wrap = GSSWRAP_RC4() | |
162 | wrap.TOK_ID = buff.read(2) | |
163 | wrap.SGN_ALG = buff.read(2) | |
164 | wrap.SEAL_ALG = buff.read(2) | |
165 | wrap.Filler = buff.read(2) | |
166 | wrap.SND_SEQ = buff.read(8) | |
167 | wrap.SGN_CKSUM = buff.read(8) | |
168 | wrap.Confounder = buff.read(8) | |
169 | ||
170 | return wrap | |
171 | ||
172 | def to_bytes(self): | |
173 | t = self.TOK_ID | |
174 | t += self.SGN_ALG | |
175 | t += self.SEAL_ALG | |
176 | t += self.Filler | |
177 | t += self.SND_SEQ | |
178 | ||
179 | if self.SGN_CKSUM: | |
180 | t += self.SGN_CKSUM | |
181 | if self.Confounder: | |
182 | t += self.Confounder | |
183 | ||
184 | ||
185 | return t | |
186 | ||
187 | class GSSAPI_RC4: | |
188 | def __init__(self, session_key): | |
189 | self.session_key = session_key | |
190 | ||
191 | def GSS_GetMIC(self, data, sequenceNumber, direction = 'init'): | |
192 | raise Exception('Not tested! Sure it needs some changes') | |
193 | GSS_GETMIC_HEADER = b'\x60\x23\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' | |
194 | ||
195 | # Let's pad the data | |
196 | pad = (4 - (len(data) % 4)) & 0x3 | |
197 | padStr = bytes([pad]) * pad | |
198 | data += padStr | |
199 | ||
200 | mic = GSSMIC_RC4() | |
201 | ||
202 | if direction == 'init': | |
203 | mic.SND_SEQ = sequenceNumber.to_bytes(4, 'big', signed = False) + b'\x00'*4 | |
204 | else: | |
205 | mic.SND_SEQ = sequenceNumber.to_bytes(4, 'big', signed = False) + b'\xff'*4 | |
206 | ||
207 | Ksign_ctx = hmac_md5(self.session_key.contents) | |
208 | Ksign_ctx.update(b'signaturekey\0') | |
209 | Ksign = Ksign_ctx.digest() | |
210 | ||
211 | id = 15 | |
212 | temp = md5( id.to_bytes(4, 'little', signed = False) + mic.to_bytes()[:8] ).digest() | |
213 | chksum_ctx = hmac_md5(Ksign) | |
214 | chksum_ctx.update(temp) | |
215 | mic.SGN_CKSUM = chksum_ctx.digest()[:8] | |
216 | ||
217 | id = 0 | |
218 | temp = hmac_md5(self.session_key.contents) | |
219 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
220 | ||
221 | Kseq_ctx = hmac_md5(temp.digest()) | |
222 | Kseq_ctx.update(mic.SGN_CKSUM) | |
223 | Kseq = Kseq_ctx.digest() | |
224 | ||
225 | mic.SGN_CKSUM = RC4(Kseq).encrypt(mic.SND_SEQ) | |
226 | ||
227 | return GSS_GETMIC_HEADER + mic.to_bytes() | |
228 | ||
229 | ||
230 | def GSS_Wrap(self, data, seq_num, direction = 'init', encrypt=True, cofounder = None): | |
231 | #direction = 'a' | |
232 | #seq_num = 0 | |
233 | #print('[GSS_Wrap] data: %s' % data) | |
234 | #print('[GSS_Wrap] seq_num: %s' % seq_num.to_bytes(4, 'big', signed = False).hex()) | |
235 | #print('[GSS_Wrap] direction: %s' % direction) | |
236 | #print('[GSS_Wrap] encrypt: %s' % encrypt) | |
237 | # | |
238 | #print('[GSS_Wrap] auth_data: %s' % auth_data) | |
239 | ||
240 | #pad = 0 | |
241 | if encrypt is True: | |
242 | data += b'\x01' | |
243 | #pad = (8 - (len(data) % 8)) & 0x7 | |
244 | #padStr = bytes([pad]) * pad | |
245 | #data += padStr | |
246 | # | |
247 | ##data += b'\x08' * 8 | |
248 | #print('[GSS_Wrap] pad: %s' % pad) | |
249 | #print('[GSS_Wrap] data padded: %s' % data) | |
250 | ||
251 | ||
252 | token = GSSWRAP_RC4() | |
253 | token.SEAL_ALG = b'\x10\x00' # RC4 | |
254 | ||
255 | if direction == 'init': | |
256 | token.SND_SEQ = seq_num.to_bytes(4, 'big', signed = False) + b'\x00'*4 | |
257 | else: | |
258 | token.SND_SEQ = seq_num.to_bytes(4, 'big', signed = False) + b'\xff'*4 | |
259 | ||
260 | token.Confounder = os.urandom(8) | |
261 | #if cofounder is not None: | |
262 | # token.Confounder = cofounder | |
263 | # #testing purposes only, pls remove | |
264 | ||
265 | ||
266 | temp = hmac_md5(self.session_key.contents) | |
267 | temp.update(b'signaturekey\0') | |
268 | Ksign = temp.digest() | |
269 | ||
270 | id = 13 | |
271 | Sgn_Cksum = md5(id.to_bytes(4, 'little', signed = False) + token.to_bytes()[:8] + token.Confounder + data).digest() | |
272 | ||
273 | klocal = b'' | |
274 | for b in self.session_key.contents: | |
275 | klocal += bytes([b ^ 0xf0]) | |
276 | ||
277 | id = 0 | |
278 | temp = hmac_md5(klocal) | |
279 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
280 | temp = hmac_md5(temp.digest()) | |
281 | temp.update(seq_num.to_bytes(4, 'big', signed = False)) | |
282 | Kcrypt = temp.digest() | |
283 | ||
284 | temp = hmac_md5(Ksign) | |
285 | temp.update(Sgn_Cksum) | |
286 | token.SGN_CKSUM = temp.digest()[:8] | |
287 | ||
288 | id = 0 | |
289 | temp = hmac_md5(self.session_key.contents) | |
290 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
291 | temp = hmac_md5(temp.digest()) | |
292 | temp.update(token.SGN_CKSUM) | |
293 | Kseq = temp.digest() | |
294 | ||
295 | token.SND_SEQ = RC4(Kseq).encrypt(token.SND_SEQ) | |
296 | ||
297 | ||
298 | #if auth_data is not None: | |
299 | if encrypt is False: | |
300 | #print('Unwrap sessionkey: %s' % self.session_key.contents.hex()) | |
301 | #print('Unwrap data : %s' % data.hex()) | |
302 | ||
303 | sspi_wrap = KRB5_MECH_INDEP_TOKEN.from_bytes(data) | |
304 | ||
305 | hdr = sspi_wrap.data[:32] | |
306 | data = sspi_wrap.data[32:] | |
307 | ||
308 | wrap = GSSWRAP_RC4.from_bytes(hdr) | |
309 | ||
310 | id = 0 | |
311 | temp = hmac_md5(self.session_key.contents) | |
312 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
313 | temp = hmac_md5(temp.digest()) | |
314 | temp.update(wrap.SGN_CKSUM) | |
315 | Kseq = temp.digest() | |
316 | ||
317 | snd_seq = RC4(Kseq).encrypt(wrap.SND_SEQ) | |
318 | ||
319 | id = 0 | |
320 | temp = hmac_md5(klocal) | |
321 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
322 | temp = hmac_md5(temp.digest()) | |
323 | temp.update(snd_seq[:4]) | |
324 | Kcrypt = temp.digest() | |
325 | ||
326 | rc4 = RC4(Kcrypt) | |
327 | dec_cofounder = rc4.decrypt(wrap.Confounder) | |
328 | dec_data = rc4.decrypt(data) | |
329 | ||
330 | id = 13 | |
331 | Sgn_Cksum_calc = md5(id.to_bytes(4, 'little', signed = False) + wrap.to_bytes()[:8] + dec_cofounder + dec_data).digest() | |
332 | ||
333 | temp = hmac_md5(Ksign) | |
334 | temp.update(Sgn_Cksum_calc) | |
335 | Sgn_Cksum_calc = temp.digest()[:8] | |
336 | ||
337 | if wrap.SGN_CKSUM != Sgn_Cksum_calc[:8]: | |
338 | return None, Exception('Integrity verification failed') | |
339 | ||
340 | pad = 1 | |
341 | return dec_data[:-pad], None | |
342 | ||
343 | elif encrypt is True: | |
344 | rc4 = RC4(Kcrypt) | |
345 | token.Confounder = rc4.encrypt(token.Confounder) | |
346 | cipherText = rc4.encrypt(data) | |
347 | finalData, cipherText = KRB5_MECH_INDEP_TOKEN( token.to_bytes() + cipherText, '1.2.840.113554.1.2.2' ).to_bytes() | |
348 | ||
349 | ||
350 | #print('cipherText %s' % cipherText.hex()) | |
351 | #print('finalData %s' % finalData.hex()) | |
352 | #print('sessionkey %s' % self.session_key.contents.hex()) | |
353 | return cipherText, finalData | |
354 | ||
355 | ||
356 | def GSS_Unwrap(self, data, seq_num, direction='init'): | |
357 | #print('GSS_Unwrap data : %s' % data) | |
358 | dec_data, err = self.GSS_Wrap(data, seq_num, direction=direction, encrypt = False) | |
359 | #print('GSS_Unwrap decrypted data : %s' % dec_data) | |
360 | return dec_data, err | |
361 | ||
362 | # 4.2.6.1. MIC Tokens | |
363 | class GSSMIC: | |
364 | def __init__(self): | |
365 | self.TOK_ID = b'\x04\x04' | |
366 | self.Flags = None | |
367 | self.Filler = b'\xFF' * 5 | |
368 | self.SND_SEQ = None | |
369 | self.SGN_CKSUM = None | |
370 | ||
371 | @staticmethod | |
372 | def from_bytes(data): | |
373 | return GSSMIC.from_buffer(io.BytesIO(data)) | |
374 | ||
375 | @staticmethod | |
376 | def from_buffer(buff): | |
377 | m = GSSMIC() | |
378 | m.TOK_ID = buff.read(2) | |
379 | m.Flags = FlagsField(int.from_bytes(buff.read(1), 'big', signed = False)) | |
380 | m.Filler = buff.read(5) | |
381 | m.SND_SEQ = int.from_bytes(buff.read(8), 'big', signed = False) | |
382 | m.SGN_CKSUM = buff.read() #should know the size based on the algo! | |
383 | return m | |
384 | ||
385 | def to_bytes(self): | |
386 | t = self.TOK_ID | |
387 | t += self.Flags.to_bytes(1, 'big', signed = False) | |
388 | t += self.Filler | |
389 | t += self.SND_SEQ.to_bytes(8, 'big', signed = False) | |
390 | if self.SGN_CKSUM is not None: | |
391 | t += self.SGN_CKSUM | |
392 | ||
393 | return t | |
394 | ||
395 | # 4.2.6.2. Wrap Tokens | |
396 | class GSSWrapToken: | |
397 | def __init__(self): | |
398 | self.TOK_ID = b'\x05\x04' | |
399 | self.Flags = None | |
400 | self.Filler = b'\xFF' | |
401 | self.EC = None | |
402 | self.RRC = None | |
403 | self.SND_SEQ = None | |
404 | self.Data = None | |
405 | ||
406 | @staticmethod | |
407 | def from_bytes(data): | |
408 | return GSSWrapToken.from_buffer(io.BytesIO(data)) | |
409 | ||
410 | @staticmethod | |
411 | def from_buffer(buff): | |
412 | m = GSSWrapToken() | |
413 | m.TOK_ID = buff.read(2) | |
414 | m.Flags = FlagsField(int.from_bytes(buff.read(1), 'big', signed = False)) | |
415 | m.Filler = buff.read(1) | |
416 | m.EC = int.from_bytes(buff.read(2), 'big', signed = False) | |
417 | m.RRC = int.from_bytes(buff.read(2), 'big', signed = False) | |
418 | m.SND_SEQ = int.from_bytes(buff.read(8), 'big', signed = False) | |
419 | return m | |
420 | ||
421 | def to_bytes(self): | |
422 | t = self.TOK_ID | |
423 | t += self.Flags.to_bytes(1, 'big', signed = False) | |
424 | t += self.Filler | |
425 | t += self.EC.to_bytes(2, 'big', signed = False) | |
426 | t += self.RRC.to_bytes(2, 'big', signed = False) | |
427 | t += self.SND_SEQ.to_bytes(8, 'big', signed = False) | |
428 | if self.Data is not None: | |
429 | t += self.Data | |
430 | ||
431 | return t | |
432 | ||
433 | class GSSAPI_AES: | |
434 | def __init__(self, session_key, cipher_type, checksum_profile): | |
435 | self.session_key = session_key | |
436 | self.checksum_profile = checksum_profile | |
437 | self.cipher_type = cipher_type | |
438 | self.cipher = None | |
439 | ||
440 | def rotate(self, data, numBytes): | |
441 | numBytes %= len(data) | |
442 | left = len(data) - numBytes | |
443 | result = data[left:] + data[:left] | |
444 | return result | |
445 | ||
446 | def unrotate(self, data, numBytes): | |
447 | numBytes %= len(data) | |
448 | result = data[numBytes:] + data[:numBytes] | |
449 | return result | |
450 | ||
451 | def GSS_GetMIC(self, data, seq_num): | |
452 | pad = (4 - (len(data) % 4)) & 0x3 | |
453 | padStr = bytes([pad]) * pad | |
454 | data += padStr | |
455 | ||
456 | m = GSSMIC() | |
457 | m.Flags = FlagsField.AcceptorSubkey | |
458 | m.SND_SEQ = seq_num | |
459 | checksum_profile = self.checksum_profile() | |
460 | m.checksum = checksum_profile.checksum(self.session_key, KG_USAGE.INITIATOR_SIGN.value, data + m.to_bytes()[:16]) | |
461 | ||
462 | return m.to_bytes() | |
463 | ||
464 | def GSS_Wrap(self, data, seq_num, use_padding = False): | |
465 | #print('[GSS_Wrap] seq_num: %s' % seq_num.to_bytes(4, 'big', signed = False).hex()) | |
466 | cipher = self.cipher_type() | |
467 | pad = 0 | |
468 | if use_padding is True: | |
469 | pad = ((cipher.blocksize - len(data)) % cipher.blocksize) #(cipher.blocksize - (len(data) % cipher.blocksize)) & 15 | |
470 | padStr = b'\xFF' * pad | |
471 | data += padStr | |
472 | ||
473 | t = GSSWrapToken() | |
474 | t.Flags = FlagsField.AcceptorSubkey | FlagsField.Sealed | |
475 | t.EC = pad | |
476 | t.RRC = 0 | |
477 | t.SND_SEQ = seq_num | |
478 | ||
479 | #print('Wrap data: %s' % (data + t.to_bytes())) | |
480 | cipher_text = cipher.encrypt(self.session_key, KG_USAGE.INITIATOR_SEAL.value, data + t.to_bytes(), None) | |
481 | t.RRC = 28 #[RFC4121] section 4.2.5 | |
482 | cipher_text = self.rotate(cipher_text, t.RRC + t.EC) | |
483 | ||
484 | ret1 = cipher_text | |
485 | ret2 = t.to_bytes() | |
486 | ||
487 | return ret1, ret2 | |
488 | ||
489 | def GSS_Unwrap(self, data, seq_num, direction='init', auth_data = None, use_padding = False): | |
490 | #print('') | |
491 | #print('Unwrap data %s' % data[16:]) | |
492 | #print('Unwrap hdr %s' % data[:16]) | |
493 | ||
494 | cipher = self.cipher_type() | |
495 | original_hdr = GSSWrapToken.from_bytes(data[:16]) | |
496 | rotated = data[16:] | |
497 | ||
498 | cipher_text = self.unrotate(rotated, original_hdr.RRC + original_hdr.EC) | |
499 | plain_text = cipher.decrypt(self.session_key, KG_USAGE.ACCEPTOR_SEAL.value, cipher_text) | |
500 | new_hdr = GSSWrapToken.from_bytes(plain_text[-16:]) | |
501 | ||
502 | #signature checking | |
503 | new_hdr.RRC = 28 | |
504 | if data[:16] != new_hdr.to_bytes(): | |
505 | return None, Exception('GSS_Unwrap signature mismatch!') | |
506 | ||
507 | ||
508 | #print('Unwrap checksum: %s' % plain_text[-(original_hdr.EC + 16):]) | |
509 | #print('Unwrap orig chk: %s' % original_hdr.to_bytes()) | |
510 | #print('Unwrap result 1: %s' % plain_text) | |
511 | #print('Unwrap result : %s' % plain_text[:-(original_hdr.EC + 16)]) | |
512 | return plain_text[:-(original_hdr.EC + 16)], None | |
513 | ||
514 | def get_gssapi(session_key): | |
515 | if session_key.enctype == encryption.Enctype.AES256: | |
516 | return GSSAPI_AES(session_key, encryption._AES256CTS, encryption._SHA1AES256) | |
517 | if session_key.enctype == encryption.Enctype.AES128: | |
518 | return GSSAPI_AES(session_key, encryption._AES128CTS, encryption._SHA1AES128) | |
519 | elif session_key.enctype == encryption.Enctype.RC4: | |
520 | return GSSAPI_RC4(session_key) | |
521 | else: | |
522 | raise Exception('Unsupported etype %s' % session_key.enctype) | |
523 | ||
524 | ||
525 | def test(): | |
526 | data = b'\xAF' * 1024 | |
527 | session_key = encryption.Key( encryption.Enctype.AES256 , bytes.fromhex('3e242e91996aadd513ecb1bc2369e44183e08e08c51550fa4b681e77f75ed8e1')) | |
528 | sequenceNumber = 0 | |
529 | gssapi = get_gssapi(session_key) | |
530 | ||
531 | r1, r2 = gssapi.GSS_Wrap(data, sequenceNumber) | |
532 | print(len(r2)) | |
533 | sent = r2 + r1 | |
534 | print(r1) | |
535 | ret1, ret2 = gssapi.GSS_Unwrap(sent, sequenceNumber) | |
536 | ||
537 | print(r1.hex()) | |
538 | print(ret1.hex()) | |
539 | ||
540 | ||
541 | if __name__ == '__main__': | |
542 | test()⏎ |
9 | 9 | ## TODO: RPC auth type is not implemented or tested!!!! |
10 | 10 | |
11 | 11 | from msldap.authentication.spnego.asn1_structs import KRB5Token |
12 | from minikerberos.gssapi.gssapi import get_gssapi | |
12 | from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken, KRB5_MECH_INDEP_TOKEN | |
13 | 13 | from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP |
14 | 14 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table |
15 | 15 | |
16 | 16 | from multiplexor.operator.external.sspi import KerberosSSPIClient |
17 | 17 | from multiplexor.operator import MultiplexorOperator |
18 | import enum | |
18 | 19 | |
19 | # SMBKerberosSSPICredential: | |
20 | # mutual auth not supported | |
21 | # encryption is always on | |
22 | # we dont get the output flags back (lack of time to do the multiplexor protocol... TODO | |
20 | 23 | |
21 | class SMBKerberosMultiplexor: | |
24 | class ISC_REQ(enum.IntFlag): | |
25 | DELEGATE = 1 | |
26 | MUTUAL_AUTH = 2 | |
27 | REPLAY_DETECT = 4 | |
28 | SEQUENCE_DETECT = 8 | |
29 | CONFIDENTIALITY = 16 | |
30 | USE_SESSION_KEY = 32 | |
31 | PROMPT_FOR_CREDS = 64 | |
32 | USE_SUPPLIED_CREDS = 128 | |
33 | ALLOCATE_MEMORY = 256 | |
34 | USE_DCE_STYLE = 512 | |
35 | DATAGRAM = 1024 | |
36 | CONNECTION = 2048 | |
37 | CALL_LEVEL = 4096 | |
38 | FRAGMENT_SUPPLIED = 8192 | |
39 | EXTENDED_ERROR = 16384 | |
40 | STREAM = 32768 | |
41 | INTEGRITY = 65536 | |
42 | IDENTIFY = 131072 | |
43 | NULL_SESSION = 262144 | |
44 | MANUAL_CRED_VALIDATION = 524288 | |
45 | RESERVED1 = 1048576 | |
46 | FRAGMENT_TO_FIT = 2097152 | |
47 | HTTP = 0x10000000 | |
48 | ||
49 | class MSLDAPKerberosMultiplexor: | |
22 | 50 | def __init__(self, settings): |
23 | 51 | self.iterations = 0 |
24 | 52 | self.settings = settings |
29 | 57 | self.gssapi = None |
30 | 58 | self.etype = None |
31 | 59 | self.session_key = None |
60 | self.seq_number = 0 | |
61 | self.flags = ISC_REQ.CONNECTION | |
32 | 62 | |
33 | 63 | self.setup() |
34 | 64 | |
35 | 65 | def setup(self): |
36 | return | |
66 | if self.settings.encrypt is True: | |
67 | self.flags = \ | |
68 | ISC_REQ.CONFIDENTIALITY |\ | |
69 | ISC_REQ.INTEGRITY |\ | |
70 | ISC_REQ.REPLAY_DETECT |\ | |
71 | ISC_REQ.SEQUENCE_DETECT | |
72 | ||
73 | def get_seq_number(self): | |
74 | """ | |
75 | Fetches the starting sequence number. This is either zero or can be found in the authenticator field of the | |
76 | AP_REQ structure. As windows uses a random seq number AND a subkey as well, we can't obtain it by decrypting the | |
77 | AP_REQ structure. Insead under the hood we perform an encryption operation via EncryptMessage API which will | |
78 | yield the start sequence number | |
79 | """ | |
80 | return self.seq_number | |
37 | 81 | |
38 | 82 | async def encrypt(self, data, message_no): |
39 | 83 | return self.gssapi.GSS_Wrap(data, message_no) |
41 | 85 | async def decrypt(self, data, message_no, direction='init', auth_data=None): |
42 | 86 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) |
43 | 87 | |
88 | def signing_needed(self): | |
89 | """ | |
90 | Checks if integrity protection was negotiated | |
91 | """ | |
92 | return ISC_REQ.INTEGRITY in self.flags | |
93 | ||
94 | def encryption_needed(self): | |
95 | """ | |
96 | Checks if confidentiality flag was negotiated | |
97 | """ | |
98 | return ISC_REQ.CONFIDENTIALITY in self.flags | |
99 | ||
44 | 100 | def get_session_key(self): |
45 | 101 | return self.session_key |
46 | 102 | |
47 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
103 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): | |
48 | 104 | #authdata is only for api compatibility reasons |
49 | 105 | if self.ksspi is None: |
50 | 106 | await self.start_remote_kerberos() |
51 | 107 | try: |
52 | if is_rpc == True: | |
53 | raise Exception('Multiplexor kerberos for RPC is not yet implemented!') | |
54 | #if self.iterations == 0: | |
55 | # flags = ISC_REQ.CONFIDENTIALITY | \ | |
56 | # ISC_REQ.INTEGRITY | \ | |
57 | # ISC_REQ.MUTUAL_AUTH | \ | |
58 | # ISC_REQ.REPLAY_DETECT | \ | |
59 | # ISC_REQ.SEQUENCE_DETECT|\ | |
60 | # ISC_REQ.USE_DCE_STYLE | |
61 | # | |
62 | # | |
63 | # #token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) | |
64 | # token = await self.ksspi.authenticate(self.settings.target, flags = flags, token_data = authData) | |
65 | # print(token.hex()) | |
66 | # self.iterations += 1 | |
67 | # return token, True | |
68 | # | |
69 | #elif self.iterations == 1: | |
70 | # flags = ISC_REQ.USE_DCE_STYLE | |
71 | # | |
72 | # #token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) | |
73 | # token = await self.ksspi.get_ticket_for_spn(self.settings.target, flags = flags, token_data = authData) | |
74 | # print(token.hex()) | |
75 | # | |
76 | # | |
77 | # aprep = AP_REP.load(token).native | |
78 | # | |
79 | # subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) | |
80 | # | |
81 | # cipher_text = aprep['enc-part']['cipher'] | |
82 | # cipher = _enctype_table[aprep['enc-part']['etype']]() | |
83 | # | |
84 | # plaintext = cipher.decrypt(subkey, 12, cipher_text) | |
85 | # | |
86 | # self.gssapi = get_gssapi(subkey) | |
87 | # | |
88 | # self.iterations += 1 | |
89 | # return token, False | |
90 | # | |
91 | #else: | |
92 | # raise Exception('Multiplexor Kerberos authentication exceeded maximum iteration counts') | |
108 | apreq, res = await self.ksspi.authenticate(self.settings.target.to_target_string(), flags=str(self.flags.value)) | |
109 | #print('MULTIPLEXOR KERBEROS SSPI, APREQ: %s ERROR: %s' % (apreq, res)) | |
110 | if res is not None: | |
111 | return None, None, res | |
112 | ||
113 | # here it seems like we get the full token not just the apreq data... | |
114 | # so we need to discard the layers | |
115 | ||
116 | self.session_key, err = await self.ksspi.get_session_key() | |
117 | if err is not None: | |
118 | return None, None, err | |
119 | ||
120 | unwrap = KRB5_MECH_INDEP_TOKEN.from_bytes(apreq) | |
121 | aprep = AP_REQ.load(unwrap.data[2:]).native | |
122 | subkey = Key(aprep['ticket']['enc-part']['etype'], self.session_key) | |
123 | self.gssapi = get_gssapi(subkey) | |
93 | 124 | |
94 | else: | |
95 | apreq, res = await self.ksspi.authenticate(self.settings.target) | |
96 | print('MULTIPLEXOR KERBEROS SSPI, APREQ: %s ERROR: %s' % (apreq, res)) | |
97 | if res is None: | |
98 | self.session_key, res = await self.ksspi.get_session_key() | |
99 | ||
100 | return apreq, res | |
125 | if aprep['ticket']['enc-part']['etype'] != 23: | |
126 | raw_seq_data, err = await self.ksspi.get_seq_number() | |
127 | if err is not None: | |
128 | return None, None, err | |
129 | self.seq_number = GSSWrapToken.from_bytes(raw_seq_data[16:]).SND_SEQ | |
130 | ||
131 | return unwrap.data[2:], False, res | |
101 | 132 | except Exception as e: |
102 | import traceback | |
103 | traceback.print_exc() | |
104 | return None | |
133 | return None, None, e | |
105 | 134 | |
106 | 135 | async def start_remote_kerberos(self): |
107 | 136 | try: |
2 | 2 | # This is just a simple interface to the minikerberos library to support SPNEGO |
3 | 3 | # |
4 | 4 | # |
5 | # - Hardships - | |
6 | # 1. DCERPC kerberos authentication requires a complete different approach and flags, | |
7 | # also requires mutual authentication | |
8 | # | |
9 | 5 | # - Links - |
10 | # 1. Most of the idea was taken from impacket | |
11 | # 2. See minikerberos library | |
6 | # 1. See minikerberos library | |
12 | 7 | |
13 | 8 | import datetime |
14 | 9 | |
10 | import os | |
15 | 11 | from minikerberos.common import * |
16 | 12 | |
17 | from minikerberos.protocol.asn1_structs import AP_REP, EncAPRepPart, EncryptedData | |
18 | from minikerberos.gssapi.gssapi import get_gssapi | |
13 | ||
14 | from minikerberos.protocol.asn1_structs import AP_REP, EncAPRepPart, EncryptedData, AP_REQ, Ticket | |
15 | from msldap.authentication.kerberos.gssapi import get_gssapi, KRB5_MECH_INDEP_TOKEN | |
16 | from msldap.commons.proxy import MSLDAPProxyType | |
19 | 17 | from minikerberos.protocol.structures import ChecksumFlags |
20 | 18 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table |
21 | 19 | from minikerberos.protocol.constants import MESSAGE_TYPE |
22 | 20 | from minikerberos.aioclient import AIOKerberosClient |
21 | from minikerberos.network.aioclientsockssocket import AIOKerberosClientSocksSocket | |
22 | from msldap import logger | |
23 | 23 | |
24 | 24 | # SMBKerberosCredential |
25 | ||
26 | MSLDAP_SOCKS_PROXY_TYPES = [ | |
27 | MSLDAPProxyType.SOCKS4, | |
28 | MSLDAPProxyType.SOCKS4_SSL, | |
29 | MSLDAPProxyType.SOCKS5, | |
30 | MSLDAPProxyType.SOCKS5_SSL, | |
31 | MSLDAPProxyType.WSNET, | |
32 | ] | |
25 | 33 | |
26 | 34 | class MSLDAPKerberos: |
27 | 35 | def __init__(self, settings): |
28 | 36 | self.settings = settings |
37 | self.signing_preferred = None | |
38 | self.encryption_preferred = None | |
29 | 39 | self.ccred = None |
30 | 40 | self.target = None |
31 | 41 | self.spn = None |
32 | 42 | self.kc = None |
43 | self.flags = None | |
44 | self.preferred_etypes = [23,17,18] | |
33 | 45 | |
34 | 46 | self.session_key = None |
35 | 47 | self.gssapi = None |
36 | 48 | self.iterations = 0 |
37 | 49 | self.etype = None |
50 | self.seq_number = 0 | |
51 | self.expected_server_seq_number = None | |
52 | self.from_ccache = False | |
38 | 53 | |
39 | 54 | self.setup() |
40 | 55 | |
56 | def get_seq_number(self): | |
57 | """ | |
58 | Returns the initial sequence number. It is 0 by default, but can be adjusted during authentication, | |
59 | by passing the 'seq_number' parameter in the 'authenticate' function | |
60 | """ | |
61 | return self.seq_number | |
62 | ||
41 | 63 | def signing_needed(self): |
42 | return False | |
64 | """ | |
65 | Checks if integrity protection was negotiated | |
66 | """ | |
67 | return ChecksumFlags.GSS_C_INTEG_FLAG in self.flags | |
43 | 68 | |
44 | 69 | def encryption_needed(self): |
45 | return False #change to true to enable encryption channel binding | |
70 | """ | |
71 | Checks if confidentiality flag was negotiated | |
72 | """ | |
73 | return ChecksumFlags.GSS_C_CONF_FLAG in self.flags | |
46 | 74 | |
47 | 75 | async def sign(self, data, message_no, direction = 'init'): |
76 | """ | |
77 | Signs a message. | |
78 | """ | |
48 | 79 | return self.gssapi.GSS_GetMIC(data, message_no, direction = direction) |
49 | 80 | |
50 | 81 | async def encrypt(self, data, message_no): |
82 | """ | |
83 | Encrypts a message. | |
84 | """ | |
85 | ||
51 | 86 | return self.gssapi.GSS_Wrap(data, message_no) |
52 | 87 | |
53 | async def decrypt(self, data, message_no, direction='init', auth_data=None): | |
54 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) | |
88 | async def decrypt(self, data, message_no, direction='init'): | |
89 | """ | |
90 | Decrypts message. Also performs integrity checking. | |
91 | """ | |
92 | ||
93 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction) | |
55 | 94 | |
56 | 95 | def setup(self): |
57 | 96 | self.ccred = self.settings.ccred |
58 | 97 | self.spn = self.settings.spn |
59 | 98 | self.target = self.settings.target |
60 | ||
61 | self.kc = AIOKerberosClient(self.ccred, self.target) | |
99 | if self.settings.enctypes is not None: | |
100 | self.preferred_etypes = self.settings.enctypes | |
101 | ||
102 | self.flags = ChecksumFlags.GSS_C_MUTUAL_FLAG | |
103 | if self.settings.encrypt is True: | |
104 | self.flags = \ | |
105 | ChecksumFlags.GSS_C_CONF_FLAG |\ | |
106 | ChecksumFlags.GSS_C_INTEG_FLAG |\ | |
107 | ChecksumFlags.GSS_C_REPLAY_FLAG |\ | |
108 | ChecksumFlags.GSS_C_SEQUENCE_FLAG | |
62 | 109 | |
63 | 110 | |
64 | 111 | def get_session_key(self): |
65 | return self.session_key.contents | |
66 | ||
67 | async def authenticate(self, authData, flags = None, seq_number = 0, is_rpc = False): | |
68 | ||
69 | if self.iterations == 0: | |
70 | #tgt = await self.kc.get_TGT(override_etype=[18]) | |
71 | tgt = await self.kc.get_TGT() | |
72 | tgs, encpart, self.session_key = await self.kc.get_TGS(self.spn) | |
73 | self.gssapi = get_gssapi(self.session_key) | |
74 | ap_opts = [] | |
75 | if is_rpc == True: | |
112 | return self.session_key.contents, None | |
113 | ||
114 | ||
115 | async def setup_kc(self): | |
116 | try: | |
117 | # sockst/wsnet proxying is handled by the minikerberos&asysocks modules | |
118 | if self.target.proxy is None or self.target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES: | |
119 | self.kc = AIOKerberosClient(self.ccred, self.target) | |
120 | ||
121 | elif self.target.proxy.type in [MSLDAPProxyType.MULTIPLEXOR, MSLDAPProxyType.MULTIPLEXOR_SSL]: | |
122 | from msldap.network.multiplexor import MultiplexorProxyConnection | |
123 | mpc = MultiplexorProxyConnection(self.target) | |
124 | socks_proxy = await mpc.connect(is_kerberos = True) | |
125 | ||
126 | self.kc = AIOKerberosClient(self.ccred, socks_proxy) | |
127 | ||
128 | else: | |
129 | raise Exception('Unknown proxy type %s' % self.target.proxy.type) | |
130 | ||
131 | return None, None | |
132 | except Exception as e: | |
133 | return None, e | |
134 | ||
135 | async def authenticate(self, authData, flags = None, seq_number = 0, cb_data = None): | |
136 | """ | |
137 | This function is called (multiple times depending on the flags) to perform authentication. | |
138 | """ | |
139 | try: | |
140 | if self.kc is None: | |
141 | _, err = await self.setup_kc() | |
142 | if err is not None: | |
143 | return None, None, err | |
144 | ||
76 | 145 | if self.iterations == 0: |
77 | ap_opts.append('mutual-required') | |
78 | flags = ChecksumFlags.GSS_C_CONF_FLAG | ChecksumFlags.GSS_C_INTEG_FLAG | ChecksumFlags.GSS_C_SEQUENCE_FLAG|\ | |
79 | ChecksumFlags.GSS_C_REPLAY_FLAG | ChecksumFlags.GSS_C_MUTUAL_FLAG | ChecksumFlags.GSS_C_DCE_STYLE | |
80 | ||
81 | apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = flags, seq_number = seq_number, ap_opts=ap_opts) | |
146 | self.seq_number = 0 | |
82 | 147 | self.iterations += 1 |
83 | return apreq, False | |
84 | ||
148 | ||
149 | try: | |
150 | #check TGS first, maybe ccache already has what we need | |
151 | for target in self.ccred.ccache.list_targets(): | |
152 | # just printing this to debug... | |
153 | logger.debug('CCACHE SPN record: %s' % target) | |
154 | tgs, encpart, self.session_key = await self.kc.get_TGS(self.spn) | |
155 | ||
156 | self.from_ccache = True | |
157 | except: | |
158 | tgt = await self.kc.get_TGT(override_etype = self.preferred_etypes) | |
159 | tgs, encpart, self.session_key = await self.kc.get_TGS(self.spn)#, override_etype = self.preferred_etypes) | |
160 | ||
161 | #self.expected_server_seq_number = encpart.get('nonce', seq_number) | |
162 | ||
163 | ap_opts = [] | |
164 | if ChecksumFlags.GSS_C_MUTUAL_FLAG in self.flags or ChecksumFlags.GSS_C_DCE_STYLE in self.flags: | |
165 | if ChecksumFlags.GSS_C_MUTUAL_FLAG in self.flags: | |
166 | ap_opts.append('mutual-required') | |
167 | if self.from_ccache is False: | |
168 | apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = self.flags, seq_number = self.seq_number, ap_opts=ap_opts, cb_data = cb_data) | |
169 | else: | |
170 | apreq = self.kc.construct_apreq_from_ticket(Ticket(tgs['ticket']).dump(), self.session_key, tgs['crealm'], tgs['cname']['name-string'][0], flags = self.flags, seq_number = self.seq_number, ap_opts = ap_opts, cb_data = cb_data) | |
171 | return apreq, True, None | |
172 | ||
173 | else: | |
174 | #no mutual or dce auth will take one step only | |
175 | if self.from_ccache is False: | |
176 | apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = self.flags, seq_number = self.seq_number, ap_opts=[], cb_data = cb_data) | |
177 | else: | |
178 | apreq = self.kc.construct_apreq_from_ticket(Ticket(tgs['ticket']).dump(), self.session_key, tgs['crealm'], tgs['cname']['name-string'][0], flags = self.flags, seq_number = self.seq_number, ap_opts = ap_opts, cb_data = cb_data) | |
179 | ||
180 | ||
181 | self.gssapi = get_gssapi(self.session_key) | |
182 | return apreq, False, None | |
183 | ||
85 | 184 | else: |
86 | #mutual authentication part here | |
87 | aprep = AP_REP.load(authData).native | |
185 | self.iterations += 1 | |
186 | if ChecksumFlags.GSS_C_DCE_STYLE in self.flags: | |
187 | # adata = authData[16:] | |
188 | # if ChecksumFlags.GSS_C_DCE_STYLE in self.flags: | |
189 | # adata = authData | |
190 | raise Exception('DCE auth Not implemented!') | |
191 | ||
192 | # at this point we are dealing with mutual authentication | |
193 | # This means that the server sent back an AP-rep wrapped in a token | |
194 | # The APREP contains a new session key we'd need to update and a seq-number | |
195 | # that is expected the server will use for future communication. | |
196 | # For mutual auth we dont need to reply anything after this step, | |
197 | # but for DCE auth a reply is expected. TODO | |
198 | ||
199 | # converting the token to aprep | |
200 | token = KRB5_MECH_INDEP_TOKEN.from_bytes(authData) | |
201 | if token.data[:2] != b'\x02\x00': | |
202 | raise Exception('Unexpected token type! %s' % token.data[:2].hex() ) | |
203 | aprep = AP_REP.load(token.data[2:]).native | |
204 | ||
205 | # decrypting aprep | |
88 | 206 | cipher = _enctype_table[int(aprep['enc-part']['etype'])]() |
89 | 207 | cipher_text = aprep['enc-part']['cipher'] |
90 | 208 | temp = cipher.decrypt(self.session_key, 12, cipher_text) |
91 | ||
92 | 209 | enc_part = EncAPRepPart.load(temp).native |
93 | cipher = _enctype_table[int(enc_part['subkey']['keytype'])]() | |
94 | ||
95 | now = datetime.datetime.now(datetime.timezone.utc) | |
96 | apreppart_data = {} | |
97 | apreppart_data['cusec'] = now.microsecond | |
98 | apreppart_data['ctime'] = now.replace(microsecond=0) | |
99 | apreppart_data['seq-number'] = enc_part['seq-number'] | |
100 | ||
101 | apreppart_data_enc = cipher.encrypt(self.session_key, 12, EncAPRepPart(apreppart_data).dump(), None) | |
102 | ||
103 | #overriding current session key | |
104 | self.session_key = Key(cipher.enctype, enc_part['subkey']['keyvalue']) | |
105 | ||
106 | ap_rep = {} | |
107 | ap_rep['pvno'] = 5 | |
108 | ap_rep['msg-type'] = MESSAGE_TYPE.KRB_AP_REP.value | |
109 | ap_rep['enc-part'] = EncryptedData({'etype': self.session_key.enctype, 'cipher': apreppart_data_enc}) | |
110 | ||
111 | token = AP_REP(ap_rep).dump() | |
210 | ||
211 | #updating session key, gssapi | |
212 | self.session_key = Key(int(enc_part['subkey']['keytype']), enc_part['subkey']['keyvalue']) | |
213 | #self.seq_number = enc_part.get('seq-number', 0) | |
112 | 214 | self.gssapi = get_gssapi(self.session_key) |
113 | self.iterations += 1 | |
114 | ||
115 | return token, False | |
116 | else: | |
117 | apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = flags, seq_number = seq_number, ap_opts=ap_opts) | |
118 | return apreq, False⏎ | |
215 | ||
216 | return b'', False, None | |
217 | ||
218 | except Exception as e: | |
219 | return None, None, e⏎ |
5 | 5 | # |
6 | 6 | |
7 | 7 | from msldap.authentication.spnego.asn1_structs import KRB5Token |
8 | from winsspi.sspi import KerberosSMBSSPI | |
9 | from winsspi.common.function_defs import ISC_REQ | |
10 | from minikerberos.gssapi.gssapi import get_gssapi | |
8 | from winsspi.sspi import KerberosMSLDAPSSPI | |
9 | from winsspi.common.function_defs import ISC_REQ, GetSequenceNumberFromEncryptdataKerberos | |
10 | from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken | |
11 | 11 | from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP |
12 | 12 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table |
13 | 13 | |
15 | 15 | def __init__(self, settings): |
16 | 16 | self.iterations = 0 |
17 | 17 | self.settings = settings |
18 | self.mode = 'CLIENT' | |
19 | self.ksspi = KerberosSMBSSPI() | |
20 | self.client = None | |
21 | self.target = None | |
18 | self.username = settings.username | |
19 | self.password = settings.password | |
20 | self.domain = settings.domain | |
21 | self.actual_ctx_flags = None #this will be popilated by the output of get_ticket_for_spn | |
22 | self.flags = ISC_REQ.CONNECTION | |
23 | if settings.encrypt is True: | |
24 | self.flags = ISC_REQ.CONFIDENTIALITY| ISC_REQ.INTEGRITY | ISC_REQ.CONNECTION #| ISC_REQ.MUTUAL_AUTH #| ISC_REQ.USE_DCE_STYLE | |
25 | self.ksspi = None | |
26 | self.spn = settings.spn | |
22 | 27 | self.gssapi = None |
23 | 28 | self.etype = None |
24 | ||
25 | self.setup() | |
26 | ||
27 | def setup(self): | |
28 | self.mode = self.settings.mode | |
29 | self.client = self.settings.client | |
30 | self.target = self.settings.target | |
31 | ||
29 | self.session_key = None | |
30 | self.seq_number = None | |
31 | ||
32 | def get_seq_number(self): | |
33 | """ | |
34 | Fetches the starting sequence number. This is either zero or can be found in the authenticator field of the | |
35 | AP_REQ structure. As windows uses a random seq number AND a subkey as well, we can't obtain it by decrypting the | |
36 | AP_REQ structure. Insead under the hood we perform an encryption operation via EncryptMessage API which will | |
37 | yield the start sequence number | |
38 | """ | |
39 | if self.seq_number is not None: | |
40 | return self.seq_number | |
41 | if ISC_REQ.CONFIDENTIALITY in self.actual_ctx_flags: | |
42 | self.seq_number = GetSequenceNumberFromEncryptdataKerberos(self.ksspi.context) | |
43 | if self.seq_number is None: | |
44 | self.seq_number = 0 | |
45 | ||
46 | return self.seq_number | |
47 | ||
48 | def signing_needed(self): | |
49 | """ | |
50 | Checks if integrity protection was enabled | |
51 | """ | |
52 | return ISC_REQ.INTEGRITY in self.actual_ctx_flags | |
53 | ||
54 | def encryption_needed(self): | |
55 | """ | |
56 | Checks if confidentiality was enabled | |
57 | """ | |
58 | return ISC_REQ.CONFIDENTIALITY in self.actual_ctx_flags | |
59 | ||
60 | async def sign(self, data, message_no, direction = 'init'): | |
61 | """ | |
62 | Signs a message. | |
63 | """ | |
64 | return self.gssapi.GSS_GetMIC(data, message_no, direction = direction) | |
65 | ||
32 | 66 | async def encrypt(self, data, message_no): |
67 | """ | |
68 | Encrypts a message. | |
69 | """ | |
33 | 70 | return self.gssapi.GSS_Wrap(data, message_no) |
34 | 71 | |
35 | async def decrypt(self, data, message_no, direction='init', auth_data=None): | |
36 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) | |
72 | async def decrypt(self, data, message_no, direction='init'): | |
73 | """ | |
74 | Decrypts message. Also performs integrity checking. | |
75 | """ | |
76 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction) | |
37 | 77 | |
38 | 78 | def get_session_key(self): |
39 | return self.ksspi.get_session_key() | |
79 | """ | |
80 | Fetches the session key. Under the hood this uses QueryContextAttributes API call. | |
81 | This will fail if the authentication is not yet finished! | |
82 | """ | |
83 | err = None | |
84 | if self.session_key is None: | |
85 | self.session_key, err = self.ksspi.get_session_key() | |
86 | return self.session_key, err | |
40 | 87 | |
41 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
42 | #authdata is only for api compatibility reasons | |
43 | if is_rpc == True: | |
88 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data = None): | |
89 | """ | |
90 | This function is called (multiple times depending on the flags) to perform authentication. | |
91 | """ | |
92 | try: | |
44 | 93 | if self.iterations == 0: |
45 | flags = ISC_REQ.CONFIDENTIALITY | \ | |
46 | ISC_REQ.INTEGRITY | \ | |
47 | ISC_REQ.MUTUAL_AUTH | \ | |
48 | ISC_REQ.REPLAY_DETECT | \ | |
49 | ISC_REQ.SEQUENCE_DETECT|\ | |
50 | ISC_REQ.USE_DCE_STYLE | |
94 | self.ksspi = KerberosMSLDAPSSPI(domain = self.domain, username=self.username, password=self.password) | |
95 | token, self.actual_ctx_flags = self.ksspi.get_ticket_for_spn(self.spn, ctx_flags = self.flags) | |
96 | self.iterations += 1 | |
97 | ||
98 | ||
99 | if ISC_REQ.MUTUAL_AUTH in self.actual_ctx_flags or ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: | |
100 | #in these cases continuation is needed | |
101 | return token, True, None | |
102 | ||
103 | else: | |
104 | #no mutual or dce auth will take one step only | |
105 | _, err = self.get_session_key() | |
106 | if err is not None: | |
107 | return None, None, err | |
108 | apreq = AP_REQ.load(token).native | |
109 | subkey = Key(apreq['ticket']['enc-part']['etype'], self.session_key) | |
110 | self.gssapi = get_gssapi(subkey) | |
111 | self.get_seq_number() | |
51 | 112 | |
113 | return token, False, None | |
52 | 114 | |
53 | token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) | |
54 | #print(token.hex()) | |
55 | self.iterations += 1 | |
56 | return token, True | |
57 | 115 | |
58 | elif self.iterations == 1: | |
59 | flags = ISC_REQ.USE_DCE_STYLE | |
60 | ||
61 | token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) | |
62 | #print(token.hex()) | |
116 | else: | |
117 | adata = authData[16:] | |
118 | if ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: | |
119 | adata = authData | |
120 | token, self.actual_ctx_flags = self.ksspi.get_ticket_for_spn(self.spn, ctx_flags = self.actual_ctx_flags, token_data = adata) | |
63 | 121 | |
64 | 122 | |
65 | aprep = AP_REP.load(token).native | |
123 | ||
124 | if ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: | |
125 | #Using DCE style 3-legged auth | |
126 | aprep = AP_REP.load(token).native | |
127 | else: | |
128 | aprep = AP_REP.load(adata).native | |
129 | subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) | |
130 | ||
131 | _, err = self.get_session_key() | |
132 | if err is not None: | |
133 | return None, None, err | |
66 | 134 | |
67 | subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) | |
68 | ||
69 | cipher_text = aprep['enc-part']['cipher'] | |
70 | cipher = _enctype_table[aprep['enc-part']['etype']]() | |
71 | ||
72 | plaintext = cipher.decrypt(subkey, 12, cipher_text) | |
73 | ||
135 | _, err = self.get_seq_number() | |
136 | if err is not None: | |
137 | return None, None, err | |
138 | ||
139 | subkey = Key(token['enc-part']['etype'], self.session_key) | |
74 | 140 | self.gssapi = get_gssapi(subkey) |
75 | 141 | |
76 | 142 | self.iterations += 1 |
77 | return token, False | |
78 | ||
79 | else: | |
80 | raise Exception('SSPI Kerberos -RPC - auth encountered too many calls for authenticate.') | |
143 | return token, False, None | |
81 | 144 | |
82 | else: | |
83 | apreq = self.ksspi.get_ticket_for_spn(self.target) | |
84 | return apreq, False | |
145 | except Exception as e: | |
146 | return None, None, e | |
85 | 147 | ⏎ |
0 | ||
1 | ## | |
2 | ## | |
3 | ## Interface to allow remote kerberos authentication via Multiplexor | |
4 | ## | |
5 | ## | |
6 | ## | |
7 | ## | |
8 | ## | |
9 | ## TODO: RPC auth type is not implemented or tested!!!! | |
10 | ||
11 | import enum | |
12 | ||
13 | from msldap.authentication.spnego.asn1_structs import KRB5Token | |
14 | from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken, KRB5_MECH_INDEP_TOKEN | |
15 | from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP | |
16 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table | |
17 | from pyodidewsnet.sspiproxyws import SSPIProxyWS | |
18 | ||
19 | ||
20 | # mutual auth not supported | |
21 | # encryption is always on | |
22 | ||
23 | class ISC_REQ(enum.IntFlag): | |
24 | DELEGATE = 1 | |
25 | MUTUAL_AUTH = 2 | |
26 | REPLAY_DETECT = 4 | |
27 | SEQUENCE_DETECT = 8 | |
28 | CONFIDENTIALITY = 16 | |
29 | USE_SESSION_KEY = 32 | |
30 | PROMPT_FOR_CREDS = 64 | |
31 | USE_SUPPLIED_CREDS = 128 | |
32 | ALLOCATE_MEMORY = 256 | |
33 | USE_DCE_STYLE = 512 | |
34 | DATAGRAM = 1024 | |
35 | CONNECTION = 2048 | |
36 | CALL_LEVEL = 4096 | |
37 | FRAGMENT_SUPPLIED = 8192 | |
38 | EXTENDED_ERROR = 16384 | |
39 | STREAM = 32768 | |
40 | INTEGRITY = 65536 | |
41 | IDENTIFY = 131072 | |
42 | NULL_SESSION = 262144 | |
43 | MANUAL_CRED_VALIDATION = 524288 | |
44 | RESERVED1 = 1048576 | |
45 | FRAGMENT_TO_FIT = 2097152 | |
46 | HTTP = 0x10000000 | |
47 | ||
48 | class MSLDAPSSPIProxyKerberosAuth: | |
49 | def __init__(self, settings): | |
50 | self.iterations = 0 | |
51 | self.settings = settings | |
52 | self.mode = 'CLIENT' | |
53 | url = '%s://%s:%s' % (self.settings.proto, self.settings.host, self.settings.port) | |
54 | self.sspi = SSPIProxyWS(url, self.settings.agent_id) | |
55 | self.client = None | |
56 | self.target = None | |
57 | self.gssapi = None | |
58 | self.etype = None | |
59 | self.session_key = None | |
60 | self.seq_number = 0 | |
61 | self.flags = ISC_REQ.CONNECTION | |
62 | ||
63 | self.setup() | |
64 | ||
65 | def setup(self): | |
66 | if self.settings.encrypt is True: | |
67 | self.flags = \ | |
68 | ISC_REQ.CONFIDENTIALITY |\ | |
69 | ISC_REQ.INTEGRITY |\ | |
70 | ISC_REQ.REPLAY_DETECT |\ | |
71 | ISC_REQ.SEQUENCE_DETECT | |
72 | ||
73 | def get_seq_number(self): | |
74 | return self.seq_number | |
75 | ||
76 | async def encrypt(self, data, message_no): | |
77 | return self.gssapi.GSS_Wrap(data, message_no) | |
78 | ||
79 | async def decrypt(self, data, message_no, direction='init', auth_data=None): | |
80 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) | |
81 | ||
82 | def signing_needed(self): | |
83 | """ | |
84 | Checks if integrity protection was negotiated | |
85 | """ | |
86 | return ISC_REQ.INTEGRITY in self.flags | |
87 | ||
88 | def encryption_needed(self): | |
89 | """ | |
90 | Checks if confidentiality flag was negotiated | |
91 | """ | |
92 | return ISC_REQ.CONFIDENTIALITY in self.flags | |
93 | ||
94 | def get_session_key(self): | |
95 | return self.session_key | |
96 | ||
97 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): | |
98 | try: | |
99 | status, ctxattr, apreq, err = await self.sspi.authenticate('KERBEROS', '', self.settings.target.to_target_string(), 3, self.flags.value, authdata = b'') | |
100 | if err is not None: | |
101 | raise err | |
102 | ||
103 | self.flags = ISC_REQ(ctxattr) | |
104 | ||
105 | self.session_key, err = await self.sspi.get_sessionkey() | |
106 | if err is not None: | |
107 | return None, None, err | |
108 | ||
109 | unwrap = KRB5_MECH_INDEP_TOKEN.from_bytes(apreq) | |
110 | aprep = AP_REQ.load(unwrap.data[2:]).native | |
111 | subkey = Key(aprep['ticket']['enc-part']['etype'], self.session_key) | |
112 | self.gssapi = get_gssapi(subkey) | |
113 | ||
114 | if aprep['ticket']['enc-part']['etype'] != 23: | |
115 | if ISC_REQ.CONFIDENTIALITY in self.flags: | |
116 | raw_seq_data, err = await self.sspi.get_sequenceno() | |
117 | if err is not None: | |
118 | return None, None, err | |
119 | self.seq_number = GSSWrapToken.from_bytes(raw_seq_data[16:]).SND_SEQ | |
120 | ||
121 | return unwrap.data[2:], False, None | |
122 | except Exception as e: | |
123 | return None, None, e | |
124 | ||
125 | ⏎ |
0 | ||
1 | ## | |
2 | ## | |
3 | ## Interface to allow remote kerberos authentication via Multiplexor | |
4 | ## | |
5 | ## | |
6 | ## | |
7 | ## | |
8 | ## | |
9 | ## TODO: RPC auth type is not implemented or tested!!!! | |
10 | ||
11 | import enum | |
12 | ||
13 | from msldap.authentication.spnego.asn1_structs import KRB5Token | |
14 | from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken, KRB5_MECH_INDEP_TOKEN | |
15 | from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP | |
16 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table | |
17 | from pyodidewsnet.clientauth import WSNETAuth | |
18 | ||
19 | ||
20 | # mutual auth not supported | |
21 | # encryption is always on | |
22 | ||
23 | class ISC_REQ(enum.IntFlag): | |
24 | DELEGATE = 1 | |
25 | MUTUAL_AUTH = 2 | |
26 | REPLAY_DETECT = 4 | |
27 | SEQUENCE_DETECT = 8 | |
28 | CONFIDENTIALITY = 16 | |
29 | USE_SESSION_KEY = 32 | |
30 | PROMPT_FOR_CREDS = 64 | |
31 | USE_SUPPLIED_CREDS = 128 | |
32 | ALLOCATE_MEMORY = 256 | |
33 | USE_DCE_STYLE = 512 | |
34 | DATAGRAM = 1024 | |
35 | CONNECTION = 2048 | |
36 | CALL_LEVEL = 4096 | |
37 | FRAGMENT_SUPPLIED = 8192 | |
38 | EXTENDED_ERROR = 16384 | |
39 | STREAM = 32768 | |
40 | INTEGRITY = 65536 | |
41 | IDENTIFY = 131072 | |
42 | NULL_SESSION = 262144 | |
43 | MANUAL_CRED_VALIDATION = 524288 | |
44 | RESERVED1 = 1048576 | |
45 | FRAGMENT_TO_FIT = 2097152 | |
46 | HTTP = 0x10000000 | |
47 | ||
48 | class MSLDAPWSNetKerberosAuth: | |
49 | def __init__(self, settings): | |
50 | self.iterations = 0 | |
51 | self.settings = settings | |
52 | self.mode = 'CLIENT' | |
53 | self.sspi = WSNETAuth() | |
54 | self.client = None | |
55 | self.target = None | |
56 | self.gssapi = None | |
57 | self.etype = None | |
58 | self.session_key = None | |
59 | self.seq_number = 0 | |
60 | self.flags = ISC_REQ.CONNECTION | |
61 | ||
62 | self.setup() | |
63 | ||
64 | def setup(self): | |
65 | if self.settings.encrypt is True: | |
66 | self.flags = \ | |
67 | ISC_REQ.CONFIDENTIALITY |\ | |
68 | ISC_REQ.INTEGRITY |\ | |
69 | ISC_REQ.REPLAY_DETECT |\ | |
70 | ISC_REQ.SEQUENCE_DETECT | |
71 | ||
72 | def get_seq_number(self): | |
73 | return self.seq_number | |
74 | ||
75 | async def encrypt(self, data, message_no): | |
76 | return self.gssapi.GSS_Wrap(data, message_no) | |
77 | ||
78 | async def decrypt(self, data, message_no, direction='init', auth_data=None): | |
79 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) | |
80 | ||
81 | def signing_needed(self): | |
82 | """ | |
83 | Checks if integrity protection was negotiated | |
84 | """ | |
85 | return ISC_REQ.INTEGRITY in self.flags | |
86 | ||
87 | def encryption_needed(self): | |
88 | """ | |
89 | Checks if confidentiality flag was negotiated | |
90 | """ | |
91 | return ISC_REQ.CONFIDENTIALITY in self.flags | |
92 | ||
93 | def get_session_key(self): | |
94 | return self.session_key | |
95 | ||
96 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): | |
97 | try: | |
98 | status, ctxattr, apreq, err = await self.sspi.authenticate('KERBEROS', '', self.settings.target.to_target_string(), 3, self.flags.value, authdata = b'') | |
99 | if err is not None: | |
100 | raise err | |
101 | ||
102 | self.flags = ISC_REQ(ctxattr) | |
103 | ||
104 | self.session_key, err = await self.sspi.get_sessionkey() | |
105 | if err is not None: | |
106 | return None, None, err | |
107 | ||
108 | unwrap = KRB5_MECH_INDEP_TOKEN.from_bytes(apreq) | |
109 | aprep = AP_REQ.load(unwrap.data[2:]).native | |
110 | subkey = Key(aprep['ticket']['enc-part']['etype'], self.session_key) | |
111 | self.gssapi = get_gssapi(subkey) | |
112 | ||
113 | if aprep['ticket']['enc-part']['etype'] != 23: | |
114 | if ISC_REQ.CONFIDENTIALITY in self.flags: | |
115 | raw_seq_data, err = await self.sspi.get_sequenceno() | |
116 | if err is not None: | |
117 | return None, None, err | |
118 | self.seq_number = GSSWrapToken.from_bytes(raw_seq_data[16:]).SND_SEQ | |
119 | ||
120 | return unwrap.data[2:], False, None | |
121 | except Exception as e: | |
122 | return None, None, e | |
123 | ||
124 | ⏎ |
228 | 228 | def test_construct(): |
229 | 229 | pass |
230 | 230 | |
231 | if __name__ == '__main__': | |
232 | from aiosmb.utils.hexdump import hexdump | |
233 | ||
234 | test()⏎ |
159 | 159 | def test_construct(): |
160 | 160 | pass |
161 | 161 | |
162 | if __name__ == '__main__': | |
163 | from aiosmb.utils.hexdump import hexdump | |
164 | ||
165 | test()⏎ |
40 | 40 | FRAGMENT_TO_FIT = 2097152 |
41 | 41 | HTTP = 0x10000000 |
42 | 42 | |
43 | class SMBNTLMMultiplexor: | |
43 | # | |
44 | # | |
45 | # Interface to support remote authentication via multiplexor | |
46 | # | |
47 | # Connects to the multiplexor server, and starts an SSPI server locally for the specific agentid | |
48 | # SSPI server will be used to perform NTLM authentication remotely, | |
49 | # while constructing a local NTLM authentication object | |
50 | # After the auth finishes, it also grabs the sessionkey. | |
51 | # The NTLM object can be used in future operations (encrypt/decrypt/sign) locally | |
52 | # without the need of future remote calls | |
53 | # | |
54 | ||
55 | class MSLDAPNTLMMultiplexor: | |
44 | 56 | def __init__(self, settings): |
45 | 57 | self.settings = settings |
46 | 58 | self.mode = None #'CLIENT' |
48 | 60 | self.operator = None |
49 | 61 | self.client = None |
50 | 62 | self.target = None |
51 | #self.ntlmChallenge = None | |
63 | self.seq_number = 0 | |
52 | 64 | |
53 | 65 | self.session_key = None |
54 | 66 | self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL')) |
65 | 77 | |
66 | 78 | def get_signkey(self, mode = 'Client'): |
67 | 79 | return self.ntlm_ctx.get_signkey(mode = mode) |
68 | ||
69 | ||
70 | def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt): | |
71 | return self.ntlm_ctx.SEAL(signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt) | |
72 | ||
73 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): | |
74 | return self.ntlm_ctx.SIGN(signingKey, message, seqNum, cipher_encrypt) | |
75 | 80 | |
76 | 81 | def get_session_key(self): |
77 | 82 | return self.session_key |
78 | 83 | |
79 | def get_extra_info(self): | |
80 | return self.ntlm_ctx.get_extra_info() | |
81 | ||
82 | 84 | def is_extended_security(self): |
83 | 85 | return self.ntlm_ctx.is_extended_security() |
86 | ||
87 | def get_seq_number(self): | |
88 | return self.seq_number | |
89 | ||
90 | def signing_needed(self): | |
91 | return self.ntlm_ctx.signing_needed() | |
92 | ||
93 | def encryption_needed(self): | |
94 | return self.ntlm_ctx.encryption_needed() | |
84 | 95 | |
85 | #async def encrypt(self, data, message_no): | |
86 | # return self.sspi.encrypt(data, message_no) | |
87 | # | |
88 | #async def decrypt(self, data, message_no): | |
89 | # return self.sspi.decrypt(data, message_no) | |
96 | async def encrypt(self, data, message_no): | |
97 | return await self.ntlm_ctx.encrypt(data, message_no) | |
98 | ||
99 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
100 | return await self.ntlm_ctx.decrypt(data, sequence_no, direction=direction, auth_data=auth_data) | |
101 | ||
102 | async def sign(self, data, message_no, direction=None, reset_cipher = False): | |
103 | return await self.ntlm_ctx.sign(data, message_no, direction=None, reset_cipher = reset_cipher) | |
90 | 104 | |
91 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
105 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): | |
106 | is_rpc = False | |
92 | 107 | if self.sspi is None: |
93 | res = await self.start_remote_sspi() | |
94 | if res is None: | |
95 | raise Exception('Failed to start remote SSPI') | |
108 | res, err = await self.start_remote_sspi() | |
109 | if err is not None: | |
110 | return None, None, err | |
96 | 111 | |
97 | 112 | if is_rpc is True and flags is None: |
98 | 113 | flags = ISC_REQ.REPLAY_DETECT | ISC_REQ.CONFIDENTIALITY| ISC_REQ.USE_SESSION_KEY| ISC_REQ.INTEGRITY| ISC_REQ.SEQUENCE_DETECT| ISC_REQ.CONNECTION |
99 | 114 | flags = int(flags) |
100 | 115 | |
101 | if self.settings.mode == 'CLIENT': | |
102 | if authData is None: | |
103 | data, res = await self.sspi.authenticate(flags = flags) | |
116 | if authData is None: | |
117 | data, res = await self.sspi.authenticate(flags = flags) | |
118 | if res is None: | |
119 | self.ntlm_ctx.load_negotiate(data) | |
120 | return data, res, None | |
121 | else: | |
122 | self.ntlm_ctx.load_challenge( authData) | |
123 | data, res = await self.sspi.challenge(authData, flags = flags) | |
124 | if res is None: | |
125 | self.ntlm_ctx.load_authenticate( data) | |
126 | self.session_key, res = await self.sspi.get_session_key() | |
104 | 127 | if res is None: |
105 | self.ntlm_ctx.load_negotiate(data) | |
106 | return data, res | |
107 | else: | |
108 | self.ntlm_ctx.load_challenge( authData) | |
109 | data, res = await self.sspi.challenge(authData, flags = flags) | |
110 | if res is None: | |
111 | self.ntlm_ctx.load_authenticate( data) | |
112 | self.session_key, res = await self.sspi.get_session_key() | |
113 | if res is None: | |
114 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
128 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
115 | 129 | |
116 | return data, res | |
117 | ||
118 | else: | |
119 | raise Exception('Server mode not implemented!') | |
130 | return data, res, None | |
120 | 131 | |
121 | 132 | |
122 | 133 | async def start_remote_sspi(self): |
133 | 144 | #print(sspi_url) |
134 | 145 | self.sspi = SSPINTLMClient(sspi_url) |
135 | 146 | await self.sspi.connect() |
136 | return True | |
147 | return True, None | |
137 | 148 | except Exception as e: |
138 | 149 | import traceback |
139 | 150 | traceback.print_exc() |
140 | return None | |
151 | return None, e | |
141 | 152 | |
142 | 153 | ⏎ |
0 | ||
1 | 0 | import os |
2 | 1 | import struct |
3 | 2 | import hmac |
4 | 3 | import copy |
4 | import hashlib | |
5 | 5 | |
6 | 6 | #from aiosmb.commons.connection.credential import SMBNTLMCredential |
7 | 7 | #from aiosmb.commons.serverinfo import NTLMServerInfo |
24 | 24 | self.mode = mode |
25 | 25 | self.template_name = template_name |
26 | 26 | self.custom_template = custom_template #for custom templates, must be dict |
27 | ||
28 | self.encrypt = False | |
27 | 29 | |
28 | 30 | self.template = None |
29 | 31 | self.ntlm_downgrade = False |
40 | 42 | |
41 | 43 | self.template = self.custom_template |
42 | 44 | |
45 | self.encrypt = self.credential.encrypt | |
46 | ||
47 | if self.encrypt is True: | |
48 | self.template_name = 'Windows10_15063_channel' | |
49 | ||
43 | 50 | if self.mode.upper() == 'SERVER': |
44 | 51 | if self.template_name in NTLMServerTemplates: |
45 | 52 | self.template = NTLMServerTemplates[self.template_name] |
46 | 53 | else: |
47 | 54 | raise Exception('No NTLM server template found with name %s' % self.template_name) |
48 | 55 | |
49 | else: | |
56 | else: | |
50 | 57 | if self.template_name in NTLMClientTemplates: |
51 | 58 | self.template = NTLMClientTemplates[self.template_name] |
52 | 59 | if 'ntlm_downgrade' in self.template: |
84 | 91 | |
85 | 92 | self.crypthandle_client = None |
86 | 93 | self.crypthandle_server = None |
87 | self.signhandle_server = None | |
88 | self.signhandle_client = None | |
89 | ||
90 | ||
94 | #self.signhandle_server = None doesnt exists, only crypthandle | |
95 | #self.signhandle_client = None doesnt exists, only crypthandle | |
96 | ||
97 | self.seq_number = 0 | |
91 | 98 | self.iteration_cnt = 0 |
92 | 99 | self.ntlm_credentials = None |
93 | 100 | self.timestamp = None #used in unittest only! |
109 | 116 | self.RandomSessionKey = self.settings.template['session_key'] |
110 | 117 | |
111 | 118 | self.timestamp = self.settings.template.get('timestamp') #used in unittest only! |
112 | ||
113 | ||
114 | if self.mode.upper() == 'SERVER': | |
115 | version = self.settings.template['version'] | |
116 | targetName = self.settings.template['targetname'] | |
117 | targetInfo = self.settings.template['targetinfo'] | |
118 | ||
119 | self.ntlmChallenge = NTLMChallenge.construct(challenge = self.challenge, targetName = targetName, targetInfo = targetInfo, version = version, flags = self.flags) | |
120 | ||
121 | #else: | |
122 | # domainname = self.settings.template['domain_name'] | |
123 | # workstationname = self.settings.template['workstation_name'] | |
124 | # version = self.settings.template.get('version') | |
125 | 119 | |
126 | 120 | def load_negotiate(self, data): |
127 | 121 | self.ntlmNegotiate = NTLMNegotiate.from_bytes(data) |
135 | 129 | def load_sessionkey(self, data): |
136 | 130 | self.RandomSessionKey = data |
137 | 131 | self.setup_crypto() |
132 | ||
133 | def get_seq_number(self): | |
134 | return self.seq_number | |
138 | 135 | |
139 | 136 | def set_sign(self, tf = True): |
140 | 137 | if tf == True: |
188 | 185 | #msg.Checksum = struct.unpack('<I',handle(messageSignature['Checksum']))[0] |
189 | 186 | |
190 | 187 | return msg.to_bytes() |
191 | ||
192 | #async def sign(self, data, message_no, direction=None): | |
193 | # return self.SIGN(self.SignKey_client, data, message_no, RC4(self.SignKey_client).encrypt ) | |
194 | 188 | |
195 | 189 | async def encrypt(self, data, sequence_no): |
190 | """ | |
191 | This function is to support SSPI encryption. | |
192 | """ | |
196 | 193 | return self.SEAL( |
197 | self.SignKey_client, | |
194 | #self.SignKey_client, | |
195 | self.SignKey_client, | |
198 | 196 | self.SealKey_client, |
199 | data, | |
200 | data, | |
197 | data, | |
198 | data, | |
201 | 199 | sequence_no, |
202 | 200 | self.crypthandle_client.encrypt |
203 | 201 | ) |
204 | 202 | |
205 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
206 | data = data[16:] | |
207 | msg_struct, signature = self.SEAL( | |
208 | self.SignKey_server, | |
209 | self.SealKey_server, | |
210 | data, | |
211 | data, | |
212 | sequence_no, | |
213 | self.crypthandle_server.encrypt | |
214 | ) | |
215 | print(data[:16]) | |
216 | print(signature) | |
217 | return msg_struct | |
218 | ||
219 | async def sign(self, data, message_no, direction=None): | |
220 | return self.SIGN( | |
221 | self.SealKey_client, | |
222 | data, | |
223 | message_no, | |
224 | self.signhandle_client.encrypt | |
225 | ) | |
203 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
204 | """ | |
205 | This function is to support SSPI decryption. | |
206 | """ | |
207 | edata = data[16:] | |
208 | srv_sig = NTLMSSP_MESSAGE_SIGNATURE.from_bytes(data[:16]) | |
209 | sealedMessage = self.crypthandle_server.encrypt(edata) | |
210 | signature = self.MAC(self.crypthandle_server.encrypt, self.SignKey_server, srv_sig.SeqNum, sealedMessage) | |
211 | #print('seqno %s' % sequence_no) | |
212 | #print('Srv sig: %s' % data[:16]) | |
213 | #print('Calc sig: %s' % signature) | |
214 | ||
215 | return sealedMessage, None | |
216 | ||
217 | async def sign(self, data, message_no, direction=None, reset_cipher = False): | |
218 | """ | |
219 | Singing outgoing messages. The reset_cipher parameter is needed for calculating mechListMIC. | |
220 | """ | |
221 | #print('sign data : %s' % data) | |
222 | #print('sign message_no : %s' % message_no) | |
223 | #print('sign direction : %s' % direction) | |
224 | signature = self.MAC(self.crypthandle_client.encrypt, self.SignKey_client, message_no, data) | |
225 | if reset_cipher is True: | |
226 | self.crypthandle_client = RC4(self.SealKey_client) | |
227 | self.crypthandle_server = RC4(self.SealKey_server) | |
228 | self.seq_number += 1 | |
229 | return signature | |
230 | ||
231 | async def verify(self, data, signature): | |
232 | """ | |
233 | Verifying incoming server message | |
234 | """ | |
235 | signature_struct = NTLMSSP_MESSAGE_SIGNATURE.from_bytes(signature) | |
236 | calc_sig = self.MAC(self.crypthandle_server.encrypt, self.SignKey_server, signature_struct.SeqNum, data) | |
237 | #print('server signature : %s' % signature) | |
238 | #print('calculates signature: %s' % calc_sig) | |
239 | return signature == calc_sig | |
226 | 240 | |
227 | 241 | def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt): |
242 | """ | |
243 | This is the official SEAL function. | |
244 | """ | |
228 | 245 | sealedMessage = cipher_encrypt(messageToEncrypt) |
229 | 246 | signature = self.MAC(cipher_encrypt, signingKey, seqNum, messageToSign) |
230 | 247 | return sealedMessage, signature |
231 | 248 | |
232 | 249 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): |
250 | """ | |
251 | This is the official SIGN function. | |
252 | """ | |
233 | 253 | return self.MAC(cipher_encrypt, signingKey, seqNum, message) |
234 | 254 | |
235 | 255 | def signing_needed(self): |
290 | 310 | |
291 | 311 | if mode == 'Client': |
292 | 312 | self.SignKey_client = signkey |
293 | if signkey is not None: | |
294 | self.signhandle_client = RC4(self.SealKey_client) | |
295 | 313 | |
296 | 314 | else: |
297 | 315 | self.SignKey_server = signkey |
298 | if signkey is not None: | |
299 | self.signhandle_server = RC4(self.SignKey_server) | |
300 | 316 | |
301 | 317 | return signkey |
302 | 318 | |
332 | 348 | self.calc_signkey('Client') |
333 | 349 | self.calc_signkey('Server') |
334 | 350 | |
335 | async def authenticate(self, authData, flags = None, seq_number = 0, is_rpc = False): | |
336 | if self.mode.upper() == 'SERVER': | |
337 | if self.ntlmNegotiate is None: | |
338 | ###parse client NTLMNegotiate message | |
339 | self.ntlmNegotiate = NTLMNegotiate.from_bytes(authData) | |
340 | return self.ntlmChallenge.to_bytes(), True | |
341 | ||
342 | elif self.ntlmAuthenticate is None: | |
343 | self.ntlmAuthenticate = NTLMAuthenticate.from_bytes(authData, self.use_NTLMv2) | |
344 | creds = NTLMcredential.construct(self.ntlmNegotiate, self.ntlmChallenge, self.ntlmAuthenticate) | |
345 | print(creds) | |
346 | ||
347 | # TODO: check when is sessionkey needed and check when is singing needed, and calculate the keys! | |
348 | # self.calc_SessionBaseKey() | |
349 | # self.calc_KeyExchangeKey() | |
350 | auth_credential = creds[0] | |
351 | #self.SessionBaseKey = auth_credential.calc_session_base_key() | |
352 | #self.calc_key_exchange_key() | |
353 | ||
354 | if auth_credential.verify(self.credential): | |
355 | return False, auth_credential | |
356 | else: | |
357 | return False, auth_credential | |
358 | ||
359 | else: | |
360 | raise Exception('Too many calls to do_AUTH function!') | |
361 | ||
362 | elif self.mode.upper() == 'CLIENT': | |
351 | async def authenticate(self, authData, flags = None, seq_number = 0, cb_data = None): | |
352 | if self.mode.upper() == 'CLIENT': | |
363 | 353 | if self.iteration_cnt == 0: |
364 | 354 | if authData is not None: |
365 | 355 | raise Exception('First call as client MUST be with empty data!') |
366 | ||
367 | if is_rpc == True: | |
368 | #rpc (unknow reason) reqauires seal to be set, otherwise it will fail to authenticate | |
369 | self.set_seal() | |
370 | 356 | |
371 | 357 | self.iteration_cnt += 1 |
372 | 358 | #negotiate message was already calulcated in setup |
373 | 359 | self.ntlmNegotiate = NTLMNegotiate.construct(self.flags, domainname = self.settings.template['domain_name'], workstationname = self.settings.template['workstation_name'], version = self.settings.template.get('version')) |
374 | 360 | self.ntlmNegotiate_raw = self.ntlmNegotiate.to_bytes() |
375 | return self.ntlmNegotiate_raw, True | |
361 | return self.ntlmNegotiate_raw, True, None | |
376 | 362 | |
377 | 363 | else: |
378 | 364 | #server challenge incoming |
379 | 365 | self.ntlmChallenge_raw = authData |
380 | 366 | self.ntlmChallenge = NTLMChallenge.from_bytes(authData) |
381 | ||
382 | ||
383 | if is_rpc == True: | |
384 | #rpc (unknow reason) reqauires seal to be set, otherwise it will fail to authenticate | |
385 | self.set_seal() | |
386 | 367 | |
387 | 368 | ##################self.flags = self.ntlmChallenge.NegotiateFlags |
388 | 369 | |
396 | 377 | lmresp = LMResponse() |
397 | 378 | lmresp.Response = b'\x00' |
398 | 379 | self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, lm_response= lmresp) |
399 | return self.ntlmAuthenticate.to_bytes(), False | |
380 | return self.ntlmAuthenticate.to_bytes(), False, None | |
400 | 381 | |
401 | 382 | if self.flags & NegotiateFlags.NEGOTIATE_EXTENDED_SESSIONSECURITY: |
402 | 383 | #Extended auth! |
422 | 403 | lmresp = LMResponse() |
423 | 404 | lmresp.Response = b'\x00' |
424 | 405 | self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, lm_response= lmresp) |
425 | return self.ntlmAuthenticate.to_bytes(), False | |
406 | return self.ntlmAuthenticate.to_bytes(), False, None | |
426 | 407 | |
427 | 408 | else: |
428 | 409 | #comment this out for testing! |
429 | 410 | ti = self.ntlmChallenge.TargetInfo |
430 | ti[AVPAIRType.MsvAvTargetName] = 'cifs/%s' % ti[AVPAIRType.MsvAvNbComputerName] | |
411 | ti[AVPAIRType.MsvAvTargetName] = 'ldaps/%s' % ti[AVPAIRType.MsvAvDnsComputerName] | |
412 | if cb_data is not None: | |
413 | md5_ctx = hashlib.new('md5') | |
414 | md5_ctx.update(cb_data) | |
415 | ti[AVPAIRType.MsvChannelBindings] = md5_ctx.digest() | |
431 | 416 | ### |
432 | 417 | |
433 | 418 | self.ntlm_credentials = netntlmv2.construct(self.ntlmChallenge.ServerChallenge, self.challenge, ti, self.settings.credential, timestamp = self.timestamp) |
438 | 423 | mic = None |
439 | 424 | |
440 | 425 | self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, domainname= self.settings.credential.domain, workstationname= self.settings.credential.workstation, username= self.settings.credential.username, lm_response= self.ntlm_credentials.LMResponse, nt_response= self.ntlm_credentials.NTResponse, version = self.ntlmNegotiate.Version, encrypted_session = self.EncryptedRandomSessionKey, mic = mic) |
441 | ||
426 | ||
427 | ||
442 | 428 | self.ntlmAuthenticate_raw = self.ntlmAuthenticate.to_bytes() |
443 | return self.ntlmAuthenticate_raw, False | |
429 | return self.ntlmAuthenticate_raw, False, None | |
444 | 430 | |
445 | 431 | elif self.mode.upper() == 'RELAY': |
446 | 432 | if self.iteration_cnt == 0: |
460 | 446 | |
461 | 447 | else: |
462 | 448 | raise Exception('Too many iterations for relay mode!') |
463 | ||
464 | ||
465 | ||
466 | #def test_msdn(): | |
467 | # credential = Credential() | |
468 | # credential.username = 'User' | |
469 | # credential.domain = 'Domain' | |
470 | # credential.password = 'Password' | |
471 | # | |
472 | # template = { | |
473 | # 'flags' : NegotiateFlags.NEGOTIATE_56| | |
474 | # NegotiateFlags.REQUEST_NON_NT_SESSION_KEY| | |
475 | # NegotiateFlags.NEGOTIATE_KEY_EXCH| | |
476 | # NegotiateFlags.NEGOTIATE_128| | |
477 | # NegotiateFlags.NEGOTIATE_VERSION| | |
478 | # NegotiateFlags.TARGET_TYPE_SERVER| | |
479 | # NegotiateFlags.NEGOTIATE_ALWAYS_SIGN| | |
480 | # NegotiateFlags.NEGOTIATE_NTLM| | |
481 | # NegotiateFlags.NEGOTIATE_SIGN| | |
482 | # NegotiateFlags.NEGOTIATE_SEAL| | |
483 | # NegotiateFlags.NTLM_NEGOTIATE_OEM| | |
484 | # NegotiateFlags.NEGOTIATE_UNICODE, | |
485 | # 'version' : Version.construct(WindowsMajorVersion.WINDOWS_MAJOR_VERSION_10, minor = WindowsMinorVersion.WINDOWS_MINOR_VERSION_0, build = 15063 ), | |
486 | # 'domain_name' : 'Domain', | |
487 | # 'workstation_name' : 'COMPUTER', | |
488 | # 'ntlm_downgrade' : True, | |
489 | # 'extended_security': False | |
490 | # } | |
491 | # settings = NTLMHandlerSettings(credential, mode = 'CLIENT', template_name = None, ntlm_downgrade = True, extended_security = False, custom_template = template) | |
492 | # handler = NTLMAUTHHandler(settings) | |
493 | # #assert handler.flags == int.from_bytes(b'\x33\x82\x02\xe2', "little", signed = False) | |
494 | # data, is_res = handler.authenticate(None) | |
495 | # print(data) | |
496 | # print(is_res) | |
497 | # | |
498 | # details = AVPairs({AVPAIRType.MsvAvNbDomainName: 'TEST', AVPAIRType.MsvAvNbComputerName: 'WIN2019AD', AVPAIRType.MsvAvDnsDomainName: 'test.corp', AVPAIRType.MsvAvDnsComputerName: 'WIN2019AD.test.corp', AVPAIRType.MsvAvTimestamp: b'\xae\xc6\x00\xbf\xc5\xfd\xd4\x01', AVPAIRType.MsvAvFlags: b'\x02\x00\x00\x00', AVPAIRType.MsvAvSingleHost: b"0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00R}'\xf24\xdet7`\x96c\x84\xd3oa\xae*\xa4\xfc*8\x06\x99\xf8\xca\xa6\x00\x01\x1bHm\x89", AVPAIRType.MsvChannelBindings: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', AVPAIRType.MsvAvTargetName: 'cifs/10.10.10.2'}) | |
499 | # | |
500 | # challenge = NTLMChallenge.construct(challenge=b'\x01\x23\x45\x67\x89\xab\xcd\xef', targetName = 'Domain', targetInfo = details, version = handler.ntlmNegotiate.Version, flags= handler.flags) | |
501 | # data, is_res = handler.authenticate(challenge.to_bytes()) | |
502 | # print(data) | |
503 | # print(is_res) | |
504 | # | |
505 | # print(handler.ntlmAuthenticate.LMChallenge.to_bytes().hex()) | |
506 | # print(handler.ntlmAuthenticate.NTChallenge.to_bytes().hex()) | |
507 | # | |
508 | # | |
509 | #def test(): | |
510 | # template_name = 'Windows10_15063' | |
511 | # credential = Credential() | |
512 | # credential.username = 'test' | |
513 | # credential.password = 'test' | |
514 | # | |
515 | # settings = NTLMHandlerSettings(credential, mode = 'CLIENT', template_name = template_name, ntlm_downgrade = False, extended_security = True) | |
516 | # handler = NTLMAUTHHandler(settings) | |
517 | # data, is_res = handler.authenticate(None) | |
518 | # print(data) | |
519 | # print(is_res) | |
520 | # | |
521 | #if __name__ == '__main__': | |
522 | # from aiosmb.ntlm.structures.version import Version, WindowsMajorVersion, WindowsMinorVersion | |
523 | # test_msdn()⏎ | |
449 | ⏎ |
1 | 1 | # |
2 | 2 | # This is just a simple interface to the winsspi library to support NTLM |
3 | 3 | # |
4 | from winsspi.sspi import NTLMSMBSSPI | |
4 | from winsspi.sspi import NTLMMSLDAPSSPI | |
5 | from winsspi.common.function_defs import ISC_REQ | |
5 | 6 | from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings |
6 | 7 | |
7 | 8 | class MSLDAPNTLMSSPI: |
8 | 9 | def __init__(self, settings): |
9 | 10 | self.settings = settings |
10 | self.mode = None #'CLIENT' | |
11 | self.sspi = NTLMSMBSSPI() | |
12 | self.client = None | |
13 | self.target = None | |
14 | #self.ntlmChallenge = None | |
15 | ||
11 | self.mode = 'CLIENT' | |
12 | self.username = settings.username | |
13 | self.password = settings.password | |
14 | self.domain = settings.domain | |
15 | self.actual_ctx_flags = None | |
16 | self.flags = ISC_REQ.CONNECTION | |
17 | if settings.encrypt is True: | |
18 | #self.flags = ISC_REQ.REPLAY_DETECT | ISC_REQ.CONFIDENTIALITY| ISC_REQ.USE_SESSION_KEY| ISC_REQ.INTEGRITY| ISC_REQ.SEQUENCE_DETECT| ISC_REQ.CONNECTION | |
19 | self.flags = ISC_REQ.CONNECTION | ISC_REQ.CONFIDENTIALITY | |
20 | self.sspi = NTLMMSLDAPSSPI() | |
21 | ||
22 | self.seq_number = 0 | |
16 | 23 | self.session_key = None |
17 | 24 | self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL')) |
18 | ||
19 | self.setup() | |
20 | 25 | |
21 | 26 | @property |
22 | 27 | def ntlmChallenge(self): |
23 | 28 | return self.ntlm_ctx.ntlmChallenge |
29 | ||
30 | def get_seq_number(self): | |
31 | return self.ntlm_ctx.get_seq_number() | |
24 | 32 | |
25 | def setup(self): | |
26 | self.mode = self.settings.mode.upper() | |
27 | self.client = self.settings.client | |
28 | self.password = self.settings.password | |
29 | ||
33 | def signing_needed(self): | |
34 | return self.ntlm_ctx.signing_needed() | |
35 | ||
36 | def encryption_needed(self): | |
37 | return self.ntlm_ctx.encryption_needed() | |
38 | ||
30 | 39 | def get_sealkey(self, mode = 'Client'): |
31 | 40 | return self.ntlm_ctx.get_sealkey(mode = mode) |
32 | 41 | |
44 | 53 | |
45 | 54 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): |
46 | 55 | return self.ntlm_ctx.SIGN(signingKey, message, seqNum, cipher_encrypt) |
56 | ||
57 | def sign(self, data, message_no = 0, direction = 'init', reset_cipher = False): | |
58 | return self.ntlm_ctx.sign(data, message_no = message_no, reset_cipher = reset_cipher) | |
59 | ||
60 | def verify(self, data, signature): | |
61 | return self.ntlm_ctx.verify(data, signature) | |
47 | 62 | |
48 | 63 | def get_session_key(self): |
49 | 64 | if not self.session_key: |
57 | 72 | def is_extended_security(self): |
58 | 73 | return self.ntlm_ctx.is_extended_security() |
59 | 74 | |
60 | async def encrypt(self, data, message_no): | |
61 | return self.sspi.encrypt(data, message_no) | |
75 | def encrypt(self, data, message_no): | |
76 | return self.ntlm_ctx.encrypt(data, message_no) | |
62 | 77 | |
63 | async def decrypt(self, data, message_no): | |
64 | return self.sspi.decrypt(data, message_no) | |
78 | def decrypt(self, data, message_no, direction='init', auth_data=None): | |
79 | return self.ntlm_ctx.decrypt(data, message_no, direction=direction, auth_data=auth_data) | |
65 | 80 | |
66 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
67 | if self.mode == 'CLIENT': | |
68 | if authData is None: | |
69 | data, res = self.sspi.negotiate(is_rpc = is_rpc) | |
81 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data = None): | |
82 | if authData is None: | |
83 | try: | |
84 | data, res = self.sspi.negotiate(ctx_flags = self.flags) | |
85 | self.actual_ctx_flags = self.sspi.ctx_outflags | |
70 | 86 | self.ntlm_ctx.load_negotiate(data) |
71 | return data, res | |
72 | else: | |
73 | self.ntlm_ctx.load_challenge( authData) | |
74 | data, res = self.sspi.authenticate(authData, is_rpc = is_rpc) | |
75 | self.ntlm_ctx.load_authenticate( data) | |
76 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
87 | return data, res, None | |
88 | except Exception as e: | |
89 | return None, None, e | |
90 | else: | |
91 | self.ntlm_ctx.load_challenge(authData) | |
92 | data, res = self.sspi.authenticate(authData, ctx_flags = self.flags) | |
93 | self.ntlm_ctx.load_authenticate( data) | |
94 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
77 | 95 | |
78 | return data, res | |
96 | return data, res, None | |
79 | 97 | |
80 | 98 | ⏎ |
0 | from msldap import logger | |
1 | from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings | |
2 | from pyodidewsnet.sspiproxyws import SSPIProxyWS | |
3 | import enum | |
4 | ||
5 | class ISC_REQ(enum.IntFlag): | |
6 | DELEGATE = 1 | |
7 | MUTUAL_AUTH = 2 | |
8 | REPLAY_DETECT = 4 | |
9 | SEQUENCE_DETECT = 8 | |
10 | CONFIDENTIALITY = 16 | |
11 | USE_SESSION_KEY = 32 | |
12 | PROMPT_FOR_CREDS = 64 | |
13 | USE_SUPPLIED_CREDS = 128 | |
14 | ALLOCATE_MEMORY = 256 | |
15 | USE_DCE_STYLE = 512 | |
16 | DATAGRAM = 1024 | |
17 | CONNECTION = 2048 | |
18 | CALL_LEVEL = 4096 | |
19 | FRAGMENT_SUPPLIED = 8192 | |
20 | EXTENDED_ERROR = 16384 | |
21 | STREAM = 32768 | |
22 | INTEGRITY = 65536 | |
23 | IDENTIFY = 131072 | |
24 | NULL_SESSION = 262144 | |
25 | MANUAL_CRED_VALIDATION = 524288 | |
26 | RESERVED1 = 1048576 | |
27 | FRAGMENT_TO_FIT = 2097152 | |
28 | HTTP = 0x10000000 | |
29 | ||
30 | class MSLDAPSSPIProxyNTLMAuth: | |
31 | def __init__(self, settings): | |
32 | self.settings = settings | |
33 | self.mode = None #'CLIENT' | |
34 | url = '%s://%s:%s' % (self.settings.proto, self.settings.host, self.settings.port) | |
35 | self.sspi = SSPIProxyWS(url, self.settings.agent_id) | |
36 | self.operator = None | |
37 | self.client = None | |
38 | self.target = None | |
39 | self.seq_number = 0 | |
40 | ||
41 | self.session_key = None | |
42 | self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL')) | |
43 | ||
44 | def setup(self): | |
45 | return | |
46 | ||
47 | @property | |
48 | def ntlmChallenge(self): | |
49 | return self.ntlm_ctx.ntlmChallenge | |
50 | ||
51 | def get_sealkey(self, mode = 'Client'): | |
52 | return self.ntlm_ctx.get_sealkey(mode = mode) | |
53 | ||
54 | def get_signkey(self, mode = 'Client'): | |
55 | return self.ntlm_ctx.get_signkey(mode = mode) | |
56 | ||
57 | def signing_needed(self): | |
58 | return self.ntlm_ctx.signing_needed() | |
59 | ||
60 | def encryption_needed(self): | |
61 | return self.ntlm_ctx.encryption_needed() | |
62 | ||
63 | async def encrypt(self, data, message_no): | |
64 | return await self.ntlm_ctx.encrypt(data, message_no) | |
65 | ||
66 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
67 | return await self.ntlm_ctx.decrypt(data, sequence_no, direction=direction, auth_data=auth_data) | |
68 | ||
69 | async def sign(self, data, message_no, direction=None, reset_cipher = False): | |
70 | return await self.ntlm_ctx.sign(data, message_no, direction=None, reset_cipher = reset_cipher) | |
71 | ||
72 | ||
73 | def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt): | |
74 | return self.ntlm_ctx.SEAL(signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt) | |
75 | ||
76 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): | |
77 | return self.ntlm_ctx.SIGN(signingKey, message, seqNum, cipher_encrypt) | |
78 | ||
79 | def get_session_key(self): | |
80 | return self.session_key | |
81 | ||
82 | def get_seq_number(self): | |
83 | return self.seq_number | |
84 | ||
85 | def is_extended_security(self): | |
86 | return self.ntlm_ctx.is_extended_security() | |
87 | ||
88 | async def authenticate(self, authData = b'', flags = None, seq_number = 0, cb_data = None): | |
89 | try: | |
90 | if flags is None: | |
91 | flags = ISC_REQ.CONNECTION | |
92 | ||
93 | if authData is None: | |
94 | status, ctxattr, data, err = await self.sspi.authenticate('NTLM', '', '', 3, flags.value, authdata = b'') | |
95 | if err is not None: | |
96 | raise err | |
97 | self.ntlm_ctx.load_negotiate(data) | |
98 | return data, True, None | |
99 | else: | |
100 | self.ntlm_ctx.load_challenge(authData) | |
101 | status, ctxattr, data, err = await self.sspi.authenticate('NTLM', '', '', 3, flags.value, authdata = authData) | |
102 | if err is not None: | |
103 | raise err | |
104 | if err is None: | |
105 | self.ntlm_ctx.load_authenticate(data) | |
106 | self.session_key, err = await self.sspi.get_sessionkey() | |
107 | if err is not None: | |
108 | raise err | |
109 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
110 | ||
111 | await self.sspi.disconnect() | |
112 | return data, False, None | |
113 | except Exception as e: | |
114 | return None, None, e | |
115 | ||
116 | ⏎ |
201 | 201 | print(repr(cc3)) |
202 | 202 | cc4 = NTLMv2ClientChallenge.from_bytes(cc3.to_bytes()) |
203 | 203 | |
204 | if __name__ == '__main__': | |
205 | from aiosmb.utils.hexdump import hexdump | |
206 | import os | |
207 | test()⏎ |
0 | from msldap import logger | |
1 | from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings | |
2 | from pyodidewsnet.clientauth import WSNETAuth | |
3 | import enum | |
4 | ||
5 | class ISC_REQ(enum.IntFlag): | |
6 | DELEGATE = 1 | |
7 | MUTUAL_AUTH = 2 | |
8 | REPLAY_DETECT = 4 | |
9 | SEQUENCE_DETECT = 8 | |
10 | CONFIDENTIALITY = 16 | |
11 | USE_SESSION_KEY = 32 | |
12 | PROMPT_FOR_CREDS = 64 | |
13 | USE_SUPPLIED_CREDS = 128 | |
14 | ALLOCATE_MEMORY = 256 | |
15 | USE_DCE_STYLE = 512 | |
16 | DATAGRAM = 1024 | |
17 | CONNECTION = 2048 | |
18 | CALL_LEVEL = 4096 | |
19 | FRAGMENT_SUPPLIED = 8192 | |
20 | EXTENDED_ERROR = 16384 | |
21 | STREAM = 32768 | |
22 | INTEGRITY = 65536 | |
23 | IDENTIFY = 131072 | |
24 | NULL_SESSION = 262144 | |
25 | MANUAL_CRED_VALIDATION = 524288 | |
26 | RESERVED1 = 1048576 | |
27 | FRAGMENT_TO_FIT = 2097152 | |
28 | HTTP = 0x10000000 | |
29 | ||
30 | class MSLDAPWSNetNTLMAuth: | |
31 | def __init__(self, settings): | |
32 | self.settings = settings | |
33 | self.mode = None #'CLIENT' | |
34 | self.sspi = WSNETAuth() | |
35 | self.operator = None | |
36 | self.client = None | |
37 | self.target = None | |
38 | self.seq_number = 0 | |
39 | ||
40 | self.session_key = None | |
41 | self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL')) | |
42 | ||
43 | def setup(self): | |
44 | return | |
45 | ||
46 | @property | |
47 | def ntlmChallenge(self): | |
48 | return self.ntlm_ctx.ntlmChallenge | |
49 | ||
50 | def get_sealkey(self, mode = 'Client'): | |
51 | return self.ntlm_ctx.get_sealkey(mode = mode) | |
52 | ||
53 | def get_signkey(self, mode = 'Client'): | |
54 | return self.ntlm_ctx.get_signkey(mode = mode) | |
55 | ||
56 | def signing_needed(self): | |
57 | return self.ntlm_ctx.signing_needed() | |
58 | ||
59 | def encryption_needed(self): | |
60 | return self.ntlm_ctx.encryption_needed() | |
61 | ||
62 | async def encrypt(self, data, message_no): | |
63 | return await self.ntlm_ctx.encrypt(data, message_no) | |
64 | ||
65 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
66 | return await self.ntlm_ctx.decrypt(data, sequence_no, direction=direction, auth_data=auth_data) | |
67 | ||
68 | async def sign(self, data, message_no, direction=None, reset_cipher = False): | |
69 | return await self.ntlm_ctx.sign(data, message_no, direction=None, reset_cipher = reset_cipher) | |
70 | ||
71 | ||
72 | def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt): | |
73 | return self.ntlm_ctx.SEAL(signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt) | |
74 | ||
75 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): | |
76 | return self.ntlm_ctx.SIGN(signingKey, message, seqNum, cipher_encrypt) | |
77 | ||
78 | def get_session_key(self): | |
79 | return self.session_key | |
80 | ||
81 | def get_seq_number(self): | |
82 | return self.seq_number | |
83 | ||
84 | def is_extended_security(self): | |
85 | return self.ntlm_ctx.is_extended_security() | |
86 | ||
87 | async def authenticate(self, authData = b'', flags = None, seq_number = 0, cb_data = None): | |
88 | try: | |
89 | if flags is None: | |
90 | flags = ISC_REQ.CONNECTION | |
91 | ||
92 | if authData is None: | |
93 | status, ctxattr, data, err = await self.sspi.authenticate('NTLM', '', '', 3, flags.value, authdata = b'') | |
94 | if err is not None: | |
95 | raise err | |
96 | self.ntlm_ctx.load_negotiate(data) | |
97 | return data, True, None | |
98 | else: | |
99 | self.ntlm_ctx.load_challenge(authData) | |
100 | status, ctxattr, data, err = await self.sspi.authenticate('NTLM', '', '', 3, flags.value, authdata = authData) | |
101 | if err is not None: | |
102 | raise err | |
103 | if err is None: | |
104 | self.ntlm_ctx.load_authenticate(data) | |
105 | self.session_key, err = await self.sspi.get_sessionkey() | |
106 | if err is not None: | |
107 | raise err | |
108 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
109 | ||
110 | await self.sspi.disconnect() | |
111 | return data, False, None | |
112 | except Exception as e: | |
113 | return None, None, e | |
114 | ||
115 | ⏎ |
5 | 5 | |
6 | 6 | import copy |
7 | 7 | from msldap.authentication.spnego.asn1_structs import * |
8 | from asn1crypto.core import OctetString | |
8 | 9 | |
9 | 10 | # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/d4f2b41c-5f9e-4e11-98d0-ade76467095d |
10 | 11 | |
46 | 47 | def encryption_needed(self): |
47 | 48 | return self.selected_authentication_context.encryption_needed() |
48 | 49 | |
50 | def get_seq_number(self): | |
51 | return self.selected_authentication_context.get_seq_number() | |
52 | ||
49 | 53 | async def unsign(self, data): |
50 | 54 | #TODO: IMPLEMENT THIS |
51 | 55 | return data |
52 | 56 | |
53 | async def sign(self, data, message_no, direction='init'): | |
54 | return await self.selected_authentication_context.sign(data, message_no, direction=direction) | |
57 | async def verify(self, data, signature): | |
58 | return await self.selected_authentication_context.verify(data, signature) | |
59 | ||
60 | async def sign(self, data, message_no, direction='init', reset_cipher = False): | |
61 | return await self.selected_authentication_context.sign(data, message_no, direction=direction, reset_cipher = reset_cipher) | |
55 | 62 | |
56 | 63 | async def encrypt(self, data, message_no): |
57 | 64 | return await self.selected_authentication_context.encrypt(data, message_no) |
58 | 65 | |
59 | 66 | async def decrypt(self, data, message_no, direction='init', auth_data=None): |
60 | return await self.selected_authentication_context.decrypt(data, message_no, direction=direction, auth_data=auth_data) | |
67 | return await self.selected_authentication_context.decrypt(data, message_no, direction=direction) | |
61 | 68 | |
62 | 69 | def add_auth_context(self, name, ctx): |
63 | 70 | """ |
77 | 84 | def select_common_athentication_type(self, mech_types): |
78 | 85 | for auth_type_name in self.authentication_contexts: |
79 | 86 | if auth_type_name in mech_types: |
80 | print(auth_type_name) | |
81 | 87 | return auth_type_name, self.authentication_contexts[auth_type_name] |
82 | 88 | |
83 | 89 | return None, None |
84 | 90 | |
85 | async def process_ctx_authenticate(self, token_data, include_negstate = False, flags = None, seq_number = 0, is_rpc = False): | |
86 | result, to_continue = await self.selected_authentication_context.authenticate(token_data, flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
91 | async def process_ctx_authenticate(self, token_data, include_negstate = False, flags = None, seq_number = 0, cb_data = None): | |
92 | result, to_continue, err = await self.selected_authentication_context.authenticate(token_data, flags = flags, seq_number = seq_number, cb_data = cb_data) | |
93 | if err is not None: | |
94 | return None, None, err | |
87 | 95 | if not result: |
88 | return None, False | |
96 | return None, False, None | |
89 | 97 | response = {} |
90 | 98 | if include_negstate == True: |
91 | 99 | if to_continue == True: |
94 | 102 | response['negState'] = NegState('accept-completed') |
95 | 103 | |
96 | 104 | response['responseToken'] = result |
97 | return response, to_continue | |
105 | return response, to_continue, None | |
98 | 106 | |
99 | 107 | def get_extra_info(self): |
100 | 108 | if hasattr(self.selected_authentication_context, 'get_extra_info'): |
115 | 123 | #spnego = GSS_SPNEGO({'NegotiationToken':negtoken}) |
116 | 124 | return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump() |
117 | 125 | |
118 | async def authenticate(self, token, flags = None, seq_number = 0, is_rpc = False): | |
126 | async def authenticate(self, token, flags = None, seq_number = 0, cb_data = None): | |
119 | 127 | """ |
120 | 128 | This function is called (multiple times) during negotiation phase of a protocol to determine hich auth mechanism to be used |
121 | 129 | Token is a byte array that is an ASN1 NegotiationToken structure. |
122 | 130 | """ |
123 | ||
124 | if self.mode == 'SERVER': | |
125 | if self.selected_authentication_context is None: | |
126 | gss = GSSAPI.load(token).native | |
127 | negtoken = gss['value'] | |
128 | if len(negtoken['mechTypes']) == 1: | |
129 | self.selected_mechtype = negtoken['mechTypes'][0] | |
130 | if negtoken['mechTypes'][0] == 'NTLMSSP - Microsoft NTLM Security Support Provider': | |
131 | self.selected_authentication_context = self.authentication_contexts[negtoken['mechTypes'][0]] | |
132 | ||
133 | ||
134 | else: | |
135 | raise Exception('This path is not yet implemented') | |
136 | #self.selected_mechtype, self.selected_authentication_context = self.select_common_athentication_type(neg_token.mechTypes) | |
137 | #if self.selected_mechtype is None: | |
138 | # raise Exception('Failed to select common authentication mechanism! Client sent: %s We have %s' % ()) | |
139 | # | |
140 | # ##server offered multiple auth types, we must choose one | |
141 | # #response = {} | |
142 | # #response['negState'] = NegState('accept-incomplete') | |
143 | # #response['supportedMech'] = MechType(self.selected_mechtype) | |
144 | # # | |
145 | #return NegTokenResp(response).dump(), True | |
146 | ||
147 | ||
148 | if self.selected_authentication_context is not None: | |
149 | response, to_continue = await self.process_ctx_authenticate(negtoken['mechToken'], flags = flags, seq_number = seq_number, is_rpc = is_rpc, include_negstate = True) | |
150 | if self.iteration_ctr == 0: | |
151 | response['supportedMech'] = MechType(self.selected_mechtype) | |
152 | negtoken = NegotiationToken({'negTokenResp':NegTokenResp(response)}) | |
131 | if self.selected_mechtype is None: | |
132 | if token is None: | |
133 | #first call to auth, we need to create NegTokenInit2 | |
134 | #we must list all available auth types, if only one is present then generate initial auth data with it | |
135 | ||
136 | selected_name = None | |
137 | mechtypes = [] | |
138 | for mechname in self.authentication_contexts: | |
139 | selected_name = mechname #only used if there is one! | |
140 | mechtypes.append(MechType(mechname)) | |
141 | ||
142 | response = {} | |
143 | response['mechTypes'] = MechTypes(mechtypes) | |
144 | ||
145 | self.negtypes_store = MechTypes(mechtypes).dump() | |
146 | ||
147 | ||
148 | if len(mechtypes) == 1: | |
149 | self.selected_authentication_context = self.authentication_contexts[selected_name] | |
150 | self.selected_mechtype = selected_name | |
151 | result, to_continue, err = await self.selected_authentication_context.authenticate(None, cb_data=cb_data) | |
152 | if err is not None: | |
153 | return None, None, err | |
154 | ||
155 | if not result: | |
156 | return None, False, None | |
157 | if str(response['mechTypes'][0]) == '1.2.840.48018.1.2.2': | |
158 | response['mechToken'] = KRB5Token(result).to_bytes() | |
159 | ||
160 | #response['mechToken'] = bytes.fromhex('2a864886f712010202') + #??????? | |
161 | else: | |
162 | response['mechToken'] = result | |
163 | #raise Exception('NTLM as RPC GSSAPI not implemented!') | |
164 | ||
165 | ### First message and ONLY the first message goes out with additional wrapping | |
166 | ||
167 | negtoken = NegotiationToken({'negTokenInit':NegTokenInit2(response)}) | |
153 | 168 | |
154 | 169 | |
155 | 170 | #spnego = GSS_SPNEGO({'NegotiationToken':negtoken}) |
156 | ||
157 | self.iteration_ctr += 1 | |
158 | #return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump(), to_continue | |
159 | return negtoken.dump(), to_continue | |
160 | ||
161 | #neg_token_raw = NegotiationToken.load(token) | |
162 | #neg_token = neg_token_raw.native | |
163 | #if isinstance(neg_token_raw, NegTokenInit2): | |
164 | # if selected_authentication_context is not None: | |
165 | # raise Exception('Authentication context already selected, but Client sent NegTokenInit2') | |
166 | # | |
167 | # if len(neg_token.mechTypes) == 1: | |
168 | # #client only sent 1 negotiation token type, we either support it or raise exception | |
169 | # if neg_token.mechTypes[0] not in self.authentication_contexts: | |
170 | # raise Exception('Client sent %s auth mechanism but we dont have that set up!' % neg_token.mechTypes[0]) | |
171 | # | |
172 | # self.selected_mechtype = neg_token.mechTypes[0] | |
173 | # self.selected_authentication_context = self.authentication_contexts[neg_token.mechTypes[0]] | |
174 | # #there is an option if onyl one auth type is set to have the auth token already in this message | |
175 | # if neg_token.mechToken is not None: | |
176 | # response, to_continue = await self.process_ctx_authenticate(neg_token.mechToken, flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
177 | # if not response: | |
178 | # return None, False | |
179 | # response['supportedMech'] = MechType(self.selected_mechtype) | |
180 | # return NegTokenResp(response).dump(), to_continue | |
181 | # | |
182 | # else: | |
183 | # response = {} | |
184 | # response['negState'] = NegState('accept-incomplete') | |
185 | # response['supportedMech'] = MechType(self.selected_mechtype) | |
186 | # return NegTokenResp(response).dump(), True | |
187 | ||
188 | ||
189 | ||
190 | #elif isinstance(neg_token_raw, NegTokenResp): | |
191 | # if selected_authentication_context is None: | |
192 | # raise Exception('NegTokenResp got, but no authentication context selected!') | |
193 | # | |
194 | # response, to_continue = await self.process_ctx_authenticate(neg_token.mechToken, flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
195 | # return NegTokenResp(response.dump()), to_continue | |
196 | ||
171 | return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump(), True, None | |
172 | ||
173 | ||
174 | else: | |
175 | #we have already send the NegTokenInit2, but it contained multiple auth types, | |
176 | #at this point server is replying which auth type to use | |
177 | neg_token_raw = NegotiationToken.load(token) | |
178 | ||
179 | neg_token = neg_token_raw.native | |
180 | ||
181 | if not isinstance(neg_token_raw, NegTokenResp): | |
182 | raise Exception('Server send init???') | |
183 | ||
184 | ||
185 | self.selected_authentication_context = self.authentication_contexts[neg_token.mechTypes[0]] | |
186 | self.selected_mechtype = neg_token['supportedMech'] | |
187 | ||
188 | ||
189 | response, to_continue, err = await self.process_ctx_authenticate(neg_token['responseToken'], flags = flags, seq_number = seq_number, cb_data = cb_data) | |
190 | if err is not None: | |
191 | return None, None, err | |
192 | return NegTokenResp(response).dump(), to_continue, None | |
193 | ||
197 | 194 | else: |
198 | if self.selected_mechtype is None: | |
199 | if token is None: | |
200 | #first call to auth, we need to create NegTokenInit2 | |
201 | #we must list all available auth types, if only one is present then generate initial auth data with it | |
202 | ||
203 | selected_name = None | |
204 | mechtypes = [] | |
205 | for mechname in self.authentication_contexts: | |
206 | selected_name = mechname #only used if there is one! | |
207 | mechtypes.append(MechType(mechname)) | |
208 | ||
209 | response = {} | |
210 | response['mechTypes'] = MechTypes(mechtypes) | |
211 | ||
212 | if len(mechtypes) == 1: | |
213 | self.selected_authentication_context = self.authentication_contexts[selected_name] | |
214 | self.selected_mechtype = selected_name | |
215 | result, to_continue = await self.selected_authentication_context.authenticate(None, is_rpc = is_rpc) | |
216 | if is_rpc == False: | |
217 | response['mechToken'] = result | |
218 | else: | |
219 | if not result: | |
220 | return None, False | |
221 | if str(response['mechTypes'][0]) == '1.2.840.48018.1.2.2': | |
222 | response['mechToken'] = KRB5Token(result).to_bytes() | |
223 | ||
224 | #response['mechToken'] = bytes.fromhex('2a864886f712010202') + #??????? | |
225 | else: | |
226 | raise Exception('NTLM as RPC GSSAPI not implemented!') | |
227 | ||
228 | ### First message and ONLY the first message goes out with additional wrapping | |
229 | ||
230 | negtoken = NegotiationToken({'negTokenInit':NegTokenInit2(response)}) | |
231 | ||
232 | ||
233 | #spnego = GSS_SPNEGO({'NegotiationToken':negtoken}) | |
234 | return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump(), True | |
235 | ||
236 | ||
237 | else: | |
238 | #we have already send the NegTokenInit2, but it contained multiple auth types, | |
239 | #at this point server is replying which auth type to use | |
240 | neg_token_raw = NegotiationToken.load(token) | |
241 | neg_token = neg_token_raw.native | |
242 | ||
243 | if not isinstance(neg_token_raw, NegTokenResp): | |
244 | raise Exception('Server send init???') | |
245 | ||
246 | self.selected_authentication_context = self.authentication_contexts[neg_token.mechTypes[0]] | |
247 | self.selected_mechtype = neg_token['supportedMech'] | |
248 | ||
249 | response, to_continue = await self.process_ctx_authenticate(neg_token['responseToken'], flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
250 | return NegTokenResp(response).dump(), to_continue | |
251 | ||
195 | #everything is netotiated, but authentication needs more setps | |
196 | neg_token_raw = NegotiationToken.load(token) | |
197 | neg_token = neg_token_raw.native | |
198 | if neg_token['negState'] == 'accept-completed': | |
199 | return None, True, None | |
200 | if neg_token['responseToken'] is None: | |
201 | # https://tools.ietf.org/html/rfc4178#section-5 | |
202 | # mechlistmic exchange happening at the end of the authentication | |
203 | return None, True, None | |
204 | #raise Exception('Should not be here....') | |
205 | #print('server mechListMIC: %s' % neg_token['mechListMIC']) | |
206 | #res = await self.verify(self.negtypes_store, neg_token['mechListMIC']) | |
207 | #print('res %s' % res) | |
208 | #print(self.negtypes_store) | |
209 | #print(self.negtypes_store.hex()) | |
210 | #ret = await self.sign(self.negtypes_store, 0) | |
211 | #print(ret) | |
212 | #print(ret.hex()) | |
213 | #res = { | |
214 | # 'mechListMIC' : ret, | |
215 | # 'negState': NegState('accept-completed') | |
216 | #} | |
217 | #return NegotiationToken({'negTokenResp':NegTokenResp(res)}).dump(), True, None | |
218 | ||
252 | 219 | else: |
253 | #everything is netotiated, but authentication needs more setps | |
254 | neg_token_raw = NegotiationToken.load(token) | |
255 | neg_token = neg_token_raw.native | |
256 | response, to_continue = await self.process_ctx_authenticate(neg_token['responseToken'], flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
220 | response, to_continue, err = await self.process_ctx_authenticate(neg_token['responseToken'], flags = flags, seq_number = seq_number, cb_data = cb_data) | |
221 | if err is not None: | |
222 | return None, None, err | |
257 | 223 | if not response: |
258 | return None, False | |
259 | return NegotiationToken({'negTokenResp':NegTokenResp(response)}).dump(), to_continue | |
224 | return None, False, None | |
225 | ||
226 | if self.selected_mechtype.startswith('NTLM'): | |
227 | response['mechListMIC'] = await self.sign(self.negtypes_store, 0, reset_cipher = True) | |
228 | #self.selected_authentication_context. | |
229 | #print(response) | |
230 | res = NegotiationToken({'negTokenResp':NegTokenResp(response)}).dump() | |
231 | ||
232 | return res, to_continue, None | |
260 | 233 | |
261 | 234 | def test(): |
262 | 235 | test_data = bytes.fromhex('a03e303ca00e300c060a2b06010401823702020aa22a04284e544c4d5353500001000000978208e2000000000000000000000000000000000a00d73a0000000f') |
43 | 43 | async def decrypt(self, data, message_no): |
44 | 44 | return await self.sspi.decrypt(data, message_no) |
45 | 45 | |
46 | async def authenticate(self, token, flags = None, seq_number = 0, is_rpc = False): | |
46 | async def authenticate(self, token, flags = None, seq_number = 0): | |
47 | 47 | try: |
48 | 48 | if self.mode.upper() == 'CLIENT': |
49 | 49 | res, data = self.sspi.authGSSClientStep(token) |
3 | 3 | # Tamas Jos (@skelsec) |
4 | 4 | # |
5 | 5 | |
6 | import copy | |
7 | import asyncio | |
8 | ||
6 | 9 | from msldap import logger |
10 | from msldap.commons.common import MSLDAPClientStatus | |
7 | 11 | from msldap.wintypes.asn1.sdflagsrequest import SDFlagsRequest, SDFlagsRequestValue |
8 | 12 | from msldap.protocol.constants import BASE, ALL_ATTRIBUTES, LEVEL |
9 | 13 | |
10 | from msldap.protocol.query import escape_filter_chars, query_syntax_converter | |
14 | from msldap.protocol.query import escape_filter_chars | |
11 | 15 | from msldap.connection import MSLDAPClientConnection |
12 | 16 | from msldap.protocol.messages import Control |
13 | 17 | from msldap.ldap_objects import * |
14 | 18 | |
19 | from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR | |
20 | from winacl.dtyp.ace import ACCESS_ALLOWED_OBJECT_ACE, ADS_ACCESS_MASK | |
21 | from winacl.dtyp.sid import SID | |
22 | from winacl.dtyp.guid import GUID | |
23 | ||
15 | 24 | class MSLDAPClient: |
16 | def __init__(self, target, creds, ldap_query_page_size = 1000): | |
25 | """ | |
26 | High level API for LDAP operations. | |
27 | ||
28 | target, creds, ldap_query_page_size | |
29 | ||
30 | :param target: The target object describing the connection info | |
31 | :type target: :class:`MSLDAPTarget` | |
32 | :param creds: The credential object describing the authentication to be used | |
33 | :type creds: :class:`MSLDAPCredential` | |
34 | :param ldap_query_page_size: | |
35 | :type ldap_query_page_size: int | |
36 | :return: A dictionary representing the LDAP tree | |
37 | :rtype: dict | |
38 | ||
39 | """ | |
40 | def __init__(self, target, creds): | |
17 | 41 | self.creds = creds |
18 | 42 | self.target = target |
19 | 43 | |
20 | self.ldap_query_page_size = ldap_query_page_size | |
44 | self.ldap_query_page_size = self.target.ldap_query_page_size | |
21 | 45 | self._tree = None |
22 | 46 | self._ldapinfo = None |
23 | 47 | self._con = None |
24 | ||
48 | ||
49 | async def __aenter__(self): | |
50 | return self | |
51 | ||
52 | async def __aexit__(self, exc_type, exc, traceback): | |
53 | await asyncio.wait_for(self.disconnect(), timeout = 1) | |
54 | ||
55 | async def disconnect(self): | |
56 | try: | |
57 | if self._con is not None: | |
58 | await self._con.disconnect() | |
59 | ||
60 | except Exception as e: | |
61 | return False, e | |
25 | 62 | |
26 | 63 | async def connect(self): |
27 | self._con = MSLDAPClientConnection(self.target, self.creds) | |
28 | await self._con.connect() | |
29 | await self._con.bind() | |
30 | res, err = await self._con.get_serverinfo() | |
31 | if err is not None: | |
32 | raise err | |
33 | self._serverinfo = res | |
34 | self._tree = res['defaultNamingContext'] | |
35 | self._ldapinfo = await self.get_ad_info() | |
36 | return True, None | |
64 | try: | |
65 | self._con = MSLDAPClientConnection(self.target, self.creds) | |
66 | _, err = await self._con.connect() | |
67 | if err is not None: | |
68 | raise err | |
69 | res, err = await self._con.bind() | |
70 | if err is not None: | |
71 | return False, err | |
72 | res, err = await self._con.get_serverinfo() | |
73 | if err is not None: | |
74 | raise err | |
75 | self._serverinfo = res | |
76 | self._tree = res['defaultNamingContext'] | |
77 | self._ldapinfo, err = await self.get_ad_info() | |
78 | if err is not None: | |
79 | raise err | |
80 | return True, None | |
81 | except Exception as e: | |
82 | return False, e | |
37 | 83 | |
38 | 84 | def get_server_info(self): |
39 | 85 | return self._serverinfo |
40 | 86 | |
41 | async def pagedsearch(self, ldap_filter, attributes, controls = None): | |
87 | async def pagedsearch(self, query, attributes, controls = None): | |
42 | 88 | """ |
43 | 89 | Performs a paged search on the AD, using the filter and attributes as a normal query does. |
44 | Needs to connect to the server first! | |
45 | ||
46 | Parameters: | |
47 | ldap_filter (str): LDAP query filter | |
48 | attributes (list): Attributes list to recieve in the result | |
49 | controls (obj): Additional control dict | |
50 | ||
51 | Returns: | |
52 | generator | |
53 | """ | |
54 | logger.debug('Paged search, filter: %s attributes: %s' % (ldap_filter, ','.join(attributes))) | |
90 | !The LDAP connection MUST be active before invoking this function! | |
91 | ||
92 | :param query: LDAP query filter | |
93 | :type query: str | |
94 | :param attributes: List of requested attributes | |
95 | :type attributes: List[str] | |
96 | :param controls: additional controls to be passed in the query | |
97 | :type controls: dict | |
98 | :param level: Recursion level | |
99 | :type level: int | |
100 | ||
101 | :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error | |
102 | :rtype: Iterator[(:class:`dict`, :class:`Exception`)] | |
103 | ||
104 | """ | |
105 | logger.debug('Paged search, filter: %s attributes: %s' % (query, ','.join(attributes))) | |
106 | if self._con.status != MSLDAPClientStatus.RUNNING: | |
107 | if self._con.status == MSLDAPClientStatus.ERROR: | |
108 | print('There was an error in the connection!') | |
109 | return | |
110 | elif self._con.status == MSLDAPClientStatus.ERROR: | |
111 | print('Theconnection is in stopped state!') | |
112 | return | |
113 | ||
114 | if self._tree is None: | |
115 | raise Exception('BIND first!') | |
55 | 116 | t = [] |
56 | 117 | for x in attributes: |
57 | 118 | t.append(x.encode()) |
58 | 119 | attributes = t |
59 | ldap_filter = query_syntax_converter(ldap_filter) | |
60 | 120 | |
61 | 121 | t = [] |
62 | 122 | if controls is not None: |
70 | 130 | controls = t |
71 | 131 | |
72 | 132 | async for entry, err in self._con.pagedsearch( |
73 | self._tree.encode(), | |
74 | ldap_filter, | |
133 | self._tree, | |
134 | query, | |
75 | 135 | attributes = attributes, |
76 | paged_size = self.ldap_query_page_size, | |
77 | controls = controls | |
136 | size_limit = self.ldap_query_page_size, | |
137 | controls = controls, | |
138 | rate_limit=self.target.ldap_query_ratelimit | |
78 | 139 | ): |
79 | 140 | |
80 | 141 | if err is not None: |
81 | raise err | |
142 | yield None, err | |
143 | return | |
82 | 144 | if entry['objectName'] == '' and entry['attributes'] == '': |
83 | 145 | #searchresref... |
84 | 146 | continue |
85 | 147 | #print('et %s ' % entry) |
86 | yield entry | |
87 | ||
88 | async def get_tree_plot(self, dn, level = 2): | |
148 | yield entry, None | |
149 | ||
150 | async def get_tree_plot(self, root_dn, level = 2): | |
89 | 151 | """ |
90 | 152 | Returns a dictionary representing a tree starting from 'dn' containing all subtrees. |
91 | Parameters: | |
92 | dn (str): Distinguished name of the root of the tree | |
93 | level (int): Recursion level | |
94 | Returns: | |
95 | dict | |
96 | """ | |
97 | logger.debug('Tree, dn: %s level: %s' % (dn, level)) | |
153 | ||
154 | :param root_dn: The start DN of the tree | |
155 | :type root_dn: str | |
156 | :param level: Recursion level | |
157 | :type level: int | |
158 | ||
159 | :return: A dictionary representing the LDAP tree | |
160 | :rtype: dict | |
161 | """ | |
162 | ||
163 | logger.debug('Tree, dn: %s level: %s' % (root_dn, level)) | |
98 | 164 | tree = {} |
99 | #entries = | |
100 | 165 | async for entry, err in self._con.pagedsearch( |
101 | dn.encode(), | |
102 | query_syntax_converter('(distinguishedName=*)'), | |
166 | root_dn, | |
167 | '(distinguishedName=*)', | |
103 | 168 | attributes = [b'distinguishedName'], |
104 | paged_size = self.ldap_query_page_size, | |
169 | size_limit = self.ldap_query_page_size, | |
105 | 170 | search_scope=LEVEL, |
106 | 171 | controls = None, |
172 | rate_limit=self.target.ldap_query_ratelimit | |
107 | 173 | ): |
108 | 174 | if err is not None: |
109 | 175 | raise err |
116 | 182 | continue |
117 | 183 | subtree = await self.get_tree_plot(entry['attributes']['distinguishedName'], level = level -1) |
118 | 184 | tree[entry['attributes']['distinguishedName']] = subtree |
119 | return {dn : tree} | |
120 | ||
121 | ||
122 | async def get_all_user_objects(self): | |
123 | """ | |
124 | Fetches all user objects from the AD, and returns MSADUser object | |
185 | return {root_dn : tree} | |
186 | ||
187 | async def get_all_users(self): | |
188 | """ | |
189 | Fetches all user objects available in the LDAP tree and yields them as MSADUser object. | |
190 | ||
191 | :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error | |
192 | :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)] | |
193 | ||
125 | 194 | """ |
126 | 195 | logger.debug('Polling AD for all user objects') |
127 | 196 | ldap_filter = r'(sAMAccountType=805306368)' |
128 | async for entry in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
129 | yield MSADUser.from_ldap(entry, self._ldapinfo) | |
197 | async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
198 | if err is not None: | |
199 | yield None, err | |
200 | return | |
201 | yield MSADUser.from_ldap(entry, self._ldapinfo), None | |
130 | 202 | logger.debug('Finished polling for entries!') |
131 | 203 | |
132 | async def get_all_user_raw(self): | |
133 | """ | |
134 | Fetches all user objects from the AD, and returns MSADUser object | |
135 | """ | |
136 | logger.debug('Polling AD for all user objects') | |
137 | ldap_filter = r'(sAMAccountType=805306368)' | |
138 | ||
139 | return self.pagedsearch(ldap_filter, MSADUser_ATTRS) | |
140 | ||
141 | async def get_all_machine_objects(self): | |
142 | """ | |
143 | Fetches all machine objects from the AD, and returns MSADMachine object | |
204 | async def get_all_machines(self, attrs = MSADMachine_ATTRS): | |
205 | """ | |
206 | Fetches all machine objects available in the LDAP tree and yields them as MSADMachine object. | |
207 | ||
208 | :param attrs: Lists of attributes to request (eg. `['sAMAccountName', 'dNSHostName']`) Default: all attrs. | |
209 | :type attrs: list | |
210 | :return: Async generator which yields (`MSADMachine`, None) tuple on success or (None, `Exception`) on error | |
211 | :rtype: Iterator[(:class:`MSADMachine`, :class:`Exception`)] | |
212 | ||
144 | 213 | """ |
145 | 214 | logger.debug('Polling AD for all user objects') |
146 | 215 | ldap_filter = r'(sAMAccountType=805306369)' |
147 | 216 | |
148 | async for entry in self.pagedsearch(ldap_filter, MSADMachine_ATTRS): | |
149 | yield MSADMachine.from_ldap(entry, self._ldapinfo) | |
217 | async for entry, err in self.pagedsearch(ldap_filter, attrs): | |
218 | if err is not None: | |
219 | yield None, err | |
220 | return | |
221 | yield MSADMachine.from_ldap(entry, self._ldapinfo), None | |
150 | 222 | logger.debug('Finished polling for entries!') |
151 | 223 | |
152 | 224 | async def get_all_gpos(self): |
225 | """ | |
226 | Fetches all GPOs available in the LDAP tree and yields them as MSADGPO object. | |
227 | ||
228 | :return: Async generator which yields (`MSADGPO`, None) tuple on success or (None, `Exception`) on error | |
229 | :rtype: Iterator[(:class:`MSADGPO`, :class:`Exception`)] | |
230 | ||
231 | """ | |
232 | ||
153 | 233 | ldap_filter = r'(objectCategory=groupPolicyContainer)' |
154 | async for entry in self.pagedsearch(ldap_filter, MSADGPO_ATTRS): | |
155 | yield MSADGPO.from_ldap(entry) | |
234 | async for entry, err in self.pagedsearch(ldap_filter, MSADGPO_ATTRS): | |
235 | if err is not None: | |
236 | yield None, err | |
237 | return | |
238 | yield MSADGPO.from_ldap(entry), None | |
156 | 239 | |
157 | 240 | async def get_all_laps(self): |
241 | """ | |
242 | Fetches all LAPS passwords for all machines. This functionality is only available to specific high-privileged users. | |
243 | ||
244 | :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error | |
245 | :rtype: Iterator[(:class:`dict`, :class:`Exception`)] | |
246 | """ | |
247 | ||
158 | 248 | ldap_filter = r'(sAMAccountType=805306369)' |
159 | 249 | attributes = ['cn','ms-mcs-AdmPwd'] |
160 | async for entry in self.pagedsearch(ldap_filter, attributes): | |
161 | yield entry | |
250 | async for entry, err in self.pagedsearch(ldap_filter, attributes): | |
251 | yield entry, err | |
252 | ||
253 | async def get_schemaentry(self, dn): | |
254 | """ | |
255 | Fetches one Schema entriy identified by dn | |
256 | ||
257 | :return: (`MSADSchemaEntry`, None) tuple on success or (None, `Exception`) on error | |
258 | :rtype: (:class:`MSADSchemaEntry`, :class:`Exception`) | |
259 | """ | |
260 | logger.debug('Polling Schema entry for %s'% dn) | |
261 | ||
262 | async for entry, err in self._con.pagedsearch( | |
263 | dn, | |
264 | r'(distinguishedName=%s)' % escape_filter_chars(dn), | |
265 | attributes = [x.encode() for x in MSADSCHEMAENTRY_ATTRS], | |
266 | size_limit = self.ldap_query_page_size, | |
267 | search_scope=BASE, | |
268 | controls = None, | |
269 | ): | |
270 | if err is not None: | |
271 | raise err | |
272 | ||
273 | return MSADSchemaEntry.from_ldap(entry), None | |
274 | else: | |
275 | return None, None | |
276 | logger.debug('Finished polling for entries!') | |
277 | ||
278 | async def get_all_schemaentry(self): | |
279 | """ | |
280 | Fetches all Schema entries under CN=Schema,CN=Configuration,... | |
281 | ||
282 | :return: Async generator which yields (`MSADSchemaEntry`, None) tuple on success or (None, `Exception`) on error | |
283 | :rtype: Iterator[(:class:`MSADSchemaEntry`, :class:`Exception`)] | |
284 | """ | |
285 | res = await self.get_tree_plot('CN=Schema,CN=Configuration,' + self._tree, level = 1) | |
286 | for x in res: | |
287 | for dn in res[x]: | |
288 | async for entry, err in self._con.pagedsearch( | |
289 | dn, | |
290 | r'(distinguishedName=%s)' % escape_filter_chars(dn), | |
291 | attributes = [x.encode() for x in MSADSCHEMAENTRY_ATTRS], | |
292 | size_limit = self.ldap_query_page_size, | |
293 | search_scope=BASE, | |
294 | controls = None, | |
295 | rate_limit=self.target.ldap_query_ratelimit | |
296 | ): | |
297 | if err is not None: | |
298 | yield None, err | |
299 | return | |
300 | ||
301 | yield MSADSchemaEntry.from_ldap(entry), None | |
302 | break | |
303 | else: | |
304 | yield None, None | |
305 | ||
306 | logger.debug('Finished polling for entries!') | |
162 | 307 | |
163 | 308 | async def get_laps(self, sAMAccountName): |
309 | """ | |
310 | Fetches the LAPS password for a machine. This functionality is only available to specific high-privileged users. | |
311 | ||
312 | :param sAMAccountName: The username of the machine (eg. `COMP123$`). | |
313 | :type sAMAccountName: str | |
314 | :return: Laps attributes as a `dict` | |
315 | :rtype: (:class:`dict`, :class:`Exception`) | |
316 | """ | |
317 | ||
164 | 318 | ldap_filter = r'(&(sAMAccountType=805306369)(sAMAccountName=%s))' % sAMAccountName |
165 | 319 | attributes = ['cn','ms-mcs-AdmPwd'] |
166 | async for entry in self.pagedsearch(ldap_filter, attributes): | |
167 | yield entry | |
320 | async for entry, err in self.pagedsearch(ldap_filter, attributes): | |
321 | return entry, err | |
168 | 322 | |
169 | 323 | async def get_user(self, sAMAccountName): |
170 | 324 | """ |
171 | 325 | Fetches one user object from the AD, based on the sAMAccountName attribute (read: username) |
326 | ||
327 | :param sAMAccountName: The username of the user. | |
328 | :type sAMAccountName: str | |
329 | :return: A tuple with the user as `MSADUser` and an `Exception` is there was any | |
330 | :rtype: (:class:`MSADUser`, :class:`Exception`) | |
172 | 331 | """ |
173 | 332 | logger.debug('Polling AD for user %s'% sAMAccountName) |
174 | 333 | ldap_filter = r'(&(objectClass=user)(sAMAccountName=%s))' % sAMAccountName |
175 | async for entry in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
176 | # TODO: return ldapuser object | |
177 | yield MSADUser.from_ldap(entry, self._ldapinfo) | |
334 | async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
335 | if err is not None: | |
336 | return None, err | |
337 | return MSADUser.from_ldap(entry, self._ldapinfo), None | |
338 | else: | |
339 | return None, None | |
178 | 340 | logger.debug('Finished polling for entries!') |
179 | 341 | |
342 | async def get_machine(self, sAMAccountName): | |
343 | """ | |
344 | Fetches one machine object from the AD, based on the sAMAccountName attribute (read: username) | |
345 | ||
346 | :param sAMAccountName: The username of the machine. | |
347 | :type sAMAccountName: str | |
348 | :return: A tuple with the user as `MSADMachine` and an `Exception` is there was any | |
349 | :rtype: (:class:`MSADMachine`, :class:`Exception`) | |
350 | """ | |
351 | logger.debug('Polling AD for user %s'% sAMAccountName) | |
352 | ldap_filter = r'(&(sAMAccountType=805306369)(sAMAccountName=%s))' % sAMAccountName | |
353 | async for entry, err in self.pagedsearch(ldap_filter, MSADMachine_ATTRS): | |
354 | if err is not None: | |
355 | return None, err | |
356 | return MSADMachine.from_ldap(entry, self._ldapinfo), None | |
357 | else: | |
358 | return None, None | |
359 | logger.debug('Finished polling for entries!') | |
360 | ||
180 | 361 | async def get_ad_info(self): |
181 | 362 | """ |
182 | 363 | Polls for basic AD information (needed for determine password usage characteristics!) |
364 | ||
365 | :return: A tuple with the domain information as `MSADInfo` and an `Exception` is there was any | |
366 | :rtype: (:class:`MSADInfo`, :class:`Exception`) | |
183 | 367 | """ |
184 | 368 | logger.debug('Polling AD for basic info') |
185 | 369 | ldap_filter = r'(distinguishedName=%s)' % self._tree |
186 | async for entry in self.pagedsearch(ldap_filter, MSADInfo_ATTRS): | |
370 | async for entry, err in self.pagedsearch(ldap_filter, MSADInfo_ATTRS): | |
371 | if err is not None: | |
372 | return None, err | |
187 | 373 | self._ldapinfo = MSADInfo.from_ldap(entry) |
188 | return self._ldapinfo | |
374 | return self._ldapinfo, None | |
189 | 375 | |
190 | 376 | logger.debug('Poll finished!') |
191 | 377 | |
192 | 378 | async def get_all_spn_entries(self): |
379 | """ | |
380 | Fetches all service user objects from the AD, and returns MSADUser object. | |
381 | Service user refers to an user with SPN (servicePrincipalName) attribute set | |
382 | ||
383 | :param include_machine: Specifies wether machine accounts should be included in the query | |
384 | :type include_machine: bool | |
385 | :return: Async generator which yields tuples with a string in SPN format and an Exception if there was any | |
386 | :rtype: Iterator[(:class:`str`, :class:`Exception`)] | |
387 | ||
388 | """ | |
389 | ||
193 | 390 | logger.debug('Polling AD for all SPN entries') |
194 | 391 | ldap_filter = r'(&(sAMAccountType=805306369))' |
195 | 392 | attributes = ['objectSid','sAMAccountName', 'servicePrincipalName'] |
196 | 393 | |
197 | async for entry in self.pagedsearch(ldap_filter, attributes): | |
198 | yield entry | |
199 | ||
200 | async def get_all_service_user_objects(self, include_machine = False): | |
394 | async for entry, err in self.pagedsearch(ldap_filter, attributes): | |
395 | yield entry, err | |
396 | ||
397 | async def get_all_service_users(self, include_machine = False): | |
201 | 398 | """ |
202 | 399 | Fetches all service user objects from the AD, and returns MSADUser object. |
203 | Service user refers to an user whith SPN (servicePrincipalName) attribute set | |
400 | Service user refers to an user with SPN (servicePrincipalName) attribute set | |
401 | ||
402 | :param include_machine: Specifies wether machine accounts should be included in the query | |
403 | :type include_machine: bool | |
404 | ||
405 | :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error | |
406 | :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)] | |
407 | ||
204 | 408 | """ |
205 | 409 | logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine) |
206 | 410 | if include_machine == True: |
208 | 412 | else: |
209 | 413 | ldap_filter = r'(&(servicePrincipalName=*)(!(sAMAccountName=*$)))' |
210 | 414 | |
211 | async for entry in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
212 | yield MSADUser.from_ldap(entry, self._ldapinfo) | |
415 | async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
416 | if err is not None: | |
417 | yield None, err | |
418 | return | |
419 | yield MSADUser.from_ldap(entry, self._ldapinfo), None | |
213 | 420 | logger.debug('Finished polling for entries!') |
214 | 421 | |
215 | async def get_all_knoreq_user_objects(self, include_machine = False): | |
422 | async def get_all_knoreq_users(self, include_machine = False): | |
216 | 423 | """ |
217 | 424 | Fetches all user objects with useraccountcontrol DONT_REQ_PREAUTH flag set from the AD, and returns MSADUser object. |
218 | 425 | |
426 | :param include_machine: Specifies wether machine accounts should be included in the query | |
427 | :type include_machine: bool | |
428 | :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error | |
429 | :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)] | |
430 | ||
219 | 431 | """ |
220 | 432 | logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine) |
221 | 433 | if include_machine == True: |
223 | 435 | else: |
224 | 436 | ldap_filter = r'(&(userAccountControl:1.2.840.113556.1.4.803:=4194304)(!(sAMAccountName=*$)))' |
225 | 437 | |
226 | async for entry in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
227 | yield MSADUser.from_ldap(entry, self._ldapinfo) | |
438 | async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
439 | if err is not None: | |
440 | yield None, err | |
441 | return | |
442 | yield MSADUser.from_ldap(entry, self._ldapinfo), None | |
228 | 443 | logger.debug('Finished polling for entries!') |
229 | ||
230 | ||
231 | #async def get_all_objectacl(self): | |
232 | # """ | |
233 | # Returns all ACL info for all AD objects | |
234 | # """ | |
235 | # | |
236 | # flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION | |
237 | # req_flags = SDFlagsRequestValue({'Flags' : flags_value}) | |
238 | # | |
239 | # ldap_filter = r'(objectClass=*)' | |
240 | # attributes = MSADSecurityInfo.ATTRS | |
241 | # controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] | |
242 | # | |
243 | # async for entry in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
244 | # yield MSADSecurityInfo.from_ldap(entry) | |
245 | ||
246 | async def get_objectacl_by_dn(self, dn): | |
247 | """ | |
248 | Returns all ACL info for all AD objects | |
249 | """ | |
250 | ||
251 | flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION | |
252 | req_flags = SDFlagsRequestValue({'Flags' : flags_value}) | |
444 | ||
445 | async def get_objectacl_by_dn_p(self, dn, flags = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION): | |
446 | """ | |
447 | Returns the full or partial Security Descriptor of the object specified by it's DN. | |
448 | The flags indicate which part of the security Descriptor to be returned. | |
449 | By default the full SD info is returned. | |
450 | ||
451 | :param object_dn: The object's DN | |
452 | :type object_dn: str | |
453 | :param flags: Flags indicate the data type to be returned. | |
454 | :type flags: :class:`SDFlagsRequest` | |
455 | :return: | |
456 | :rtype: :class:`MSADSecurityInfo` | |
457 | ||
458 | """ | |
459 | ||
460 | req_flags = SDFlagsRequestValue({'Flags' : flags}) | |
253 | 461 | |
254 | 462 | ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn) |
255 | 463 | attributes = MSADSecurityInfo.ATTRS |
256 | 464 | controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] |
257 | 465 | |
258 | async for entry in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
259 | yield MSADSecurityInfo.from_ldap(entry) | |
260 | ||
261 | ||
466 | async for entry, err in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
467 | if err is not None: | |
468 | yield None, err | |
469 | return | |
470 | yield MSADSecurityInfo.from_ldap(entry), None | |
471 | ||
472 | async def get_objectacl_by_dn(self, dn, flags = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION): | |
473 | """ | |
474 | Returns the full or partial Security Descriptor of the object specified by it's DN. | |
475 | The flags indicate which part of the security Descriptor to be returned. | |
476 | By default the full SD info is returned. | |
477 | ||
478 | :param object_dn: The object's DN | |
479 | :type object_dn: str | |
480 | :param flags: Flags indicate the data type to be returned. | |
481 | :type flags: :class:`SDFlagsRequest` | |
482 | :return: nTSecurityDescriptor attribute of the object as `bytes` and an `Exception` is there was any | |
483 | :rtype: (:class:`bytes`, :class:`Exception`) | |
484 | ||
485 | """ | |
486 | ||
487 | req_flags = SDFlagsRequestValue({'Flags' : flags}) | |
488 | ||
489 | ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn) | |
490 | attributes = ['nTSecurityDescriptor'] | |
491 | controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] | |
492 | ||
493 | async for entry, err in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
494 | if err is not None: | |
495 | return None, err | |
496 | return entry['attributes'].get('nTSecurityDescriptor'), None | |
497 | return None, None | |
498 | ||
499 | async def set_objectacl_by_dn(self, object_dn, data, flags = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION): | |
500 | """ | |
501 | Updates the security descriptor of the LDAP object | |
502 | ||
503 | :param object_dn: The object's DN | |
504 | :type object_dn: str | |
505 | :param data: The actual data as bytearray to be updated in the Security Descriptor of the specified object | |
506 | :type data: bytes | |
507 | :param flags: Flags indicate the data type to be updated. | |
508 | :type flags: :class:`SDFlagsRequest` | |
509 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
510 | :rtype: tuple | |
511 | ||
512 | """ | |
513 | ||
514 | req_flags = SDFlagsRequestValue({'Flags' : flags}) | |
515 | controls = [ | |
516 | Control({ | |
517 | 'controlType' : b'1.2.840.113556.1.4.801', | |
518 | 'controlValue': req_flags.dump(), | |
519 | 'criticality' : True, | |
520 | }) | |
521 | ] | |
522 | ||
523 | changes = { | |
524 | 'nTSecurityDescriptor': [('replace', [data])] | |
525 | } | |
526 | return await self._con.modify(object_dn, changes, controls = controls) | |
527 | ||
528 | async def get_all_groups(self): | |
529 | """ | |
530 | Yields all Groups present in the LDAP tree. | |
531 | ||
532 | :return: Async generator which yields (`MSADGroup`, None) tuple on success or (None, `Exception`) on error | |
533 | :rtype: Iterator[(:class:`MSADGroup`, :class:`Exception`)] | |
534 | """ | |
535 | ldap_filter = r'(objectClass=group)' | |
536 | async for entry, err in self.pagedsearch(ldap_filter, MSADGroup_ATTRS): | |
537 | if err is not None: | |
538 | yield None, err | |
539 | return | |
540 | yield MSADGroup.from_ldap(entry), None | |
541 | ||
542 | async def get_all_ous(self): | |
543 | """ | |
544 | Yields all OUs present in the LDAP tree. | |
545 | ||
546 | :return: Async generator which yields (`MSADOU`, None) tuple on success or (None, `Exception`) on error | |
547 | :rtype: Iterator[(:class:`MSADOU`, :class:`Exception`)] | |
548 | """ | |
549 | ldap_filter = r'(objectClass=organizationalUnit)' | |
550 | async for entry, err in self.pagedsearch(ldap_filter, MSADOU_ATTRS): | |
551 | if err is not None: | |
552 | yield None, err | |
553 | return | |
554 | yield MSADOU.from_ldap(entry), None | |
555 | ||
556 | async def get_group_by_dn(self, group_dn): | |
557 | """ | |
558 | Returns an `MSADGroup` object for the group specified by group_dn | |
559 | ||
560 | :param group_dn: The user's DN | |
561 | :type group_dn: str | |
562 | :return: tuple of `MSADGroup` and an `Exception` is there was any | |
563 | :rtype: (:class:`MSADGroup`, :class:`Exception`) | |
564 | """ | |
565 | ||
566 | ldap_filter = r'(&(objectClass=group)(distinguishedName=%s))' % escape_filter_chars(group_dn) | |
567 | async for entry, err in self.pagedsearch(ldap_filter, MSADGroup_ATTRS): | |
568 | if err is not None: | |
569 | return None, err | |
570 | return MSADGroup.from_ldap(entry), None | |
571 | ||
572 | async def get_user_by_dn(self, user_dn): | |
573 | """ | |
574 | Fetches the DN for an object specified by `user_dn` | |
575 | ||
576 | :param user_dn: The user's DN | |
577 | :type user_dn: str | |
578 | :return: The user object | |
579 | :rtype: (:class:`MSADUser`, :class:`Exception`) | |
580 | """ | |
581 | ||
582 | ldap_filter = r'(&(objectClass=user)(distinguishedName=%s))' % user_dn | |
583 | async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS): | |
584 | if err is not None: | |
585 | return None, err | |
586 | return MSADUser.from_ldap(entry), None | |
587 | ||
588 | async def get_group_members(self, dn, recursive = False): | |
589 | """ | |
590 | Fetches the DN for an object specified by `objectsid` | |
591 | ||
592 | :param dn: The object's DN | |
593 | :type dn: str | |
594 | :param recursive: Indicates wether the lookup should recursively affect all groups | |
595 | :type recursive: bool | |
596 | :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error | |
597 | :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)] | |
598 | """ | |
599 | ||
600 | group, err = self.get_group_by_dn(dn) | |
601 | if err is not None: | |
602 | yield None, err | |
603 | return | |
604 | for member in group.member: | |
605 | async for result in self.get_object_by_dn(member): | |
606 | if isinstance(result, MSADGroup) and recursive: | |
607 | async for user, err in self.get_group_members(result.distinguishedName, recursive = True): | |
608 | yield user, err | |
609 | else: | |
610 | yield result, err | |
611 | ||
612 | async def get_dn_for_objectsid(self, objectsid): | |
613 | """ | |
614 | Fetches the DN for an object specified by `objectsid` | |
615 | ||
616 | :param objectsid: The object's SID | |
617 | :type objectsid: str | |
618 | :return: The distinguishedName | |
619 | :rtype: (:class:`str`, :class:`Exception`) | |
620 | ||
621 | """ | |
622 | ||
623 | ldap_filter = r'(objectSid=%s)' % str(objectsid) | |
624 | async for entry, err in self.pagedsearch(ldap_filter, ['distinguishedName']): | |
625 | if err is not None: | |
626 | return None, err | |
627 | ||
628 | return entry['attributes']['distinguishedName'], None | |
629 | ||
630 | async def get_objectsid_for_dn(self, dn): | |
631 | """ | |
632 | Fetches the objectsid for an object specified by `dn` | |
633 | ||
634 | :param dn: The object's distinguishedName | |
635 | :type dn: str | |
636 | :return: The SID of the pobject | |
637 | :rtype: (:class:`str`, :class:`Exception`) | |
638 | ||
639 | """ | |
640 | ||
641 | ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn) | |
642 | async for entry, err in self.pagedsearch(ldap_filter, ['objectSid']): | |
643 | if err is not None: | |
644 | return None, err | |
645 | ||
646 | return entry['attributes']['objectSid'], None | |
647 | ||
648 | async def get_tokengroups(self, dn): | |
649 | """ | |
650 | Yields SIDs of groups that the given DN is a member of. | |
651 | ||
652 | :return: Async generator which yields (`str`, None) tuple on success or (None, `Exception`) on error | |
653 | :rtype: Iterator[(:class:`str`, :class:`Exception`)] | |
654 | ||
655 | """ | |
656 | ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn) | |
657 | attributes=[b'tokenGroups'] | |
658 | ||
659 | async for entry, err in self._con.pagedsearch( | |
660 | dn, | |
661 | ldap_filter, | |
662 | attributes = attributes, | |
663 | size_limit = self.ldap_query_page_size, | |
664 | search_scope=BASE, | |
665 | rate_limit=self.target.ldap_query_ratelimit | |
666 | ): | |
667 | if err is not None: | |
668 | yield None, err | |
669 | return | |
670 | ||
671 | #print(entry['attributes']) | |
672 | if 'tokenGroups' in entry['attributes']: | |
673 | for sid_data in entry['attributes']['tokenGroups']: | |
674 | yield sid_data, None | |
675 | ||
676 | async def get_all_tokengroups(self): | |
677 | """ | |
678 | Yields all effective group membership information for all objects of the following type: | |
679 | Users, Groups, Computers | |
680 | ||
681 | :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error | |
682 | :rtype: Iterator[(:class:`dict`, :class:`Exception`)] | |
683 | ||
684 | """ | |
685 | ||
686 | ldap_filter = r'(|(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))' | |
687 | async for entry, err in self.pagedsearch( | |
688 | ldap_filter, | |
689 | attributes = ['dn', 'cn', 'objectSid','objectClass', 'objectGUID'] | |
690 | ): | |
691 | if err is not None: | |
692 | yield None, err | |
693 | return | |
694 | if 'objectName' in entry: | |
695 | #print(entry['objectName']) | |
696 | async for entry2, err in self._con.pagedsearch( | |
697 | entry['objectName'], | |
698 | r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName']), | |
699 | attributes = [b'tokenGroups'], | |
700 | size_limit = self.ldap_query_page_size, | |
701 | search_scope=BASE, | |
702 | rate_limit=self.target.ldap_query_ratelimit | |
703 | ): | |
704 | ||
705 | #print(entry2) | |
706 | if err is not None: | |
707 | yield None, err | |
708 | break | |
709 | if 'tokenGroups' in entry2['attributes']: | |
710 | for token in entry2['attributes']['tokenGroups']: | |
711 | yield { | |
712 | 'cn' : entry['attributes']['cn'], | |
713 | 'dn' : entry['objectName'], | |
714 | 'guid' : entry['attributes']['objectGUID'], | |
715 | 'sid' : entry['attributes']['objectSid'], | |
716 | 'type' : entry['attributes']['objectClass'][-1], | |
717 | 'token' : token | |
718 | ||
719 | }, None | |
720 | ||
721 | async def get_all_objectacl(self): | |
722 | """ | |
723 | Yields the security descriptor of all objects in the LDAP tree of the following types: | |
724 | Users, Computers, GPOs, OUs, Groups | |
725 | ||
726 | :return: Async generator which yields (`MSADSecurityInfo`, None) tuple on success or (None, `Exception`) on error | |
727 | :rtype: Iterator[(:class:`MSADSecurityInfo`, :class:`Exception`)] | |
728 | ||
729 | """ | |
730 | ||
731 | flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION | |
732 | req_flags = SDFlagsRequestValue({'Flags' : flags_value}) | |
733 | ||
734 | ldap_filter = r'(|(objectClass=organizationalUnit)(objectCategory=groupPolicyContainer)(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))' | |
735 | async for entry, err in self.pagedsearch(ldap_filter, attributes = ['dn']): | |
736 | if err is not None: | |
737 | yield None, err | |
738 | return | |
739 | ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName']) | |
740 | attributes = MSADSecurityInfo.ATTRS | |
741 | controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] | |
742 | ||
743 | async for entry2, err in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
744 | if err is not None: | |
745 | yield None, err | |
746 | return | |
747 | yield MSADSecurityInfo.from_ldap(entry2), None | |
748 | ||
749 | ||
750 | async def get_all_trusts(self): | |
751 | """ | |
752 | Yields all trusted domains. | |
753 | ||
754 | :return: Async generator which yields (`MSADDomainTrust`, None) tuple on success or (None, `Exception`) on error | |
755 | :rtype: Iterator[(:class:`MSADDomainTrust`, :class:`Exception`)] | |
756 | ||
757 | """ | |
758 | ||
759 | ldap_filter = r'(objectClass=trustedDomain)' | |
760 | async for entry, err in self.pagedsearch(ldap_filter, attributes = MSADDomainTrust_ATTRS): | |
761 | if err is not None: | |
762 | yield None, err | |
763 | return | |
764 | yield MSADDomainTrust.from_ldap(entry), None | |
765 | ||
766 | async def create_user_dn(self, user_dn, password): | |
767 | """ | |
768 | Creates a new user object with a password and enables the user so it can be used immediately. | |
769 | ||
770 | :param user_dn: The user's DN | |
771 | :type user_dn: str | |
772 | :param password: The password of the user | |
773 | :type password: str | |
774 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
775 | :rtype: (:class:`bool`, :class:`Exception`) | |
776 | ||
777 | """ | |
778 | try: | |
779 | sn = user_dn.split(',')[0][3:] | |
780 | domain = self._tree[3:].replace(',DC=','.') | |
781 | attributes = { | |
782 | 'objectClass': ['organizationalPerson', 'person', 'top', 'user'], | |
783 | 'sn': sn, | |
784 | 'sAMAccountName': sn, | |
785 | 'displayName': sn, | |
786 | 'userPrincipalName' : "{}@{}".format(sn, domain), | |
787 | } | |
788 | ||
789 | _, err = await self._con.add(user_dn, attributes) | |
790 | if err is not None: | |
791 | return False, err | |
792 | ||
793 | _, err = await self.change_password(user_dn, password) | |
794 | if err is not None: | |
795 | return False, err | |
796 | ||
797 | _, err = await self.enable_user(user_dn) | |
798 | if err is not None: | |
799 | return False, err | |
800 | ||
801 | return True, None | |
802 | except Exception as e: | |
803 | return False, e | |
804 | ||
805 | ||
806 | async def unlock_user(self, user_dn): | |
807 | """ | |
808 | Unlocks the user by clearing the lockoutTime attribute. | |
809 | ||
810 | :param user_dn: The user's DN | |
811 | :type user_dn: str | |
812 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
813 | :rtype: (:class:`bool`, :class:`Exception`) | |
814 | ||
815 | """ | |
816 | changes = { | |
817 | 'lockoutTime': [('replace', [0])] | |
818 | } | |
819 | return await self._con.modify(user_dn, changes) | |
820 | ||
821 | async def enable_user(self, user_dn): | |
822 | """ | |
823 | Sets the user object to enabled by modifying the UserAccountControl attribute. | |
824 | ||
825 | :param user_dn: The user's DN | |
826 | :type user_dn: str | |
827 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
828 | :rtype: (:class:`bool`, :class:`Exception`) | |
829 | ||
830 | """ | |
831 | changes = { | |
832 | 'userAccountControl': [('replace', [512])] | |
833 | } | |
834 | return await self._con.modify(user_dn, changes) | |
835 | ||
836 | async def disable_user(self, user_dn): | |
837 | """ | |
838 | Sets the user object to disabled by modifying the UserAccountControl attribute. | |
839 | ||
840 | :param user_dn: The user's DN | |
841 | :type user_dn: str | |
842 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
843 | :rtype: (:class:`bool`, :class:`Exception`) | |
844 | ||
845 | """ | |
846 | changes = { | |
847 | 'userAccountControl': [('replace', [2])] | |
848 | } | |
849 | return await self._con.modify(user_dn, changes) | |
850 | ||
851 | async def add_user_spn(self, user_dn, spn): | |
852 | """ | |
853 | Adds an SPN record to the user object. | |
854 | ||
855 | :param user_dn: The user's DN | |
856 | :type user_dn: str | |
857 | :param spn: The SPN to be added. It must follow the SPN string format specifications. | |
858 | :type spn: str | |
859 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
860 | :rtype: (:class:`bool`, :class:`Exception`) | |
861 | ||
862 | """ | |
863 | changes = { | |
864 | 'servicePrincipalName': [('add', [spn])] | |
865 | } | |
866 | return await self._con.modify(user_dn, changes) | |
867 | ||
868 | async def add_additional_hostname(self, user_dn, hostname): | |
869 | """ | |
870 | Adds additional hostname to the user object. | |
871 | ||
872 | :param user_dn: The user's DN | |
873 | :type user_dn: str | |
874 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
875 | :rtype: (:class:`bool`, :class:`Exception`) | |
876 | ||
877 | """ | |
878 | changes = { | |
879 | 'msds-additionaldnshostname': [('add', [hostname])] | |
880 | } | |
881 | return await self._con.modify(user_dn, changes) | |
882 | ||
883 | ||
884 | async def delete_user(self, user_dn): | |
885 | """ | |
886 | Deletes the user. | |
887 | This action is destructive! | |
888 | ||
889 | :param user_dn: The user's DN | |
890 | :type user_dn: str | |
891 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
892 | :rtype: (:class:`bool`, :class:`Exception`) | |
893 | ||
894 | """ | |
895 | return await self._con.delete(user_dn) | |
896 | ||
897 | async def change_password(self, user_dn: str, newpass: str, oldpass = None): | |
898 | """ | |
899 | Changes the password of a user. | |
900 | If used with a high-privileged account (eg. Domain admin, Account operator...), the old password can be `None` | |
901 | ||
902 | :param user_dn: The user's DN | |
903 | :type user_dn: str | |
904 | :param newpass: The new password | |
905 | :type newpass: str | |
906 | :param oldpass: The current password | |
907 | :type oldpass: str | |
908 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
909 | :rtype: (:class:`bool`, :class:`Exception`) | |
910 | ||
911 | """ | |
912 | changes = { | |
913 | 'unicodePwd': [] | |
914 | } | |
915 | if oldpass is not None: | |
916 | changes['unicodePwd'].append(('delete', ['"%s"' % oldpass])) | |
917 | changes['unicodePwd'].append(('add', ['"%s"' % newpass])) | |
918 | else: | |
919 | #if you are admin... | |
920 | changes['unicodePwd'].append(('replace', ['"%s"' % newpass])) | |
921 | ||
922 | return await self._con.modify(user_dn, changes) | |
923 | ||
924 | ||
925 | async def add_user_to_group(self, user_dn: str, group_dn: str): | |
926 | """ | |
927 | Adds a user to a group | |
928 | ||
929 | :param user_dn: The user's DN | |
930 | :type user_dn: str | |
931 | :param group_dn: The groups's DN | |
932 | :type group_dn: str | |
933 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
934 | :rtype: (:class:`bool`, :class:`Exception`) | |
935 | ||
936 | ||
937 | """ | |
938 | changes = { | |
939 | 'member': [('add', [user_dn])] | |
940 | } | |
941 | return await self._con.modify(group_dn, changes) | |
942 | ||
943 | async def del_user_from_group(self, user_dn: str, group_dn: str): | |
944 | """ | |
945 | Removes user from group | |
946 | ||
947 | :param user_dn: The user's DN | |
948 | :type user_dn: str | |
949 | :param group_dn: The groups's DN | |
950 | :type group_dn: str | |
951 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
952 | :rtype: (:class:`bool`, :class:`Exception`) | |
953 | ||
954 | ||
955 | """ | |
956 | changes = { | |
957 | 'member': [('delete', [user_dn])] | |
958 | } | |
959 | return await self._con.modify(group_dn, changes) | |
960 | ||
961 | ||
962 | async def get_object_by_dn(self, dn, expected_class = None): | |
963 | ldap_filter = r'(distinguishedName=%s)' % dn | |
964 | async for entry, err in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
965 | if err is not None: | |
966 | yield None, err | |
967 | return | |
968 | temp = entry['attributes'].get('objectClass') | |
969 | if expected_class: | |
970 | yield expected_class.from_ldap(entry), None | |
971 | ||
972 | if not temp: | |
973 | yield entry, None | |
974 | elif 'user' in temp: | |
975 | yield MSADUser.from_ldap(entry), None | |
976 | elif 'group' in temp: | |
977 | yield MSADGroup.from_ldap(entry), None | |
978 | ||
979 | async def modify(self, dn, changes, controls = None): | |
980 | """ | |
981 | Performs the modify operation. | |
982 | ||
983 | :param dn: The DN of the object whose attributes are to be modified | |
984 | :type dn: str | |
985 | :param changes: Describes the changes to be made on the object. Must be a dictionary of the following format: {'attribute': [('change_type', [value])]} | |
986 | :type changes: dict | |
987 | :param controls: additional controls to be passed in the query | |
988 | :type controls: dict | |
989 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
990 | :rtype: (:class:`bool`, :class:`Exception`) | |
991 | """ | |
992 | if controls is None: | |
993 | controls = [] | |
994 | controls_conv = [] | |
995 | for control in controls: | |
996 | controls_conv.append(Control(control)) | |
997 | return await self._con.modify(dn, changes, controls=controls_conv) | |
998 | ||
999 | ||
1000 | async def add(self, dn, attributes): | |
1001 | """ | |
1002 | Performs the add operation. | |
1003 | ||
1004 | :param dn: The DN of the object to be added | |
1005 | :type dn: str | |
1006 | :param attributes: Attributes to be used in the operation | |
1007 | :type attributes: dict | |
1008 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
1009 | :rtype: (:class:`bool`, :class:`Exception`) | |
1010 | """ | |
1011 | ||
1012 | return await self._con.add(dn, attributes) | |
1013 | ||
1014 | async def delete(self, dn): | |
1015 | """ | |
1016 | Performs the delete operation. | |
1017 | ||
1018 | :param dn: The DN of the object to be deleted | |
1019 | :type dn: str | |
1020 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
1021 | :rtype: (:class:`bool`, :class:`Exception`) | |
1022 | """ | |
1023 | ||
1024 | return await self._con.delete(dn) | |
1025 | ||
1026 | async def add_priv_addmember(self, user_dn, group_dn): | |
1027 | """Adds AddMember rights to the user on the group specified by group_dn""" | |
1028 | try: | |
1029 | #getting SID of target dn | |
1030 | user_sid, err = await self.get_objectsid_for_dn(user_dn) | |
1031 | if err is not None: | |
1032 | raise err | |
1033 | ||
1034 | res, err = await self.get_objectacl_by_dn(group_dn) | |
1035 | if err is not None: | |
1036 | raise err | |
1037 | if res is None: | |
1038 | raise Exception('Failed to get forest\'s SD') | |
1039 | group_sd = SECURITY_DESCRIPTOR.from_bytes(res) | |
1040 | ||
1041 | new_sd = copy.deepcopy(group_sd) | |
1042 | ||
1043 | ace_1 = ACCESS_ALLOWED_OBJECT_ACE() | |
1044 | ace_1.Sid = SID.from_string(user_sid) | |
1045 | ace_1.ObjectType = GUID.from_string('bf9679c0-0de6-11d0-a285-00aa003049e2') | |
1046 | ace_1.Mask = ADS_ACCESS_MASK.WRITE_PROP | |
1047 | ace_1.AceFlags = 0 | |
1048 | ||
1049 | new_sd.Dacl.aces.append(ace_1) | |
1050 | ||
1051 | changes = { | |
1052 | 'nTSecurityDescriptor' : [('replace', [new_sd.to_bytes()])] | |
1053 | } | |
1054 | _, err = await self.modify(group_dn, changes) | |
1055 | if err is not None: | |
1056 | raise err | |
1057 | ||
1058 | return True, None | |
1059 | except Exception as e: | |
1060 | return False, e | |
1061 | ||
1062 | async def add_priv_dcsync(self, user_dn, forest_dn = None): | |
1063 | """Adds DCSync rights to the given user by modifying the forest's Security Descriptor to add GetChanges and GetChangesAll ACE""" | |
1064 | try: | |
1065 | #getting SID of target dn | |
1066 | user_sid, err = await self.get_objectsid_for_dn(user_dn) | |
1067 | if err is not None: | |
1068 | raise err | |
1069 | ||
1070 | if forest_dn is None: | |
1071 | forest_dn = self._ldapinfo.distinguishedName | |
1072 | ||
1073 | res, err = await self.get_objectacl_by_dn(forest_dn) | |
1074 | if err is not None: | |
1075 | raise err | |
1076 | if res is None: | |
1077 | raise Exception('Failed to get forest\'s SD') | |
1078 | forest_sd = SECURITY_DESCRIPTOR.from_bytes(res) | |
1079 | ||
1080 | ||
1081 | new_sd = copy.deepcopy(forest_sd) | |
1082 | ||
1083 | ace_1 = ACCESS_ALLOWED_OBJECT_ACE() | |
1084 | ace_1.Sid = SID.from_string(user_sid) | |
1085 | ace_1.ObjectType = GUID.from_string('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2') | |
1086 | ace_1.Mask = ADS_ACCESS_MASK.CONTROL_ACCESS | |
1087 | ace_1.AceFlags = 0 | |
1088 | ||
1089 | ||
1090 | new_sd.Dacl.aces.append(ace_1) | |
1091 | ||
1092 | ace_2 = ACCESS_ALLOWED_OBJECT_ACE() | |
1093 | ace_2.Sid = SID.from_string(user_sid) | |
1094 | ace_2.ObjectType = GUID.from_string('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2') | |
1095 | ace_2.Mask = ADS_ACCESS_MASK.CONTROL_ACCESS | |
1096 | ace_2.AceFlags = 0 | |
1097 | ||
1098 | new_sd.Dacl.aces.append(ace_2) | |
1099 | ||
1100 | changes = { | |
1101 | 'nTSecurityDescriptor' : [('replace', [new_sd.to_bytes()])] | |
1102 | } | |
1103 | _, err = await self.modify(forest_dn, changes) | |
1104 | if err is not None: | |
1105 | raise err | |
1106 | ||
1107 | return True, None | |
1108 | except Exception as e: | |
1109 | return False, e | |
1110 | ||
1111 | async def change_priv_owner(self, new_owner_sid, target_dn, target_attribute = None): | |
1112 | """Changes the owner in a Security Descriptor to the new_owner_sid on an LDAP object or on an LDAP object's attribute identified by target_dn and target_attribute. target_attribute can be omitted to change the target_dn's SD's owner""" | |
1113 | try: | |
1114 | try: | |
1115 | new_owner_sid = SID.from_string(new_owner_sid) | |
1116 | except: | |
1117 | return False, Exception('Incorrect SID') | |
1118 | ||
1119 | ||
1120 | target_sd = None | |
1121 | if target_attribute is None or target_attribute == '': | |
1122 | target_attribute = 'nTSecurityDescriptor' | |
1123 | res, err = await self.get_objectacl_by_dn(target_dn) | |
1124 | if err is not None: | |
1125 | raise err | |
1126 | target_sd = SECURITY_DESCRIPTOR.from_bytes(res) | |
1127 | else: | |
1128 | query = '(distinguishedName=%s)' % target_dn | |
1129 | async for entry, err in self.pagedsearch(query, [target_attribute]): | |
1130 | if err is not None: | |
1131 | raise err | |
1132 | target_sd = SECURITY_DESCRIPTOR.from_bytes(entry['attributes'][target_attribute]) | |
1133 | break | |
1134 | else: | |
1135 | raise Exception('Target DN not found!') | |
1136 | ||
1137 | new_sd = copy.deepcopy(target_sd) | |
1138 | new_sd.Owner = new_owner_sid | |
1139 | ||
1140 | changes = { | |
1141 | target_attribute : [('replace', [new_sd.to_bytes()])] | |
1142 | } | |
1143 | _, err = await self.modify(target_dn, changes) | |
1144 | if err is not None: | |
1145 | raise err | |
1146 | ||
1147 | return True, None | |
1148 | except Exception as e: | |
1149 | return False, e | |
1150 | ||
1151 | #async def get_permissions_for_dn(self, dn): | |
1152 | # """ | |
1153 | # Lists all users who can modify the specified dn | |
1154 | # """ | |
1155 | # async for secinfo in self.get_objectacl_by_dn(dn): | |
1156 | # for sdec in secinfo.nTSecurityDescriptor: | |
1157 | # sids_to_lookup = {} | |
1158 | # if not sdec.Dacl: | |
1159 | # continue | |
1160 | # | |
1161 | # for ace in sdec.Dacl.aces: | |
1162 | # sids_to_lookup[str(ace.Sid)] = 1 | |
1163 | # | |
1164 | # for sid in sids_to_lookup: | |
1165 | # sids_to_lookup[sid] = self.get_dn_for_objectsid(sid) | |
1166 | # | |
1167 | # print(sids_to_lookup) | |
1168 | # | |
1169 | # for ace in sdec.Dacl.aces: | |
1170 | # if not sids_to_lookup[str(ace.Sid)]: | |
1171 | # print(str(ace.Sid)) | |
1172 | # #print('===== %s =====' % sids_to_lookup[str(ace.Sid)]) | |
1173 | # #if | |
1174 | # #print(str(ace)) | |
1175 | ||
262 | 1176 | #async def get_all_tokengroups(self): |
263 | 1177 | # """ |
264 | 1178 | # returns the tokengroups attribute for all user and machine on the server |
281 | 1195 | # async for entry, err in self._con.response: |
282 | 1196 | # #yield MSADTokenGroup.from_ldap(entry) |
283 | 1197 | # print(str(MSADTokenGroup.from_ldap(entry))) |
284 | ||
285 | async def get_pdcroleowner(self): | |
286 | #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ | |
287 | #get adinfo -> get ridmanagerreference attr -> look up the dn of ridmanagerreference -> get fsmoroleowner attr (which is a DN) | |
288 | if not self._ldapinfo: | |
289 | self.get_ad_info() | |
290 | ||
291 | ldap_filter = r'(distinguishedName=%s)' % self._ldapinfo.rIDManagerReference | |
292 | async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): | |
293 | return entry['attributes']['fSMORoleOwner'] | |
294 | ||
295 | async def get_infrastructureowner(self): | |
296 | #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ | |
297 | #"CN=Infrastructure,DC=concorp,DC=contoso,DC=com" -l fSMORoleOwner | |
298 | if not self._ldapinfo: | |
299 | self.get_ad_info() | |
300 | ||
301 | ldap_filter = r'(distinguishedName=%s)' % ('CN=Infrastructure,' + self._ldapinfo.distinguishedName) | |
302 | async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): | |
303 | return entry['attributes']['fSMORoleOwner'] | |
304 | ||
305 | async def get_ridroleowner(self): | |
306 | #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ | |
307 | if not self._ldapinfo: | |
308 | self.get_ad_info() | |
309 | ||
310 | ldap_filter = r'(distinguishedName=%s)' % ('CN=RID Manager$,CN=System,' + self._ldapinfo.distinguishedName) | |
311 | async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): | |
312 | return entry['attributes']['fSMORoleOwner'] | |
313 | ||
314 | ||
315 | async def get_netdomain(self): | |
316 | def nameconvert(x): | |
317 | return x.split(',CN=')[1] | |
318 | """ | |
319 | gets the name of the current user's domain | |
320 | """ | |
321 | if not self._ldapinfo: | |
322 | self.get_ad_info() | |
323 | print(self._ldapinfo) | |
324 | dname = self._ldapinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') | |
325 | domain_controllers = ','.join(nameconvert(x) + '.' +dname for x in self._ldapinfo.masteredBy) | |
326 | ||
327 | ridroleowner = nameconvert(self.get_ridroleowner()) + '.' +dname | |
328 | infraowner = nameconvert(self.get_infrastructureowner()) + '.' +dname | |
329 | pdcroleowner = nameconvert(self.get_pdcroleowner()) + '.' +dname | |
330 | ||
331 | print('name : %s' % dname) | |
332 | print('Domain Controllers : %s' % domain_controllers) | |
333 | print('DomainModeLevel : %s' % self._ldapinfo.domainmodelevel) | |
334 | print('PdcRoleOwner : %s' % pdcroleowner) | |
335 | print('RidRoleOwner : %s' % ridroleowner) | |
336 | print('InfrastructureRoleOwner : %s' % infraowner) | |
337 | ||
338 | async def get_domaincontroller(self): | |
339 | ldap_filter = r'(userAccountControl:1.2.840.113556.1.4.803:=8192)' | |
340 | async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
341 | print('Forest: %s' % '') | |
342 | print('Name: %s' % entry['attributes'].get('dNSHostName')) | |
343 | print('OSVersion: %s' % entry['attributes'].get('operatingSystem')) | |
344 | print(entry['attributes']) | |
345 | ||
346 | ||
347 | async def get_all_groups(self): | |
348 | ldap_filter = r'(objectClass=group)' | |
349 | async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
350 | yield MSADGroup.from_ldap(entry) | |
351 | ||
352 | async def get_all_ous(self): | |
353 | ldap_filter = r'(objectClass=organizationalUnit)' | |
354 | async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
355 | yield MSADOU.from_ldap(entry) | |
356 | ||
357 | async def get_group_by_dn(self, dn): | |
358 | ldap_filter = r'(&(objectClass=group)(distinguishedName=%s))' % escape_filter_chars(dn) | |
359 | async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
360 | yield MSADGroup.from_ldap(entry) | |
361 | ||
362 | async def get_object_by_dn(self, dn, expected_class = None): | |
363 | ldap_filter = r'(distinguishedName=%s)' % dn | |
364 | async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
365 | temp = entry['attributes'].get('objectClass') | |
366 | if expected_class: | |
367 | yield expected_class.from_ldap(entry) | |
368 | ||
369 | if not temp: | |
370 | yield entry | |
371 | elif 'user' in temp: | |
372 | yield MSADUser.from_ldap(entry) | |
373 | elif 'group' in temp: | |
374 | yield MSADGroup.from_ldap(entry) | |
375 | ||
376 | async def get_user_by_dn(self, dn): | |
377 | ldap_filter = r'(&(objectClass=user)(distinguishedName=%s))' % dn | |
378 | async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
379 | yield MSADUser.from_ldap(entry) | |
380 | ||
381 | async def get_group_members(self, dn, recursive = False): | |
382 | async for group in self.get_group_by_dn(dn): | |
383 | for member in group.member: | |
384 | async for result in self.get_object_by_dn(member): | |
385 | if isinstance(result, MSADGroup) and recursive: | |
386 | async for user in self.get_group_members(result.distinguishedName, recursive = True): | |
387 | yield(user) | |
388 | else: | |
389 | yield(result) | |
390 | ||
391 | async def get_dn_for_objectsid(self, objectsid): | |
392 | ldap_filter = r'(objectSid=%s)' % str(objectsid) | |
393 | async for entry in self.pagedsearch(ldap_filter, ['distinguishedName']): | |
394 | return entry['attributes']['distinguishedName'] | |
395 | ||
396 | async def get_permissions_for_dn(self, dn): | |
397 | """ | |
398 | Lists all users who can modify the specified dn | |
399 | """ | |
400 | async for secinfo in self.get_objectacl_by_dn(dn): | |
401 | for sdec in secinfo.nTSecurityDescriptor: | |
402 | sids_to_lookup = {} | |
403 | if not sdec.Dacl: | |
404 | continue | |
405 | ||
406 | for ace in sdec.Dacl.aces: | |
407 | sids_to_lookup[str(ace.Sid)] = 1 | |
408 | ||
409 | for sid in sids_to_lookup: | |
410 | sids_to_lookup[sid] = self.get_dn_for_objectsid(sid) | |
411 | ||
412 | print(sids_to_lookup) | |
413 | ||
414 | for ace in sdec.Dacl.aces: | |
415 | if not sids_to_lookup[str(ace.Sid)]: | |
416 | print(str(ace.Sid)) | |
417 | #print('===== %s =====' % sids_to_lookup[str(ace.Sid)]) | |
418 | #if | |
419 | #print(str(ace)) | |
420 | ||
421 | ||
422 | ||
423 | async def get_tokengroups(self, dn): | |
424 | """ | |
425 | returns the tokengroups attribute for a given DN | |
426 | """ | |
427 | ldap_filter = query_syntax_converter( r'(distinguishedName=%s)' % escape_filter_chars(dn) ) | |
428 | attributes=[b'tokenGroups'] | |
429 | ||
430 | async for entry, err in self._con.pagedsearch( | |
431 | dn.encode(), | |
432 | ldap_filter, | |
433 | attributes = attributes, | |
434 | paged_size = self.ldap_query_page_size, | |
435 | search_scope=BASE, | |
436 | ): | |
437 | if err is not None: | |
438 | yield None, err | |
439 | break | |
440 | ||
441 | #print(entry['attributes']) | |
442 | if 'tokenGroups' in entry: | |
443 | for sid_data in entry['tokenGroups']: | |
444 | yield sid_data | |
445 | ||
446 | async def get_all_tokengroups(self): | |
447 | """ | |
448 | returns the tokengroups attribute for a given DN | |
449 | """ | |
450 | ldap_filter = r'(|(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))' | |
451 | async for entry in self.pagedsearch( | |
452 | ldap_filter, | |
453 | attributes = ['dn', 'cn', 'objectSid','objectClass', 'objectGUID'] | |
454 | ): | |
455 | ||
456 | if 'objectName' in entry: | |
457 | #print(entry['objectName']) | |
458 | async for entry2, err in self._con.pagedsearch( | |
459 | entry['objectName'].encode(), | |
460 | query_syntax_converter( r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName']) ), | |
461 | attributes = [b'tokenGroups'], | |
462 | paged_size = self.ldap_query_page_size, | |
463 | search_scope=BASE, | |
464 | ): | |
465 | ||
466 | #print(entry2) | |
467 | if err is not None: | |
468 | yield None, err | |
469 | break | |
470 | if 'tokenGroups' in entry2['attributes']: | |
471 | for token in entry2['attributes']['tokenGroups']: | |
472 | yield { | |
473 | 'cn' : entry['attributes']['cn'], | |
474 | 'dn' : entry['objectName'], | |
475 | 'guid' : entry['attributes']['objectGUID'], | |
476 | 'sid' : entry['attributes']['objectSid'], | |
477 | 'type' : entry['attributes']['objectClass'][-1], | |
478 | 'token' : token | |
479 | ||
480 | } | |
481 | ||
482 | async def get_all_objectacl(self): | |
483 | """ | |
484 | bbbbbb | |
485 | """ | |
486 | ||
487 | flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION | |
488 | req_flags = SDFlagsRequestValue({'Flags' : flags_value}) | |
489 | ||
490 | ldap_filter = r'(|(objectClass=organizationalUnit)(objectCategory=groupPolicyContainer)(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))' | |
491 | async for entry in self.pagedsearch(ldap_filter, attributes = ['dn']): | |
492 | ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName']) | |
493 | attributes = MSADSecurityInfo.ATTRS | |
494 | controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] | |
495 | ||
496 | async for entry2 in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
497 | yield MSADSecurityInfo.from_ldap(entry2) | |
498 | ||
499 | ||
500 | async def get_all_trusts(self): | |
501 | ldap_filter = r'(objectClass=trustedDomain)' | |
502 | async for entry in self.pagedsearch(ldap_filter, attributes = MSADDomainTrust_ATTRS): | |
503 | yield MSADDomainTrust.from_ldap(entry)⏎ | |
1198 | ||
1199 | ||
1200 | #async def get_all_objectacl(self): | |
1201 | # """ | |
1202 | # Returns all ACL info for all AD objects | |
1203 | # """ | |
1204 | # | |
1205 | # flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION | |
1206 | # req_flags = SDFlagsRequestValue({'Flags' : flags_value}) | |
1207 | # | |
1208 | # ldap_filter = r'(objectClass=*)' | |
1209 | # attributes = MSADSecurityInfo.ATTRS | |
1210 | # controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] | |
1211 | # | |
1212 | # async for entry in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
1213 | # yield MSADSecurityInfo.from_ldap(entry) | |
1214 | ||
1215 | ||
1216 | #async def get_netdomain(self): | |
1217 | # def nameconvert(x): | |
1218 | # return x.split(',CN=')[1] | |
1219 | # """ | |
1220 | # gets the name of the current user's domain | |
1221 | # """ | |
1222 | # if not self._ldapinfo: | |
1223 | # self.get_ad_info() | |
1224 | # print(self._ldapinfo) | |
1225 | # dname = self._ldapinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') | |
1226 | # domain_controllers = ','.join(nameconvert(x) + '.' +dname for x in self._ldapinfo.masteredBy) | |
1227 | # | |
1228 | # ridroleowner = nameconvert(self.get_ridroleowner()) + '.' +dname | |
1229 | # infraowner = nameconvert(self.get_infrastructureowner()) + '.' +dname | |
1230 | # pdcroleowner = nameconvert(self.get_pdcroleowner()) + '.' +dname | |
1231 | # | |
1232 | # print('name : %s' % dname) | |
1233 | # print('Domain Controllers : %s' % domain_controllers) | |
1234 | # print('DomainModeLevel : %s' % self._ldapinfo.domainmodelevel) | |
1235 | # print('PdcRoleOwner : %s' % pdcroleowner) | |
1236 | # print('RidRoleOwner : %s' % ridroleowner) | |
1237 | # print('InfrastructureRoleOwner : %s' % infraowner) | |
1238 | # | |
1239 | #async def get_domaincontroller(self): | |
1240 | # ldap_filter = r'(userAccountControl:1.2.840.113556.1.4.803:=8192)' | |
1241 | # async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): | |
1242 | # print('Forest: %s' % '') | |
1243 | # print('Name: %s' % entry['attributes'].get('dNSHostName')) | |
1244 | # print('OSVersion: %s' % entry['attributes'].get('operatingSystem')) | |
1245 | # print(entry['attributes']) | |
1246 | ||
1247 | #async def get_pdcroleowner(self): | |
1248 | # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ | |
1249 | # #get adinfo -> get ridmanagerreference attr -> look up the dn of ridmanagerreference -> get fsmoroleowner attr (which is a DN) | |
1250 | # if not self._ldapinfo: | |
1251 | # self.get_ad_info() | |
1252 | # | |
1253 | # ldap_filter = r'(distinguishedName=%s)' % self._ldapinfo.rIDManagerReference | |
1254 | # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): | |
1255 | # return entry['attributes']['fSMORoleOwner'] | |
1256 | # | |
1257 | #async def get_infrastructureowner(self): | |
1258 | # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ | |
1259 | # #"CN=Infrastructure,DC=concorp,DC=contoso,DC=com" -l fSMORoleOwner | |
1260 | # if not self._ldapinfo: | |
1261 | # self.get_ad_info() | |
1262 | # | |
1263 | # ldap_filter = r'(distinguishedName=%s)' % ('CN=Infrastructure,' + self._ldapinfo.distinguishedName) | |
1264 | # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): | |
1265 | # return entry['attributes']['fSMORoleOwner'] | |
1266 | # | |
1267 | #async def get_ridroleowner(self): | |
1268 | # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ | |
1269 | # if not self._ldapinfo: | |
1270 | # self.get_ad_info() | |
1271 | # | |
1272 | # ldap_filter = r'(distinguishedName=%s)' % ('CN=RID Manager$,CN=System,' + self._ldapinfo.distinguishedName) | |
1273 | # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): | |
1274 | # return entry['attributes']['fSMORoleOwner'] | |
1275 | ||
1276 | #async def get_all_user_raw(self): | |
1277 | # """ | |
1278 | # Fetches all user objects from the AD, and returns MSADUser object | |
1279 | # """ | |
1280 | # logger.debug('Polling AD for all user objects') | |
1281 | # ldap_filter = r'(sAMAccountType=805306368)' | |
1282 | # | |
1283 | # return self.pagedsearch(ldap_filter, MSADUser_ATTRS) |
5 | 5 | from msldap.authentication.spnego.native import SPNEGO |
6 | 6 | from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings |
7 | 7 | from msldap.authentication.kerberos.native import MSLDAPKerberos |
8 | from msldap.commons.proxy import MSLDAPProxyType | |
8 | 9 | from minikerberos.common.target import KerberosTarget |
9 | 10 | from minikerberos.common.proxy import KerberosProxy |
10 | 11 | from minikerberos.common.creds import KerberosCredential |
26 | 27 | self.is_guest = False |
27 | 28 | self.nt_hash = None |
28 | 29 | self.lm_hash = None |
30 | self.encrypt = False | |
29 | 31 | |
30 | 32 | class MSLDAPSIMPLECredential: |
31 | 33 | def __init__(self): |
45 | 47 | self.target = None #KerberosTarget |
46 | 48 | self.ksoc = None #KerberosSocketAIO |
47 | 49 | self.ccred = None |
50 | self.encrypt = False | |
51 | self.enctypes = None #[23,17,18] | |
48 | 52 | |
49 | 53 | class MSLDAPKerberosSSPICredential: |
50 | 54 | def __init__(self): |
51 | self.client = None | |
55 | self.domain = None | |
52 | 56 | self.password = None |
53 | self.target = None | |
57 | self.username = None | |
58 | self.encrypt = False | |
54 | 59 | |
55 | 60 | class MSLDAPNTLMSSPICredential: |
56 | 61 | def __init__(self): |
57 | self.client = None | |
58 | self.passwrd = None | |
62 | self.username = None | |
63 | self.password = None | |
64 | self.domain = None | |
65 | self.encrypt = False | |
66 | ||
67 | class MSLDAPWSNETCredential: | |
68 | def __init__(self): | |
69 | self.type = 'NTLM' | |
70 | self.username = '<CURRENT>' | |
71 | self.domain = '<CURRENT>' | |
72 | self.password = '<CURRENT>' | |
73 | self.target = None | |
74 | self.is_guest = False | |
75 | self.agent_id = None | |
76 | self.encrypt = False | |
77 | ||
78 | class MSLDAPSSPIProxyCredential: | |
79 | def __init__(self): | |
80 | self.type = 'NTLM' | |
81 | self.username = '<CURRENT>' | |
82 | self.domain = '<CURRENT>' | |
83 | self.password = '<CURRENT>' | |
84 | self.target = None | |
85 | self.is_guest = False | |
86 | self.agent_id = None | |
87 | self.encrypt = False | |
88 | self.host = '127.0.0.1' | |
89 | self.port = 9999 | |
90 | self.proto = 'ws' | |
91 | ||
92 | ||
93 | ||
94 | class MSLDAPMultiplexorCredential: | |
95 | def __init__(self): | |
96 | self.type = 'NTLM' | |
97 | self.username = '<CURRENT>' | |
98 | self.domain = '<CURRENT>' | |
99 | self.password = '<CURRENT>' | |
100 | self.target = None | |
101 | self.is_guest = False | |
102 | self.is_ssl = False | |
103 | self.mp_host = '127.0.0.1' | |
104 | self.mp_port = 9999 | |
105 | self.mp_username = None | |
106 | self.mp_domain = None | |
107 | self.mp_password = None | |
108 | self.agent_id = None | |
109 | self.encrypt = False | |
110 | ||
111 | def get_url(self): | |
112 | url_temp = 'ws://%s:%s' | |
113 | if self.is_ssl is True: | |
114 | url_temp = 'wss://%s:%s' | |
115 | url = url_temp % (self.mp_host, self.mp_port) | |
116 | return url | |
117 | ||
118 | def parse_settings(self, settings): | |
119 | req = ['agentid'] | |
120 | for r in req: | |
121 | if r not in settings: | |
122 | raise Exception('%s parameter missing' % r) | |
123 | self.mp_host = settings.get('host', ['127.0.0.1'])[0] | |
124 | self.mp_port = settings.get('port', ['9999'])[0] | |
125 | if self.mp_port is None: | |
126 | self.mp_port = '9999' | |
127 | if 'user' in settings: | |
128 | self.mp_username = settings.get('user')[0] | |
129 | if 'domain' in settings: | |
130 | self.mp_domain = settings.get('domain')[0] | |
131 | if 'password' in settings: | |
132 | self.mp_password = settings.get('password')[0] | |
133 | self.agent_id = settings['agentid'][0] | |
59 | 134 | |
60 | 135 | |
61 | 136 | |
81 | 156 | ntlmcred.domain = self.creds.domain if self.creds.domain is not None else '' |
82 | 157 | ntlmcred.workstation = None |
83 | 158 | ntlmcred.is_guest = False |
159 | ntlmcred.encrypt = self.creds.encrypt | |
160 | ||
84 | 161 | |
85 | 162 | if self.creds.password is None: |
86 | raise Exception('NTLM authentication requres password!') | |
87 | ntlmcred.password = self.creds.password | |
163 | raise Exception('NTLM authentication requres password/NT hash!') | |
164 | ||
165 | ||
166 | if len(self.creds.password) == 32: | |
167 | try: | |
168 | bytes.fromhex(self.creds.password) | |
169 | except: | |
170 | ntlmcred.password = self.creds.password | |
171 | else: | |
172 | ntlmcred.nt_hash = self.creds.password | |
173 | ||
174 | else: | |
175 | ntlmcred.password = self.creds.password | |
88 | 176 | |
89 | 177 | settings = NTLMHandlerSettings(ntlmcred) |
90 | 178 | return NTLMAUTHHandler(settings) |
109 | 197 | ntlmcred.domain = self.creds.domain if self.creds.domain is not None else '' |
110 | 198 | ntlmcred.workstation = None |
111 | 199 | ntlmcred.is_guest = False |
200 | ntlmcred.encrypt = self.creds.encrypt | |
112 | 201 | |
113 | 202 | if self.creds.password is None: |
114 | 203 | raise Exception('NTLM authentication requres password!') |
135 | 224 | LDAPAuthProtocol.KERBEROS_AES, |
136 | 225 | LDAPAuthProtocol.KERBEROS_PASSWORD, |
137 | 226 | LDAPAuthProtocol.KERBEROS_CCACHE, |
138 | LDAPAuthProtocol.KERBEROS_KEYTAB]: | |
227 | LDAPAuthProtocol.KERBEROS_KEYTAB, | |
228 | LDAPAuthProtocol.KERBEROS_KIRBI]: | |
139 | 229 | |
140 | 230 | if self.target is None: |
141 | 231 | raise Exception('Target must be specified with Kerberos!') |
146 | 236 | if self.target.dc_ip is None: |
147 | 237 | raise Exception('target must have a dc_ip for kerberos!') |
148 | 238 | |
149 | ||
150 | kc = KerberosCredential() | |
151 | kc.username = self.creds.username | |
152 | kc.domain = self.creds.domain | |
239 | kcred = MSLDAPKerberosCredential() | |
240 | if self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KIRBI: | |
241 | kc = KerberosCredential.from_kirbi(self.creds.password, self.creds.username, self.creds.domain) | |
242 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_CCACHE: | |
243 | kc = KerberosCredential.from_ccache_file(self.creds.password, self.creds.username, self.creds.domain) | |
244 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KEYTAB: | |
245 | kc = KerberosCredential.from_kirbi(self.creds.password, self.creds.username, self.creds.domain) | |
246 | else: | |
247 | kc = KerberosCredential() | |
248 | kc.username = self.creds.username | |
249 | kc.domain = self.creds.domain | |
250 | kcred.enctypes = [] | |
153 | 251 | if self.creds.auth_method == LDAPAuthProtocol.KERBEROS_PASSWORD: |
154 | 252 | kc.password = self.creds.password |
253 | kcred.enctypes = [23,17,18] | |
155 | 254 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_NT: |
156 | 255 | kc.nt_hash = self.creds.password |
256 | kcred.enctypes = [23] | |
157 | 257 | |
158 | 258 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_AES: |
159 | 259 | if len(self.creds.password) == 32: |
160 | 260 | kc.kerberos_key_aes_128 = self.creds.password |
261 | kcred.enctypes = [17] | |
161 | 262 | elif len(self.creds.password) == 64: |
162 | 263 | kc.kerberos_key_aes_256 = self.creds.password |
264 | kcred.enctypes = [18] | |
163 | 265 | |
164 | 266 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_RC4: |
165 | 267 | kc.kerberos_key_rc4 = self.creds.password |
268 | kcred.enctypes = [23] | |
166 | 269 | |
167 | 270 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_CCACHE: |
168 | 271 | kc.ccache = self.creds.password |
272 | kcred.enctypes = [23,17,18] # TODO: fix this | |
169 | 273 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KEYTAB: |
170 | 274 | kc.keytab = self.creds.password |
275 | kcred.enctypes = [23,17,18] # TODO: fix this | |
276 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KIRBI: | |
277 | kcred.enctypes = [23,17,18] # TODO: fix this | |
171 | 278 | else: |
172 | 279 | raise Exception('No suitable secret type found to set up kerberos!') |
173 | ||
174 | ||
175 | kcred = MSLDAPKerberosCredential() | |
280 | ||
281 | if self.creds.etypes is not None: | |
282 | kcred.enctypes = list(set(self.creds.etypes).intersection(set(kcred.enctypes))) | |
283 | ||
176 | 284 | kcred.ccred = kc |
177 | 285 | kcred.spn = KerberosSPN.from_target_string(self.target.to_target_string()) |
178 | 286 | kcred.target = KerberosTarget(self.target.dc_ip) |
287 | kcred.encrypt = self.creds.encrypt | |
288 | ||
179 | 289 | if self.target.proxy is not None: |
180 | kcred.target.proxy = KerberosProxy() | |
181 | kcred.target.proxy.target = copy.deepcopy(self.target.proxy.target) | |
182 | kcred.target.proxy.target.endpoint_ip = self.target.dc_ip | |
183 | kcred.target.proxy.target.endpoint_port = 88 | |
184 | kcred.target.proxy.creds = copy.deepcopy(self.target.proxy.auth) | |
290 | kcred.target.proxy = KerberosProxy() | |
291 | kcred.target.proxy.type = self.target.proxy.type | |
292 | kcred.target.proxy.target = copy.deepcopy(self.target.proxy.target) | |
293 | kcred.target.proxy.target[-1].endpoint_ip = self.target.dc_ip | |
294 | kcred.target.proxy.target[-1].endpoint_port = 88 | |
185 | 295 | |
186 | 296 | handler = MSLDAPKerberos(kcred) |
187 | 297 | |
195 | 305 | raise Exception('Target must be specified with Kerberos SSPI!') |
196 | 306 | |
197 | 307 | kerbcred = MSLDAPKerberosSSPICredential() |
198 | kerbcred.client = None #creds.username #here we could submit the domain as well for impersonation? TODO! | |
199 | kerbcred.password = self.creds.password | |
200 | kerbcred.target = self.target.to_target_string() | |
308 | kerbcred.username = self.creds.domain if self.creds.domain is not None else '<CURRENT>' | |
309 | kerbcred.username = self.creds.username if self.creds.username is not None else '<CURRENT>' | |
310 | kerbcred.password = self.creds.password if self.creds.password is not None else '<CURRENT>' | |
311 | kerbcred.spn = self.target.to_target_string() | |
312 | kerbcred.encrypt = self.creds.encrypt | |
201 | 313 | |
202 | 314 | handler = MSLDAPKerberosSSPI(kerbcred) |
203 | 315 | #setting up SPNEGO |
207 | 319 | |
208 | 320 | elif self.creds.auth_method == LDAPAuthProtocol.SSPI_NTLM: |
209 | 321 | ntlmcred = MSLDAPNTLMSSPICredential() |
210 | ntlmcred.client = self.creds.username #here we could submit the domain as well for impersonation? TODO! | |
211 | ntlmcred.password = self.creds.password | |
212 | ||
322 | ntlmcred.username = self.creds.domain if self.creds.domain is not None else '<CURRENT>' | |
323 | ntlmcred.username = self.creds.username if self.creds.username is not None else '<CURRENT>' | |
324 | ntlmcred.password = self.creds.password if self.creds.password is not None else '<CURRENT>' | |
325 | ntlmcred.encrypt = self.creds.encrypt | |
326 | ||
213 | 327 | handler = MSLDAPNTLMSSPI(ntlmcred) |
214 | 328 | #setting up SPNEGO |
215 | 329 | spneg = SPNEGO() |
216 | 330 | spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) |
217 | 331 | return spneg |
218 | 332 | |
219 | ||
220 | """ | |
221 | elif creds.authentication_type.value.startswith('MULTIPLEXOR'): | |
222 | if creds.authentication_type in [SMBAuthProtocol.MULTIPLEXOR_SSL_NTLM, SMBAuthProtocol.MULTIPLEXOR_NTLM]: | |
223 | from aiosmb.authentication.ntlm.multiplexor import SMBNTLMMultiplexor | |
224 | ||
225 | ntlmcred = SMBMultiplexorCredential() | |
333 | elif self.creds.auth_method.value.startswith('MULTIPLEXOR'): | |
334 | if self.creds.auth_method in [LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM, LDAPAuthProtocol.MULTIPLEXOR_NTLM]: | |
335 | from msldap.authentication.ntlm.multiplexor import MSLDAPNTLMMultiplexor | |
336 | ntlmcred = MSLDAPMultiplexorCredential() | |
226 | 337 | ntlmcred.type = 'NTLM' |
227 | if creds.username is not None: | |
228 | ntlmcred.username = '<CURRENT>' | |
229 | if creds.domain is not None: | |
230 | ntlmcred.domain = '<CURRENT>' | |
231 | if creds.secret is not None: | |
232 | ntlmcred.password = '<CURRENT>' | |
233 | ntlmcred.is_guest = False | |
234 | ntlmcred.is_ssl = True if creds.authentication_type == SMBAuthProtocol.MULTIPLEXOR_SSL_NTLM else False | |
235 | ntlmcred.parse_settings(creds.settings) | |
236 | ||
237 | handler = SMBNTLMMultiplexor(ntlmcred) | |
338 | if self.creds.username is not None: | |
339 | ntlmcred.username = '<CURRENT>' | |
340 | if self.creds.domain is not None: | |
341 | ntlmcred.domain = '<CURRENT>' | |
342 | if self.creds.password is not None: | |
343 | ntlmcred.password = '<CURRENT>' | |
344 | ntlmcred.is_guest = False | |
345 | ntlmcred.is_ssl = True if self.creds.auth_method == LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM else False | |
346 | ntlmcred.parse_settings(self.creds.settings) | |
347 | ntlmcred.encrypt = self.creds.encrypt | |
348 | ||
349 | handler = MSLDAPNTLMMultiplexor(ntlmcred) | |
238 | 350 | #setting up SPNEGO |
239 | 351 | spneg = SPNEGO() |
240 | 352 | spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) |
241 | 353 | return spneg |
242 | 354 | |
243 | elif creds.authentication_type in [SMBAuthProtocol.MULTIPLEXOR_SSL_KERBEROS, SMBAuthProtocol.MULTIPLEXOR_KERBEROS]: | |
244 | from aiosmb.authentication.kerberos.multiplexor import SMBKerberosMultiplexor | |
245 | ||
246 | ntlmcred = SMBMultiplexorCredential() | |
355 | elif self.creds.auth_method in [LDAPAuthProtocol.MULTIPLEXOR_SSL_KERBEROS, LDAPAuthProtocol.MULTIPLEXOR_KERBEROS]: | |
356 | from msldap.authentication.kerberos.multiplexor import MSLDAPKerberosMultiplexor | |
357 | ||
358 | ntlmcred = MSLDAPMultiplexorCredential() | |
247 | 359 | ntlmcred.type = 'KERBEROS' |
248 | ntlmcred.target = creds.target | |
249 | if creds.username is not None: | |
250 | ntlmcred.username = '<CURRENT>' | |
251 | if creds.domain is not None: | |
252 | ntlmcred.domain = '<CURRENT>' | |
253 | if creds.secret is not None: | |
254 | ntlmcred.password = '<CURRENT>' | |
255 | ntlmcred.is_guest = False | |
256 | ntlmcred.is_ssl = True if creds.authentication_type == SMBAuthProtocol.MULTIPLEXOR_SSL_NTLM else False | |
257 | ntlmcred.parse_settings(creds.settings) | |
258 | ||
259 | handler = SMBKerberosMultiplexor(ntlmcred) | |
360 | ntlmcred.target = self.target | |
361 | if self.creds.username is not None: | |
362 | ntlmcred.username = '<CURRENT>' | |
363 | if self.creds.domain is not None: | |
364 | ntlmcred.domain = '<CURRENT>' | |
365 | if self.creds.password is not None: | |
366 | ntlmcred.password = '<CURRENT>' | |
367 | ntlmcred.is_guest = False | |
368 | ntlmcred.is_ssl = True if self.creds.auth_method == LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM else False | |
369 | ntlmcred.parse_settings(self.creds.settings) | |
370 | ntlmcred.encrypt = self.creds.encrypt | |
371 | ||
372 | handler = MSLDAPKerberosMultiplexor(ntlmcred) | |
260 | 373 | #setting up SPNEGO |
261 | 374 | spneg = SPNEGO() |
262 | 375 | spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler) |
263 | 376 | return spneg |
264 | """ | |
377 | ||
378 | elif self.creds.auth_method.value.startswith('SSPIPROXY'): | |
379 | if self.creds.auth_method == LDAPAuthProtocol.SSPIPROXY_NTLM: | |
380 | from msldap.authentication.ntlm.sspiproxy import MSLDAPSSPIProxyNTLMAuth | |
381 | ntlmcred = MSLDAPSSPIProxyCredential() | |
382 | ntlmcred.type = 'NTLM' | |
383 | if self.creds.username is not None: | |
384 | ntlmcred.username = '<CURRENT>' | |
385 | if self.creds.domain is not None: | |
386 | ntlmcred.domain = '<CURRENT>' | |
387 | if self.creds.password is not None: | |
388 | ntlmcred.password = '<CURRENT>' | |
389 | ntlmcred.is_guest = False | |
390 | ntlmcred.encrypt = self.creds.encrypt | |
391 | ntlmcred.host = self.creds.settings['host'][0] | |
392 | ntlmcred.port = int(self.creds.settings['port'][0]) | |
393 | ntlmcred.proto = 'ws' | |
394 | if 'proto' in self.creds.settings: | |
395 | ntlmcred.proto = self.creds.settings['proto'][0] | |
396 | if 'agentid' in self.creds.settings: | |
397 | ntlmcred.agent_id = bytes.fromhex(self.creds.settings['agentid'][0]) | |
398 | ||
399 | handler = MSLDAPSSPIProxyNTLMAuth(ntlmcred) | |
400 | #setting up SPNEGO | |
401 | spneg = SPNEGO() | |
402 | spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) | |
403 | return spneg | |
404 | ||
405 | elif self.creds.auth_method == LDAPAuthProtocol.SSPIPROXY_KERBEROS: | |
406 | from msldap.authentication.kerberos.sspiproxyws import MSLDAPSSPIProxyKerberosAuth | |
407 | ||
408 | ntlmcred = MSLDAPSSPIProxyCredential() | |
409 | ntlmcred.type = 'KERBEROS' | |
410 | ntlmcred.target = self.target | |
411 | if self.creds.username is not None: | |
412 | ntlmcred.username = '<CURRENT>' | |
413 | if self.creds.domain is not None: | |
414 | ntlmcred.domain = '<CURRENT>' | |
415 | if self.creds.password is not None: | |
416 | ntlmcred.password = '<CURRENT>' | |
417 | ntlmcred.is_guest = False | |
418 | ntlmcred.encrypt = self.creds.encrypt | |
419 | ntlmcred.host = self.creds.settings['host'][0] | |
420 | ntlmcred.port = self.creds.settings['port'][0] | |
421 | ntlmcred.proto = 'ws' | |
422 | if 'proto' in self.creds.settings: | |
423 | ntlmcred.proto = self.creds.settings['proto'][0] | |
424 | if 'agentid' in self.creds.settings: | |
425 | ntlmcred.agent_id = bytes.fromhex(self.creds.settings['agentid'][0]) | |
426 | ||
427 | handler = MSLDAPSSPIProxyKerberosAuth(ntlmcred) | |
428 | #setting up SPNEGO | |
429 | spneg = SPNEGO() | |
430 | spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler) | |
431 | return spneg | |
432 | ||
433 | elif self.creds.auth_method.value.startswith('WSNET'): | |
434 | if self.creds.auth_method in [LDAPAuthProtocol.WSNET_NTLM]: | |
435 | from msldap.authentication.ntlm.wsnet import MSLDAPWSNetNTLMAuth | |
436 | ||
437 | ntlmcred = MSLDAPWSNETCredential() | |
438 | ntlmcred.type = 'NTLM' | |
439 | if self.creds.username is not None: | |
440 | ntlmcred.username = '<CURRENT>' | |
441 | if self.creds.domain is not None: | |
442 | ntlmcred.domain = '<CURRENT>' | |
443 | if self.creds.password is not None: | |
444 | ntlmcred.password = '<CURRENT>' | |
445 | ntlmcred.is_guest = False | |
446 | ||
447 | handler = MSLDAPWSNetNTLMAuth(ntlmcred) | |
448 | spneg = SPNEGO() | |
449 | spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) | |
450 | return spneg | |
451 | ||
452 | ||
453 | elif self.creds.auth_method in [LDAPAuthProtocol.WSNET_KERBEROS]: | |
454 | from msldap.authentication.kerberos.wsnet import MSLDAPWSNetKerberosAuth | |
455 | ||
456 | ntlmcred = MSLDAPWSNETCredential() | |
457 | ntlmcred.type = 'KERBEROS' | |
458 | ntlmcred.target = self.target | |
459 | if self.creds.username is not None: | |
460 | ntlmcred.username = '<CURRENT>' | |
461 | if self.creds.domain is not None: | |
462 | ntlmcred.domain = '<CURRENT>' | |
463 | if self.creds.password is not None: | |
464 | ntlmcred.password = '<CURRENT>' | |
465 | ntlmcred.is_guest = False | |
466 | ||
467 | handler = MSLDAPWSNetKerberosAuth(ntlmcred) | |
468 | #setting up SPNEGO | |
469 | spneg = SPNEGO() | |
470 | spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler) | |
471 | return spneg⏎ |
0 | import enum | |
1 | ||
2 | class MSLDAPClientStatus(enum.Enum): | |
3 | RUNNING = 'RUNNING' | |
4 | STOPPED = 'STOPPED' | |
5 | ERROR = 'ERROR' |
28 | 28 | SICILY = 'SICILY' #NTLM (old proprietary from MS) |
29 | 29 | NTLM_PASSWORD = 'NTLM_PASSWORD' #actually SASL-GSSAPI-SPNEGO-NTLM |
30 | 30 | NTLM_NT = 'NTLM_NT' #actually SASL-GSSAPI-SPNEGO-NTLM |
31 | KERBEROS_RC4 = 'KERBEROS_RC4' #actually SASL-GSSAPI-SPNEGO-KERBEROS | |
32 | KERBEROS_NT = 'KERBEROS_NT' #actually SASL-GSSAPI-SPNEGO-KERBEROS | |
33 | KERBEROS_AES = 'KERBEROS_AES' #actually SASL-GSSAPI-SPNEGO-KERBEROS | |
34 | KERBEROS_PASSWORD = 'KERBEROS_PASSWORD' #actually SASL-GSSAPI-SPNEGO-KERBEROS | |
35 | KERBEROS_CCACHE = 'KERBEROS_CCACHE' #actually SASL-GSSAPI-SPNEGO-KERBEROS | |
36 | KERBEROS_KEYTAB = 'KERBEROS_KEYTAB' #actually SASL-GSSAPI-SPNEGO-KERBEROS | |
37 | MULTIPLEXOR = 'MULTIPLEXOR' | |
38 | MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL' | |
31 | KERBEROS_RC4 = 'KERBEROS_RC4' | |
32 | KERBEROS_NT = 'KERBEROS_NT' | |
33 | KERBEROS_AES = 'KERBEROS_AES' | |
34 | KERBEROS_PASSWORD = 'KERBEROS_PASSWORD' | |
35 | KERBEROS_CCACHE = 'KERBEROS_CCACHE' | |
36 | KERBEROS_KEYTAB = 'KERBEROS_KEYTAB' | |
37 | KERBEROS_KIRBI = 'KERBEROS_KIRBI' | |
38 | MULTIPLEXOR_KERBEROS = 'MULTIPLEXOR_KERBEROS' | |
39 | MULTIPLEXOR_NTLM = 'MULTIPLEXOR_NTLM' | |
40 | MULTIPLEXOR_SSL_KERBEROS = 'MULTIPLEXOR_SSL_KERBEROS' | |
41 | MULTIPLEXOR_SSL_NTLM = 'MULTIPLEXOR_SSL_NTLM' | |
39 | 42 | SSPI_NTLM = 'SSPI_NTLM' #actually SASL-GSSAPI-SPNEGO-NTLM but with integrated SSPI |
40 | 43 | SSPI_KERBEROS = 'SSPI_KERBEROS' #actually SASL-GSSAPI-SPNEGO-KERBEROS but with integrated SSPI |
44 | WSNET_NTLM = 'WSNET_NTLM' | |
45 | WSNET_KERBEROS = 'WSNET_KERBEROS' | |
46 | SSPIPROXY_NTLM = 'SSPIPROXY_NTLM' | |
47 | SSPIPROXY_KERBEROS = 'SSPIPROXY_KERBEROS' | |
41 | 48 | |
42 | 49 | MSLDAP_GSS_METHODS = [ |
43 | 50 | LDAPAuthProtocol.NTLM_PASSWORD , |
48 | 55 | LDAPAuthProtocol.KERBEROS_PASSWORD , |
49 | 56 | LDAPAuthProtocol.KERBEROS_CCACHE , |
50 | 57 | LDAPAuthProtocol.KERBEROS_KEYTAB , |
58 | LDAPAuthProtocol.KERBEROS_KIRBI , | |
51 | 59 | LDAPAuthProtocol.SSPI_NTLM , |
52 | 60 | LDAPAuthProtocol.SSPI_KERBEROS, |
53 | ||
61 | LDAPAuthProtocol.MULTIPLEXOR_KERBEROS, | |
62 | LDAPAuthProtocol.MULTIPLEXOR_NTLM, | |
63 | LDAPAuthProtocol.MULTIPLEXOR_SSL_KERBEROS, | |
64 | LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM, | |
65 | LDAPAuthProtocol.WSNET_NTLM, | |
66 | LDAPAuthProtocol.WSNET_KERBEROS, | |
67 | LDAPAuthProtocol.SSPIPROXY_NTLM, | |
68 | LDAPAuthProtocol.SSPIPROXY_KERBEROS, | |
69 | ] | |
70 | ||
71 | MSLDAP_KERBEROS_PROTOCOLS = [ | |
72 | LDAPAuthProtocol.KERBEROS_RC4 , | |
73 | LDAPAuthProtocol.KERBEROS_NT , | |
74 | LDAPAuthProtocol.KERBEROS_AES , | |
75 | LDAPAuthProtocol.KERBEROS_PASSWORD , | |
76 | LDAPAuthProtocol.KERBEROS_CCACHE , | |
77 | LDAPAuthProtocol.KERBEROS_KEYTAB , | |
78 | LDAPAuthProtocol.KERBEROS_KIRBI , | |
54 | 79 | ] |
55 | 80 | |
56 | 81 | class MSLDAPCredential: |
57 | def __init__(self, domain=None, username= None, password = None, auth_method = None, settings = None): | |
82 | """ | |
83 | Describes the user's credentials to be used for authentication during the bind operation. | |
84 | ||
85 | :param domain: Domain of the user | |
86 | :type domain: str | |
87 | :param username: Username of the user | |
88 | :type username: str | |
89 | :param password: The authentication secret. The actual contents depend on the `auth_method` | |
90 | :type password: str | |
91 | :param auth_method: The ahtentication method to be performed during bind operation | |
92 | :type auth_method: :class:`LDAPAuthProtocol` | |
93 | :param settings: Additional settings | |
94 | :type settings: dict | |
95 | :param etypes: Supported encryption types for Kerberos authentication. | |
96 | :type etypes: List[:class:`int`] | |
97 | :param encrypt: Use protocol-level encryption. Doesnt work on LDAPS | |
98 | :type encrypt: bool | |
99 | """ | |
100 | def __init__(self, domain=None, username= None, password = None, auth_method = None, settings = None, etypes = None, encrypt = False): | |
58 | 101 | self.auth_method = auth_method |
59 | 102 | self.domain = domain |
60 | 103 | self.username = username |
61 | 104 | self.password = password |
105 | self.signing_preferred = False | |
106 | self.encryption_preferred = False | |
62 | 107 | self.settings = settings |
108 | self.etypes = etypes | |
109 | self.encrypt = encrypt | |
63 | 110 | |
64 | 111 | def get_msuser(self): |
65 | 112 | if not self.domain: |
0 | ||
1 | from msldap.protocol.messages import resultCode | |
2 | ||
3 | ||
4 | LDAPResultCodeLookup ={ | |
5 | 0 : 'success', | |
6 | 1 : 'operationsError', | |
7 | 2 : 'protocolError', | |
8 | 3 : 'timeLimitExceeded', | |
9 | 4 : 'sizeLimitExceeded', | |
10 | 5 : 'compareFalse', | |
11 | 6 : 'compareTrue', | |
12 | 7 : 'authMethodNotSupported', | |
13 | 8 : 'strongerAuthRequired', | |
14 | 10 : 'referral', | |
15 | 11 : 'adminLimitExceeded', | |
16 | 12 : 'unavailableCriticalExtension', | |
17 | 13 : 'confidentialityRequired', | |
18 | 14 : 'saslBindInProgress', | |
19 | 16 : 'noSuchAttribute', | |
20 | 17 : 'undefinedAttributeType', | |
21 | 18 : 'inappropriateMatching', | |
22 | 19 : 'constraintViolation', | |
23 | 20 : 'attributeOrValueExists', | |
24 | 21 : 'invalidAttributeSyntax', | |
25 | 32 : 'noSuchObject', | |
26 | 33 : 'aliasProblem', | |
27 | 34 : 'invalidDNSyntax', | |
28 | 36 : 'aliasDereferencingProblem', | |
29 | 48 : 'inappropriateAuthentication', | |
30 | 49 : 'invalidCredentials', | |
31 | 50 : 'insufficientAccessRights', | |
32 | 51 : 'busy', | |
33 | 52 : 'unavailable', | |
34 | 53 : 'unwillingToPerform', | |
35 | 54 : 'loopDetect', | |
36 | 64 : 'namingViolation', | |
37 | 65 : 'objectClassViolation', | |
38 | 66 : 'notAllowedOnNonLeaf', | |
39 | 67 : 'notAllowedOnRDN', | |
40 | 68 : 'entryAlreadyExists', | |
41 | 69 : 'objectClassModsProhibited', | |
42 | 71 : 'affectsMultipleDSAs', | |
43 | 80 : 'other', | |
44 | } | |
45 | LDAPResultCodeLookup_inv = {v: k for k, v in LDAPResultCodeLookup.items()} | |
46 | ||
47 | class LDAPServerException(Exception): | |
48 | def __init__(self, resultname, diagnostic_message, message = None): | |
49 | self.resultcode = LDAPResultCodeLookup_inv[resultname] | |
50 | self.resultname = resultname | |
51 | self.diagnostic_message = diagnostic_message | |
52 | self.message = message | |
53 | if self.message is None: | |
54 | self.message = 'LDAP server sent error! Result code: "%s" Reason: "%s"' % (self.resultcode, self.diagnostic_message) | |
55 | super().__init__(self.message) | |
56 | ||
57 | class LDAPBindException(LDAPServerException): | |
58 | def __init__(self, resultcode, diagnostic_message): | |
59 | message = 'LDAP Bind failed! Result code: "%s" Reason: "%s"' % (resultcode, diagnostic_message) | |
60 | super().__init__(resultcode, diagnostic_message, message) | |
61 | ||
62 | class LDAPAddException(LDAPServerException): | |
63 | def __init__(self, dn, resultcode, diagnostic_message): | |
64 | self.dn = dn | |
65 | message = 'LDAP Add operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) | |
66 | super().__init__(resultcode, diagnostic_message, message) | |
67 | ||
68 | class LDAPModifyException(LDAPServerException): | |
69 | def __init__(self, dn, resultcode, diagnostic_message): | |
70 | self.dn = dn | |
71 | message = 'LDAP Modify operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) | |
72 | super().__init__(resultcode, diagnostic_message, message) | |
73 | ||
74 | class LDAPDeleteException(LDAPServerException): | |
75 | def __init__(self, dn, resultcode, diagnostic_message): | |
76 | self.dn = dn | |
77 | message = 'LDAP Delete operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) | |
78 | super().__init__(resultcode, diagnostic_message, message) |
16 | 16 | SOCKS5_SSL = 'SOCKS5_SSL' |
17 | 17 | MULTIPLEXOR = 'MULTIPLEXOR' |
18 | 18 | MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL' |
19 | WSNET = 'WSNET' | |
20 | WSNETWS = 'WSNETWS' | |
21 | WSNETWSS = 'WSNETWSS' | |
19 | 22 | |
20 | 23 | class MSLDAPProxy: |
21 | def __init__(self): | |
22 | self.type = None | |
23 | self.target = None | |
24 | self.auth = None | |
24 | """ | |
25 | Describes the proxy to be used when connecting to the server. Used as a parameter to the `MSLDAPTarget` object | |
26 | ||
27 | :param type: Specifies the proxy type | |
28 | :type type: :class:`MSLDAPProxyType` | |
29 | :param target: | |
30 | :type target: | |
31 | :param auth: Specifies the proxy authentication if any | |
32 | :type auth: | |
33 | """ | |
34 | def __init__(self, type = None, target = None, auth = None): | |
35 | self.type = type | |
36 | self.target = target | |
37 | self.auth = auth | |
25 | 38 | |
26 | 39 | |
27 | 40 | @staticmethod |
28 | 41 | def from_params(url_str): |
42 | """ | |
43 | Creates a proxy object from the parameters found in an LDAP URL string | |
44 | ||
45 | :param type: url_str | |
46 | :type type: str | |
47 | :return: The proxy object | |
48 | :rtype: :class:`MSLDAPProxy` | |
49 | """ | |
29 | 50 | proxy = MSLDAPProxy() |
30 | 51 | url = urlparse(url_str) |
31 | 52 | if url.query is None: |
36 | 57 | return None |
37 | 58 | |
38 | 59 | proxy.type = MSLDAPProxyType(query['proxytype'][0].upper()) |
39 | if proxy.type in [MSLDAPProxyType.SOCKS4, MSLDAPProxyType.SOCKS4_SSL, MSLDAPProxyType.SOCKS5, MSLDAPProxyType.SOCKS5_SSL]: | |
40 | cu = SocksClientURL.from_params(url_str) | |
60 | if proxy.type in [MSLDAPProxyType.WSNET, MSLDAPProxyType.WSNETWS, MSLDAPProxyType.WSNETWSS,MSLDAPProxyType.SOCKS4, MSLDAPProxyType.SOCKS4_SSL, MSLDAPProxyType.SOCKS5, MSLDAPProxyType.SOCKS5_SSL]: | |
61 | proxy.target = SocksClientURL.from_params(url_str) | |
41 | 62 | else: |
42 | raise Exception('Multiplexor not yet implemented as a proxy!') | |
43 | #cu = SocksClientURL.from_params(url_str) | |
63 | proxy.target = MSLDAPMultiplexorProxy.from_params(url_str) | |
44 | 64 | |
45 | proxy.target = cu.get_target() | |
46 | proxy.auth = cu.get_creds() | |
47 | 65 | return proxy |
48 | 66 | |
49 | 67 | def __str__(self): |
52 | 70 | t += '%s: %s\r\n' % (k, self.__dict__[k]) |
53 | 71 | |
54 | 72 | return t |
55 | ||
56 | ||
57 | 73 | |
58 | 74 | |
75 | class MSLDAPMultiplexorProxy: | |
76 | def __init__(self): | |
77 | self.ip = None | |
78 | self.port = None | |
79 | self.timeout = 10 | |
80 | self.type = MSLDAPProxyType.MULTIPLEXOR | |
81 | self.username = None | |
82 | self.password = None | |
83 | self.domain = None | |
84 | self.agent_id = None | |
85 | self.virtual_socks_port = None | |
86 | self.virtual_socks_ip = None | |
87 | ||
88 | def sanity_check(self): | |
89 | if self.ip is None: | |
90 | raise Exception('MULTIPLEXOR server IP is missing!') | |
91 | if self.port is None: | |
92 | raise Exception('MULTIPLEXOR server port is missing!') | |
93 | if self.agent_id is None: | |
94 | raise Exception('MULTIPLEXOR proxy requires agentid to be set!') | |
95 | ||
96 | def get_server_url(self): | |
97 | con_str = 'ws://%s:%s' % (self.ip, self.port) | |
98 | if self.type == MSLDAPProxyType.MULTIPLEXOR_SSL: | |
99 | con_str = 'wss://%s:%s' % (self.ip, self.port) | |
100 | return con_str | |
101 | ||
102 | @staticmethod | |
103 | def from_params(url_str): | |
104 | res = MSLDAPMultiplexorProxy() | |
105 | url = urlparse(url_str) | |
106 | res.endpoint_ip = url.hostname | |
107 | if url.port: | |
108 | res.endpoint_port = int(url.port) | |
109 | if url.query is not None: | |
110 | query = parse_qs(url.query) | |
111 | ||
112 | for k in query: | |
113 | if k.startswith('proxy'): | |
114 | if k[5:] in multiplexorproxyurl_param2var: | |
115 | ||
116 | data = query[k][0] | |
117 | for c in multiplexorproxyurl_param2var[k[5:]][1]: | |
118 | data = c(data) | |
119 | ||
120 | setattr( | |
121 | res, | |
122 | multiplexorproxyurl_param2var[k[5:]][0], | |
123 | data | |
124 | ) | |
125 | res.sanity_check() | |
126 | ||
127 | return res | |
128 | ||
129 | def stru(x): | |
130 | return str(x).upper() | |
131 | ||
132 | multiplexorproxyurl_param2var = { | |
133 | 'type' : ('version', [stru, MSLDAPProxyType]), | |
134 | 'host' : ('ip', [str]), | |
135 | 'port' : ('port', [int]), | |
136 | 'timeout': ('timeout', [int]), | |
137 | 'user' : ('username', [str]), | |
138 | 'pass' : ('password', [str]), | |
139 | #'authtype' : ('authtype', [SOCKS5Method]), | |
140 | 'agentid' : ('agent_id', [str]), | |
141 | 'domain' : ('domain', [str]) | |
142 | ||
143 | } | |
144 |
5 | 5 | # |
6 | 6 | |
7 | 7 | import enum |
8 | import ssl | |
8 | ||
9 | import platform | |
10 | try: | |
11 | import ssl | |
12 | except: | |
13 | if platform.system() == 'Emscripten': | |
14 | pass | |
9 | 15 | |
10 | 16 | class LDAPProtocol(enum.Enum): |
11 | 17 | TCP = 'TCP' |
14 | 20 | |
15 | 21 | |
16 | 22 | class MSLDAPTarget: |
17 | def __init__(self, host, port = 389, proto = LDAPProtocol.TCP, tree = None, proxy = None, timeout = 10): | |
23 | """ | |
24 | Describes the connection to the server. | |
25 | ||
26 | :param host: IP address or hostname of the server | |
27 | :type host: str | |
28 | :param port: port of the LDAP service running on the server | |
29 | :type port: int | |
30 | :param proto: Connection protocol to be used | |
31 | :type proto: :class:`LDAPProtocol` | |
32 | :param tree: The tree to connect to | |
33 | :type tree: str | |
34 | :param proxy: specifies what kind of proxy to be used | |
35 | :type proxy: :class:`MSLDAPProxy` | |
36 | :param timeout: connection timeout in seconds | |
37 | :type timeout: int | |
38 | :param ldap_query_page_size: Maximum number of elements to fetch in each paged_query call. | |
39 | :type ldap_query_page_size: int | |
40 | :param ldap_query_ratelimit: rate limit of paged queries. This will cause a sleep (in seconds) between fetching of each page of the query | |
41 | :type ldap_query_ratelimit: float | |
42 | """ | |
43 | def __init__(self, host, port = 389, proto = LDAPProtocol.TCP, tree = None, proxy = None, timeout = 10, ldap_query_page_size = 1000, ldap_query_ratelimit = 0): | |
18 | 44 | self.proto = proto |
19 | 45 | self.host = host |
20 | 46 | self.tree = tree |
22 | 48 | self.proxy = proxy |
23 | 49 | self.timeout = timeout |
24 | 50 | self.dc_ip = None |
51 | self.serverip = None | |
25 | 52 | self.domain = None |
26 | 53 | self.sslctx = None |
54 | self.ldap_query_page_size = ldap_query_page_size | |
55 | self.ldap_query_ratelimit = ldap_query_ratelimit | |
27 | 56 | |
28 | 57 | def get_ssl_context(self): |
29 | 58 | if self.proto == LDAPProtocol.SSL: |
30 | 59 | if self.sslctx is None: |
31 | self.sslctx = ssl.create_default_context() | |
60 | # TODO ssl verification :) | |
61 | self.sslctx = ssl._create_unverified_context() | |
62 | #self.sslctx.verify = False | |
32 | 63 | return self.sslctx |
33 | 64 | return None |
34 | 65 |
6 | 6 | |
7 | 7 | import platform |
8 | 8 | import hashlib |
9 | import getpass | |
10 | import base64 | |
11 | import enum | |
9 | 12 | from urllib.parse import urlparse, parse_qs |
10 | 13 | |
11 | from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol | |
14 | from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol, MSLDAP_KERBEROS_PROTOCOLS | |
12 | 15 | from msldap.commons.target import MSLDAPTarget, LDAPProtocol |
13 | 16 | from msldap.commons.proxy import MSLDAPProxy, MSLDAPProxyType |
14 | 17 | from msldap.client import MSLDAPClient |
15 | ||
18 | from msldap.connection import MSLDAPClientConnection | |
19 | ||
20 | class PLAINTEXTSCHEME(enum.Enum): | |
21 | """ | |
22 | Additional conveinence functions. | |
23 | """ | |
24 | SIMPLE_PROMPT = 'SIMPLE_PROMPT' | |
25 | SIMPLE_HEX = 'SIMPLE_HEX' | |
26 | SIMPLE_B64 = 'SIMPLE_B64' | |
27 | PLAIN_PROMPT = 'PLAIN_PROMPT' | |
28 | PLAIN_HEX = 'PLAIN_HEX' | |
29 | PLAIN_B64 = 'PLAIN_B64' | |
30 | SICILY_PROMPT = 'SICILY_PROMPT' | |
31 | SICILY_HEX = 'SICILY_HEX' | |
32 | SICILY_B64 = 'SICILY_B64' | |
33 | NTLM_PROMPT = 'NTLM_PROMPT' | |
34 | NTLM_HEX = 'NTLM_HEX' | |
35 | NTLM_B64 = 'NTLM_B64' | |
16 | 36 | |
17 | 37 | class MSLDAPURLDecoder: |
38 | """ | |
39 | The URL describes both the connection target and the credentials. This class creates all necessary objects to set up the client. | |
40 | ||
41 | :param url: | |
42 | :type url: str | |
43 | """ | |
18 | 44 | |
19 | 45 | help_epilog = """ |
20 | 46 | MSLDAP URL Format: <protocol>+<auth>://<username>:<password>@<ip_or_host>:<port>/<tree>/?<param>=<value> |
21 | 47 | <protocol> sets the ldap protocol following values supported: |
22 | 48 | - ldap |
23 | 49 | - ldaps |
24 | <auth> can be omitted if plaintext authentication is to be performed, otherwise: | |
25 | - ntlm | |
26 | - sspi (windows only!) | |
50 | <auth> can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: | |
51 | - ntlm-password | |
52 | - ntlm-nt | |
53 | - kerberos-password (dc option param must be used) | |
54 | - kerberos-rc4 / kerberos-nt (dc option param must be used) | |
55 | - kerberos-aes (dc option param must be used) | |
56 | - kerberos-keytab (dc option param must be used) | |
57 | - kerberos-ccache (dc option param must be used) | |
58 | - sspi-ntlm (windows only!) | |
59 | - sspi-kerberos (windows only!) | |
27 | 60 | - anonymous |
28 | 61 | - plain |
62 | - simple | |
63 | - sicily (same format as ntlm-nt but using the SICILY authentication) | |
64 | <tree>: | |
65 | OPTIONAL. Specifies the root tree of all queries | |
29 | 66 | <param> can be: |
30 | 67 | - timeout : connction timeout in seconds |
31 | 68 | - proxytype: currently only socks5 proxy is supported |
32 | 69 | - proxyhost: Ip or hostname of the proxy server |
33 | 70 | - proxyport: port of the proxy server |
34 | - proxytimeout: timeout ins ecodns for the proxy connection | |
71 | - proxytimeout: timeout in secodns for the proxy connection | |
72 | - dc: the IP address of the domain controller, MUST be used for kerberos authentication | |
73 | - encrypt: enable encryption. Only for NTLM. DOESNT WORK WITH LDAPS | |
74 | - etype: Supported encryption types for Kerberos authentication. Multiple can be specified. | |
75 | - rate: LDAP paged search query rate limit. Will sleep for seconds between each new page. Default: 0 (no limit) | |
76 | - pagesize: LDAP paged search query size per page. Max: 1000. Default: 1000 | |
35 | 77 | |
36 | 78 | Examples: |
37 | 79 | ldap://10.10.10.2 (anonymous bind) |
38 | 80 | ldaps://test.corp (anonymous bind) |
39 | ldap+sspi:///test.corp | |
81 | ldap+sspi-ntlm://test.corp | |
82 | ldap+sspi-kerberos://test.corp | |
40 | 83 | ldap://TEST\\victim:<password>@10.10.10.2 (defaults to SASL GSSAPI NTLM) |
41 | 84 | ldap+simple://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) |
42 | 85 | ldap+plain://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) |
57 | 100 | self.domain = None |
58 | 101 | self.username = None |
59 | 102 | self.password = None |
103 | self.encrypt = False | |
60 | 104 | self.auth_settings = {} |
105 | self.etypes = None | |
61 | 106 | |
62 | 107 | self.ldap_proto = None |
63 | 108 | self.ldap_host = None |
65 | 110 | self.ldap_tree = None |
66 | 111 | self.target_timeout = 5 |
67 | 112 | self.target_pagesize = 1000 |
113 | self.target_ratelimit = 0 | |
68 | 114 | self.dc_ip = None |
69 | 115 | self.serverip = None |
70 | 116 | self.proxy = None |
71 | 117 | |
118 | self.__pwpreprocess = None | |
119 | ||
72 | 120 | self.parse() |
73 | 121 | |
74 | 122 | |
75 | 123 | def get_credential(self): |
76 | return MSLDAPCredential( | |
124 | """ | |
125 | Creates a credential object | |
126 | ||
127 | :return: Credential object | |
128 | :rtype: :class:`MSLDAPCredential` | |
129 | """ | |
130 | t = MSLDAPCredential( | |
77 | 131 | domain=self.domain, |
78 | 132 | username=self.username, |
79 | 133 | password = self.password, |
80 | 134 | auth_method=self.auth_scheme, |
81 | 135 | settings = self.auth_settings |
82 | 136 | ) |
137 | t.encrypt = self.encrypt | |
138 | t.etypes = self.etypes | |
139 | ||
140 | return t | |
83 | 141 | |
84 | 142 | def get_target(self): |
143 | """ | |
144 | Creates a target object | |
145 | ||
146 | :return: Target object | |
147 | :rtype: :class:`MSLDAPTarget` | |
148 | """ | |
85 | 149 | target = MSLDAPTarget( |
86 | 150 | self.ldap_host, |
87 | 151 | port = self.ldap_port, |
88 | 152 | proto = self.ldap_scheme, |
89 | 153 | tree=self.ldap_tree, |
90 | timeout = self.target_timeout | |
154 | timeout = self.target_timeout, | |
155 | ldap_query_page_size = self.target_pagesize, | |
156 | ldap_query_ratelimit = self.target_ratelimit | |
91 | 157 | ) |
92 | 158 | target.domain = self.domain |
93 | 159 | target.dc_ip = self.dc_ip |
94 | 160 | target.proxy = self.proxy |
161 | target.serverip = self.serverip | |
95 | 162 | return target |
96 | 163 | |
97 | 164 | def get_client(self): |
165 | """ | |
166 | Creates a client that can be used to interface with the server | |
167 | ||
168 | :return: LDAP client | |
169 | :rtype: :class:`MSLDAPClient` | |
170 | """ | |
98 | 171 | cred = self.get_credential() |
99 | 172 | target = self.get_target() |
100 | return MSLDAPClient(target, cred, ldap_query_page_size = self.target_pagesize) | |
173 | return MSLDAPClient(target, cred) | |
174 | ||
175 | def get_connection(self): | |
176 | """ | |
177 | Creates a connection that can be used to interface with the server | |
178 | ||
179 | :return: LDAP connection | |
180 | :rtype: :class:`MSLDAPClientConnection` | |
181 | """ | |
182 | cred = self.get_credential() | |
183 | target = self.get_target() | |
184 | return MSLDAPClientConnection(target, cred) | |
101 | 185 | |
102 | 186 | def scheme_decoder(self, scheme): |
103 | 187 | schemes = [] |
126 | 210 | return |
127 | 211 | |
128 | 212 | try: |
129 | self.auth_scheme = LDAPAuthProtocol(schemes[1]) | |
213 | x = PLAINTEXTSCHEME(schemes[1]) | |
214 | if x == PLAINTEXTSCHEME.SIMPLE_PROMPT: | |
215 | self.auth_scheme = LDAPAuthProtocol.SIMPLE | |
216 | self.__pwpreprocess = 'PROMPT' | |
217 | ||
218 | if x == PLAINTEXTSCHEME.SIMPLE_HEX: | |
219 | self.auth_scheme = LDAPAuthProtocol.SIMPLE | |
220 | self.__pwpreprocess = 'HEX' | |
221 | ||
222 | if x == PLAINTEXTSCHEME.SIMPLE_B64: | |
223 | self.auth_scheme = LDAPAuthProtocol.SIMPLE | |
224 | self.__pwpreprocess = 'B64' | |
225 | ||
226 | if x == PLAINTEXTSCHEME.PLAIN_PROMPT: | |
227 | self.auth_scheme = LDAPAuthProtocol.PLAIN | |
228 | self.__pwpreprocess = 'PROMPT' | |
229 | ||
230 | if x == PLAINTEXTSCHEME.PLAIN_HEX: | |
231 | self.auth_scheme = LDAPAuthProtocol.PLAIN | |
232 | self.__pwpreprocess = 'HEX' | |
233 | ||
234 | if x == PLAINTEXTSCHEME.PLAIN_B64: | |
235 | self.auth_scheme = LDAPAuthProtocol.PLAIN | |
236 | self.__pwpreprocess = 'B64' | |
237 | ||
238 | if x == PLAINTEXTSCHEME.SICILY_PROMPT: | |
239 | self.auth_scheme = LDAPAuthProtocol.SICILY | |
240 | self.__pwpreprocess = 'PROMPT' | |
241 | ||
242 | if x == PLAINTEXTSCHEME.SICILY_HEX: | |
243 | self.auth_scheme = LDAPAuthProtocol.SICILY | |
244 | self.__pwpreprocess = 'HEX' | |
245 | ||
246 | if x == PLAINTEXTSCHEME.SICILY_B64: | |
247 | self.auth_scheme = LDAPAuthProtocol.SICILY | |
248 | self.__pwpreprocess = 'B64' | |
249 | ||
250 | if x == PLAINTEXTSCHEME.NTLM_PROMPT: | |
251 | self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD | |
252 | self.__pwpreprocess = 'PROMPT' | |
253 | ||
254 | if x == PLAINTEXTSCHEME.NTLM_HEX: | |
255 | self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD | |
256 | self.__pwpreprocess = 'HEX' | |
257 | ||
258 | if x == PLAINTEXTSCHEME.NTLM_B64: | |
259 | self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD | |
260 | self.__pwpreprocess = 'B64' | |
130 | 261 | except: |
131 | raise Exception('Uknown scheme!') | |
262 | try: | |
263 | self.auth_scheme = LDAPAuthProtocol(schemes[1]) | |
264 | except: | |
265 | raise Exception('Uknown scheme!') | |
132 | 266 | |
133 | 267 | return |
134 | 268 | |
137 | 271 | self.scheme_decoder(url_e.scheme) |
138 | 272 | |
139 | 273 | self.password = url_e.password |
274 | if self.__pwpreprocess is not None: | |
275 | if self.__pwpreprocess == 'PROMPT': | |
276 | self.password = getpass.getpass() | |
277 | ||
278 | elif self.__pwpreprocess == 'HEX': | |
279 | self.password = bytes.fromhex(self.password).decode() | |
280 | ||
281 | elif self.__pwpreprocess == 'B64': | |
282 | self.password = base64.b64decode(self.password).decode() | |
283 | ||
284 | else: | |
285 | raise Exception('Unknown password preprocess directive %s' % self.__pwpreprocess) | |
286 | ||
140 | 287 | |
141 | 288 | if url_e.username is not None: |
142 | 289 | if url_e.username.find('\\') != -1: |
172 | 319 | proxy_present = False |
173 | 320 | if url_e.query is not None: |
174 | 321 | query = parse_qs(url_e.query) |
322 | if 'etype' in query: | |
323 | self.etypes = [] | |
175 | 324 | for k in query: |
176 | 325 | if k.startswith('proxy') is True: |
177 | 326 | proxy_present = True |
180 | 329 | elif k == 'timeout': |
181 | 330 | self.timeout = int(query[k][0]) |
182 | 331 | elif k == 'serverip': |
183 | self.server_ip = query[k][0] | |
332 | self.serverip = query[k][0] | |
184 | 333 | elif k == 'dns': |
185 | 334 | self.dns = query[k] #multiple dns can be set, so not trimming here |
335 | elif k == 'encrypt': | |
336 | self.encrypt = bool(int(query[k][0])) | |
337 | elif k == 'etype': | |
338 | self.etypes = [int(x) for x in query[k]] | |
186 | 339 | elif k.startswith('auth'): |
187 | 340 | self.auth_settings[k[len('auth'):]] = query[k] |
341 | elif k == 'rate': | |
342 | self.target_ratelimit = float(query[k][0]) | |
343 | elif k == 'pagesize': | |
344 | self.target_pagesize = int(query[k][0]) | |
188 | 345 | #elif k.startswith('same'): |
189 | 346 | # self.auth_settings[k[len('same'):]] = query[k] |
190 | 347 | |
201 | 358 | if self.domain is None: |
202 | 359 | self.domain = '<CURRENT>' |
203 | 360 | |
204 | ||
205 | ||
206 | # | |
207 | # if self.auth_scheme == LDAPAuthProtocol.SSPI: | |
208 | # if self.username is None: | |
209 | # self.username = '<CURRENT>' | |
210 | # if self.password is None: | |
211 | # self.password = '<CURRENT>' | |
212 | # if self.domain is None: | |
213 | # self.domain = '<CURRENT>' | |
214 | # | |
215 | # if self.auth_scheme == LDAPAuthProtocol.NTLM: | |
216 | # if len(self.password) == 32: | |
217 | # try: | |
218 | # bytes.fromhex(self.password) | |
219 | # except: | |
220 | # a = hashlib.new('md4') | |
221 | # a.update(self.password.encode('utf-16-le')) | |
222 | # hs = a.hexdigest() | |
223 | # self.password = '%s:%s' % (hs, hs) | |
224 | # else: | |
225 | # self.password = '%s:%s' % (self.password, self.password) | |
226 | # else: | |
227 | # a = hashlib.new('md4') | |
228 | # a.update(self.password.encode('utf-16-le')) | |
229 | # hs = a.hexdigest() | |
230 | # self.password = '%s:%s' % (hs, hs) | |
231 | ||
232 | # | |
233 | # #now for the url parameters | |
234 | # """ | |
235 | # ldaps://user:[email protected]/?proxyhost=127.0.0.1&proxyport=8888&proxyuser=dddd&proxypass=ssss&dns=127.0.0.1 | |
236 | # """ | |
237 | # if url_e.query is not None: | |
238 | # query = parse_qs(url_e.query) | |
239 | # for k in query: | |
240 | # if k == 'dns': | |
241 | # self.dns = query[k] #multiple dns can be set, so not trimming here | |
242 | # elif k.startswith('auth'): | |
243 | # self.auth_settings[k[len('auth'):]] = query[k] #the result is a list for each entry because this preprocessor is not aware which elements should be lists! | |
244 | # elif k == 'timeout': | |
245 | # self.target_timeout = int(query[k][0]) | |
246 | # elif k == 'pagesize': | |
247 | # self.target_pagesize = int(query[k][0]) | |
248 | # elif k.startswith('proxy'): | |
249 | # if k == 'proxytype': | |
250 | # self.proxy_scheme = LDAPProxyType(query[k][0].upper()) | |
251 | # elif k == 'proxyhost': | |
252 | # self.proxy_ip = query[k][0] | |
253 | # elif k == 'proxyuser': | |
254 | # if query[k][0].find('\\') != -1: | |
255 | # self.proxy_domain, self.proxy_username = query[k][0].split('\\') | |
256 | # else: | |
257 | # self.proxy_username = query[k][0] | |
258 | # elif k == 'proxypass': | |
259 | # self.proxy_password = query[k][0] | |
260 | # elif k == 'proxytimeout': | |
261 | # self.proxy_timeout = int(query[k][0]) | |
262 | # elif k == 'proxyport': | |
263 | # self.proxy_port = int(query[k][0]) | |
264 | # else: | |
265 | # self.proxy_settings[k[len('proxy'):]] = query[k] #the result is a list for each entry because this preprocessor is not aware which elements should be lists! | |
266 | # | |
267 | # #####TODOOOO FIX THIS!!!! | |
268 | # elif k.startswith('same'): | |
269 | # self.auth_settings[k[len('same'):]] = query[k] | |
270 | # if k == 'sametype': | |
271 | # self.proxy_scheme = LDAPProxyType(query[k][0].upper()) | |
272 | # elif k == 'samehost': | |
273 | # self.proxy_ip = query[k][0] | |
274 | # elif k == 'sametimeout': | |
275 | # self.proxy_timeout = int(query[k][0]) | |
276 | # elif k == 'sameuser': | |
277 | # if query[k][0].find('\\') != -1: | |
278 | # self.proxy_domain, self.proxy_username = query[k][0].split('\\') | |
279 | # else: | |
280 | # self.proxy_username = query[k][0] | |
281 | # elif k == 'samepass': | |
282 | # self.proxy_password = query[k][0] | |
283 | # elif k == 'sameport': | |
284 | # self.proxy_port = int(query[k][0]) | |
285 | # else: | |
286 | # self.proxy_settings[k[len('same'):]] = query[k] #the result is a list for each entry because this preprocessor is not aware which elements should be lists! | |
287 | # | |
288 | # #setting default proxy ports | |
289 | # if self.proxy_scheme in [LDAPProxyType.SOCKS5, LDAPProxyType.SOCKS5_SSL]: | |
290 | # if self.proxy_port is None: | |
291 | # self.proxy_port = 1080 | |
292 | # | |
361 | if self.auth_scheme in MSLDAP_KERBEROS_PROTOCOLS and self.dc_ip is None: | |
362 | raise Exception('The "dc" parameter MUST be used for kerberos authentication types!') | |
363 | ||
364 | ||
293 | 365 | # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]: |
294 | 366 | # if self.proxy_port is None: |
295 | 367 | # self.proxy_port = 9999 |
296 | # | |
297 | # #sanity checks... | |
298 | # if self.proxy_scheme is not None: | |
299 | # if self.proxy_ip is None: | |
300 | # raise Exception('proxyserver MUST be provided if using proxy') | |
301 | 368 | # |
302 | 369 | # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]: |
303 | 370 | # if 'agentid' not in self.proxy_settings: |
304 | 371 | # raise Exception('multiplexor proxy reuires agentid to be set! Set it via proxyagentid parameter!') |
305 | 372 | # |
306 | # if self.auth_scheme in [LDAPAuthProtocol.PLAIN, LDAPAuthProtocol.NTLM, LDAPAuthProtocol.SSPI]: | |
307 | # if self.username is None: | |
308 | # raise Exception('For authentication protocol %s the username MUST be specified!' % self.auth_scheme.value) | |
309 | # if self.password is None: | |
310 | # raise Exception('For authentication protocol %s the password MUST be specified!' % self.auth_scheme.value) | |
311 | # | |
312 | # if self.auth_scheme is None: | |
313 | # if self.username is None and self.password is None: | |
314 | # self.auth_scheme = LDAPAuthProtocol.ANONYMOUS | |
315 | # else: | |
316 | # raise Exception('Could not parse authentication protocol!') | |
373 | ||
317 | 374 | |
318 | 375 | |
319 | 376 |
0 | 0 | import asyncio |
1 | 1 | |
2 | ||
2 | 3 | from msldap import logger |
4 | from msldap.commons.common import MSLDAPClientStatus | |
3 | 5 | from msldap.protocol.messages import LDAPMessage, BindRequest, \ |
4 | 6 | protocolOp, AuthenticationChoice, SaslCredentials, \ |
5 | 7 | SearchRequest, AttributeDescription, Filter, Filters, \ |
6 | Controls, Control, SearchControlValue | |
8 | Controls, Control, SearchControlValue, AddRequest, \ | |
9 | ModifyRequest, DelRequest | |
7 | 10 | |
8 | 11 | from msldap.protocol.utils import calcualte_length |
9 | from msldap.protocol.typeconversion import convert_result, convert_attributes | |
12 | from msldap.protocol.typeconversion import convert_result, convert_attributes, encode_attributes, encode_changes | |
13 | from msldap.protocol.query import escape_filter_chars, query_syntax_converter | |
10 | 14 | from msldap.commons.authbuilder import AuthenticatorBuilder |
11 | 15 | from msldap.commons.credential import MSLDAP_GSS_METHODS |
12 | 16 | from msldap.network.selector import MSLDAPNetworkSelector |
13 | 17 | from msldap.commons.credential import LDAPAuthProtocol |
18 | from msldap.commons.target import LDAPProtocol | |
19 | from msldap.commons.exceptions import LDAPServerException, LDAPBindException, LDAPAddException, LDAPModifyException, LDAPDeleteException | |
20 | from asn1crypto.x509 import Certificate | |
21 | from hashlib import sha256 | |
22 | from minikerberos.gssapi.channelbindings import ChannelBindingsStruct | |
14 | 23 | |
15 | 24 | class MSLDAPClientConnection: |
16 | 25 | def __init__(self, target, creds): |
26 | if target is None: | |
27 | raise Exception('Target cant be none!') | |
17 | 28 | self.target = target |
18 | 29 | self.creds = creds |
19 | 30 | self.auth = AuthenticatorBuilder(self.creds, self.target).build() |
24 | 35 | self.network = None |
25 | 36 | |
26 | 37 | self.handle_incoming_task = None |
38 | self.status = MSLDAPClientStatus.RUNNING | |
39 | self.lasterror = None | |
27 | 40 | |
28 | 41 | self.message_id = 0 |
29 | 42 | self.message_table = {} |
30 | 43 | self.message_table_notify = {} |
31 | self.encryption_sequence_counter = 0 #for whatever reason it's only used during encryption, but decryption always uses 0 | |
44 | self.encryption_sequence_counter = 0 # this will be set by the inderlying auth algo | |
45 | self.cb_data = None #for channel binding | |
46 | ||
47 | async def __aenter__(self): | |
48 | return self | |
49 | ||
50 | async def __aexit__(self, exc_type, exc, traceback): | |
51 | await asyncio.wait_for(self.disconnect(), timeout = 1) | |
32 | 52 | |
33 | 53 | async def __handle_incoming(self): |
34 | 54 | try: |
35 | 55 | while True: |
36 | 56 | message_data, err = await self.network.in_queue.get() |
37 | 57 | if err is not None: |
38 | logger.debug('Client terminating bc __handle_incoming!') | |
58 | logger.debug('Client terminating bc __handle_incoming got an error!') | |
39 | 59 | raise err |
40 | 60 | |
41 | ################################ | |
42 | # # | |
43 | # ADD CHANNEL BINDING HERE! # | |
44 | ################################ | |
45 | ||
61 | #print('Incoming message data: %s' % message_data) | |
46 | 62 | if self.bind_ok is True: |
47 | 63 | if self.__encrypt_messages is True: |
48 | #print('Encrypted %s' % message_data) | |
49 | 64 | #removing size |
50 | 65 | message_data = message_data[4:] |
51 | 66 | try: |
52 | message_data = await self.auth.decrypt(message_data, 0) | |
67 | # seq number doesnt matter here, a it's in the header | |
68 | message_data, err = await self.auth.decrypt(message_data, 0 ) | |
69 | if err is not None: | |
70 | raise err | |
53 | 71 | #print('Decrypted %s' % message_data.hex()) |
72 | #print('Decrypted %s' % message_data) | |
54 | 73 | except: |
55 | 74 | import traceback |
56 | 75 | traceback.print_exc() |
76 | raise | |
57 | 77 | |
58 | 78 | elif self.__sign_messages is True: |
59 | 79 | #print('Signed %s' % message_data) |
64 | 84 | except: |
65 | 85 | import traceback |
66 | 86 | traceback.print_exc() |
87 | raise | |
88 | ||
67 | 89 | |
68 | 90 | msg_len = calcualte_length(message_data) |
69 | 91 | msg_total_len = len(message_data) |
90 | 112 | self.message_table_notify[message_id].set() |
91 | 113 | |
92 | 114 | except asyncio.CancelledError: |
93 | #not notifying clients, at this point the client is terminating | |
115 | self.status = MSLDAPClientStatus.STOPPED | |
94 | 116 | return |
95 | 117 | |
96 | 118 | except Exception as e: |
97 | import traceback | |
98 | traceback.print_exc() | |
119 | self.status = MSLDAPClientStatus.ERROR | |
120 | self.lasterror = e | |
99 | 121 | for msgid in self.message_table_notify: |
100 | 122 | self.message_table[msgid] = [e] |
101 | 123 | self.message_table_notify[msgid].set() |
124 | ||
125 | self.status = MSLDAPClientStatus.STOPPED | |
102 | 126 | |
103 | 127 | |
104 | 128 | async def send_message(self, message): |
142 | 166 | return messages |
143 | 167 | |
144 | 168 | async def connect(self): |
145 | logger.debug('Connecting!') | |
146 | self.network = MSLDAPNetworkSelector.select(self.target) | |
147 | res, err = await self.network.run() | |
148 | if res is False: | |
149 | raise err | |
150 | ||
151 | self.handle_incoming_task = asyncio.create_task(self.__handle_incoming()) | |
152 | logger.debug('Connection succsessful!') | |
169 | """ | |
170 | Connects to the remote server. Establishes the session, but doesn't perform binding. | |
171 | This function MUST be called first before the `bind` operation. | |
172 | ||
173 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
174 | :rtype: (:class:`bool`, :class:`Exception`) | |
175 | """ | |
176 | try: | |
177 | logger.debug('Connecting!') | |
178 | self.network = await MSLDAPNetworkSelector.select(self.target) | |
179 | res, err = await self.network.run() | |
180 | if res is False: | |
181 | return False, err | |
182 | ||
183 | # now processing channel binding options | |
184 | if self.target.proto == LDAPProtocol.SSL: | |
185 | certdata = self.network.get_peer_certificate() | |
186 | #cert = Certificate.load(certdata).native | |
187 | #print(cert) | |
188 | cb_struct = ChannelBindingsStruct() | |
189 | cb_struct.application_data = b'tls-server-end-point:' + sha256(certdata).digest() | |
190 | ||
191 | self.cb_data = cb_struct.to_bytes() | |
192 | ||
193 | self.handle_incoming_task = asyncio.create_task(self.__handle_incoming()) | |
194 | logger.debug('Connection succsessful!') | |
195 | return True, None | |
196 | except Exception as e: | |
197 | return False, e | |
153 | 198 | |
154 | 199 | async def disconnect(self): |
200 | """ | |
201 | Tears down the connection. | |
202 | ||
203 | :return: Nothing | |
204 | :rtype: None | |
205 | """ | |
206 | ||
155 | 207 | logger.debug('Disconnecting!') |
156 | 208 | self.bind_ok = False |
157 | self.handle_incoming_task.cancel() | |
158 | await self.network.terminate() | |
209 | if self.handle_incoming_task is not None: | |
210 | self.handle_incoming_task.cancel() | |
211 | if self.network is not None: | |
212 | await self.network.terminate() | |
159 | 213 | |
160 | 214 | |
161 | 215 | def __bind_success(self): |
172 | 226 | self.network.is_plain_msg = False |
173 | 227 | |
174 | 228 | async def bind(self): |
229 | """ | |
230 | Performs the bind operation. | |
231 | This is where the authentication happens. Remember to call `connect` before this function! | |
232 | ||
233 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
234 | :rtype: (:class:`bool`, :class:`Exception`) | |
235 | """ | |
175 | 236 | logger.debug('BIND in progress...') |
176 | 237 | try: |
177 | 238 | if self.creds.auth_method == LDAPAuthProtocol.SICILY: |
178 | data, _ = await self.auth.authenticate(None) | |
179 | ||
239 | ||
240 | data, to_continue, err = await self.auth.authenticate(None) | |
241 | if err is not None: | |
242 | return None, err | |
243 | ||
180 | 244 | auth = { |
181 | 245 | 'sicily_disco' : b'' |
182 | 246 | } |
197 | 261 | return False, res |
198 | 262 | res = res.native |
199 | 263 | if res['protocolOp']['resultCode'] != 'success': |
200 | return False, Exception( | |
201 | 'BIND failed! Result code: "%s" Reason: "%s"' % ( | |
264 | return False, LDAPBindException( | |
202 | 265 | res['protocolOp']['resultCode'], |
203 | 266 | res['protocolOp']['diagnosticMessage'] |
204 | )) | |
267 | ) | |
205 | 268 | |
206 | 269 | auth = { |
207 | 270 | 'sicily_nego' : data |
223 | 286 | return False, res |
224 | 287 | res = res.native |
225 | 288 | if res['protocolOp']['resultCode'] != 'success': |
226 | return False, Exception( | |
227 | 'BIND failed! Result code: "%s" Reason: "%s"' % ( | |
289 | return False, LDAPBindException( | |
228 | 290 | res['protocolOp']['resultCode'], |
229 | 291 | res['protocolOp']['diagnosticMessage'] |
230 | )) | |
231 | ||
232 | data, _ = await self.auth.authenticate(res['protocolOp']['matchedDN']) | |
292 | ) | |
293 | ||
294 | data, to_continue, err = await self.auth.authenticate(res['protocolOp']['matchedDN']) | |
295 | if err is not None: | |
296 | return None, err | |
233 | 297 | |
234 | 298 | auth = { |
235 | 299 | 'sicily_resp' : data |
251 | 315 | return False, res |
252 | 316 | res = res.native |
253 | 317 | if res['protocolOp']['resultCode'] != 'success': |
254 | return False, Exception( | |
255 | 'BIND failed! Result code: "%s" Reason: "%s"' % ( | |
318 | return False, LDAPBindException( | |
256 | 319 | res['protocolOp']['resultCode'], |
257 | 320 | res['protocolOp']['diagnosticMessage'] |
258 | )) | |
321 | ) | |
259 | 322 | |
260 | 323 | |
261 | 324 | self.__bind_success() |
294 | 357 | return True, None |
295 | 358 | |
296 | 359 | else: |
297 | return False, Exception( | |
298 | 'BIND failed! Result code: "%s" Reason: "%s"' % ( | |
360 | return False, LDAPBindException( | |
299 | 361 | res['protocolOp']['resultCode'], |
300 | 362 | res['protocolOp']['diagnosticMessage'] |
301 | )) | |
363 | ) | |
302 | 364 | |
303 | 365 | elif self.creds.auth_method in MSLDAP_GSS_METHODS: |
304 | 366 | challenge = None |
305 | 367 | while True: |
306 | data, _ = await self.auth.authenticate(challenge) | |
368 | try: | |
369 | data, to_continue, err = await self.auth.authenticate(challenge, cb_data = self.cb_data) | |
370 | if err is not None: | |
371 | raise err | |
372 | except Exception as e: | |
373 | return False, e | |
307 | 374 | |
308 | 375 | sasl = { |
309 | 376 | 'mechanism' : 'GSS-SPNEGO'.encode(), |
315 | 382 | |
316 | 383 | bindreq = { |
317 | 384 | 'version' : 3, |
318 | 'name': ''.encode(), | |
385 | 'name': b'', | |
319 | 386 | 'authentication': AuthenticationChoice(auth), |
320 | 387 | } |
321 | 388 | |
329 | 396 | return False, res |
330 | 397 | res = res.native |
331 | 398 | if res['protocolOp']['resultCode'] == 'success': |
399 | if 'serverSaslCreds' in res['protocolOp']: | |
400 | data, _, err = await self.auth.authenticate(res['protocolOp']['serverSaslCreds'], cb_data = self.cb_data) | |
401 | if err is not None: | |
402 | return False, err | |
403 | ||
404 | self.encryption_sequence_counter = self.auth.get_seq_number() | |
332 | 405 | self.__bind_success() |
406 | ||
333 | 407 | return True, None |
334 | 408 | |
335 | 409 | elif res['protocolOp']['resultCode'] == 'saslBindInProgress': |
337 | 411 | continue |
338 | 412 | |
339 | 413 | else: |
340 | return False, Exception( | |
341 | 'BIND failed! Result code: "%s" Reason: "%s"' % ( | |
414 | return False, LDAPBindException( | |
342 | 415 | res['protocolOp']['resultCode'], |
343 | 416 | res['protocolOp']['diagnosticMessage'] |
344 | )) | |
417 | ) | |
345 | 418 | |
346 | #print(res) | |
419 | else: | |
420 | raise Exception('Not implemented authentication method: %s' % self.creds.auth_method.name) | |
347 | 421 | except Exception as e: |
348 | print(str(e)) | |
422 | return False, e | |
423 | ||
424 | async def add(self, entry, attributes): | |
425 | """ | |
426 | Performs the add operation. | |
427 | ||
428 | :param entry: The DN of the object to be added | |
429 | :type entry: str | |
430 | :param attributes: Attributes to be used in the operation | |
431 | :type attributes: dict | |
432 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
433 | :rtype: (:class:`bool`, :class:`Exception`) | |
434 | """ | |
435 | try: | |
436 | req = { | |
437 | 'entry' : entry.encode(), | |
438 | 'attributes' : encode_attributes(attributes) | |
439 | } | |
440 | br = { 'addRequest' : AddRequest(req)} | |
441 | msg = { 'protocolOp' : protocolOp(br)} | |
442 | ||
443 | msg_id = await self.send_message(msg) | |
444 | results = await self.recv_message(msg_id) | |
445 | if isinstance(results[0], Exception): | |
446 | return False, results[0] | |
447 | ||
448 | for message in results: | |
449 | msg_type = message['protocolOp'].name | |
450 | message = message.native | |
451 | if msg_type == 'addResponse': | |
452 | if message['protocolOp']['resultCode'] != 'success': | |
453 | return False, LDAPAddException( | |
454 | entry, | |
455 | message['protocolOp']['resultCode'], | |
456 | message['protocolOp']['diagnosticMessage'] | |
457 | ) | |
458 | ||
459 | return True, None | |
460 | except Exception as e: | |
461 | return False, e | |
462 | ||
463 | async def modify(self, entry, changes, controls = None): | |
464 | """ | |
465 | Performs the modify operation. | |
466 | ||
467 | :param entry: The DN of the object whose attributes are to be modified | |
468 | :type entry: str | |
469 | :param changes: Describes the changes to be made on the object. Must be a dictionary of the following format: {'attribute': [('change_type', [value])]} | |
470 | :type changes: dict | |
471 | :param controls: additional controls to be passed in the query | |
472 | :type controls: List[class:`Control`] | |
473 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
474 | :rtype: (:class:`bool`, :class:`Exception`) | |
475 | """ | |
476 | try: | |
477 | req = { | |
478 | 'object' : entry.encode(), | |
479 | 'changes' : encode_changes(changes) | |
480 | } | |
481 | br = { 'modifyRequest' : ModifyRequest(req)} | |
482 | msg = { 'protocolOp' : protocolOp(br)} | |
483 | if controls is not None: | |
484 | msg['controls'] = controls | |
485 | ||
486 | msg_id = await self.send_message(msg) | |
487 | results = await self.recv_message(msg_id) | |
488 | if isinstance(results[0], Exception): | |
489 | return False, results[0] | |
490 | ||
491 | for message in results: | |
492 | msg_type = message['protocolOp'].name | |
493 | message = message.native | |
494 | if msg_type == 'modifyResponse': | |
495 | if message['protocolOp']['resultCode'] != 'success': | |
496 | return False, LDAPModifyException( | |
497 | entry, | |
498 | message['protocolOp']['resultCode'], | |
499 | message['protocolOp']['diagnosticMessage'] | |
500 | ) | |
501 | ||
502 | return True, None | |
503 | except Exception as e: | |
504 | return False, e | |
505 | ||
506 | async def delete(self, entry): | |
507 | """ | |
508 | Performs the delete operation. | |
509 | ||
510 | :param entry: The DN of the object to be deleted | |
511 | :type entry: str | |
512 | :return: A tuple of (True, None) on success or (False, Exception) on error. | |
513 | :rtype: (:class:`bool`, :class:`Exception`) | |
514 | """ | |
515 | try: | |
516 | br = { 'delRequest' : DelRequest(entry.encode())} | |
517 | msg = { 'protocolOp' : protocolOp(br)} | |
518 | ||
519 | msg_id = await self.send_message(msg) | |
520 | results = await self.recv_message(msg_id) | |
521 | if isinstance(results[0], Exception): | |
522 | return False, results[0] | |
523 | ||
524 | for message in results: | |
525 | msg_type = message['protocolOp'].name | |
526 | message = message.native | |
527 | if msg_type == 'delResponse': | |
528 | if message['protocolOp']['resultCode'] != 'success': | |
529 | return False, LDAPDeleteException( | |
530 | entry, | |
531 | message['protocolOp']['resultCode'], | |
532 | message['protocolOp']['diagnosticMessage'] | |
533 | ) | |
534 | ||
535 | return True, None | |
536 | except Exception as e: | |
349 | 537 | return False, e |
350 | 538 | |
351 | async def search(self, base, filter, attributes, search_scope = 2, paged_size = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None, return_done = False): | |
352 | """ | |
353 | This function is a generator!!!!! Dont just call it but use it with "async for" | |
354 | """ | |
539 | async def search(self, base, query, attributes, search_scope = 2, size_limit = 1000, types_only = False, derefAliases = 0, timeLimit = None, controls = None, return_done = False): | |
540 | """ | |
541 | Performs the search operation. | |
542 | ||
543 | :param base: base tree on which the search should be performed | |
544 | :type base: str | |
545 | :param query: filter query that defines what should be searched for | |
546 | :type query: str | |
547 | :param attributes: a list of attributes to be included in the response | |
548 | :type attributes: List[str] | |
549 | :param search_scope: Specifies the search operation's scope. Default: 2 (Subtree) | |
550 | :type search_scope: int | |
551 | :param types_only: indicates whether the entries returned should include attribute types only or both types and values. Default: False (both) | |
552 | :type types_only: bool | |
553 | :param size_limit: Size limit of result elements per query. Default: 1000 | |
554 | :type size_limit: int | |
555 | :param derefAliases: Specifies the behavior on how aliases are dereferenced. Default: 0 (never) | |
556 | :type derefAliases: int | |
557 | :param timeLimit: Maximum time the search should take. If time limit reached the server SHOULD return an error | |
558 | :type timeLimit: int | |
559 | :param controls: additional controls to be passed in the query | |
560 | :type controls: List[class:`Control`] | |
561 | :param return_done: Controls wether the final 'done' LDAP message should be returned, or just the actual results | |
562 | :type return_done: bool | |
563 | ||
564 | :return: Async generator which yields (`LDAPMessage`, None) tuple on success or (None, `Exception`) on error | |
565 | :rtype: Iterator[(:class:`LDAPMessage`, :class:`Exception`)] | |
566 | """ | |
567 | if self.status != MSLDAPClientStatus.RUNNING: | |
568 | yield None, Exception('Connection not running! Probably encountered an error') | |
569 | return | |
355 | 570 | try: |
356 | 571 | if timeLimit is None: |
357 | 572 | timeLimit = 600 #not sure |
573 | ||
574 | flt = query_syntax_converter(query) | |
358 | 575 | |
359 | 576 | searchreq = { |
360 | 'baseObject' : base, | |
577 | 'baseObject' : base.encode(), | |
361 | 578 | 'scope': search_scope, |
362 | 579 | 'derefAliases': derefAliases, |
363 | 'sizeLimit': paged_size, | |
580 | 'sizeLimit': size_limit, | |
364 | 581 | 'timeLimit': timeLimit, |
365 | 'typesOnly': typesOnly, | |
366 | 'filter': filter, | |
582 | 'typesOnly': types_only, | |
583 | 'filter': flt, | |
367 | 584 | 'attributes': attributes, |
368 | 585 | } |
369 | 586 | |
380 | 597 | msg_type = message['protocolOp'].name |
381 | 598 | message = message.native |
382 | 599 | if msg_type == 'searchResDone': |
383 | #print(message) | |
384 | #print('BREAKING!') | |
385 | 600 | if return_done is True: |
386 | 601 | yield (message, None) |
387 | 602 | break |
402 | 617 | except Exception as e: |
403 | 618 | yield (None, e) |
404 | 619 | |
405 | async def pagedsearch(self, base, filter, attributes, search_scope = 2, paged_size = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None): | |
620 | async def pagedsearch(self, base, query, attributes, search_scope = 2, size_limit = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None, rate_limit = 0): | |
621 | """ | |
622 | Paged search is the same as the search operation and uses it under the hood. Adds automatic control to read all results in a paged manner. | |
623 | ||
624 | :param base: base tree on which the search should be performed | |
625 | :type base: str | |
626 | :param query: filter query that defines what should be searched for | |
627 | :type query: str | |
628 | :param attributes: a list of attributes to be included in the response | |
629 | :type attributes: List[str] | |
630 | :param search_scope: Specifies the search operation's scope. Default: 2 (Subtree) | |
631 | :type search_scope: int | |
632 | :param types_only: indicates whether the entries returned should include attribute types only or both types and values. Default: False (both) | |
633 | :type types_only: bool | |
634 | :param size_limit: Size limit of result elements per query. Default: 1000 | |
635 | :type size_limit: int | |
636 | :param derefAliases: Specifies the behavior on how aliases are dereferenced. Default: 0 (never) | |
637 | :type derefAliases: int | |
638 | :param timeLimit: Maximum time the search should take. If time limit reached the server SHOULD return an error | |
639 | :type timeLimit: int | |
640 | :param controls: additional controls to be passed in the query | |
641 | :type controls: dict | |
642 | :param rate_limit: time to sleep bwetween each query | |
643 | :type rate_limit: float | |
644 | :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error | |
645 | :rtype: Iterator[(:class:`dict`, :class:`Exception`)] | |
646 | """ | |
647 | ||
648 | if self.status != MSLDAPClientStatus.RUNNING: | |
649 | yield None, Exception('Connection not running! Probably encountered an error') | |
650 | return | |
406 | 651 | try: |
407 | 652 | cookie = b'' |
408 | 653 | while True: |
409 | ||
654 | await asyncio.sleep(rate_limit) | |
410 | 655 | ctrl_list_temp = [ |
411 | 656 | Control({ |
412 | 657 | 'controlType' : b'1.2.840.113556.1.4.319', |
413 | 658 | 'controlValue': SearchControlValue({ |
414 | 'size' : paged_size, | |
659 | 'size' : size_limit, | |
415 | 660 | 'cookie': cookie |
416 | 661 | }).dump() |
417 | 662 | }) |
426 | 671 | |
427 | 672 | async for res, err in self.search( |
428 | 673 | base, |
429 | filter, | |
674 | query, | |
430 | 675 | attributes, |
431 | 676 | search_scope = search_scope, |
432 | paged_size=paged_size, | |
433 | typesOnly=typesOnly, | |
677 | size_limit=size_limit, | |
678 | types_only=typesOnly, | |
434 | 679 | derefAliases=derefAliases, |
435 | 680 | timeLimit=timeLimit, |
436 | 681 | controls = ctrs, |
439 | 684 | if err is not None: |
440 | 685 | yield (None, err) |
441 | 686 | return |
442 | ||
687 | ||
443 | 688 | if 'resultCode' in res['protocolOp']: |
444 | 689 | for control in res['controls']: |
445 | 690 | if control['controlType'] == b'1.2.840.113556.1.4.319': |
461 | 706 | |
462 | 707 | |
463 | 708 | async def get_serverinfo(self): |
709 | if self.status != MSLDAPClientStatus.RUNNING: | |
710 | return None, Exception('Connection not running! Probably encountered an error') | |
711 | ||
464 | 712 | attributes = [ |
465 | 713 | b'subschemaSubentry', |
466 | 714 | b'dsServiceName', |
496 | 744 | |
497 | 745 | msg_id = await self.send_message(msg) |
498 | 746 | res = await self.recv_message(msg_id) |
499 | res = res[0].native | |
500 | ||
747 | res = res[0] | |
501 | 748 | if isinstance(res, Exception): |
502 | 749 | return None, res |
503 | 750 | |
504 | 751 | #print('res') |
505 | 752 | #print(res) |
506 | return convert_attributes(res['protocolOp']['attributes']), None | |
753 | return convert_attributes(res.native['protocolOp']['attributes']), None | |
507 | 754 | |
508 | 755 | |
509 | 756 | async def amain(): |
524 | 771 | #target.dc_ip = '10.10.10.2' |
525 | 772 | #target.domain = 'TEST' |
526 | 773 | |
527 | url = 'ldap+kerberos-password://test\\victim:Passw0rd!1@WIN2019AD/?dc=10.10.10.2' | |
774 | url = 'ldaps+ntlm-password://test\\Administrator:QLFbT8zkiFGlJuf0B3Qq@WIN2019AD/?dc=10.10.10.2' | |
528 | 775 | |
529 | 776 | dec = MSLDAPURLDecoder(url) |
530 | 777 | cred = dec.get_credential() |
540 | 787 | res, err = await client.bind() |
541 | 788 | if err is not None: |
542 | 789 | raise err |
543 | ||
544 | #res = await client.search_test_2() | |
545 | #pprint.pprint(res) | |
546 | #search = bytes.fromhex('30840000007702012663840000006e043c434e3d3430392c434e3d446973706c6179537065636966696572732c434e3d436f6e66696775726174696f6e2c44433d746573742c44433d636f72700a01000a010002010002020258010100870b6f626a656374436c61737330840000000d040b6f626a656374436c617373') | |
547 | #msg = LDAPMessage.load(search) | |
548 | ||
549 | 790 | |
791 | user = "CN=ldaptest_2,CN=Users,DC=test,DC=corp" | |
792 | #attributes = {'objectClass': ['inetOrgPerson', 'posixGroup', 'top'], 'sn': 'user_sn', 'gidNumber': 0} | |
793 | #res, err = await client.add(user, attributes) | |
794 | #if err is not None: | |
795 | # print(err) | |
796 | ||
797 | #changes = { | |
798 | # 'unicodePwd': [('replace', ['"TESTPassw0rd!1"'])], | |
799 | # #'lockoutTime': [('replace', [0])] | |
800 | #} | |
801 | ||
802 | #res, err = await client.modify(user, changes) | |
803 | #if err is not None: | |
804 | # print('ERR! %s' % err) | |
805 | #else: | |
806 | # print('OK!') | |
550 | 807 | |
551 | qry = r'(sAMAccountName=*)' #'(userAccountControl:1.2.840.113556.1.4.803:=4194304)' #'(sAMAccountName=*)' | |
552 | #qry = r'(sAMAccountType=805306368)' | |
553 | #a = query_syntax_converter(qry) | |
554 | #print(a.native) | |
555 | #input('press bacon!') | |
808 | res, err = await client.delete(user) | |
809 | if err is not None: | |
810 | print('ERR! %s' % err) | |
556 | 811 | |
557 | flt = query_syntax_converter(qry) | |
558 | i = 0 | |
559 | async for res, err in client.pagedsearch(base.encode(), flt, ['*'.encode()], derefAliases=3, typesOnly=False): | |
560 | if err is not None: | |
561 | print('Error!') | |
562 | raise err | |
563 | i += 1 | |
564 | if i % 1000 == 0: | |
565 | print(i) | |
566 | #pprint.pprint(res) | |
567 | ||
568 | 812 | await client.disconnect() |
569 | 813 | |
570 | 814 | |
577 | 821 | |
578 | 822 | logger.setLevel(2) |
579 | 823 | |
580 | #from asn1crypto.core import ObjectIdentifier | |
581 | ||
582 | #o = ObjectIdentifier('1.2.840.113556.1.4.803') | |
583 | #print(o.dump()) | |
584 | ||
585 | #from pprint import pprint | |
586 | #a = bytes.fromhex('3082026202010b63820235040f44433d746573742c44433d636f72700a01020a0103020100020100010100a050a9358116312e322e3834302e3131333535362e312e342e3830338212757365724163636f756e74436f6e74726f6c830734313934333034a217a415040e73414d4163636f756e744e616d653003820124308201bf040e6163636f756e7445787069726573040f62616450617373776f726454696d65040b626164507764436f756e740402636e0408636f646550616765040b636f756e747279436f6465040b646973706c61794e616d65041164697374696e677569736865644e616d650409676976656e4e616d650408696e697469616c73040a6c6173744c6f676f666604096c6173744c6f676f6e04126c6173744c6f676f6e54696d657374616d70040a6c6f676f6e436f756e7404046e616d65040b6465736372697074696f6e040e6f626a65637443617465676f7279040b6f626a656374436c617373040a6f626a6563744755494404096f626a656374536964040e7072696d61727947726f75704944040a7077644c617374536574040e73414d4163636f756e744e616d65040e73414d4163636f756e74547970650402736e0412757365724163636f756e74436f6e74726f6c0411757365725072696e636970616c4e616d65040b7768656e4368616e676564040b7768656e4372656174656404086d656d6265724f6604066d656d6265720414736572766963655072696e636970616c4e616d6504186d7344532d416c6c6f776564546f44656c6567617465546fa02430220416312e322e3834302e3131333535362e312e342e33313904083006020203e80400') | |
587 | #msg = LDAPMessage.load(a) | |
588 | #pprint.pprint(msg.native) | |
824 | ||
825 | asyncio.run(amain()) | |
826 | ||
589 | 827 | |
590 | #input() | |
591 | ||
592 | asyncio.run(amain()) | |
593 | ||
594 | ||
595 | #qry = '(&(sAMAccountType=805306369)(sAMAccountName=test))' | |
596 | #qry = '(sAMAccountName=*)' | |
597 | #flt = LF.parse(qry) | |
598 | #print(flt) | |
599 | #print(flt.__dict__) | |
600 | #for f in flt.filters: | |
601 | # print(f.__dict__) | |
602 | ||
603 | #x = convert(flt) | |
604 | #print(x) | |
605 | #print(x.native) | |
606 | ||
607 | #qry = '(sAMAccountType=0x100)' | |
608 | #flt = Filter.parse(qry) | |
609 | #print(flt) | |
610 | #print(flt.__dict__) | |
611 | #print(flt.filters) | |
612 | 828 | |
613 | 829 | |
614 | 830 |
0 | #!/usr/bin/env python3 | |
1 | # -*- coding: utf-8 -*- | |
2 | # | |
3 | # Copyright © 2019 James Seo <[email protected]> (github.com/kangtastic). | |
4 | # | |
5 | # This file is released under the WTFPL, version 2 (wtfpl.net). | |
6 | # | |
7 | # md4.py: An implementation of the MD4 hash algorithm in pure Python 3. | |
8 | # | |
9 | # Description: Zounds! Yet another rendition of pseudocode from RFC1320! | |
10 | # Bonus points for the algorithm literally being from 1992. | |
11 | # | |
12 | # Usage: Why would anybody use this? This is self-rolled crypto, and | |
13 | # self-rolled *obsolete* crypto at that. DO NOT USE if you need | |
14 | # something "performant" or "secure". :P | |
15 | # | |
16 | # Anyway, from the command line: | |
17 | # | |
18 | # $ ./md4.py [messages] | |
19 | # | |
20 | # where [messages] are some strings to be hashed. | |
21 | # | |
22 | # In Python, use similarly to hashlib (not that it even has MD4): | |
23 | # | |
24 | # from .md4 import MD4 | |
25 | # | |
26 | # digest = MD4("BEES").hexdigest() | |
27 | # | |
28 | # print(digest) # "501af1ef4b68495b5b7e37b15b4cda68" | |
29 | # | |
30 | # | |
31 | # Sample console output: | |
32 | # | |
33 | # Testing the MD4 class. | |
34 | # | |
35 | # Message: b'' | |
36 | # Expected: 31d6cfe0d16ae931b73c59d7e0c089c0 | |
37 | # Actual: 31d6cfe0d16ae931b73c59d7e0c089c0 | |
38 | # | |
39 | # Message: b'The quick brown fox jumps over the lazy dog' | |
40 | # Expected: 1bee69a46ba811185c194762abaeae90 | |
41 | # Actual: 1bee69a46ba811185c194762abaeae90 | |
42 | # | |
43 | # Message: b'BEES' | |
44 | # Expected: 501af1ef4b68495b5b7e37b15b4cda68 | |
45 | # Actual: 501af1ef4b68495b5b7e37b15b4cda68 | |
46 | # | |
47 | import struct | |
48 | ||
49 | ||
50 | class MD4: | |
51 | """An implementation of the MD4 hash algorithm.""" | |
52 | ||
53 | width = 32 | |
54 | mask = 0xFFFFFFFF | |
55 | ||
56 | # Unlike, say, SHA-1, MD4 uses little-endian. Fascinating! | |
57 | h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476] | |
58 | ||
59 | def __init__(self, msg=None): | |
60 | """:param ByteString msg: The message to be hashed.""" | |
61 | if msg is None: | |
62 | msg = b"" | |
63 | ||
64 | self.msg = msg | |
65 | ||
66 | # Pre-processing: Total length is a multiple of 512 bits. | |
67 | ml = len(msg) * 8 | |
68 | msg += b"\x80" | |
69 | msg += b"\x00" * (-(len(msg) + 8) % 64) | |
70 | msg += struct.pack("<Q", ml) | |
71 | ||
72 | # Process the message in successive 512-bit chunks. | |
73 | self._process([msg[i : i + 64] for i in range(0, len(msg), 64)]) | |
74 | ||
75 | def __repr__(self): | |
76 | if self.msg: | |
77 | return f"{self.__class__.__name__}({self.msg:s})" | |
78 | return f"{self.__class__.__name__}()" | |
79 | ||
80 | def __str__(self): | |
81 | return self.hexdigest() | |
82 | ||
83 | def __eq__(self, other): | |
84 | return self.h == other.h | |
85 | ||
86 | def bytes(self): | |
87 | """:return: The final hash value as a `bytes` object.""" | |
88 | return struct.pack("<4L", *self.h) | |
89 | ||
90 | def hexbytes(self): | |
91 | """:return: The final hash value as hexbytes.""" | |
92 | return self.hexdigest().encode | |
93 | ||
94 | def hexdigest(self): | |
95 | """:return: The final hash value as a hexstring.""" | |
96 | return "".join(f"{value:02x}" for value in self.bytes()) | |
97 | ||
98 | def digest(self): | |
99 | return self.bytes() | |
100 | ||
101 | def _process(self, chunks): | |
102 | for chunk in chunks: | |
103 | X, h = list(struct.unpack("<16I", chunk)), self.h.copy() | |
104 | ||
105 | # Round 1. | |
106 | Xi = [3, 7, 11, 19] | |
107 | for n in range(16): | |
108 | i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4)) | |
109 | K, S = n, Xi[n % 4] | |
110 | hn = h[i] + MD4.F(h[j], h[k], h[l]) + X[K] | |
111 | h[i] = MD4.lrot(hn & MD4.mask, S) | |
112 | ||
113 | # Round 2. | |
114 | Xi = [3, 5, 9, 13] | |
115 | for n in range(16): | |
116 | i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4)) | |
117 | K, S = n % 4 * 4 + n // 4, Xi[n % 4] | |
118 | hn = h[i] + MD4.G(h[j], h[k], h[l]) + X[K] + 0x5A827999 | |
119 | h[i] = MD4.lrot(hn & MD4.mask, S) | |
120 | ||
121 | # Round 3. | |
122 | Xi = [3, 9, 11, 15] | |
123 | Ki = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15] | |
124 | for n in range(16): | |
125 | i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4)) | |
126 | K, S = Ki[n], Xi[n % 4] | |
127 | hn = h[i] + MD4.H(h[j], h[k], h[l]) + X[K] + 0x6ED9EBA1 | |
128 | h[i] = MD4.lrot(hn & MD4.mask, S) | |
129 | ||
130 | self.h = [((v + n) & MD4.mask) for v, n in zip(self.h, h)] | |
131 | ||
132 | @staticmethod | |
133 | def F(x, y, z): | |
134 | return (x & y) | (~x & z) | |
135 | ||
136 | @staticmethod | |
137 | def G(x, y, z): | |
138 | return (x & y) | (x & z) | (y & z) | |
139 | ||
140 | @staticmethod | |
141 | def H(x, y, z): | |
142 | return x ^ y ^ z | |
143 | ||
144 | @staticmethod | |
145 | def lrot(value, n): | |
146 | lbits, rbits = (value << n) & MD4.mask, value >> (MD4.width - n) | |
147 | return lbits | rbits | |
148 | ||
149 | ||
150 | def main(): | |
151 | # Import is intentionally delayed. | |
152 | import sys | |
153 | ||
154 | if len(sys.argv) > 1: | |
155 | messages = [msg.encode() for msg in sys.argv[1:]] | |
156 | for message in messages: | |
157 | print(MD4(message).hexdigest()) | |
158 | else: | |
159 | messages = [b"", b"The quick brown fox jumps over the lazy dog", b"BEES"] | |
160 | known_hashes = [ | |
161 | "31d6cfe0d16ae931b73c59d7e0c089c0", | |
162 | "1bee69a46ba811185c194762abaeae90", | |
163 | "501af1ef4b68495b5b7e37b15b4cda68", | |
164 | ] | |
165 | ||
166 | print("Testing the MD4 class.") | |
167 | print() | |
168 | ||
169 | for message, expected in zip(messages, known_hashes): | |
170 | print("Message: ", message) | |
171 | print("Expected:", expected) | |
172 | print("Actual: ", MD4(message).hexdigest()) | |
173 | print() | |
174 | ||
175 | ||
176 | if __name__ == "__main__": | |
177 | try: | |
178 | main() | |
179 | except KeyboardInterrupt: | |
180 | pass |
4 | 4 | currently it's not the perfect wrapper, needs to be extended |
5 | 5 | """ |
6 | 6 | |
7 | from aiosmb.crypto.BASE import symmetricBASE, cipherMODE | |
8 | from aiosmb.crypto.pure.RC4.RC4 import RC4 as _pureRC4 | |
7 | from msldap.crypto.BASE import symmetricBASE, cipherMODE | |
8 | from msldap.crypto.pure.RC4.RC4 import RC4 as _pureRC4 | |
9 | 9 | try: |
10 | 10 | from Crypto.Cipher import ARC4 as _pyCryptoRC4 |
11 | 11 | except Exception as e: |
0 | 0 | import hashlib |
1 | 1 | import hmac |
2 | 2 | |
3 | from aiosmb.crypto.BASE import hashBASE, hmacBASE | |
3 | from msldap.crypto.BASE import hashBASE, hmacBASE | |
4 | from msldap.crypto.MD4 import MD4 | |
4 | 5 | |
5 | 6 | class md5(hashBASE): |
6 | 7 | def __init__(self, data = None): |
14 | 15 | def hexdigest(self): |
15 | 16 | return self._hash.hexdigest() |
16 | 17 | |
17 | class md4(hashBASE): | |
18 | def __init__(self, data = None): | |
19 | hashBASE.__init__(self, data) | |
20 | def setup_hash(self): | |
21 | self._hash = hashlib.new('md4') | |
22 | def update(self, data): | |
23 | return self._hash.update(data) | |
24 | def digest(self): | |
25 | return self._hash.digest() | |
26 | def hexdigest(self): | |
27 | return self._hash.hexdigest() | |
18 | #class md4(hashBASE): | |
19 | # def __init__(self, data = None): | |
20 | # hashBASE.__init__(self, data) | |
21 | # def setup_hash(self): | |
22 | # self._hash = hashlib.new('md4') | |
23 | # def update(self, data): | |
24 | # return self._hash.update(data) | |
25 | # def digest(self): | |
26 | # return self._hash.digest() | |
27 | # def hexdigest(self): | |
28 | # return self._hash.hexdigest() | |
29 | ||
30 | md4 = MD4 | |
28 | 31 | |
29 | 32 | class hmac_md5(hmacBASE): |
30 | 33 | def __init__(self, key): |
9 | 9 | import csv |
10 | 10 | import shlex |
11 | 11 | import datetime |
12 | ||
13 | from aiocmd import aiocmd | |
14 | from asciitree import LeftAligned | |
12 | import copy | |
13 | ||
14 | from msldap.external.aiocmd.aiocmd import aiocmd | |
15 | from msldap.external.asciitree.asciitree import LeftAligned | |
15 | 16 | from tqdm import tqdm |
16 | 17 | |
17 | 18 | from msldap import logger |
21 | 22 | from msldap.ldap_objects import MSADUser, MSADMachine, MSADUser_TSV_ATTRS |
22 | 23 | |
23 | 24 | from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR |
25 | from winacl.dtyp.ace import ACCESS_ALLOWED_OBJECT_ACE, ADS_ACCESS_MASK | |
26 | from winacl.dtyp.sid import SID | |
27 | from winacl.dtyp.guid import GUID | |
24 | 28 | |
25 | 29 | |
26 | 30 | class MSLDAPClientConsole(aiocmd.PromptToolkitCmd): |
32 | 36 | self.connection = None |
33 | 37 | self.adinfo = None |
34 | 38 | self.ldapinfo = None |
39 | self.domain_name = None | |
35 | 40 | |
36 | 41 | async def do_login(self, url = None): |
37 | 42 | """Performs connection and login""" |
38 | try: | |
39 | print('url %s' % repr(url)) | |
40 | ||
43 | try: | |
41 | 44 | if self.conn_url is None and url is None: |
42 | 45 | print('Not url was set, cant do logon') |
43 | 46 | if url is not None: |
44 | 47 | self.conn_url = MSLDAPURLDecoder(url) |
45 | 48 | |
46 | print(self.conn_url.get_credential()) | |
47 | print(self.conn_url.get_target()) | |
49 | logger.debug(self.conn_url.get_credential()) | |
50 | logger.debug(self.conn_url.get_target()) | |
48 | 51 | |
49 | 52 | |
50 | 53 | self.connection = self.conn_url.get_client() |
51 | await self.connection.connect() | |
52 | ||
53 | except: | |
54 | traceback.print_exc() | |
54 | _, err = await self.connection.connect() | |
55 | if err is not None: | |
56 | raise err | |
57 | ||
58 | return True | |
59 | except: | |
60 | traceback.print_exc() | |
61 | return False | |
55 | 62 | |
56 | 63 | async def do_ldapinfo(self, show = True): |
57 | 64 | """Prints detailed LDAP connection info (DSA)""" |
59 | 66 | if self.ldapinfo is None: |
60 | 67 | self.ldapinfo = self.connection.get_server_info() |
61 | 68 | if show is True: |
62 | print(self.ldapinfo) | |
63 | except: | |
64 | traceback.print_exc() | |
69 | for k in self.ldapinfo: | |
70 | print('%s : %s' % (k, self.ldapinfo[k])) | |
71 | return True | |
72 | except: | |
73 | traceback.print_exc() | |
74 | return False | |
65 | 75 | |
66 | 76 | async def do_adinfo(self, show = True): |
67 | 77 | """Prints detailed Active Driectory info""" |
68 | 78 | try: |
69 | 79 | if self.adinfo is None: |
70 | 80 | self.adinfo = self.connection._ldapinfo |
81 | self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') | |
71 | 82 | if show is True: |
72 | 83 | print(self.adinfo) |
73 | except: | |
74 | traceback.print_exc() | |
84 | return True | |
85 | except: | |
86 | traceback.print_exc() | |
87 | return False | |
75 | 88 | |
76 | 89 | async def do_spns(self): |
77 | 90 | """Fetches kerberoastable user accounts""" |
78 | 91 | try: |
79 | 92 | await self.do_ldapinfo(False) |
80 | async for user in self.connection.get_all_service_user_objects(): | |
93 | async for user, err in self.connection.get_all_service_users(): | |
94 | if err is not None: | |
95 | raise err | |
81 | 96 | print(user.sAMAccountName) |
82 | except: | |
83 | traceback.print_exc() | |
84 | ||
97 | ||
98 | return True | |
99 | except: | |
100 | traceback.print_exc() | |
101 | return False | |
102 | ||
85 | 103 | async def do_asrep(self): |
86 | 104 | """Fetches ASREP-roastable user accounts""" |
87 | 105 | try: |
88 | 106 | await self.do_ldapinfo(False) |
89 | async for user in self.connection.get_all_knoreq_user_objects(): | |
107 | async for user, err in self.connection.get_all_knoreq_users(): | |
108 | if err is not None: | |
109 | raise err | |
90 | 110 | print(user.sAMAccountName) |
91 | except: | |
92 | traceback.print_exc() | |
93 | ||
111 | return True | |
112 | except: | |
113 | traceback.print_exc() | |
114 | return False | |
115 | ||
116 | async def do_computeraddr(self): | |
117 | """Fetches all computer accounts""" | |
118 | try: | |
119 | await self.do_adinfo(False) | |
120 | #machine_filename = '%s_computers_%s.txt' % (self.domain_name, datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) | |
121 | ||
122 | async for machine, err in self.connection.get_all_machines(attrs=['sAMAccountName', 'dNSHostName']): | |
123 | if err is not None: | |
124 | raise err | |
125 | ||
126 | dns = machine.dNSHostName | |
127 | if dns is None: | |
128 | dns = '%s.%s' % (machine.sAMAccountName[:-1], self.domain_name) | |
129 | ||
130 | print(str(dns)) | |
131 | return True | |
132 | except: | |
133 | traceback.print_exc() | |
134 | return False | |
94 | 135 | |
95 | 136 | async def do_dump(self): |
96 | 137 | """Fetches ALL user and machine accounts from the domain with a LOT of attributes""" |
101 | 142 | users_filename = 'users_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") |
102 | 143 | pbar = tqdm(desc = 'Writing users to file %s' % users_filename) |
103 | 144 | with open(users_filename, 'w', newline='', encoding = 'utf8') as f: |
104 | async for user in self.connection.get_all_user_objects(): | |
145 | async for user, err in self.connection.get_all_users(): | |
146 | if err is not None: | |
147 | raise err | |
105 | 148 | pbar.update() |
106 | 149 | f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS))) |
107 | 150 | print('Users dump was written to %s' % users_filename) |
109 | 152 | users_filename = 'computers_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") |
110 | 153 | pbar = tqdm(desc = 'Writing computers to file %s' % users_filename) |
111 | 154 | with open(users_filename, 'w', newline='', encoding = 'utf8') as f: |
112 | async for user in self.connection.get_all_machine_objects(): | |
155 | async for user, err in self.connection.get_all_machines(): | |
156 | if err is not None: | |
157 | raise err | |
113 | 158 | pbar.update() |
114 | 159 | f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS))) |
115 | 160 | print('Computer dump was written to %s' % users_filename) |
116 | except: | |
117 | traceback.print_exc() | |
118 | ||
161 | return True | |
162 | except: | |
163 | traceback.print_exc() | |
164 | return False | |
165 | ||
119 | 166 | async def do_query(self, query, attributes = None): |
120 | 167 | """Performs a raw LDAP query against the server. Secondary parameter is the requested attributes SEPARATED WITH COMMA (,)""" |
121 | 168 | try: |
126 | 173 | attributes = attributes.split(',') |
127 | 174 | logging.debug('Query: %s' % (query)) |
128 | 175 | logging.debug('Attributes: %s' % (attributes)) |
129 | async for entry in self.connection.pagedsearch(query, attributes): | |
176 | async for entry, err in self.connection.pagedsearch(query, attributes): | |
177 | if err is not None: | |
178 | raise err | |
130 | 179 | print(entry) |
131 | except: | |
132 | traceback.print_exc() | |
180 | return True | |
181 | except: | |
182 | traceback.print_exc() | |
183 | return False | |
133 | 184 | |
134 | 185 | async def do_tree(self, dn = None, level = 1): |
135 | 186 | """Prints a tree from the given DN (if not set, the top) and with a given depth (default: 1)""" |
155 | 206 | tr = LeftAligned() |
156 | 207 | print(tr(tree_data)) |
157 | 208 | |
158 | ||
159 | except: | |
160 | traceback.print_exc() | |
209 | return True | |
210 | except: | |
211 | traceback.print_exc() | |
212 | return False | |
161 | 213 | |
162 | 214 | async def do_user(self, samaccountname): |
163 | 215 | """Feteches a user object based on the sAMAccountName of the user""" |
164 | 216 | try: |
165 | 217 | await self.do_ldapinfo(False) |
166 | 218 | await self.do_adinfo(False) |
167 | async for user in self.connection.get_user(samaccountname): | |
219 | user, err = await self.connection.get_user(samaccountname) | |
220 | if err is not None: | |
221 | raise err | |
222 | if user is None: | |
223 | print('User not found!') | |
224 | else: | |
168 | 225 | print(user) |
169 | except: | |
170 | traceback.print_exc() | |
171 | ||
172 | async def do_acl(self, dn): | |
226 | ||
227 | return True | |
228 | except: | |
229 | traceback.print_exc() | |
230 | return False | |
231 | ||
232 | async def do_machine(self, samaccountname): | |
233 | """Feteches a machine object based on the sAMAccountName of the machine""" | |
234 | try: | |
235 | await self.do_ldapinfo(False) | |
236 | await self.do_adinfo(False) | |
237 | machine, err = await self.connection.get_machine(samaccountname) | |
238 | if err is not None: | |
239 | raise err | |
240 | if machine is None: | |
241 | print('machine not found!') | |
242 | else: | |
243 | print(machine) | |
244 | ####TEST | |
245 | x = SECURITY_DESCRIPTOR.from_bytes(machine.allowedtoactonbehalfofotheridentity) | |
246 | print(x) | |
247 | ||
248 | return True | |
249 | except: | |
250 | traceback.print_exc() | |
251 | return False | |
252 | ||
253 | async def do_schemaentry(self, cn): | |
254 | """Feteches a schema object entry object based on the DN of the object (must start with CN=)""" | |
255 | try: | |
256 | await self.do_ldapinfo(False) | |
257 | await self.do_adinfo(False) | |
258 | schemaentry, err = await self.connection.get_schemaentry(cn) | |
259 | if err is not None: | |
260 | raise err | |
261 | ||
262 | print(str(schemaentry)) | |
263 | return True | |
264 | except: | |
265 | traceback.print_exc() | |
266 | return False | |
267 | ||
268 | async def do_allschemaentry(self): | |
269 | """Feteches all schema object entry objects""" | |
270 | try: | |
271 | await self.do_ldapinfo(False) | |
272 | await self.do_adinfo(False) | |
273 | async for schemaentry, err in self.connection.get_all_schemaentry(): | |
274 | if err is not None: | |
275 | raise err | |
276 | ||
277 | print(str(schemaentry)) | |
278 | return True | |
279 | except: | |
280 | traceback.print_exc() | |
281 | return False | |
282 | ||
283 | #async def do_addallowedtoactonbehalfofotheridentity(self, target_name, add_computer_name): | |
284 | # """Adds a SID to the msDS-AllowedToActOnBehalfOfOtherIdentity protperty of target_dn""" | |
285 | # try: | |
286 | # await self.do_ldapinfo(False) | |
287 | # await self.do_adinfo(False) | |
288 | # | |
289 | # try: | |
290 | # new_owner_sid = SID.from_string(sid) | |
291 | # except: | |
292 | # print('Incorrect SID!') | |
293 | # return False, Exception('Incorrect SID') | |
294 | # | |
295 | # | |
296 | # target_sd = None | |
297 | # if target_attribute is None or target_attribute == '': | |
298 | # target_attribute = 'nTSecurityDescriptor' | |
299 | # res, err = await self.connection.get_objectacl_by_dn(target_dn) | |
300 | # if err is not None: | |
301 | # raise err | |
302 | # target_sd = SECURITY_DESCRIPTOR.from_bytes(res) | |
303 | # else: | |
304 | # | |
305 | # query = '(distinguishedName=%s)' % target_dn | |
306 | # async for entry, err in self.connection.pagedsearch(query, [target_attribute]): | |
307 | # if err is not None: | |
308 | # raise err | |
309 | # print(entry['attributes'][target_attribute]) | |
310 | # target_sd = SECURITY_DESCRIPTOR.from_bytes(entry['attributes'][target_attribute]) | |
311 | # break | |
312 | # else: | |
313 | # print('Target DN not found!') | |
314 | # return False, Exception('Target DN not found!') | |
315 | # | |
316 | # print(target_sd) | |
317 | # new_sd = copy.deepcopy(target_sd) | |
318 | # new_sd.Owner = new_owner_sid | |
319 | # print(new_sd) | |
320 | # | |
321 | # changes = { | |
322 | # target_attribute : [('replace', [new_sd.to_bytes()])] | |
323 | # } | |
324 | # _, err = await self.connection.modify(target_dn, changes) | |
325 | # if err is not None: | |
326 | # raise err | |
327 | # | |
328 | # print('Change OK!') | |
329 | # except: | |
330 | # traceback.print_exc() | |
331 | ||
332 | async def do_changeowner(self, new_owner_sid, target_dn, target_attribute = None): | |
333 | """Changes the owner in a Security Descriptor to the new_owner_sid on an LDAP object or on an LDAP object's attribute identified by target_dn and target_attribute. target_attribute can be omitted to change the target_dn's SD's owner""" | |
334 | try: | |
335 | await self.do_ldapinfo(False) | |
336 | await self.do_adinfo(False) | |
337 | ||
338 | _, err = await self.connection.change_priv_owner(new_owner_sid, target_dn, target_attribute = target_attribute) | |
339 | if err is not None: | |
340 | raise err | |
341 | except: | |
342 | traceback.print_exc() | |
343 | return False | |
344 | ||
345 | async def do_addprivdcsync(self, user_dn, forest = None): | |
346 | """Adds DCSync rights to the given user by modifying the forest's Security Descriptor to add GetChanges and GetChangesAll ACE""" | |
347 | try: | |
348 | await self.do_ldapinfo(False) | |
349 | await self.do_adinfo(False) | |
350 | ||
351 | _, err = await self.connection.add_priv_dcsync(user_dn, self.adinfo.distinguishedName) | |
352 | if err is not None: | |
353 | raise err | |
354 | ||
355 | print('Change OK!') | |
356 | return True | |
357 | except: | |
358 | traceback.print_exc() | |
359 | return False | |
360 | ||
361 | async def do_addprivaddmember(self, user_dn, group_dn): | |
362 | """Adds AddMember rights to the user on the group specified by group_dn""" | |
363 | try: | |
364 | await self.do_ldapinfo(False) | |
365 | await self.do_adinfo(False) | |
366 | ||
367 | _, err = await self.connection.add_priv_addmember(user_dn, group_dn) | |
368 | if err is not None: | |
369 | raise err | |
370 | ||
371 | print('Change OK!') | |
372 | return True | |
373 | except: | |
374 | traceback.print_exc() | |
375 | return False | |
376 | ||
377 | async def do_setsd(self, target_dn, sddl): | |
378 | """Updates the security descriptor of an object""" | |
379 | try: | |
380 | await self.do_ldapinfo(False) | |
381 | await self.do_adinfo(False) | |
382 | ||
383 | try: | |
384 | new_sd = SECURITY_DESCRIPTOR.from_sddl(sddl) | |
385 | except: | |
386 | print('Incorrect SDDL input!') | |
387 | return False, Exception('Incorrect SDDL input!') | |
388 | ||
389 | _, err = await self.connection.set_objectacl_by_dn(target_dn, new_sd.to_bytes()) | |
390 | if err is not None: | |
391 | raise err | |
392 | print('Change OK!') | |
393 | return True | |
394 | except: | |
395 | print('Erro while updating security descriptor!') | |
396 | traceback.print_exc() | |
397 | return False | |
398 | ||
399 | async def do_getsd(self, dn): | |
173 | 400 | """Feteches security info for a given DN""" |
174 | 401 | try: |
175 | 402 | await self.do_ldapinfo(False) |
176 | 403 | await self.do_adinfo(False) |
177 | async for sec_info in self.connection.get_objectacl_by_dn(dn): | |
178 | print(str(SECURITY_DESCRIPTOR.from_bytes(sec_info.nTSecurityDescriptor))) | |
179 | except: | |
180 | traceback.print_exc() | |
404 | sec_info, err = await self.connection.get_objectacl_by_dn(dn) | |
405 | if err is not None: | |
406 | raise err | |
407 | sd = SECURITY_DESCRIPTOR.from_bytes(sec_info) | |
408 | print(sd.to_sddl()) | |
409 | return True | |
410 | except: | |
411 | traceback.print_exc() | |
412 | return False | |
181 | 413 | |
182 | 414 | async def do_gpos(self): |
183 | 415 | """Feteches security info for a given DN""" |
184 | 416 | try: |
185 | 417 | await self.do_ldapinfo(False) |
186 | 418 | await self.do_adinfo(False) |
187 | async for gpo in self.connection.get_all_gpos(): | |
419 | async for gpo, err in self.connection.get_all_gpos(): | |
420 | if err is not None: | |
421 | raise err | |
188 | 422 | print(gpo) |
189 | except: | |
190 | traceback.print_exc() | |
423 | ||
424 | return True | |
425 | except: | |
426 | traceback.print_exc() | |
427 | return False | |
191 | 428 | |
192 | 429 | async def do_laps(self): |
193 | 430 | """Feteches all laps passwords""" |
194 | 431 | try: |
195 | async for entry in self.connection.get_all_laps(): | |
432 | async for entry, err in self.connection.get_all_laps(): | |
433 | if err is not None: | |
434 | raise err | |
196 | 435 | pwd = '<MISSING>' |
197 | if 'ms-mcs-AdmPwd' in entry['attributes']: | |
198 | pwd = entry['attributes']['ms-mcs-AdmPwd'] | |
436 | if 'ms-Mcs-AdmPwd' in entry['attributes']: | |
437 | pwd = entry['attributes']['ms-Mcs-AdmPwd'] | |
199 | 438 | print('%s : %s' % (entry['attributes']['cn'], pwd)) |
200 | except: | |
201 | traceback.print_exc() | |
439 | ||
440 | return True | |
441 | except: | |
442 | traceback.print_exc() | |
443 | return False | |
202 | 444 | |
203 | 445 | async def do_groupmembership(self, dn): |
204 | 446 | """Feteches names all groupnames the user is a member of for a given DN""" |
206 | 448 | await self.do_ldapinfo(False) |
207 | 449 | await self.do_adinfo(False) |
208 | 450 | group_sids = [] |
209 | async for group_sid in self.connection.get_tokengroups(dn): | |
451 | async for group_sid, err in self.connection.get_tokengroups(dn): | |
452 | if err is not None: | |
453 | raise err | |
210 | 454 | group_sids.append(group_sids) |
211 | group_dn = await self.connection.get_dn_for_objectsid(group_sid) | |
455 | group_dn, err = await self.connection.get_dn_for_objectsid(group_sid) | |
456 | if err is not None: | |
457 | raise err | |
212 | 458 | print('%s - %s' % (group_dn, group_sid)) |
213 | 459 | |
214 | 460 | if len(group_sids) == 0: |
215 | 461 | print('No memberships found') |
216 | except: | |
217 | traceback.print_exc() | |
462 | ||
463 | return True | |
464 | except Exception as e: | |
465 | traceback.print_exc() | |
466 | return False | |
218 | 467 | |
219 | 468 | async def do_bindtree(self, newtree): |
220 | 469 | """Changes the LDAP TREE for future queries. |
225 | 474 | async def do_trusts(self): |
226 | 475 | """Feteches gives back domain trusts""" |
227 | 476 | try: |
228 | async for entry in self.connection.get_all_trusts(): | |
477 | async for entry, err in self.connection.get_all_trusts(): | |
478 | if err is not None: | |
479 | raise err | |
229 | 480 | print(entry.get_line()) |
230 | except: | |
231 | traceback.print_exc() | |
232 | ||
481 | ||
482 | return True | |
483 | except: | |
484 | traceback.print_exc() | |
485 | return False | |
486 | ||
487 | async def do_adduser(self, user_dn, password): | |
488 | """Creates a new domain user with password""" | |
489 | try: | |
490 | _, err = await self.connection.create_user_dn(user_dn, password) | |
491 | if err is not None: | |
492 | raise err | |
493 | print('User added') | |
494 | return True | |
495 | except: | |
496 | traceback.print_exc() | |
497 | return False | |
498 | ||
499 | ||
500 | async def do_deluser(self, user_dn): | |
501 | """Deletes the user! This action is irrecoverable (actually domain admins can do that but probably will shout with you)""" | |
502 | try: | |
503 | _, err = await self.connection.delete_user(user_dn) | |
504 | if err is not None: | |
505 | raise err | |
506 | print('Goodbye, Caroline.') | |
507 | return True | |
508 | except: | |
509 | traceback.print_exc() | |
510 | return False | |
511 | ||
512 | async def do_changeuserpw(self, user_dn, newpass, oldpass = None): | |
513 | """Changes user password, if you are admin then old pw doesnt need to be supplied""" | |
514 | try: | |
515 | _, err = await self.connection.change_password(user_dn, newpass, oldpass) | |
516 | if err is not None: | |
517 | raise err | |
518 | print('User password changed') | |
519 | return True | |
520 | except: | |
521 | traceback.print_exc() | |
522 | return False | |
523 | ||
524 | async def do_unlockuser(self, user_dn): | |
525 | """Unlock user by setting lockoutTime to 0""" | |
526 | try: | |
527 | _, err = await self.connection.unlock_user(user_dn) | |
528 | if err is not None: | |
529 | raise err | |
530 | print('User unlocked') | |
531 | return True | |
532 | except: | |
533 | traceback.print_exc() | |
534 | return False | |
535 | ||
536 | async def do_enableuser(self, user_dn): | |
537 | """Unlock user by flipping useraccountcontrol bits""" | |
538 | try: | |
539 | _, err = await self.connection.enable_user(user_dn) | |
540 | if err is not None: | |
541 | raise err | |
542 | print('User enabled') | |
543 | return True | |
544 | except: | |
545 | traceback.print_exc() | |
546 | return False | |
547 | ||
548 | async def do_disableuser(self, user_dn): | |
549 | """Unlock user by flipping useraccountcontrol bits""" | |
550 | try: | |
551 | _, err = await self.connection.disable_user(user_dn) | |
552 | if err is not None: | |
553 | raise err | |
554 | print('User disabled') | |
555 | return True | |
556 | except: | |
557 | traceback.print_exc() | |
558 | return False | |
559 | ||
560 | async def do_addspn(self, user_dn, spn): | |
561 | """Adds an SPN entry to the users account""" | |
562 | try: | |
563 | _, err = await self.connection.add_user_spn(user_dn, spn) | |
564 | if err is not None: | |
565 | raise err | |
566 | print('SPN added!') | |
567 | return True | |
568 | except: | |
569 | traceback.print_exc() | |
570 | return False | |
571 | ||
572 | async def do_addhostname(self, user_dn, hostname): | |
573 | """Adds additional hostname to computer account""" | |
574 | try: | |
575 | _, err = await self.connection.add_additional_hostname(user_dn, hostname) | |
576 | if err is not None: | |
577 | raise err | |
578 | print('Hostname added!') | |
579 | return True | |
580 | except: | |
581 | traceback.print_exc() | |
582 | return False | |
583 | ||
584 | async def do_addusertogroup(self, user_dn, group_dn): | |
585 | """Adds user to specified group. Both user and group must be in DN format!""" | |
586 | try: | |
587 | _, err = await self.connection.add_user_to_group(user_dn, group_dn) | |
588 | if err is not None: | |
589 | raise err | |
590 | print('User added to group!') | |
591 | return True | |
592 | except: | |
593 | traceback.print_exc() | |
594 | return False | |
595 | ||
596 | async def do_deluserfromgroup(self, user_dn, group_dn): | |
597 | """Removes user from specified group. Both user and group must be in DN format!""" | |
598 | try: | |
599 | _, err = await self.connection.del_user_from_group(user_dn, group_dn) | |
600 | if err is not None: | |
601 | raise err | |
602 | print('User added to group!') | |
603 | return True | |
604 | except: | |
605 | traceback.print_exc() | |
606 | return False | |
607 | ||
233 | 608 | async def do_test(self): |
234 | 609 | """testing, dontuse""" |
235 | 610 | try: |
236 | async for entry in self.connection.get_all_objectacl(): | |
611 | async for entry, err in self.connection.get_all_objectacl(): | |
612 | if err is not None: | |
613 | raise err | |
614 | ||
237 | 615 | if entry.objectClass[-1] != 'user': |
238 | 616 | print(entry.objectClass) |
239 | except: | |
240 | traceback.print_exc() | |
617 | ||
618 | return True | |
619 | except: | |
620 | traceback.print_exc() | |
621 | return False | |
241 | 622 | |
242 | 623 | """ |
243 | 624 | async def do_info(self): |
258 | 639 | await client.run() |
259 | 640 | else: |
260 | 641 | for command in args.commands: |
642 | if command == 'i': | |
643 | await client.run() | |
644 | return | |
261 | 645 | cmd = shlex.split(command) |
262 | await client._run_single_command(cmd[0], cmd[1:]) | |
646 | res = await client._run_single_command(cmd[0], cmd[1:]) | |
647 | if res is False: | |
648 | return | |
263 | 649 | |
264 | 650 | def main(): |
265 | 651 | import argparse |
267 | 653 | parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') |
268 | 654 | parser.add_argument('-n', '--no-interactive', action='store_true') |
269 | 655 | parser.add_argument('url', help='Connection string in URL format.') |
270 | parser.add_argument('commands', nargs='*') | |
656 | parser.add_argument('commands', nargs='*', help="Takes a series of commands which will be executed until error encountered. If the command is 'i' is encountered during execution it drops back to interactive shell.") | |
271 | 657 | |
272 | 658 | args = parser.parse_args() |
273 | 659 |
0 | #!/usr/bin/env python3 | |
1 | # | |
2 | # Author: | |
3 | # Tamas Jos (@skelsec) | |
4 | # | |
5 | ||
6 | import asyncio | |
7 | import traceback | |
8 | import logging | |
9 | import csv | |
10 | import shlex | |
11 | import datetime | |
12 | ||
13 | from msldap.external.aiocmd.aiocmd import aiocmd | |
14 | from msldap.external.asciitree.asciitree import LeftAligned | |
15 | from tqdm import tqdm | |
16 | ||
17 | from msldap import logger | |
18 | from asysocks import logger as sockslogger | |
19 | from msldap.client import MSLDAPClient | |
20 | from msldap.commons.url import MSLDAPURLDecoder | |
21 | from msldap.ldap_objects import MSADUser, MSADMachine, MSADUser_TSV_ATTRS | |
22 | ||
23 | from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR | |
24 | ||
25 | ||
26 | class MSLDAPCompDomainList: | |
27 | def __init__(self, ldap_url): | |
28 | self.conn_url = ldap_url | |
29 | self.connection = None | |
30 | self.adinfo = None | |
31 | self.ldapinfo = None | |
32 | self.domain_name = None | |
33 | ||
34 | async def login(self): | |
35 | """Performs connection and login""" | |
36 | try: | |
37 | logger.debug(self.conn_url.get_credential()) | |
38 | logger.debug(self.conn_url.get_target()) | |
39 | ||
40 | ||
41 | self.connection = self.conn_url.get_client() | |
42 | _, err = await self.connection.connect() | |
43 | if err is not None: | |
44 | raise err | |
45 | ||
46 | return True, None | |
47 | except Exception as e: | |
48 | return False, e | |
49 | ||
50 | async def do_adinfo(self, show = True): | |
51 | """Prints detailed Active Driectory info""" | |
52 | try: | |
53 | if self.adinfo is None: | |
54 | self.adinfo = self.connection._ldapinfo | |
55 | self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') | |
56 | if show is True: | |
57 | print(self.adinfo) | |
58 | ||
59 | return True, None | |
60 | except Exception as e: | |
61 | return False, e | |
62 | ||
63 | async def run(self): | |
64 | try: | |
65 | _, err = await self.login() | |
66 | if err is not None: | |
67 | raise err | |
68 | _, err = await self.do_adinfo(False) | |
69 | if err is not None: | |
70 | raise err | |
71 | #machine_filename = '%s_computers_%s.txt' % (self.domain_name, datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) | |
72 | ||
73 | async for machine, err in self.connection.get_all_machines(attrs=['sAMAccountName', 'dNSHostName']): | |
74 | if err is not None: | |
75 | raise err | |
76 | ||
77 | dns = machine.dNSHostName | |
78 | if dns is None: | |
79 | dns = '%s.%s' % (machine.sAMAccountName[:-1], self.domain_name) | |
80 | ||
81 | print(str(dns)) | |
82 | except: | |
83 | traceback.print_exc() | |
84 | ||
85 | ||
86 | def main(): | |
87 | import argparse | |
88 | parser = argparse.ArgumentParser(description='MS LDAP library') | |
89 | parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') | |
90 | parser.add_argument('-n', '--no-interactive', action='store_true') | |
91 | parser.add_argument('url', help='Connection string in URL format.') | |
92 | ||
93 | args = parser.parse_args() | |
94 | ||
95 | ||
96 | ###### VERBOSITY | |
97 | if args.verbose == 0: | |
98 | logging.basicConfig(level=logging.INFO) | |
99 | else: | |
100 | sockslogger.setLevel(logging.DEBUG) | |
101 | logger.setLevel(logging.DEBUG) | |
102 | logging.basicConfig(level=logging.DEBUG) | |
103 | ||
104 | ldap_url = MSLDAPURLDecoder(args.url) | |
105 | compdomlist = MSLDAPCompDomainList(ldap_url) | |
106 | ||
107 | ||
108 | asyncio.run(compdomlist.run()) | |
109 | ||
110 | if __name__ == '__main__': | |
111 | main()⏎ |
0 | import asyncio | |
1 | import inspect | |
2 | import shlex | |
3 | import signal | |
4 | import sys | |
5 | ||
6 | from prompt_toolkit import PromptSession | |
7 | from prompt_toolkit.completion import WordCompleter | |
8 | from prompt_toolkit.key_binding import KeyBindings | |
9 | from prompt_toolkit.patch_stdout import patch_stdout | |
10 | ||
11 | try: | |
12 | from prompt_toolkit.completion.nested import NestedCompleter | |
13 | except ImportError: | |
14 | from aiocmd.nested_completer import NestedCompleter | |
15 | ||
16 | ||
17 | class ExitPromptException(Exception): | |
18 | pass | |
19 | ||
20 | ||
21 | class PromptToolkitCmd: | |
22 | """Baseclass for custom CLIs | |
23 | ||
24 | Works similarly to the built-in Cmd class. You can inherit from this class and implement: | |
25 | - do_<action> - This will add the "<action>" command to the cli. | |
26 | The method may receive arguments (required) and keyword arguments (optional). | |
27 | - _<action>_completions - Returns a custom Completer class to use as a completer for this action. | |
28 | Additionally, the user cant change the "prompt" variable to change how the prompt looks, and add | |
29 | command aliases to the 'aliases' dict. | |
30 | """ | |
31 | ATTR_START = "do_" | |
32 | prompt = "$ " | |
33 | doc_header = "Documented commands:" | |
34 | aliases = {"?": "help", "exit": "quit"} | |
35 | ||
36 | def __init__(self, ignore_sigint=True): | |
37 | self.completer = self._make_completer() | |
38 | self.session = None | |
39 | self._ignore_sigint = ignore_sigint | |
40 | self._currently_running_task = None | |
41 | ||
42 | async def run(self): | |
43 | if self._ignore_sigint and sys.platform != "win32": | |
44 | asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self._sigint_handler) | |
45 | self.session = PromptSession(enable_history_search=True, key_bindings=self._get_bindings()) | |
46 | try: | |
47 | with patch_stdout(): | |
48 | await self._run_prompt_forever() | |
49 | finally: | |
50 | if self._ignore_sigint and sys.platform != "win32": | |
51 | asyncio.get_event_loop().remove_signal_handler(signal.SIGINT) | |
52 | self._on_close() | |
53 | ||
54 | async def _run_prompt_forever(self): | |
55 | while True: | |
56 | try: | |
57 | result = await self.session.prompt_async(self.prompt, completer=self.completer) | |
58 | except EOFError: | |
59 | return | |
60 | ||
61 | if not result: | |
62 | continue | |
63 | args = shlex.split(result) | |
64 | if args[0] in self.command_list: | |
65 | try: | |
66 | self._currently_running_task = asyncio.ensure_future( | |
67 | self._run_single_command(args[0], args[1:])) | |
68 | await self._currently_running_task | |
69 | except asyncio.CancelledError: | |
70 | print() | |
71 | continue | |
72 | except ExitPromptException: | |
73 | return | |
74 | else: | |
75 | print("Command %s not found!" % args[0]) | |
76 | ||
77 | def _sigint_handler(self): | |
78 | if self._currently_running_task: | |
79 | self._currently_running_task.cancel() | |
80 | ||
81 | def _get_bindings(self): | |
82 | bindings = KeyBindings() | |
83 | bindings.add("c-c")(lambda event: self._interrupt_handler(event)) | |
84 | return bindings | |
85 | ||
86 | async def _run_single_command(self, command, args): | |
87 | command_real_args, command_real_kwargs = self._get_command_args(command) | |
88 | if len(args) < len(command_real_args) or len(args) > (len(command_real_args) | |
89 | + len(command_real_kwargs)): | |
90 | print("Bad command args. Usage: %s" % self._get_command_usage(command, command_real_args, | |
91 | command_real_kwargs)) | |
92 | return | |
93 | ||
94 | try: | |
95 | com_func = self._get_command(command) | |
96 | if asyncio.iscoroutinefunction(com_func): | |
97 | res = await com_func(*args) | |
98 | else: | |
99 | res = com_func(*args) | |
100 | return res | |
101 | except (ExitPromptException, asyncio.CancelledError): | |
102 | raise | |
103 | except Exception as ex: | |
104 | print("Command failed: ", ex) | |
105 | ||
106 | def _interrupt_handler(self, event): | |
107 | event.cli.current_buffer.text = "" | |
108 | ||
109 | def _make_completer(self): | |
110 | return NestedCompleter({com: self._completer_for_command(com) for com in self.command_list}) | |
111 | ||
112 | def _completer_for_command(self, command): | |
113 | if not hasattr(self, "_%s_completions" % command): | |
114 | return WordCompleter([]) | |
115 | return getattr(self, "_%s_completions" % command)() | |
116 | ||
117 | def _get_command(self, command): | |
118 | if command in self.aliases: | |
119 | command = self.aliases[command] | |
120 | return getattr(self, self.ATTR_START + command) | |
121 | ||
122 | def _get_command_args(self, command): | |
123 | args = [param for param in inspect.signature(self._get_command(command)).parameters.values() | |
124 | if param.default == param.empty] | |
125 | kwargs = [param for param in inspect.signature(self._get_command(command)).parameters.values() | |
126 | if param.default != param.empty] | |
127 | return args, kwargs | |
128 | ||
129 | def _get_command_usage(self, command, args, kwargs): | |
130 | return ("%s %s %s" % (command, | |
131 | " ".join("<%s>" % arg for arg in args), | |
132 | " ".join("[%s]" % kwarg for kwarg in kwargs), | |
133 | )).strip() | |
134 | ||
135 | @property | |
136 | def command_list(self): | |
137 | return [attr[len(self.ATTR_START):] | |
138 | for attr in dir(self) if attr.startswith(self.ATTR_START)] + list(self.aliases.keys()) | |
139 | ||
140 | def do_help(self): | |
141 | print() | |
142 | print(self.doc_header) | |
143 | print("=" * len(self.doc_header)) | |
144 | print() | |
145 | ||
146 | get_usage = lambda command: self._get_command_usage(command, *self._get_command_args(command)) | |
147 | max_usage_len = max([len(get_usage(command)) for command in self.command_list]) | |
148 | for command in sorted(self.command_list): | |
149 | command_doc = self._get_command(command).__doc__ | |
150 | print(("%-" + str(max_usage_len + 2) + "s%s") % (get_usage(command), command_doc or "")) | |
151 | ||
152 | def do_quit(self): | |
153 | """Exit the prompt""" | |
154 | raise ExitPromptException() | |
155 | ||
156 | def _on_close(self): | |
157 | """Optional hook to call on closing the cmd""" | |
158 | pass |
0 | """ | |
1 | Nestedcompleter for completion of hierarchical data structures. | |
2 | """ | |
3 | from typing import Dict, Iterable, Mapping, Optional, Set, Union | |
4 | ||
5 | from prompt_toolkit.completion import CompleteEvent, Completer, Completion | |
6 | from prompt_toolkit.completion.word_completer import WordCompleter | |
7 | from prompt_toolkit.document import Document | |
8 | ||
9 | __all__ = [ | |
10 | 'NestedCompleter' | |
11 | ] | |
12 | ||
13 | NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] | |
14 | ||
15 | ||
16 | class NestedCompleter(Completer): | |
17 | """ | |
18 | Completer which wraps around several other completers, and calls any the | |
19 | one that corresponds with the first word of the input. | |
20 | By combining multiple `NestedCompleter` instances, we can achieve multiple | |
21 | hierarchical levels of autocompletion. This is useful when `WordCompleter` | |
22 | is not sufficient. | |
23 | If you need multiple levels, check out the `from_nested_dict` classmethod. | |
24 | """ | |
25 | def __init__(self, options: Dict[str, Optional[Completer]], | |
26 | ignore_case: bool = True) -> None: | |
27 | ||
28 | self.options = options | |
29 | self.ignore_case = ignore_case | |
30 | ||
31 | def __repr__(self) -> str: | |
32 | return 'NestedCompleter(%r, ignore_case=%r)' % (self.options, self.ignore_case) | |
33 | ||
34 | @classmethod | |
35 | def from_nested_dict(cls, data: NestedDict) -> 'NestedCompleter': | |
36 | """ | |
37 | Create a `NestedCompleter`, starting from a nested dictionary data | |
38 | structure, like this: | |
39 | .. code:: | |
40 | data = { | |
41 | 'show': { | |
42 | 'version': None, | |
43 | 'interfaces': None, | |
44 | 'clock': None, | |
45 | 'ip': {'interface': {'brief'}} | |
46 | }, | |
47 | 'exit': None | |
48 | 'enable': None | |
49 | } | |
50 | The value should be `None` if there is no further completion at some | |
51 | point. If all values in the dictionary are None, it is also possible to | |
52 | use a set instead. | |
53 | Values in this data structure can be a completers as well. | |
54 | """ | |
55 | options = {} | |
56 | for key, value in data.items(): | |
57 | if isinstance(value, Completer): | |
58 | options[key] = value | |
59 | elif isinstance(value, dict): | |
60 | options[key] = cls.from_nested_dict(value) | |
61 | elif isinstance(value, set): | |
62 | options[key] = cls.from_nested_dict({item: None for item in value}) | |
63 | else: | |
64 | assert value is None | |
65 | options[key] = None | |
66 | ||
67 | return cls(options) | |
68 | ||
69 | def get_completions(self, document: Document, | |
70 | complete_event: CompleteEvent) -> Iterable[Completion]: | |
71 | # Split document. | |
72 | text = document.text_before_cursor.lstrip() | |
73 | ||
74 | # If there is a space, check for the first term, and use a | |
75 | # subcompleter. | |
76 | if ' ' in text: | |
77 | first_term = text.split()[0] | |
78 | completer = self.options.get(first_term) | |
79 | ||
80 | # If we have a sub completer, use this for the completions. | |
81 | if completer is not None: | |
82 | remaining_text = document.text[len(first_term):].lstrip() | |
83 | move_cursor = len(document.text) - len(remaining_text) | |
84 | ||
85 | new_document = Document( | |
86 | remaining_text, | |
87 | cursor_position=document.cursor_position - move_cursor) | |
88 | ||
89 | for c in completer.get_completions(new_document, complete_event): | |
90 | yield c | |
91 | ||
92 | # No space in the input: behave exactly like `WordCompleter`. | |
93 | else: | |
94 | completer = WordCompleter(list(self.options.keys()), ignore_case=self.ignore_case) | |
95 | for c in completer.get_completions(document, complete_event): | |
96 | yield c |
0 | from setuptools import setup, find_packages | |
1 | ||
2 | setup(name='aiocmd', | |
3 | packages=find_packages("."), | |
4 | version='0.1.4', | |
5 | author='Dor Green', | |
6 | author_email='[email protected]', | |
7 | description='Coroutine-based CLI generator using prompt_toolkit', | |
8 | url='http://github.com/KimiNewt/aiocmd', | |
9 | keywords=['asyncio', 'cmd'], | |
10 | license='MIT', | |
11 | install_requires=[ | |
12 | 'prompt_toolkit>=2.0.9' | |
13 | ], | |
14 | classifiers=[ | |
15 | 'License :: OSI Approved :: MIT License', | |
16 | ||
17 | 'Programming Language :: Python :: 3', | |
18 | 'Programming Language :: Python :: 3.5', | |
19 | 'Programming Language :: Python :: 3.6', | |
20 | 'Programming Language :: Python :: 3.7' | |
21 | ]) |
0 | #!/usr/bin/env python | |
1 | # -*- coding: utf-8 -*- | |
2 | ||
3 | from .drawing import BoxStyle | |
4 | from .traversal import DictTraversal | |
5 | from .util import KeyArgsConstructor | |
6 | ||
7 | ||
8 | class LeftAligned(KeyArgsConstructor): | |
9 | """Creates a renderer for a left-aligned tree. | |
10 | ||
11 | Any attributes of the resulting class instances can be set using | |
12 | constructor arguments.""" | |
13 | ||
14 | draw = BoxStyle() | |
15 | "The draw style used. See :class:`~asciitree.drawing.Style`." | |
16 | traverse = DictTraversal() | |
17 | "Traversal method. See :class:`~asciitree.traversal.Traversal`." | |
18 | ||
19 | def render(self, node): | |
20 | """Renders a node. This function is used internally, as it returns | |
21 | a list of lines. Use :func:`~asciitree.LeftAligned.__call__` instead. | |
22 | """ | |
23 | lines = [] | |
24 | ||
25 | children = self.traverse.get_children(node) | |
26 | lines.append(self.draw.node_label(self.traverse.get_text(node))) | |
27 | ||
28 | for n, child in enumerate(children): | |
29 | child_tree = self.render(child) | |
30 | ||
31 | if n == len(children) - 1: | |
32 | # last child does not get the line drawn | |
33 | lines.append(self.draw.last_child_head(child_tree.pop(0))) | |
34 | lines.extend(self.draw.last_child_tail(l) | |
35 | for l in child_tree) | |
36 | else: | |
37 | lines.append(self.draw.child_head(child_tree.pop(0))) | |
38 | lines.extend(self.draw.child_tail(l) | |
39 | for l in child_tree) | |
40 | ||
41 | return lines | |
42 | ||
43 | def __call__(self, tree): | |
44 | """Render the tree into string suitable for console output. | |
45 | ||
46 | :param tree: A tree.""" | |
47 | return '\n'.join(self.render(self.traverse.get_root(tree))) | |
48 | ||
49 | ||
50 | # legacy support below | |
51 | ||
52 | from .drawing import Style | |
53 | from .traversal import Traversal | |
54 | ||
55 | ||
56 | class LegacyStyle(Style): | |
57 | def node_label(self, text): | |
58 | return text | |
59 | ||
60 | def child_head(self, label): | |
61 | return ' +--' + label | |
62 | ||
63 | def child_tail(self, line): | |
64 | return ' |' + line | |
65 | ||
66 | def last_child_head(self, label): | |
67 | return ' +--' + label | |
68 | ||
69 | def last_child_tail(self, line): | |
70 | return ' ' + line | |
71 | ||
72 | ||
73 | def draw_tree(node, | |
74 | child_iter=lambda n: n.children, | |
75 | text_str=str): | |
76 | """Support asciitree 0.2 API. | |
77 | ||
78 | This function solely exist to not break old code (using asciitree 0.2). | |
79 | Its use is deprecated.""" | |
80 | return LeftAligned(traverse=Traversal(get_text=text_str, | |
81 | get_children=child_iter), | |
82 | draw=LegacyStyle())(node) |
0 | from .util import KeyArgsConstructor | |
1 | ||
2 | BOX_LIGHT = { | |
3 | 'UP_AND_RIGHT': u'\u2514', | |
4 | 'HORIZONTAL': u'\u2500', | |
5 | 'VERTICAL': u'\u2502', | |
6 | 'VERTICAL_AND_RIGHT': u'\u251C', | |
7 | } #: Unicode box-drawing glyphs, light style | |
8 | ||
9 | ||
10 | BOX_HEAVY = { | |
11 | 'UP_AND_RIGHT': u'\u2517', | |
12 | 'HORIZONTAL': u'\u2501', | |
13 | 'VERTICAL': u'\u2503', | |
14 | 'VERTICAL_AND_RIGHT': u'\u2523', | |
15 | } #: Unicode box-drawing glyphs, heavy style | |
16 | ||
17 | ||
18 | BOX_DOUBLE = { | |
19 | 'UP_AND_RIGHT': u'\u255A', | |
20 | 'HORIZONTAL': u'\u2550', | |
21 | 'VERTICAL': u'\u2551', | |
22 | 'VERTICAL_AND_RIGHT': u'\u2560', | |
23 | } #: Unicode box-drawing glyphs, double-line style | |
24 | ||
25 | ||
26 | BOX_ASCII = { | |
27 | 'UP_AND_RIGHT': u'+', | |
28 | 'HORIZONTAL': u'-', | |
29 | 'VERTICAL': u'|', | |
30 | 'VERTICAL_AND_RIGHT': u'+', | |
31 | } #: Unicode box-drawing glyphs, using only ascii ``|+-`` characters. | |
32 | ||
33 | ||
34 | BOX_BLANK = { | |
35 | 'UP_AND_RIGHT': u' ', | |
36 | 'HORIZONTAL': u' ', | |
37 | 'VERTICAL': u' ', | |
38 | 'VERTICAL_AND_RIGHT': u' ', | |
39 | } #: Unicode box-drawing glyphs, using only spaces. | |
40 | ||
41 | ||
42 | class Style(KeyArgsConstructor): | |
43 | """Rendering style for trees.""" | |
44 | label_format = u'{}' #: Format for labels. | |
45 | ||
46 | def node_label(self, text): | |
47 | """Render a node text into a label.""" | |
48 | return self.label_format.format(text) | |
49 | ||
50 | def child_head(self, label): | |
51 | """Render a node label into final output.""" | |
52 | return label | |
53 | ||
54 | def child_tail(self, line): | |
55 | """Render a node line that is not a label into final output.""" | |
56 | return line | |
57 | ||
58 | def last_child_head(self, label): | |
59 | """Like :func:`~asciitree.drawing.Style.child_head` but only called | |
60 | for the last child.""" | |
61 | return label | |
62 | ||
63 | def last_child_tail(self, line): | |
64 | """Like :func:`~asciitree.drawing.Style.child_tail` but only called | |
65 | for the last child.""" | |
66 | return line | |
67 | ||
68 | ||
69 | class BoxStyle(Style): | |
70 | """A rendering style that uses box draw characters and a common layout.""" | |
71 | gfx = BOX_ASCII #: Glyhps to use. | |
72 | label_space = 1 #: Space between glyphs and label. | |
73 | horiz_len = 2 #: Length of horizontal lines | |
74 | indent = 1 #: Indent for subtrees | |
75 | ||
76 | def child_head(self, label): | |
77 | return (' ' * self.indent | |
78 | + self.gfx['VERTICAL_AND_RIGHT'] | |
79 | + self.gfx['HORIZONTAL'] * self.horiz_len | |
80 | + ' ' * self.label_space | |
81 | + label) | |
82 | ||
83 | def child_tail(self, line): | |
84 | return (' ' * self.indent | |
85 | + self.gfx['VERTICAL'] | |
86 | + ' ' * self.horiz_len | |
87 | + line) | |
88 | ||
89 | def last_child_head(self, label): | |
90 | return (' ' * self.indent | |
91 | + self.gfx['UP_AND_RIGHT'] | |
92 | + self.gfx['HORIZONTAL'] * self.horiz_len | |
93 | + ' ' * self.label_space | |
94 | + label) | |
95 | ||
96 | def last_child_tail(self, line): | |
97 | return (' ' * self.indent | |
98 | + ' ' * len(self.gfx['VERTICAL']) | |
99 | + ' ' * self.horiz_len | |
100 | + line) |
0 | from .util import KeyArgsConstructor | |
1 | ||
2 | ||
3 | class Traversal(KeyArgsConstructor): | |
4 | """Traversal method. | |
5 | ||
6 | Used by the tree rendering functions like :class:`~asciitree.LeftAligned`. | |
7 | """ | |
8 | def get_children(self, node): | |
9 | """Return a list of children of a node.""" | |
10 | raise NotImplementedError | |
11 | ||
12 | def get_root(self, tree): | |
13 | """Return a node representing the tree root from the tree.""" | |
14 | return tree | |
15 | ||
16 | def get_text(self, node): | |
17 | """Return the text associated with a node.""" | |
18 | return str(node) | |
19 | ||
20 | ||
21 | class DictTraversal(Traversal): | |
22 | """Traversal suitable for a dictionary. Keys are tree labels, all values | |
23 | must be dictionaries as well.""" | |
24 | def get_children(self, node): | |
25 | return list(node[1].items()) | |
26 | ||
27 | def get_root(self, tree): | |
28 | return list(tree.items())[0] | |
29 | ||
30 | def get_text(self, node): | |
31 | return node[0] | |
32 | ||
33 | ||
34 | class AttributeTraversal(Traversal): | |
35 | """Attribute traversal. | |
36 | ||
37 | Uses an attribute of a node as its list of children. | |
38 | """ | |
39 | attribute = 'children' #: Attribute to use. | |
40 | ||
41 | def get_children(self, node): | |
42 | return getattr(node, self.attribute) |
0 | class KeyArgsConstructor(object): | |
1 | def __init__(self, **kwargs): | |
2 | for k, v in kwargs.items(): | |
3 | setattr(self, k, v) |
0 | #!/usr/bin/env python | |
1 | # -*- coding: utf-8 -*- | |
2 | ||
3 | import os | |
4 | ||
5 | from setuptools import setup, find_packages | |
6 | ||
7 | ||
8 | def read(fname): | |
9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() | |
10 | ||
11 | ||
12 | setup( | |
13 | name='asciitree', | |
14 | version='0.3.3', | |
15 | description='Draws ASCII trees.', | |
16 | long_description=read('README.rst'), | |
17 | author='Marc Brinkmann', | |
18 | author_email='[email protected]', | |
19 | url='http://github.com/mbr/asciitree', | |
20 | license='MIT', | |
21 | packages=find_packages(exclude=['tests']), | |
22 | install_requires=[], | |
23 | ) |
8 | 8 | from msldap.ldap_objects.adcomp import MSADMachine, MSADMachine_ATTRS, MSADMachine_TSV_ATTRS |
9 | 9 | from msldap.ldap_objects.adsec import MSADSecurityInfo, MSADTokenGroup |
10 | 10 | from msldap.ldap_objects.common import MSLDAP_UAC |
11 | from msldap.ldap_objects.adgroup import MSADGroup | |
12 | from msldap.ldap_objects.adou import MSADOU | |
11 | from msldap.ldap_objects.adgroup import MSADGroup, MSADGroup_ATTRS | |
12 | from msldap.ldap_objects.adou import MSADOU, MSADOU_ATTRS | |
13 | 13 | from msldap.ldap_objects.adgpo import MSADGPO, MSADGPO_ATTRS |
14 | 14 | from msldap.ldap_objects.adtrust import MSADDomainTrust, MSADDomainTrust_ATTRS |
15 | from msldap.ldap_objects.adschemaentry import MSADSCHEMAENTRY_ATTRS, MSADSchemaEntry | |
15 | 16 | |
16 | 17 | __all__ = [ |
17 | 18 | 'MSADUser', |
31 | 32 | 'MSADGPO_ATTRS', |
32 | 33 | 'MSADDomainTrust', |
33 | 34 | 'MSADDomainTrust_ATTRS', |
35 | 'MSADGroup_ATTRS', | |
36 | 'MSADOU_ATTRS', | |
37 | 'MSADSCHEMAENTRY_ATTRS', | |
38 | 'MSADSchemaEntry', | |
34 | 39 | ]⏎ |
3 | 3 | # Tamas Jos (@skelsec) |
4 | 4 | # |
5 | 5 | |
6 | import datetime | |
6 | 7 | from msldap.ldap_objects.common import MSLDAP_UAC, vn |
7 | 8 | |
8 | 9 | MSADMachine_ATTRS = [ |
14 | 15 | 'operatingSystem', 'operatingSystemVersion','primaryGroupID', |
15 | 16 | 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl', |
16 | 17 | 'whenChanged', 'whenCreated', 'servicePrincipalName','msDS-AllowedToDelegateTo', |
18 | 'msDS-AllowedToActOnBehalfOfOtherIdentity' | |
17 | 19 | ] |
18 | 20 | |
19 | 21 | MSADMachine_TSV_ATTRS = [ |
62 | 64 | self.whenCreated = None |
63 | 65 | self.servicePrincipalName = None |
64 | 66 | self.allowedtodelegateto = None |
67 | self.allowedtoactonbehalfofotheridentity = None | |
68 | ||
69 | ## calculated properties | |
70 | self.when_pw_change = None #datetime | |
71 | self.when_pw_expires = None #datetime | |
72 | self.must_change_pw = None #datetime | |
73 | self.canLogon = None #bool | |
74 | ||
75 | # https://msdn.microsoft.com/en-us/library/cc245739.aspx | |
76 | def calc_PasswordMustChange(self, adinfo): | |
77 | # Crtieria 1 | |
78 | flags = [MSLDAP_UAC.DONT_EXPIRE_PASSWD, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT] | |
79 | for flag in flags: | |
80 | if flag & self.userAccountControl: | |
81 | return datetime.datetime.max #never | |
82 | ||
83 | #criteria 2 | |
84 | if self.pwdLastSet == datetime.timedelta(): | |
85 | return datetime.datetime.min | |
86 | ||
87 | if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta | |
88 | return datetime.datetime.max #never | |
89 | ||
90 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
91 | return datetime.datetime.max #never | |
92 | ||
93 | return (self.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) | |
94 | ||
95 | ||
96 | # https://msdn.microsoft.com/en-us/library/cc223991.aspx | |
97 | def calc_CanLogon(self): | |
98 | flags = [MSLDAP_UAC.ACCOUNTDISABLE, MSLDAP_UAC.LOCKOUT, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT] | |
99 | for flag in flags: | |
100 | if flag & self.userAccountControl: | |
101 | return False | |
102 | ||
103 | if (not (MSLDAP_UAC.DONT_EXPIRE_PASSWD & self.userAccountControl)) and (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: | |
104 | return False | |
105 | ||
106 | # | |
107 | # TODO: logonHours check! | |
108 | # | |
109 | ||
110 | if self.must_change_pw == datetime.datetime.min: | |
111 | #can logon, but must change the password! | |
112 | return True | |
113 | ||
114 | if (self.must_change_pw - datetime.datetime.now()).total_seconds() < 0: | |
115 | return False | |
116 | ||
117 | return True | |
65 | 118 | |
66 | 119 | @staticmethod |
67 | 120 | def from_ldap(entry, adinfo = None): |
101 | 154 | adi.servicePrincipalName = entry['attributes'].get('servicePrincipalName') |
102 | 155 | |
103 | 156 | adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo') |
157 | adi.allowedtoactonbehalfofotheridentity = entry['attributes'].get('msDS-AllowedToActOnBehalfOfOtherIdentity') | |
104 | 158 | |
105 | 159 | temp = entry['attributes'].get('userAccountControl') |
106 | 160 | if temp: |
107 | 161 | adi.userAccountControl = MSLDAP_UAC(temp) |
162 | ||
163 | if adinfo: | |
164 | adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge).replace(tzinfo=None) | |
165 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
166 | adi.when_pw_expires = datetime.datetime.max | |
167 | else: | |
168 | adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) if adinfo.maxPwdAge != 0 else adi.pwdLastSet | |
169 | adi.must_change_pw = adi.calc_PasswordMustChange(adinfo) #datetime | |
170 | adi.canLogon = adi.calc_CanLogon() #bool | |
171 | ||
108 | 172 | return adi |
109 | 173 | |
110 | 174 | def to_dict(self): |
155 | 219 | |
156 | 220 | def get_row(self, attrs): |
157 | 221 | t = self.to_dict() |
158 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs]⏎ | |
222 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] | |
223 | ||
224 | ||
225 | def __str__(self): | |
226 | t = '' | |
227 | for k in self.__dict__: | |
228 | t += '%s : %s\r\n' % (k, self.__dict__[k]) | |
229 | return t⏎ |
7 | 7 | |
8 | 8 | MSADGPO_ATTRS = [ |
9 | 9 | 'cn', 'displayName', 'distinguishedName', 'flags', 'gPCFileSysPath', |
10 | 'gPCFunctionalityVersion', 'gPCMachineExtensionNames', 'objectClass', | |
11 | 'objectGUID', 'systemFlags', 'versionNumber', 'whenChanged', 'whenCreated', | |
10 | 'gPCFunctionalityVersion', 'gPCMachineExtensionNames', 'gPCUserExtensionNames', | |
11 | 'objectClass', 'objectGUID', 'systemFlags', 'versionNumber', 'whenChanged', | |
12 | 'whenCreated', | |
12 | 13 | ] |
13 | 14 | |
14 | 15 | class MSADGPO: |
20 | 21 | self.gPCFileSysPath = None #str |
21 | 22 | self.gPCFunctionalityVersion = None #str |
22 | 23 | self.gPCMachineExtensionNames = None |
24 | self.gPCUserExtensionNames = None | |
23 | 25 | self.objectClass = None #str |
24 | 26 | self.objectGUID = None #uid |
25 | 27 | self.systemFlags = None #str |
26 | 28 | self.whenChanged = None #uid |
27 | 29 | self.whenCreated = None #str |
30 | self.versionNumber = None | |
28 | 31 | |
29 | 32 | |
30 | 33 | @staticmethod |
37 | 40 | adi.gPCFileSysPath = entry['attributes'].get('gPCFileSysPath') |
38 | 41 | adi.gPCFunctionalityVersion = entry['attributes'].get('gPCFunctionalityVersion') |
39 | 42 | adi.gPCMachineExtensionNames = entry['attributes'].get('gPCMachineExtensionNames') |
43 | adi.gPCUserExtensionNames = entry['attributes'].get('gPCUserExtensionNames') | |
40 | 44 | adi.objectClass = entry['attributes'].get('objectClass') |
41 | 45 | adi.objectGUID = entry['attributes'].get('objectGUID') |
42 | 46 | adi.systemFlags = entry['attributes'].get('systemFlags') |
43 | 47 | adi.whenChanged = entry['attributes'].get('whenChanged') |
44 | 48 | adi.whenCreated = entry['attributes'].get('whenCreated') |
49 | adi.versionNumber = entry['attributes'].get('versionNumber') | |
45 | 50 | |
46 | 51 | return adi |
47 | 52 | |
54 | 59 | t['gPCFileSysPath'] = vn(self.gPCFileSysPath) |
55 | 60 | t['gPCFunctionalityVersion'] = vn(self.gPCFunctionalityVersion) |
56 | 61 | t['gPCMachineExtensionNames'] = vn(self.gPCMachineExtensionNames) |
62 | t['gPCUserExtensionNames'] = vn(self.gPCUserExtensionNames) | |
57 | 63 | t['systemFlags'] = vn(self.systemFlags) |
58 | 64 | t['objectClass'] = vn(self.objectClass) |
59 | 65 | t['objectGUID'] = vn(self.objectGUID) |
60 | 66 | t['whenChanged'] = vn(self.whenChanged) |
61 | 67 | t['whenCreated'] = vn(self.whenCreated) |
68 | t['versionNumber'] = vn(self.versionNumber) | |
62 | 69 | return t |
63 | 70 | |
64 | 71 | def __str__(self): |
5 | 5 | |
6 | 6 | from msldap.wintypes import * |
7 | 7 | from msldap.ldap_objects.common import MSLDAP_UAC, vn |
8 | from winacl.dtyp.sid import SID | |
9 | ||
10 | MSADGroup_ATTRS = [ | |
11 | 'cn', 'distinguishedName', 'objectGUID', 'objectSid', 'groupType', | |
12 | 'instanceType', 'name', 'member', 'sAMAccountName', 'systemFlags', | |
13 | 'whenChanged', 'whenCreated', 'description', 'nTSecurityDescriptor', | |
14 | 'sAMAccountType', | |
15 | ] | |
16 | ||
8 | 17 | |
9 | 18 | class MSADGroup: |
10 | 19 | def __init__(self): |
68 | 77 | t.description = ', '.join(t.description) |
69 | 78 | |
70 | 79 | |
71 | temp = entry['attributes'].get('nTSecurityDescriptor') | |
72 | if temp: | |
73 | t.nTSecurityDescriptor = SID.from_bytes(temp) | |
80 | #temp = entry['attributes'].get('nTSecurityDescriptor') | |
81 | #if temp: | |
82 | # t.nTSecurityDescriptor = SID.from_bytes(temp) | |
74 | 83 | return t |
75 | 84 | |
76 | 85 |
10 | 10 | 'name', 'nextRid', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', |
11 | 11 | 'objectGUID', 'objectSid', 'pwdHistoryLength', |
12 | 12 | 'pwdProperties', 'serverState', 'systemFlags', 'uASCompat', 'uSNChanged', |
13 | 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference','msDS-Behavior-Version' | |
13 | 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference', | |
14 | 'msDS-Behavior-Version' | |
14 | 15 | ] |
15 | 16 | class MSADInfo: |
16 | 17 | def __init__(self): |
151 | 152 | t += 'uSNCreated: %s\n' % self.uSNCreated |
152 | 153 | t += 'whenChanged: %s\n' % self.whenChanged |
153 | 154 | t += 'whenCreated: %s\n' % self.whenCreated |
155 | t += 'domainmodelevel: %s\n' % self.domainmodelevel | |
154 | 156 | return t ⏎ |
2 | 2 | # Author: |
3 | 3 | # Tamas Jos (@skelsec) |
4 | 4 | # |
5 | ||
6 | ||
7 | MSADOU_ATTRS = [ | |
8 | 'description', 'distinguishedName', 'dSCorePropagationData', 'gPLink', 'instanceType', | |
9 | 'isCriticalSystemObject', 'name', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', | |
10 | 'objectGUID', 'ou', 'showInAdvancedViewOnly', 'systemFlags', 'uSNChanged', 'uSNCreated', | |
11 | 'whenChanged', 'whenCreated', | |
12 | ] | |
5 | 13 | |
6 | 14 | class MSADOU: |
7 | 15 | def __init__(self): |
0 | #!/usr/bin/env python3 | |
1 | # | |
2 | # Author: | |
3 | # Tamas Jos (@skelsec) | |
4 | # | |
5 | ||
6 | ||
7 | MSADSCHEMAENTRY_ATTRS = [ | |
8 | 'cn', 'distinguishedName', 'adminDescription', | |
9 | 'adminDisplayName', 'objectGUID', 'schemaIDGUID', | |
10 | 'lDAPDisplayName', 'name', | |
11 | ] | |
12 | ||
13 | class MSADSchemaEntry: | |
14 | def __init__(self): | |
15 | self.cn = None #str | |
16 | self.distinguishedName = None #dn | |
17 | self.adminDescription = None #dunno | |
18 | self.adminDisplayName = None #datetime | |
19 | self.objectGUID = None #int | |
20 | self.schemaIDGUID = None | |
21 | self.lDAPDisplayName = None | |
22 | self.name = None #int | |
23 | ||
24 | ||
25 | @staticmethod | |
26 | def from_ldap(entry): | |
27 | adi = MSADSchemaEntry() | |
28 | adi.cn = entry['attributes'].get('cn') | |
29 | adi.distinguishedName = entry['attributes'].get('distinguishedName') | |
30 | adi.adminDescription = entry['attributes'].get('adminDescription') | |
31 | adi.adminDisplayName = entry['attributes'].get('adminDisplayName') | |
32 | adi.objectGUID = entry['attributes'].get('objectGUID') #str | |
33 | adi.schemaIDGUID = entry['attributes'].get('schemaIDGUID') #list | |
34 | adi.lDAPDisplayName = entry['attributes'].get('lDAPDisplayName') #int | |
35 | adi.name = entry['attributes'].get('name') #int | |
36 | return adi | |
37 | ||
38 | def to_dict(self): | |
39 | d = {} | |
40 | d['cn'] = self.cn | |
41 | d['distinguishedName'] = self.distinguishedName | |
42 | d['adminDescription'] = self.adminDescription | |
43 | d['adminDisplayName'] = self.adminDisplayName | |
44 | d['objectGUID'] = self.objectGUID | |
45 | d['schemaIDGUID'] = self.schemaIDGUID | |
46 | d['lDAPDisplayName'] = self.lDAPDisplayName | |
47 | d['name'] = self.name | |
48 | return d | |
49 | ||
50 | ||
51 | def __str__(self): | |
52 | t = 'MSADSchemaEntry\r\n' | |
53 | d = self.to_dict() | |
54 | for k in d: | |
55 | t += '%s: %s\r\n' % (k, d[k]) | |
56 | return t ⏎ |
13 | 13 | 'objectCategory', 'objectClass', 'objectGUID', 'objectSid', 'primaryGroupID', |
14 | 14 | 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl', |
15 | 15 | 'userPrincipalName', 'whenChanged', 'whenCreated','memberOf','member', 'servicePrincipalName', |
16 | 'msDS-AllowedToDelegateTo', | |
16 | 'msDS-AllowedToDelegateTo', 'adminCount' | |
17 | 17 | ] |
18 | 18 | MSADUser_TSV_ATTRS = [ |
19 | 19 | 'sAMAccountName', 'userPrincipalName' ,'canLogon', 'badPasswordTime', 'description', |
21 | 21 | 'whenCreated', 'whenChanged', 'member', 'memberOf', 'servicePrincipalName', |
22 | 22 | 'objectSid', 'cn', 'UAC_SCRIPT', 'UAC_ACCOUNTDISABLE', 'UAC_LOCKOUT', 'UAC_PASSWD_NOTREQD', |
23 | 23 | 'UAC_PASSWD_CANT_CHANGE', 'UAC_ENCRYPTED_TEXT_PASSWORD_ALLOWED', 'UAC_DONT_EXPIRE_PASSWD', |
24 | 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED' | |
24 | 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED', 'adminCount' | |
25 | 25 | ] |
26 | 26 | |
27 | 27 | class MSADUser: |
66 | 66 | self.sAMAccountType = None #int |
67 | 67 | self.userAccountControl = None #UserAccountControl intflag |
68 | 68 | self.allowedtodelegateto = None |
69 | self.admincount = None | |
69 | 70 | |
70 | 71 | |
71 | 72 | ## other |
79 | 80 | self.canLogon = None #bool |
80 | 81 | |
81 | 82 | # https://msdn.microsoft.com/en-us/library/cc245739.aspx |
82 | def calc_PasswordMustChange(self): | |
83 | def calc_PasswordMustChange(self, adinfo): | |
83 | 84 | # Crtieria 1 |
84 | 85 | flags = [MSLDAP_UAC.DONT_EXPIRE_PASSWD, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT] |
85 | 86 | for flag in flags: |
87 | 88 | return datetime.datetime.max #never |
88 | 89 | |
89 | 90 | #criteria 2 |
90 | if self.pwdLastSet == 0: | |
91 | if self.pwdLastSet == datetime.timedelta(): | |
91 | 92 | return datetime.datetime.min |
92 | 93 | |
93 | if (self.when_pw_expires - datetime.datetime.now()).total_seconds() > 0: | |
94 | if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta | |
94 | 95 | return datetime.datetime.max #never |
95 | 96 | |
96 | return self.pwdLastSet.replace(tzinfo=None) | |
97 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
98 | return datetime.datetime.max #never | |
99 | ||
100 | return (self.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) | |
97 | 101 | |
98 | 102 | |
99 | 103 | # https://msdn.microsoft.com/en-us/library/cc223991.aspx |
102 | 106 | for flag in flags: |
103 | 107 | if flag & self.userAccountControl: |
104 | 108 | return False |
105 | ||
106 | if (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: | |
109 | ||
110 | if (not (MSLDAP_UAC.DONT_EXPIRE_PASSWD & self.userAccountControl)) and (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: | |
107 | 111 | return False |
108 | 112 | |
109 | 113 | # |
155 | 159 | adi.countryCode = entry['attributes'].get('countryCode') |
156 | 160 | |
157 | 161 | adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo') |
162 | adi.admincount = entry['attributes'].get('adminCount') | |
158 | 163 | |
159 | 164 | temp = entry['attributes'].get('userAccountControl') |
160 | 165 | if temp: |
161 | 166 | adi.userAccountControl = MSLDAP_UAC(temp) |
162 | 167 | |
163 | 168 | if adinfo: |
164 | adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge/10000000).replace(tzinfo=None) | |
165 | adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge/10000000).replace(tzinfo=None) | |
166 | adi.must_change_pw = adi.calc_PasswordMustChange() #datetime | |
167 | if adi.sAMAccountName[-1] != '$': | |
168 | adi.canLogon = adi.calc_CanLogon() #bool | |
169 | adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge).replace(tzinfo=None) | |
170 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
171 | adi.when_pw_expires = datetime.datetime.max | |
172 | else: | |
173 | adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) if adinfo.maxPwdAge != 0 else adi.pwdLastSet | |
174 | adi.must_change_pw = adi.calc_PasswordMustChange(adinfo) #datetime | |
175 | adi.canLogon = adi.calc_CanLogon() #bool | |
169 | 176 | |
170 | 177 | |
171 | 178 | return adi |
207 | 214 | t['when_pw_change'] = vn(self.when_pw_change) |
208 | 215 | t['when_pw_expires'] = vn(self.when_pw_expires) |
209 | 216 | t['must_change_pw'] = vn(self.must_change_pw) |
217 | t['admincount'] = self.admincount | |
210 | 218 | t['canLogon'] = vn(self.canLogon) |
211 | 219 | return t |
212 | 220 | |
255 | 263 | t += 'when_pw_change: %s\n' % self.when_pw_change |
256 | 264 | t += 'when_pw_expires: %s\n' % self.when_pw_expires |
257 | 265 | t += 'must_change_pw: %s\n' % self.must_change_pw |
266 | t += 'admincount: %s\n' % self.admincount | |
258 | 267 | t += 'canLogon: %s\n' % self.canLogon |
259 | 268 | |
260 | 269 | return t |
0 | import enum | |
1 | import asyncio | |
2 | import ipaddress | |
3 | import copy | |
4 | ||
5 | from asysocks.common.clienturl import SocksClientURL | |
6 | from asysocks.common.constants import SocksServerVersion, SocksProtocol, SOCKS5Method | |
7 | from asysocks.common.target import SocksTarget | |
8 | ||
9 | from msldap import logger | |
10 | from msldap.network.socks import SocksProxyConnection | |
11 | from msldap.commons.proxy import MSLDAPProxy, MSLDAPProxyType | |
12 | from minikerberos.common.target import KerberosTarget | |
13 | from minikerberos.common.proxy import KerberosProxy | |
14 | ||
15 | ||
16 | ||
17 | class MultiplexorProxyConnection: | |
18 | """ | |
19 | """ | |
20 | def __init__(self, target): | |
21 | self.target = target | |
22 | ||
23 | async def connect(self, is_kerberos = False): | |
24 | """ | |
25 | ||
26 | """ | |
27 | #hiding the import, so you'll only need to install multiplexor only when actually using it | |
28 | from multiplexor.operator import MultiplexorOperator | |
29 | ||
30 | con_str = self.target.proxy.target.get_server_url() | |
31 | #creating operator and connecting to multiplexor server | |
32 | self.operator = MultiplexorOperator(con_str, logging_sink = logger) | |
33 | await self.operator.connect() | |
34 | #creating socks5 proxy | |
35 | server_info = await self.operator.start_socks5(self.target.proxy.target.agent_id) | |
36 | await self.operator.terminate() | |
37 | #print(server_info) | |
38 | if is_kerberos is False: | |
39 | ||
40 | #copying the original target, then feeding it to socks5proxy object. it will hold the actual socks5 proxy server address we created before | |
41 | tp = MSLDAPProxy() | |
42 | tp.target = SocksTarget() | |
43 | tp.target.version = SocksServerVersion.SOCKS5 | |
44 | tp.target.server_ip = server_info['listen_ip'] | |
45 | tp.target.server_port = server_info['listen_port'] | |
46 | tp.target.is_bind = False | |
47 | tp.target.proto = SocksProtocol.TCP | |
48 | tp.target.timeout = self.target.timeout | |
49 | tp.target.buffer_size = 4096 | |
50 | ||
51 | tp.target.endpoint_ip = self.target.host | |
52 | tp.target.endpoint_port = self.target.port | |
53 | tp.target.endpoint_timeout = None # TODO: maybe implement endpoint timeout in the msldap target? | |
54 | tp.type = MSLDAPProxyType.SOCKS5 | |
55 | ||
56 | newtarget = copy.deepcopy(self.target) | |
57 | newtarget.proxy = tp | |
58 | ||
59 | ||
60 | ||
61 | return SocksProxyConnection(target = newtarget) | |
62 | ||
63 | else: | |
64 | kt = copy.deepcopy(self.target) | |
65 | kt.proxy = KerberosProxy() | |
66 | kt.proxy.target = SocksTarget() | |
67 | kt.proxy.target.version = SocksServerVersion.SOCKS5 | |
68 | kt.proxy.target.server_ip = server_info['listen_ip'] | |
69 | kt.proxy.target.server_port = server_info['listen_port'] | |
70 | kt.proxy.target.is_bind = False | |
71 | kt.proxy.target.proto = SocksProtocol.TCP | |
72 | kt.proxy.target.timeout = 10 | |
73 | kt.proxy.target.buffer_size = 4096 | |
74 | ||
75 | kt.proxy.target.endpoint_ip = self.target.ip | |
76 | kt.proxy.target.endpoint_port = self.target.port | |
77 | #kt.proxy.creds = copy.deepcopy(self.target.proxy.auth) | |
78 | ||
79 | return kt | |
80 |
0 | 0 | |
1 | from msldap import logger | |
1 | 2 | from msldap.network.tcp import MSLDAPTCPNetwork |
2 | 3 | from msldap.network.socks import SocksProxyConnection |
4 | from msldap.network.multiplexor import MultiplexorProxyConnection | |
3 | 5 | from msldap.commons.proxy import MSLDAPProxyType |
4 | 6 | |
5 | 7 | MSLDAP_SOCKS_PROXY_TYPES = [ |
6 | MSLDAPProxyType.SOCKS4 , | |
7 | MSLDAPProxyType.SOCKS4_SSL , | |
8 | MSLDAPProxyType.SOCKS5 , | |
9 | MSLDAPProxyType.SOCKS5_SSL] | |
8 | MSLDAPProxyType.SOCKS4, | |
9 | MSLDAPProxyType.SOCKS4_SSL, | |
10 | MSLDAPProxyType.SOCKS5, | |
11 | MSLDAPProxyType.SOCKS5_SSL, | |
12 | MSLDAPProxyType.WSNET, | |
13 | MSLDAPProxyType.WSNETWS, | |
14 | MSLDAPProxyType.WSNETWSS, | |
15 | ] | |
10 | 16 | |
11 | 17 | class MSLDAPNetworkSelector: |
12 | 18 | def __init__(self): |
13 | 19 | pass |
14 | 20 | |
15 | 21 | @staticmethod |
16 | def select(target): | |
22 | async def select(target): | |
17 | 23 | if target.proxy is not None: |
18 | 24 | if target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES: |
19 | 25 | return SocksProxyConnection(target) |
20 | 26 | else: |
21 | raise Exception('Multiplexor coming soon!') | |
27 | mpc = MultiplexorProxyConnection(target) | |
28 | socks_proxy = await mpc.connect() | |
29 | return socks_proxy | |
22 | 30 | |
23 | 31 | return MSLDAPTCPNetwork(target)⏎ |
0 | ||
1 | # | |
2 | # | |
3 | # | |
4 | # | |
5 | # | |
6 | # | |
7 | ||
8 | 0 | |
9 | 1 | import enum |
10 | 2 | import asyncio |
40 | 32 | Disconnects from the socket. |
41 | 33 | Stops the reader and writer streams. |
42 | 34 | """ |
43 | self.proxy_task.cancel() | |
44 | self.handle_in_task.cancel() | |
35 | if self.client is not None: | |
36 | await self.client.terminate() | |
37 | if self.proxy_task is not None: | |
38 | self.proxy_task.cancel() | |
39 | if self.handle_in_q is not None: | |
40 | self.handle_in_task.cancel() | |
41 | ||
42 | async def terminate(self): | |
43 | await self.disconnect() | |
44 | ||
45 | def get_peer_certificate(self): | |
46 | raise Exception('Not yet implemented! SSL implementation on socks is missing!') | |
47 | return self.writer.get_extra_info('socket').getpeercert(True) | |
45 | 48 | |
46 | 49 | def get_one_message(self,data): |
47 | 50 | if len(data) < 6: |
82 | 85 | data += temp |
83 | 86 | continue |
84 | 87 | |
85 | #except asyncio.CancelledError: | |
86 | # return | |
88 | except asyncio.CancelledError: | |
89 | return | |
87 | 90 | except Exception as e: |
88 | 91 | logger.exception('handle_in_q') |
89 | 92 | await self.in_queue.put((None, e)) |
104 | 107 | self.proxy_in_queue = asyncio.Queue() |
105 | 108 | comms = SocksQueueComms(self.out_queue, self.proxy_in_queue) |
106 | 109 | |
107 | self.target.proxy.target.endpoint_ip = self.target.host | |
108 | self.target.proxy.target.endpoint_port = int(self.target.port) | |
109 | ||
110 | self.client = SOCKSClient(comms, self.target.proxy.target, self.target.proxy.auth) | |
110 | self.target.proxy.target[-1].endpoint_ip = self.target.host if self.target.serverip is None else self.target.serverip | |
111 | self.target.proxy.target[-1].endpoint_port = int(self.target.port) | |
112 | self.target.proxy.target[-1].endpoint_timeout = None #TODO: maybe implement endpoint timeout? | |
113 | self.target.proxy.target[-1].timeout = self.target.timeout | |
114 | self.client = SOCKSClient(comms, self.target.proxy.target) | |
111 | 115 | self.proxy_task = asyncio.create_task(self.client.run()) |
112 | 116 | self.handle_in_task = asyncio.create_task(self.handle_in_q()) |
113 | 117 | return True, None |
20 | 20 | async def terminate(self): |
21 | 21 | self.handle_in_task.cancel() |
22 | 22 | self.handle_out_task.cancel() |
23 | ||
23 | ||
24 | def get_peer_certificate(self): | |
25 | return self.writer.get_extra_info('ssl_object').getpeercert(True) | |
24 | 26 | |
25 | 27 | async def handle_in_q(self): |
26 | 28 | try: |
81 | 83 | self.in_queue = asyncio.Queue() |
82 | 84 | self.out_queue = asyncio.Queue() |
83 | 85 | self.reader, self.writer = await asyncio.wait_for( |
84 | asyncio.open_connection(self.target.host, self.target.port, ssl=self.target.get_ssl_context()), | |
86 | asyncio.open_connection( | |
87 | self.target.serverip if self.target.serverip is not None else self.target.host, | |
88 | self.target.port, | |
89 | ssl=self.target.get_ssl_context() | |
90 | ), | |
85 | 91 | timeout = self.target.timeout |
86 | 92 | ) |
87 | 93 |
0 | ||
1 | # | |
2 | # | |
3 | # | |
4 | # | |
5 | # | |
6 | # | |
7 | ||
8 | ||
9 | import enum | |
10 | import asyncio | |
11 | import ipaddress | |
12 | ||
13 | from msldap import logger | |
14 | from msldap.protocol.utils import calcualte_length | |
15 | ||
16 | from pyodidewsnet.client import WSNetworkTCP | |
17 | ||
18 | ||
19 | ||
20 | class WSNetProxyConnection: | |
21 | """ | |
22 | Generic asynchronous TCP socket class, nothing SMB related. | |
23 | Creates the connection and channels incoming/outgoing bytes via asynchonous queues. | |
24 | """ | |
25 | def __init__(self, target): | |
26 | self.target = target | |
27 | ||
28 | self.client = None | |
29 | self.handle_in_task = None | |
30 | ||
31 | self.out_queue = None#asyncio.Queue() | |
32 | self.in_queue = None#asyncio.Queue() | |
33 | ||
34 | self.proxy_in_queue = None#asyncio.Queue() | |
35 | self.is_plain_msg = True | |
36 | ||
37 | async def disconnect(self): | |
38 | """ | |
39 | Disconnects from the socket. | |
40 | Stops the reader and writer streams. | |
41 | """ | |
42 | if self.client is not None: | |
43 | await self.client.terminate() | |
44 | if self.handle_in_q is not None: | |
45 | self.handle_in_task.cancel() | |
46 | ||
47 | async def terminate(self): | |
48 | await self.disconnect() | |
49 | ||
50 | def get_peer_certificate(self): | |
51 | raise Exception('Not yet implemented! SSL implementation on socks is missing!') | |
52 | return self.writer.get_extra_info('socket').getpeercert(True) | |
53 | ||
54 | def get_one_message(self,data): | |
55 | if len(data) < 6: | |
56 | return None | |
57 | ||
58 | if self.is_plain_msg is True: | |
59 | dl = calcualte_length(data[:6]) | |
60 | else: | |
61 | dl = int.from_bytes(data[:4], byteorder = 'big', signed = False) | |
62 | dl = dl + 4 | |
63 | ||
64 | ||
65 | #print(dl) | |
66 | if len(data) >= dl: | |
67 | return data[:dl] | |
68 | ||
69 | async def handle_in_q(self): | |
70 | try: | |
71 | data = b'' | |
72 | while True: | |
73 | while True: | |
74 | msg_data = self.get_one_message(data) | |
75 | if msg_data is None: | |
76 | break | |
77 | ||
78 | await self.in_queue.put((msg_data, None)) | |
79 | data = data[len(msg_data):] | |
80 | ||
81 | temp, err = await self.proxy_in_queue.get() | |
82 | #print(temp) | |
83 | if err is not None: | |
84 | raise err | |
85 | ||
86 | if temp == b'' or temp is None: | |
87 | logger.debug('Server finished!') | |
88 | return | |
89 | ||
90 | data += temp | |
91 | continue | |
92 | ||
93 | except asyncio.CancelledError: | |
94 | return | |
95 | except Exception as e: | |
96 | logger.exception('handle_in_q') | |
97 | await self.in_queue.put((None, e)) | |
98 | ||
99 | finally: | |
100 | await self.client.terminate() | |
101 | ||
102 | ||
103 | ||
104 | async def run(self): | |
105 | """ | |
106 | ||
107 | """ | |
108 | try: | |
109 | self.out_queue = asyncio.Queue() | |
110 | self.in_queue = asyncio.Queue() | |
111 | self.proxy_in_queue = asyncio.Queue() | |
112 | ||
113 | self.client = WSNetworkTCP(self.target.host, int(self.target.port), self.proxy_in_queue, self.out_queue) | |
114 | _, err = await self.client.run() | |
115 | if err is not None: | |
116 | raise err | |
117 | ||
118 | self.handle_in_task = asyncio.create_task(self.handle_in_q()) | |
119 | ||
120 | return True, None | |
121 | ||
122 | except Exception as e: | |
123 | return False, e | |
124 |
0 | 0 | import re |
1 | 1 | import platform |
2 | 2 | |
3 | import msldap.protocol.ldap_filter.parser as parser | |
3 | from msldap.protocol.ldap_filter import parser | |
4 | 4 | from msldap.protocol.ldap_filter.soundex import soundex_compare |
5 | 5 | |
6 | 6 |
141 | 141 | _input_size = None |
142 | 142 | _actions = None |
143 | 143 | |
144 | REGEX_1 = re.compile('^[^!*\\x29]') | |
144 | REGEX_1 = re.compile('^[^*\\x29]') #re.compile('^[^!*\\x29]') | |
145 | 145 | REGEX_2 = re.compile('^[a-fA-F0-9]') |
146 | 146 | REGEX_3 = re.compile('^[\\x20]') |
147 | 147 | REGEX_4 = re.compile('^[\\x09]') |
49 | 49 | |
50 | 50 | class resultCode(core.Enumerated): |
51 | 51 | _map = { |
52 | 0 : 'success', | |
52 | 0 : 'success', | |
53 | 53 | 1 : 'operationsError', |
54 | 54 | 2 : 'protocolError', |
55 | 55 | 3 : 'timeLimitExceeded', |
90 | 90 | 80 : 'other', |
91 | 91 | } |
92 | 92 | |
93 | class changeoperation(core.Enumerated): | |
93 | class ChangeOperation(core.Enumerated): | |
94 | 94 | _map = { |
95 | 95 | 0 : 'add', |
96 | 96 | 1 : 'delete', |
287 | 287 | |
288 | 288 | class Change(core.Sequence): |
289 | 289 | _fields = [ |
290 | ('operation', changeoperation), | |
290 | ('operation', ChangeOperation), | |
291 | 291 | ('modification', PartialAttribute), |
292 | 292 | ] |
293 | 293 |
6 | 6 | |
7 | 7 | |
8 | 8 | def equality(attr, value): |
9 | #print(attr) | |
10 | #print(value) | |
11 | 9 | if attr[-1] == ':': |
12 | #possible OID | |
13 | 10 | name, oid_raw = attr[:-1].split(':') |
14 | #print(oid_raw) | |
15 | 11 | return Filter({ |
16 | 12 | 'extensibleMatch' : MatchingRuleAssertion({ |
17 | 13 | 'matchingRule' : oid_raw.encode(), |
60 | 56 | |
61 | 57 | |
62 | 58 | def query_syntax_converter_inner(ftr): |
63 | #print(ftr.__dict__) | |
64 | #print(ftr.comp) | |
65 | #print(ftr.type) | |
66 | 59 | if ftr.type == 'filter': |
67 | 60 | if ftr.comp == '=': |
68 | 61 | return equality(ftr.attr, ftr.val) |
4 | 4 | from winacl.dtyp.guid import GUID |
5 | 5 | from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR |
6 | 6 | from msldap import logger |
7 | from msldap.protocol.messages import Attribute, Change, PartialAttribute | |
7 | 8 | |
8 | 9 | MSLDAP_DT_WIN_EPOCH = datetime.datetime(1601, 1, 1) |
9 | 10 | |
61 | 62 | def list_str(x): |
62 | 63 | return [e.decode() for e in x ] |
63 | 64 | |
65 | def list_str_enc(x): | |
66 | return [e.encode() for e in x ] | |
67 | ||
64 | 68 | def list_int(x): |
65 | 69 | return [int(e) for e in x ] |
66 | 70 | |
71 | def list_int_enc(x): | |
72 | return [str(e).encode() for e in x ] | |
73 | ||
67 | 74 | def list_int_one(x): |
68 | 75 | return int(x[0]) |
69 | 76 | |
77 | def list_int_one_enc(x): | |
78 | return [str(x[0]).encode()] | |
79 | ||
70 | 80 | def list_str_one(x): |
71 | 81 | return x[0].decode() |
72 | 82 | |
83 | def list_str_one_enc(x): | |
84 | return [x[0].encode()] | |
85 | ||
86 | def list_str_one_utf16le_enc(x): | |
87 | return [x[0].encode('utf-16-le')] | |
88 | ||
73 | 89 | def list_bytes_one(x): |
74 | 90 | return x[0] |
91 | ||
92 | def list_bytes_one_enc(x): | |
93 | return x | |
75 | 94 | |
76 | 95 | def int2timedelta(x): |
77 | 96 | x = int(x[0]) |
146 | 165 | t.append(ts2dt((a, None))) |
147 | 166 | return t |
148 | 167 | |
168 | ||
149 | 169 | LDAP_ATTRIBUTE_TYPES = { |
150 | 170 | 'supportedCapabilities' : list_str, |
151 | 171 | 'serverName' : list_str_one, |
157 | 177 | 'supportedControl' : list_str, |
158 | 178 | 'rootDomainNamingContext' : list_str_one, |
159 | 179 | 'configurationNamingContext' : list_str_one, |
180 | 'schemaIDGUID' : x2guid, | |
181 | 'lDAPDisplayName' : list_str_one, | |
160 | 182 | 'schemaNamingContext' : list_str_one, |
161 | 183 | 'defaultNamingContext' : list_str_one, |
184 | 'adminDescription' : list_str_one, | |
185 | 'adminDisplayName' : list_str_one, | |
162 | 186 | 'namingContexts' : list_str, |
163 | 187 | 'dsServiceName' : list_str_one, |
164 | 188 | 'subschemaSubentry' : list_str_one, |
203 | 227 | 'lockoutThreshold' : list_int_one, |
204 | 228 | 'lockOutObservationWindow' : list_int_one, |
205 | 229 | 'lockoutDuration' : list_int_one, |
206 | 'forceLogoff' : list_str_one, | |
230 | 'forceLogoff' : int2timedelta, | |
207 | 231 | 'creationTime' : int2dt, |
208 | 232 | 'maxPwdAge' : int2timedelta, |
209 | 233 | 'pwdHistoryLength' : list_int_one, |
233 | 257 | 'versionNumber' : list_int_one, |
234 | 258 | 'gPCFunctionalityVersion' : list_int_one, |
235 | 259 | 'gPCMachineExtensionNames' : list_str, |
260 | 'gPCUserExtensionNames' : list_str, | |
236 | 261 | 'groupType' : list_int_one, |
237 | 262 | 'member' : list_str, |
238 | 263 | 'adminCount' : list_int_one, |
246 | 271 | 'trustPartner' : list_str_one, |
247 | 272 | 'securityIdentifier' : list_bytes_one, |
248 | 273 | 'versionNumber' : list_int_one, |
249 | ||
274 | 'unicodePwd' : list_str_one, | |
275 | 'ms-Mcs-AdmPwd' : list_str_one, | |
276 | 'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one, | |
250 | 277 | } |
278 | ||
279 | LDAP_ATTRIBUTE_TYPES_ENC = { | |
280 | 'objectClass' : list_str_enc, | |
281 | 'sn' : list_str_one_enc, | |
282 | 'gidNumber' : list_int_one_enc, | |
283 | 'unicodePwd' : list_str_one_utf16le_enc, | |
284 | 'lockoutTime' : list_int_one_enc, | |
285 | 'sAMAccountName' : list_str_one_enc, | |
286 | 'userAccountControl' : list_int_one_enc, | |
287 | 'displayName' : list_str_one_enc, | |
288 | 'userPrincipalName' : list_str_one_enc, | |
289 | 'servicePrincipalName' : list_str_enc, | |
290 | 'msds-additionaldnshostname' : list_str_enc, | |
291 | 'gPCMachineExtensionNames' : list_str_enc, | |
292 | 'gPCUserExtensionNames' : list_str_enc, | |
293 | 'versionNumber' : list_int_one_enc, | |
294 | 'member' : list_str_enc, | |
295 | 'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one_enc, | |
296 | 'nTSecurityDescriptor' : list_bytes_one_enc, | |
297 | } | |
298 | ||
299 | def encode_attributes(x): | |
300 | """converts a dict to attributelist""" | |
301 | res = [] | |
302 | for k in x: | |
303 | if k not in LDAP_ATTRIBUTE_TYPES_ENC: | |
304 | raise Exception('Unknown conversion type for key "%s"' % k) | |
305 | ||
306 | res.append(Attribute({ | |
307 | 'type' : k.encode(), | |
308 | 'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](x[k]) | |
309 | })) | |
310 | ||
311 | return res | |
251 | 312 | |
252 | 313 | def convert_attributes(x): |
253 | 314 | t = {} |
269 | 330 | return { |
270 | 331 | 'objectName' : x['objectName'].decode(), |
271 | 332 | 'attributes' : convert_attributes(x['attributes']) |
272 | }⏎ | |
333 | } | |
334 | ||
335 | ||
336 | def encode_changes(x): | |
337 | res = [] | |
338 | for k in x: | |
339 | if k not in LDAP_ATTRIBUTE_TYPES_ENC: | |
340 | raise Exception('Unknown conversion type for key "%s"' % k) | |
341 | ||
342 | for mod, value in x[k]: | |
343 | res.append(Change({ | |
344 | 'operation' : mod, | |
345 | 'modification' : PartialAttribute({ | |
346 | 'type' : k.encode(), | |
347 | 'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](value) | |
348 | }) | |
349 | })) | |
350 | return res⏎ |
0 | 0 | Metadata-Version: 1.2 |
1 | 1 | Name: msldap |
2 | Version: 0.2.10 | |
2 | Version: 0.3.30 | |
3 | 3 | Summary: Python library to play with MS LDAP |
4 | 4 | Home-page: https://github.com/skelsec/msldap |
5 | 5 | Author: Tamas Jos |
6 | Author-email: [email protected] | |
6 | Author-email: [email protected] | |
7 | 7 | License: UNKNOWN |
8 | 8 | Description: Python library to play with MS LDAP |
9 | 9 | Platform: UNKNOWN |
10 | Classifier: Programming Language :: Python :: 3.6 | |
10 | Classifier: Programming Language :: Python :: 3.7 | |
11 | Classifier: Programming Language :: Python :: 3.8 | |
11 | 12 | Classifier: License :: OSI Approved :: MIT License |
12 | 13 | Classifier: Operating System :: OS Independent |
13 | Requires-Python: >=3.6 | |
14 | Requires-Python: >=3.7 |
10 | 10 | msldap.egg-info/SOURCES.txt |
11 | 11 | msldap.egg-info/dependency_links.txt |
12 | 12 | msldap.egg-info/entry_points.txt |
13 | msldap.egg-info/not-zip-safe | |
13 | 14 | msldap.egg-info/requires.txt |
14 | 15 | msldap.egg-info/top_level.txt |
15 | msldap.egg-info/zip-safe | |
16 | 16 | msldap/authentication/__init__.py |
17 | 17 | msldap/authentication/kerberos/__init__.py |
18 | msldap/authentication/kerberos/gssapi.py | |
18 | 19 | msldap/authentication/kerberos/multiplexor.py |
19 | 20 | msldap/authentication/kerberos/native.py |
20 | 21 | msldap/authentication/kerberos/sspi.py |
22 | msldap/authentication/kerberos/sspiproxyws.py | |
23 | msldap/authentication/kerberos/wsnet.py | |
21 | 24 | msldap/authentication/ntlm/__init__.py |
22 | 25 | msldap/authentication/ntlm/creds_calc.py |
23 | 26 | msldap/authentication/ntlm/multiplexor.py |
24 | 27 | msldap/authentication/ntlm/native.py |
25 | 28 | msldap/authentication/ntlm/sspi.py |
29 | msldap/authentication/ntlm/sspiproxy.py | |
30 | msldap/authentication/ntlm/wsnet.py | |
26 | 31 | msldap/authentication/ntlm/messages/__init__.py |
27 | 32 | msldap/authentication/ntlm/messages/authenticate.py |
28 | 33 | msldap/authentication/ntlm/messages/challenge.py |
44 | 49 | msldap/authentication/spnego/sspi.py |
45 | 50 | msldap/commons/__init__.py |
46 | 51 | msldap/commons/authbuilder.py |
52 | msldap/commons/common.py | |
47 | 53 | msldap/commons/credential.py |
54 | msldap/commons/exceptions.py | |
48 | 55 | msldap/commons/proxy.py |
49 | 56 | msldap/commons/target.py |
50 | 57 | msldap/commons/url.py |
52 | 59 | msldap/crypto/AES.py |
53 | 60 | msldap/crypto/BASE.py |
54 | 61 | msldap/crypto/DES.py |
62 | msldap/crypto/MD4.py | |
55 | 63 | msldap/crypto/RC4.py |
56 | 64 | msldap/crypto/TDES.py |
57 | 65 | msldap/crypto/__init__.py |
68 | 76 | msldap/crypto/pure/RC4/__init__.py |
69 | 77 | msldap/examples/__init__.py |
70 | 78 | msldap/examples/msldapclient.py |
79 | msldap/examples/msldapcompdnslist.py | |
80 | msldap/external/__init__.py | |
81 | msldap/external/aiocmd/__init__.py | |
82 | msldap/external/aiocmd/setup.py | |
83 | msldap/external/aiocmd/aiocmd/__init__.py | |
84 | msldap/external/aiocmd/aiocmd/aiocmd.py | |
85 | msldap/external/aiocmd/aiocmd/nested_completer.py | |
86 | msldap/external/asciitree/__init__.py | |
87 | msldap/external/asciitree/setup.py | |
88 | msldap/external/asciitree/asciitree/__init__.py | |
89 | msldap/external/asciitree/asciitree/drawing.py | |
90 | msldap/external/asciitree/asciitree/traversal.py | |
91 | msldap/external/asciitree/asciitree/util.py | |
71 | 92 | msldap/ldap_objects/__init__.py |
72 | 93 | msldap/ldap_objects/adcomp.py |
73 | 94 | msldap/ldap_objects/adgpo.py |
74 | 95 | msldap/ldap_objects/adgroup.py |
75 | 96 | msldap/ldap_objects/adinfo.py |
76 | 97 | msldap/ldap_objects/adou.py |
98 | msldap/ldap_objects/adschemaentry.py | |
77 | 99 | msldap/ldap_objects/adsec.py |
78 | 100 | msldap/ldap_objects/adtrust.py |
79 | 101 | msldap/ldap_objects/aduser.py |
80 | 102 | msldap/ldap_objects/common.py |
81 | 103 | msldap/network/__init__.py |
104 | msldap/network/multiplexor.py | |
82 | 105 | msldap/network/selector.py |
83 | 106 | msldap/network/socks.py |
84 | 107 | msldap/network/tcp.py |
108 | msldap/network/wsnet.py | |
85 | 109 | msldap/network/proxy/__init__.py |
86 | 110 | msldap/network/proxy/handler.py |
87 | 111 | msldap/protocol/__init__.py |
0 | 0 | [console_scripts] |
1 | 1 | msldap = msldap.examples.msldapclient:main |
2 | msldapcompdns = msldap.examples.msldapcompdnslist:main | |
2 | 3 |
0 | 0 | asn1crypto |
1 | aiocmd | |
2 | asciitree | |
3 | asysocks | |
4 | winacl>=0.0.2 | |
1 | asysocks>=0.1.1 | |
2 | minikerberos>=0.2.14 | |
3 | prompt-toolkit>=3.0.2 | |
4 | tqdm | |
5 | winacl>=0.1.1 | |
5 | 6 | |
6 | 7 | [:platform_system == "Windows"] |
7 | winsspi | |
8 | winsspi>=0.0.9 |
19 | 19 | |
20 | 20 | # Application author details: |
21 | 21 | author="Tamas Jos", |
22 | author_email="[email protected]", | |
22 | author_email="[email protected]", | |
23 | 23 | |
24 | 24 | # Packages |
25 | 25 | packages=find_packages(), |
31 | 31 | # Details |
32 | 32 | url="https://github.com/skelsec/msldap", |
33 | 33 | |
34 | zip_safe = True, | |
34 | zip_safe = False, | |
35 | 35 | # |
36 | 36 | # license="LICENSE.txt", |
37 | 37 | description="Python library to play with MS LDAP", |
38 | 38 | long_description="Python library to play with MS LDAP", |
39 | 39 | |
40 | 40 | # long_description=open("README.txt").read(), |
41 | python_requires='>=3.6', | |
41 | python_requires='>=3.7', | |
42 | 42 | classifiers=( |
43 | "Programming Language :: Python :: 3.6", | |
43 | "Programming Language :: Python :: 3.7", | |
44 | "Programming Language :: Python :: 3.8", | |
44 | 45 | "License :: OSI Approved :: MIT License", |
45 | 46 | "Operating System :: OS Independent", |
46 | 47 | ), |
47 | 48 | install_requires=[ |
48 | 49 | 'asn1crypto', |
49 | 'winsspi;platform_system=="Windows"', | |
50 | 'aiocmd', | |
51 | 'asciitree', | |
52 | #'ldap_filter', | |
53 | 'asysocks', | |
54 | 'winacl>=0.0.2' | |
50 | 'winsspi>=0.0.9;platform_system=="Windows"', | |
51 | 'minikerberos>=0.2.14', | |
52 | 'asysocks>=0.1.1', | |
53 | 'winacl>=0.1.1', | |
54 | 'prompt-toolkit>=3.0.2', | |
55 | 'tqdm', | |
55 | 56 | ], |
56 | 57 | entry_points={ |
57 | 58 | 'console_scripts': [ |
58 | 59 | 'msldap = msldap.examples.msldapclient:main', |
60 | 'msldapcompdns = msldap.examples.msldapcompdnslist:main', | |
59 | 61 | ], |
60 | 62 | } |
61 | 63 | )⏎ |