Codebase list msldap / 75409fe
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
72 changed file(s) with 5826 addition(s) and 1388 deletion(s). Raw diff Collapse all Expand all
0 This projects contains two other project written by a 3rd party.
1 All code is licensed under MIT.
2
3 License for MSLDAP:
04 MIT License
15
2 Copyright (c) 2018
6 Copyright (c) 2018 Tamas Jos
37
48 Permission is hereby granted, free of charge, to any person obtaining a copy
59 of this software and associated documentation files (the "Software"), to deal
1822 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1923 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2024 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.
00 Metadata-Version: 1.2
11 Name: msldap
2 Version: 0.2.10
2 Version: 0.3.30
33 Summary: Python library to play with MS LDAP
44 Home-page: https://github.com/skelsec/msldap
55 Author: Tamas Jos
6 Author-email: [email protected]
6 Author-email: [email protected]
77 License: UNKNOWN
88 Description: Python library to play with MS LDAP
99 Platform: UNKNOWN
10 Classifier: Programming Language :: Python :: 3.6
10 Classifier: Programming Language :: Python :: 3.7
11 Classifier: Programming Language :: Python :: 3.8
1112 Classifier: License :: OSI Approved :: MIT License
1213 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
05 # msldap
16 LDAP library for MS AD
7
8 # Documentation
9 [Awesome documentation here!](https://msldap.readthedocs.io/en/latest/)
210
311 # Features
412 - Comes with a built-in console LDAP client
513 - 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)
718 - Supports SOCKS5 proxy withot the need of extra proxifyer
819 - Minimal footprint
920 - A lot of pre-built queries for convenient information polling
1021 - Easy to integrate to your project
11 - Completely missing documentation
1222 - No testing suite
1323
1424 # Installation
1828 `pip install msldap`
1929
2030 # Prerequisites
21 - `ldap3` module. It's pure python so you dont have to compile anything.
2231 - `winsspi` module. For windows only. This supports SSPI based authentication.
2332 - `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.
2534 - `aiocmd` For the interactive client
2635 - `asciitree` For plotting nice trees in the interactive client
2736
3443 The new connection string is composed in the following manner:
3544 `<protocol>+<auth_method>://<domain>\<username>:<password>@<ip>:<port>/?<param>=<value>&<param>=<value>&...`
3645 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
3950 <protocol> sets the ldap protocol following values supported:
4051 - 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!)
4564 - anonymous
4665 - 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
4772 <param> can be:
4873 - timeout : connction timeout in seconds
4974 - proxytype: currently only socks5 proxy is supported
5075 - proxyhost: Ip or hostname of the proxy server
5176 - proxyport: port of the proxy server
5277 - proxytimeout: timeout ins ecodns for the proxy connection
78 - dc: the IP address of the domain controller, MUST be used for kerberos authentication
5379
5480 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
5993 ldap://TEST\\victim:[email protected]/DC=test,DC=corp/
6094 ldap://TEST\\victim:[email protected]/DC=test,DC=corp/?timeout=99&proxytype=socks5&proxyhost=127.0.0.1&proxyport=1080&proxytimeout=44
6195 ```
6296
6397 # Kudos
64 This project is built on top of the [ldap3](https://github.com/cannatag/ldap3) project.
98
6565
6666 elif args.command == 'spn':
6767 connection.connect()
68 adinfo = connection.get_ad_info()
68 adinfo, err = connection.get_ad_info()
6969 with open(args.outfile, 'w', newline='', encoding = 'utf8') as f:
7070 for user in connection.get_all_service_user_objects():
7171 f.write(user.sAMAccountName + '\r\n')
00
1 __version__ = "0.2.10"
1 __version__ = "0.3.30"
22 __banner__ = \
33 """
44 # msldap %s
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()
99 ## TODO: RPC auth type is not implemented or tested!!!!
1010
1111 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
1313 from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP
1414 from minikerberos.protocol.encryption import Enctype, Key, _enctype_table
1515
1616 from multiplexor.operator.external.sspi import KerberosSSPIClient
1717 from multiplexor.operator import MultiplexorOperator
18 import enum
1819
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
2023
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:
2250 def __init__(self, settings):
2351 self.iterations = 0
2452 self.settings = settings
2957 self.gssapi = None
3058 self.etype = None
3159 self.session_key = None
60 self.seq_number = 0
61 self.flags = ISC_REQ.CONNECTION
3262
3363 self.setup()
3464
3565 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
3781
3882 async def encrypt(self, data, message_no):
3983 return self.gssapi.GSS_Wrap(data, message_no)
4185 async def decrypt(self, data, message_no, direction='init', auth_data=None):
4286 return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data)
4387
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
44100 def get_session_key(self):
45101 return self.session_key
46102
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):
48104 #authdata is only for api compatibility reasons
49105 if self.ksspi is None:
50106 await self.start_remote_kerberos()
51107 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)
93124
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
101132 except Exception as e:
102 import traceback
103 traceback.print_exc()
104 return None
133 return None, None, e
105134
106135 async def start_remote_kerberos(self):
107136 try:
22 # This is just a simple interface to the minikerberos library to support SPNEGO
33 #
44 #
5 # - Hardships -
6 # 1. DCERPC kerberos authentication requires a complete different approach and flags,
7 # also requires mutual authentication
8 #
95 # - Links -
10 # 1. Most of the idea was taken from impacket
11 # 2. See minikerberos library
6 # 1. See minikerberos library
127
138 import datetime
149
10 import os
1511 from minikerberos.common import *
1612
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
1917 from minikerberos.protocol.structures import ChecksumFlags
2018 from minikerberos.protocol.encryption import Enctype, Key, _enctype_table
2119 from minikerberos.protocol.constants import MESSAGE_TYPE
2220 from minikerberos.aioclient import AIOKerberosClient
21 from minikerberos.network.aioclientsockssocket import AIOKerberosClientSocksSocket
22 from msldap import logger
2323
2424 # 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 ]
2533
2634 class MSLDAPKerberos:
2735 def __init__(self, settings):
2836 self.settings = settings
37 self.signing_preferred = None
38 self.encryption_preferred = None
2939 self.ccred = None
3040 self.target = None
3141 self.spn = None
3242 self.kc = None
43 self.flags = None
44 self.preferred_etypes = [23,17,18]
3345
3446 self.session_key = None
3547 self.gssapi = None
3648 self.iterations = 0
3749 self.etype = None
50 self.seq_number = 0
51 self.expected_server_seq_number = None
52 self.from_ccache = False
3853
3954 self.setup()
4055
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
4163 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
4368
4469 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
4674
4775 async def sign(self, data, message_no, direction = 'init'):
76 """
77 Signs a message.
78 """
4879 return self.gssapi.GSS_GetMIC(data, message_no, direction = direction)
4980
5081 async def encrypt(self, data, message_no):
82 """
83 Encrypts a message.
84 """
85
5186 return self.gssapi.GSS_Wrap(data, message_no)
5287
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)
5594
5695 def setup(self):
5796 self.ccred = self.settings.ccred
5897 self.spn = self.settings.spn
5998 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
62109
63110
64111 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
76145 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
82147 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
85184 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
88206 cipher = _enctype_table[int(aprep['enc-part']['etype'])]()
89207 cipher_text = aprep['enc-part']['cipher']
90208 temp = cipher.decrypt(self.session_key, 12, cipher_text)
91
92209 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)
112214 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
55 #
66
77 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
1111 from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP
1212 from minikerberos.protocol.encryption import Enctype, Key, _enctype_table
1313
1515 def __init__(self, settings):
1616 self.iterations = 0
1717 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
2227 self.gssapi = None
2328 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
3266 async def encrypt(self, data, message_no):
67 """
68 Encrypts a message.
69 """
3370 return self.gssapi.GSS_Wrap(data, message_no)
3471
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)
3777
3878 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
4087
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:
4493 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()
51112
113 return token, False, None
52114
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
57115
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)
63121
64122
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
66134
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)
74140 self.gssapi = get_gssapi(subkey)
75141
76142 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
81144
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
85147
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
228228 def test_construct():
229229 pass
230230
231 if __name__ == '__main__':
232 from aiosmb.utils.hexdump import hexdump
233
234 test()
159159 def test_construct():
160160 pass
161161
162 if __name__ == '__main__':
163 from aiosmb.utils.hexdump import hexdump
164
165 test()
4040 FRAGMENT_TO_FIT = 2097152
4141 HTTP = 0x10000000
4242
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:
4456 def __init__(self, settings):
4557 self.settings = settings
4658 self.mode = None #'CLIENT'
4860 self.operator = None
4961 self.client = None
5062 self.target = None
51 #self.ntlmChallenge = None
63 self.seq_number = 0
5264
5365 self.session_key = None
5466 self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL'))
6577
6678 def get_signkey(self, mode = 'Client'):
6779 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)
7580
7681 def get_session_key(self):
7782 return self.session_key
7883
79 def get_extra_info(self):
80 return self.ntlm_ctx.get_extra_info()
81
8284 def is_extended_security(self):
8385 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()
8495
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)
90104
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
92107 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
96111
97112 if is_rpc is True and flags is None:
98113 flags = ISC_REQ.REPLAY_DETECT | ISC_REQ.CONFIDENTIALITY| ISC_REQ.USE_SESSION_KEY| ISC_REQ.INTEGRITY| ISC_REQ.SEQUENCE_DETECT| ISC_REQ.CONNECTION
99114 flags = int(flags)
100115
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()
104127 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())
115129
116 return data, res
117
118 else:
119 raise Exception('Server mode not implemented!')
130 return data, res, None
120131
121132
122133 async def start_remote_sspi(self):
133144 #print(sspi_url)
134145 self.sspi = SSPINTLMClient(sspi_url)
135146 await self.sspi.connect()
136 return True
147 return True, None
137148 except Exception as e:
138149 import traceback
139150 traceback.print_exc()
140 return None
151 return None, e
141152
142153
0
10 import os
21 import struct
32 import hmac
43 import copy
4 import hashlib
55
66 #from aiosmb.commons.connection.credential import SMBNTLMCredential
77 #from aiosmb.commons.serverinfo import NTLMServerInfo
2424 self.mode = mode
2525 self.template_name = template_name
2626 self.custom_template = custom_template #for custom templates, must be dict
27
28 self.encrypt = False
2729
2830 self.template = None
2931 self.ntlm_downgrade = False
4042
4143 self.template = self.custom_template
4244
45 self.encrypt = self.credential.encrypt
46
47 if self.encrypt is True:
48 self.template_name = 'Windows10_15063_channel'
49
4350 if self.mode.upper() == 'SERVER':
4451 if self.template_name in NTLMServerTemplates:
4552 self.template = NTLMServerTemplates[self.template_name]
4653 else:
4754 raise Exception('No NTLM server template found with name %s' % self.template_name)
4855
49 else:
56 else:
5057 if self.template_name in NTLMClientTemplates:
5158 self.template = NTLMClientTemplates[self.template_name]
5259 if 'ntlm_downgrade' in self.template:
8491
8592 self.crypthandle_client = None
8693 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
9198 self.iteration_cnt = 0
9299 self.ntlm_credentials = None
93100 self.timestamp = None #used in unittest only!
109116 self.RandomSessionKey = self.settings.template['session_key']
110117
111118 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')
125119
126120 def load_negotiate(self, data):
127121 self.ntlmNegotiate = NTLMNegotiate.from_bytes(data)
135129 def load_sessionkey(self, data):
136130 self.RandomSessionKey = data
137131 self.setup_crypto()
132
133 def get_seq_number(self):
134 return self.seq_number
138135
139136 def set_sign(self, tf = True):
140137 if tf == True:
188185 #msg.Checksum = struct.unpack('<I',handle(messageSignature['Checksum']))[0]
189186
190187 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 )
194188
195189 async def encrypt(self, data, sequence_no):
190 """
191 This function is to support SSPI encryption.
192 """
196193 return self.SEAL(
197 self.SignKey_client,
194 #self.SignKey_client,
195 self.SignKey_client,
198196 self.SealKey_client,
199 data,
200 data,
197 data,
198 data,
201199 sequence_no,
202200 self.crypthandle_client.encrypt
203201 )
204202
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
226240
227241 def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt):
242 """
243 This is the official SEAL function.
244 """
228245 sealedMessage = cipher_encrypt(messageToEncrypt)
229246 signature = self.MAC(cipher_encrypt, signingKey, seqNum, messageToSign)
230247 return sealedMessage, signature
231248
232249 def SIGN(self, signingKey, message, seqNum, cipher_encrypt):
250 """
251 This is the official SIGN function.
252 """
233253 return self.MAC(cipher_encrypt, signingKey, seqNum, message)
234254
235255 def signing_needed(self):
290310
291311 if mode == 'Client':
292312 self.SignKey_client = signkey
293 if signkey is not None:
294 self.signhandle_client = RC4(self.SealKey_client)
295313
296314 else:
297315 self.SignKey_server = signkey
298 if signkey is not None:
299 self.signhandle_server = RC4(self.SignKey_server)
300316
301317 return signkey
302318
332348 self.calc_signkey('Client')
333349 self.calc_signkey('Server')
334350
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':
363353 if self.iteration_cnt == 0:
364354 if authData is not None:
365355 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()
370356
371357 self.iteration_cnt += 1
372358 #negotiate message was already calulcated in setup
373359 self.ntlmNegotiate = NTLMNegotiate.construct(self.flags, domainname = self.settings.template['domain_name'], workstationname = self.settings.template['workstation_name'], version = self.settings.template.get('version'))
374360 self.ntlmNegotiate_raw = self.ntlmNegotiate.to_bytes()
375 return self.ntlmNegotiate_raw, True
361 return self.ntlmNegotiate_raw, True, None
376362
377363 else:
378364 #server challenge incoming
379365 self.ntlmChallenge_raw = authData
380366 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()
386367
387368 ##################self.flags = self.ntlmChallenge.NegotiateFlags
388369
396377 lmresp = LMResponse()
397378 lmresp.Response = b'\x00'
398379 self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, lm_response= lmresp)
399 return self.ntlmAuthenticate.to_bytes(), False
380 return self.ntlmAuthenticate.to_bytes(), False, None
400381
401382 if self.flags & NegotiateFlags.NEGOTIATE_EXTENDED_SESSIONSECURITY:
402383 #Extended auth!
422403 lmresp = LMResponse()
423404 lmresp.Response = b'\x00'
424405 self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, lm_response= lmresp)
425 return self.ntlmAuthenticate.to_bytes(), False
406 return self.ntlmAuthenticate.to_bytes(), False, None
426407
427408 else:
428409 #comment this out for testing!
429410 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()
431416 ###
432417
433418 self.ntlm_credentials = netntlmv2.construct(self.ntlmChallenge.ServerChallenge, self.challenge, ti, self.settings.credential, timestamp = self.timestamp)
438423 mic = None
439424
440425 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
442428 self.ntlmAuthenticate_raw = self.ntlmAuthenticate.to_bytes()
443 return self.ntlmAuthenticate_raw, False
429 return self.ntlmAuthenticate_raw, False, None
444430
445431 elif self.mode.upper() == 'RELAY':
446432 if self.iteration_cnt == 0:
460446
461447 else:
462448 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
11 #
22 # This is just a simple interface to the winsspi library to support NTLM
33 #
4 from winsspi.sspi import NTLMSMBSSPI
4 from winsspi.sspi import NTLMMSLDAPSSPI
5 from winsspi.common.function_defs import ISC_REQ
56 from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings
67
78 class MSLDAPNTLMSSPI:
89 def __init__(self, settings):
910 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
1623 self.session_key = None
1724 self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL'))
18
19 self.setup()
2025
2126 @property
2227 def ntlmChallenge(self):
2328 return self.ntlm_ctx.ntlmChallenge
29
30 def get_seq_number(self):
31 return self.ntlm_ctx.get_seq_number()
2432
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
3039 def get_sealkey(self, mode = 'Client'):
3140 return self.ntlm_ctx.get_sealkey(mode = mode)
3241
4453
4554 def SIGN(self, signingKey, message, seqNum, cipher_encrypt):
4655 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)
4762
4863 def get_session_key(self):
4964 if not self.session_key:
5772 def is_extended_security(self):
5873 return self.ntlm_ctx.is_extended_security()
5974
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)
6277
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)
6580
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
7086 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())
7795
78 return data, res
96 return data, res, None
7997
8098
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
201201 print(repr(cc3))
202202 cc4 = NTLMv2ClientChallenge.from_bytes(cc3.to_bytes())
203203
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
55
66 import copy
77 from msldap.authentication.spnego.asn1_structs import *
8 from asn1crypto.core import OctetString
89
910 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/d4f2b41c-5f9e-4e11-98d0-ade76467095d
1011
4647 def encryption_needed(self):
4748 return self.selected_authentication_context.encryption_needed()
4849
50 def get_seq_number(self):
51 return self.selected_authentication_context.get_seq_number()
52
4953 async def unsign(self, data):
5054 #TODO: IMPLEMENT THIS
5155 return data
5256
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)
5562
5663 async def encrypt(self, data, message_no):
5764 return await self.selected_authentication_context.encrypt(data, message_no)
5865
5966 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)
6168
6269 def add_auth_context(self, name, ctx):
6370 """
7784 def select_common_athentication_type(self, mech_types):
7885 for auth_type_name in self.authentication_contexts:
7986 if auth_type_name in mech_types:
80 print(auth_type_name)
8187 return auth_type_name, self.authentication_contexts[auth_type_name]
8288
8389 return None, None
8490
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
8795 if not result:
88 return None, False
96 return None, False, None
8997 response = {}
9098 if include_negstate == True:
9199 if to_continue == True:
94102 response['negState'] = NegState('accept-completed')
95103
96104 response['responseToken'] = result
97 return response, to_continue
105 return response, to_continue, None
98106
99107 def get_extra_info(self):
100108 if hasattr(self.selected_authentication_context, 'get_extra_info'):
115123 #spnego = GSS_SPNEGO({'NegotiationToken':negtoken})
116124 return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump()
117125
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):
119127 """
120128 This function is called (multiple times) during negotiation phase of a protocol to determine hich auth mechanism to be used
121129 Token is a byte array that is an ASN1 NegotiationToken structure.
122130 """
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)})
153168
154169
155170 #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
197194 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
252219 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
257223 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
260233
261234 def test():
262235 test_data = bytes.fromhex('a03e303ca00e300c060a2b06010401823702020aa22a04284e544c4d5353500001000000978208e2000000000000000000000000000000000a00d73a0000000f')
4343 async def decrypt(self, data, message_no):
4444 return await self.sspi.decrypt(data, message_no)
4545
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):
4747 try:
4848 if self.mode.upper() == 'CLIENT':
4949 res, data = self.sspi.authGSSClientStep(token)
33 # Tamas Jos (@skelsec)
44 #
55
6 import copy
7 import asyncio
8
69 from msldap import logger
10 from msldap.commons.common import MSLDAPClientStatus
711 from msldap.wintypes.asn1.sdflagsrequest import SDFlagsRequest, SDFlagsRequestValue
812 from msldap.protocol.constants import BASE, ALL_ATTRIBUTES, LEVEL
913
10 from msldap.protocol.query import escape_filter_chars, query_syntax_converter
14 from msldap.protocol.query import escape_filter_chars
1115 from msldap.connection import MSLDAPClientConnection
1216 from msldap.protocol.messages import Control
1317 from msldap.ldap_objects import *
1418
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
1524 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):
1741 self.creds = creds
1842 self.target = target
1943
20 self.ldap_query_page_size = ldap_query_page_size
44 self.ldap_query_page_size = self.target.ldap_query_page_size
2145 self._tree = None
2246 self._ldapinfo = None
2347 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
2562
2663 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
3783
3884 def get_server_info(self):
3985 return self._serverinfo
4086
41 async def pagedsearch(self, ldap_filter, attributes, controls = None):
87 async def pagedsearch(self, query, attributes, controls = None):
4288 """
4389 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!')
55116 t = []
56117 for x in attributes:
57118 t.append(x.encode())
58119 attributes = t
59 ldap_filter = query_syntax_converter(ldap_filter)
60120
61121 t = []
62122 if controls is not None:
70130 controls = t
71131
72132 async for entry, err in self._con.pagedsearch(
73 self._tree.encode(),
74 ldap_filter,
133 self._tree,
134 query,
75135 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
78139 ):
79140
80141 if err is not None:
81 raise err
142 yield None, err
143 return
82144 if entry['objectName'] == '' and entry['attributes'] == '':
83145 #searchresref...
84146 continue
85147 #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):
89151 """
90152 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))
98164 tree = {}
99 #entries =
100165 async for entry, err in self._con.pagedsearch(
101 dn.encode(),
102 query_syntax_converter('(distinguishedName=*)'),
166 root_dn,
167 '(distinguishedName=*)',
103168 attributes = [b'distinguishedName'],
104 paged_size = self.ldap_query_page_size,
169 size_limit = self.ldap_query_page_size,
105170 search_scope=LEVEL,
106171 controls = None,
172 rate_limit=self.target.ldap_query_ratelimit
107173 ):
108174 if err is not None:
109175 raise err
116182 continue
117183 subtree = await self.get_tree_plot(entry['attributes']['distinguishedName'], level = level -1)
118184 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
125194 """
126195 logger.debug('Polling AD for all user objects')
127196 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
130202 logger.debug('Finished polling for entries!')
131203
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
144213 """
145214 logger.debug('Polling AD for all user objects')
146215 ldap_filter = r'(sAMAccountType=805306369)'
147216
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
150222 logger.debug('Finished polling for entries!')
151223
152224 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
153233 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
156239
157240 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
158248 ldap_filter = r'(sAMAccountType=805306369)'
159249 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!')
162307
163308 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
164318 ldap_filter = r'(&(sAMAccountType=805306369)(sAMAccountName=%s))' % sAMAccountName
165319 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
168322
169323 async def get_user(self, sAMAccountName):
170324 """
171325 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`)
172331 """
173332 logger.debug('Polling AD for user %s'% sAMAccountName)
174333 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
178340 logger.debug('Finished polling for entries!')
179341
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
180361 async def get_ad_info(self):
181362 """
182363 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`)
183367 """
184368 logger.debug('Polling AD for basic info')
185369 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
187373 self._ldapinfo = MSADInfo.from_ldap(entry)
188 return self._ldapinfo
374 return self._ldapinfo, None
189375
190376 logger.debug('Poll finished!')
191377
192378 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
193390 logger.debug('Polling AD for all SPN entries')
194391 ldap_filter = r'(&(sAMAccountType=805306369))'
195392 attributes = ['objectSid','sAMAccountName', 'servicePrincipalName']
196393
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):
201398 """
202399 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
204408 """
205409 logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine)
206410 if include_machine == True:
208412 else:
209413 ldap_filter = r'(&(servicePrincipalName=*)(!(sAMAccountName=*$)))'
210414
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
213420 logger.debug('Finished polling for entries!')
214421
215 async def get_all_knoreq_user_objects(self, include_machine = False):
422 async def get_all_knoreq_users(self, include_machine = False):
216423 """
217424 Fetches all user objects with useraccountcontrol DONT_REQ_PREAUTH flag set from the AD, and returns MSADUser object.
218425
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
219431 """
220432 logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine)
221433 if include_machine == True:
223435 else:
224436 ldap_filter = r'(&(userAccountControl:1.2.840.113556.1.4.803:=4194304)(!(sAMAccountName=*$)))'
225437
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
228443 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})
253461
254462 ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn)
255463 attributes = MSADSecurityInfo.ATTRS
256464 controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())]
257465
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
2621176 #async def get_all_tokengroups(self):
2631177 # """
2641178 # returns the tokengroups attribute for all user and machine on the server
2811195 # async for entry, err in self._con.response:
2821196 # #yield MSADTokenGroup.from_ldap(entry)
2831197 # 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)
55 from msldap.authentication.spnego.native import SPNEGO
66 from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings
77 from msldap.authentication.kerberos.native import MSLDAPKerberos
8 from msldap.commons.proxy import MSLDAPProxyType
89 from minikerberos.common.target import KerberosTarget
910 from minikerberos.common.proxy import KerberosProxy
1011 from minikerberos.common.creds import KerberosCredential
2627 self.is_guest = False
2728 self.nt_hash = None
2829 self.lm_hash = None
30 self.encrypt = False
2931
3032 class MSLDAPSIMPLECredential:
3133 def __init__(self):
4547 self.target = None #KerberosTarget
4648 self.ksoc = None #KerberosSocketAIO
4749 self.ccred = None
50 self.encrypt = False
51 self.enctypes = None #[23,17,18]
4852
4953 class MSLDAPKerberosSSPICredential:
5054 def __init__(self):
51 self.client = None
55 self.domain = None
5256 self.password = None
53 self.target = None
57 self.username = None
58 self.encrypt = False
5459
5560 class MSLDAPNTLMSSPICredential:
5661 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]
59134
60135
61136
81156 ntlmcred.domain = self.creds.domain if self.creds.domain is not None else ''
82157 ntlmcred.workstation = None
83158 ntlmcred.is_guest = False
159 ntlmcred.encrypt = self.creds.encrypt
160
84161
85162 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
88176
89177 settings = NTLMHandlerSettings(ntlmcred)
90178 return NTLMAUTHHandler(settings)
109197 ntlmcred.domain = self.creds.domain if self.creds.domain is not None else ''
110198 ntlmcred.workstation = None
111199 ntlmcred.is_guest = False
200 ntlmcred.encrypt = self.creds.encrypt
112201
113202 if self.creds.password is None:
114203 raise Exception('NTLM authentication requres password!')
135224 LDAPAuthProtocol.KERBEROS_AES,
136225 LDAPAuthProtocol.KERBEROS_PASSWORD,
137226 LDAPAuthProtocol.KERBEROS_CCACHE,
138 LDAPAuthProtocol.KERBEROS_KEYTAB]:
227 LDAPAuthProtocol.KERBEROS_KEYTAB,
228 LDAPAuthProtocol.KERBEROS_KIRBI]:
139229
140230 if self.target is None:
141231 raise Exception('Target must be specified with Kerberos!')
146236 if self.target.dc_ip is None:
147237 raise Exception('target must have a dc_ip for kerberos!')
148238
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 = []
153251 if self.creds.auth_method == LDAPAuthProtocol.KERBEROS_PASSWORD:
154252 kc.password = self.creds.password
253 kcred.enctypes = [23,17,18]
155254 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_NT:
156255 kc.nt_hash = self.creds.password
256 kcred.enctypes = [23]
157257
158258 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_AES:
159259 if len(self.creds.password) == 32:
160260 kc.kerberos_key_aes_128 = self.creds.password
261 kcred.enctypes = [17]
161262 elif len(self.creds.password) == 64:
162263 kc.kerberos_key_aes_256 = self.creds.password
264 kcred.enctypes = [18]
163265
164266 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_RC4:
165267 kc.kerberos_key_rc4 = self.creds.password
268 kcred.enctypes = [23]
166269
167270 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_CCACHE:
168271 kc.ccache = self.creds.password
272 kcred.enctypes = [23,17,18] # TODO: fix this
169273 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KEYTAB:
170274 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
171278 else:
172279 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
176284 kcred.ccred = kc
177285 kcred.spn = KerberosSPN.from_target_string(self.target.to_target_string())
178286 kcred.target = KerberosTarget(self.target.dc_ip)
287 kcred.encrypt = self.creds.encrypt
288
179289 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
185295
186296 handler = MSLDAPKerberos(kcred)
187297
195305 raise Exception('Target must be specified with Kerberos SSPI!')
196306
197307 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
201313
202314 handler = MSLDAPKerberosSSPI(kerbcred)
203315 #setting up SPNEGO
207319
208320 elif self.creds.auth_method == LDAPAuthProtocol.SSPI_NTLM:
209321 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
213327 handler = MSLDAPNTLMSSPI(ntlmcred)
214328 #setting up SPNEGO
215329 spneg = SPNEGO()
216330 spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler)
217331 return spneg
218332
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()
226337 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)
238350 #setting up SPNEGO
239351 spneg = SPNEGO()
240352 spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler)
241353 return spneg
242354
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()
247359 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)
260373 #setting up SPNEGO
261374 spneg = SPNEGO()
262375 spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler)
263376 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'
2828 SICILY = 'SICILY' #NTLM (old proprietary from MS)
2929 NTLM_PASSWORD = 'NTLM_PASSWORD' #actually SASL-GSSAPI-SPNEGO-NTLM
3030 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'
3942 SSPI_NTLM = 'SSPI_NTLM' #actually SASL-GSSAPI-SPNEGO-NTLM but with integrated SSPI
4043 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'
4148
4249 MSLDAP_GSS_METHODS = [
4350 LDAPAuthProtocol.NTLM_PASSWORD ,
4855 LDAPAuthProtocol.KERBEROS_PASSWORD ,
4956 LDAPAuthProtocol.KERBEROS_CCACHE ,
5057 LDAPAuthProtocol.KERBEROS_KEYTAB ,
58 LDAPAuthProtocol.KERBEROS_KIRBI ,
5159 LDAPAuthProtocol.SSPI_NTLM ,
5260 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 ,
5479 ]
5580
5681 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):
58101 self.auth_method = auth_method
59102 self.domain = domain
60103 self.username = username
61104 self.password = password
105 self.signing_preferred = False
106 self.encryption_preferred = False
62107 self.settings = settings
108 self.etypes = etypes
109 self.encrypt = encrypt
63110
64111 def get_msuser(self):
65112 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)
1616 SOCKS5_SSL = 'SOCKS5_SSL'
1717 MULTIPLEXOR = 'MULTIPLEXOR'
1818 MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL'
19 WSNET = 'WSNET'
20 WSNETWS = 'WSNETWS'
21 WSNETWSS = 'WSNETWSS'
1922
2023 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
2538
2639
2740 @staticmethod
2841 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 """
2950 proxy = MSLDAPProxy()
3051 url = urlparse(url_str)
3152 if url.query is None:
3657 return None
3758
3859 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)
4162 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)
4464
45 proxy.target = cu.get_target()
46 proxy.auth = cu.get_creds()
4765 return proxy
4866
4967 def __str__(self):
5270 t += '%s: %s\r\n' % (k, self.__dict__[k])
5371
5472 return t
55
56
5773
5874
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
55 #
66
77 import enum
8 import ssl
8
9 import platform
10 try:
11 import ssl
12 except:
13 if platform.system() == 'Emscripten':
14 pass
915
1016 class LDAPProtocol(enum.Enum):
1117 TCP = 'TCP'
1420
1521
1622 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):
1844 self.proto = proto
1945 self.host = host
2046 self.tree = tree
2248 self.proxy = proxy
2349 self.timeout = timeout
2450 self.dc_ip = None
51 self.serverip = None
2552 self.domain = None
2653 self.sslctx = None
54 self.ldap_query_page_size = ldap_query_page_size
55 self.ldap_query_ratelimit = ldap_query_ratelimit
2756
2857 def get_ssl_context(self):
2958 if self.proto == LDAPProtocol.SSL:
3059 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
3263 return self.sslctx
3364 return None
3465
66
77 import platform
88 import hashlib
9 import getpass
10 import base64
11 import enum
912 from urllib.parse import urlparse, parse_qs
1013
11 from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol
14 from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol, MSLDAP_KERBEROS_PROTOCOLS
1215 from msldap.commons.target import MSLDAPTarget, LDAPProtocol
1316 from msldap.commons.proxy import MSLDAPProxy, MSLDAPProxyType
1417 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'
1636
1737 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 """
1844
1945 help_epilog = """
2046 MSLDAP URL Format: <protocol>+<auth>://<username>:<password>@<ip_or_host>:<port>/<tree>/?<param>=<value>
2147 <protocol> sets the ldap protocol following values supported:
2248 - ldap
2349 - 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!)
2760 - anonymous
2861 - 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
2966 <param> can be:
3067 - timeout : connction timeout in seconds
3168 - proxytype: currently only socks5 proxy is supported
3269 - proxyhost: Ip or hostname of the proxy server
3370 - 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
3577
3678 Examples:
3779 ldap://10.10.10.2 (anonymous bind)
3880 ldaps://test.corp (anonymous bind)
39 ldap+sspi:///test.corp
81 ldap+sspi-ntlm://test.corp
82 ldap+sspi-kerberos://test.corp
4083 ldap://TEST\\victim:<password>@10.10.10.2 (defaults to SASL GSSAPI NTLM)
4184 ldap+simple://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth)
4285 ldap+plain://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth)
57100 self.domain = None
58101 self.username = None
59102 self.password = None
103 self.encrypt = False
60104 self.auth_settings = {}
105 self.etypes = None
61106
62107 self.ldap_proto = None
63108 self.ldap_host = None
65110 self.ldap_tree = None
66111 self.target_timeout = 5
67112 self.target_pagesize = 1000
113 self.target_ratelimit = 0
68114 self.dc_ip = None
69115 self.serverip = None
70116 self.proxy = None
71117
118 self.__pwpreprocess = None
119
72120 self.parse()
73121
74122
75123 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(
77131 domain=self.domain,
78132 username=self.username,
79133 password = self.password,
80134 auth_method=self.auth_scheme,
81135 settings = self.auth_settings
82136 )
137 t.encrypt = self.encrypt
138 t.etypes = self.etypes
139
140 return t
83141
84142 def get_target(self):
143 """
144 Creates a target object
145
146 :return: Target object
147 :rtype: :class:`MSLDAPTarget`
148 """
85149 target = MSLDAPTarget(
86150 self.ldap_host,
87151 port = self.ldap_port,
88152 proto = self.ldap_scheme,
89153 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
91157 )
92158 target.domain = self.domain
93159 target.dc_ip = self.dc_ip
94160 target.proxy = self.proxy
161 target.serverip = self.serverip
95162 return target
96163
97164 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 """
98171 cred = self.get_credential()
99172 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)
101185
102186 def scheme_decoder(self, scheme):
103187 schemes = []
126210 return
127211
128212 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'
130261 except:
131 raise Exception('Uknown scheme!')
262 try:
263 self.auth_scheme = LDAPAuthProtocol(schemes[1])
264 except:
265 raise Exception('Uknown scheme!')
132266
133267 return
134268
137271 self.scheme_decoder(url_e.scheme)
138272
139273 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
140287
141288 if url_e.username is not None:
142289 if url_e.username.find('\\') != -1:
172319 proxy_present = False
173320 if url_e.query is not None:
174321 query = parse_qs(url_e.query)
322 if 'etype' in query:
323 self.etypes = []
175324 for k in query:
176325 if k.startswith('proxy') is True:
177326 proxy_present = True
180329 elif k == 'timeout':
181330 self.timeout = int(query[k][0])
182331 elif k == 'serverip':
183 self.server_ip = query[k][0]
332 self.serverip = query[k][0]
184333 elif k == 'dns':
185334 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]]
186339 elif k.startswith('auth'):
187340 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])
188345 #elif k.startswith('same'):
189346 # self.auth_settings[k[len('same'):]] = query[k]
190347
201358 if self.domain is None:
202359 self.domain = '<CURRENT>'
203360
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
293365 # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]:
294366 # if self.proxy_port is None:
295367 # 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')
301368 #
302369 # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]:
303370 # if 'agentid' not in self.proxy_settings:
304371 # raise Exception('multiplexor proxy reuires agentid to be set! Set it via proxyagentid parameter!')
305372 #
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
317374
318375
319376
00 import asyncio
11
2
23 from msldap import logger
4 from msldap.commons.common import MSLDAPClientStatus
35 from msldap.protocol.messages import LDAPMessage, BindRequest, \
46 protocolOp, AuthenticationChoice, SaslCredentials, \
57 SearchRequest, AttributeDescription, Filter, Filters, \
6 Controls, Control, SearchControlValue
8 Controls, Control, SearchControlValue, AddRequest, \
9 ModifyRequest, DelRequest
710
811 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
1014 from msldap.commons.authbuilder import AuthenticatorBuilder
1115 from msldap.commons.credential import MSLDAP_GSS_METHODS
1216 from msldap.network.selector import MSLDAPNetworkSelector
1317 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
1423
1524 class MSLDAPClientConnection:
1625 def __init__(self, target, creds):
26 if target is None:
27 raise Exception('Target cant be none!')
1728 self.target = target
1829 self.creds = creds
1930 self.auth = AuthenticatorBuilder(self.creds, self.target).build()
2435 self.network = None
2536
2637 self.handle_incoming_task = None
38 self.status = MSLDAPClientStatus.RUNNING
39 self.lasterror = None
2740
2841 self.message_id = 0
2942 self.message_table = {}
3043 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)
3252
3353 async def __handle_incoming(self):
3454 try:
3555 while True:
3656 message_data, err = await self.network.in_queue.get()
3757 if err is not None:
38 logger.debug('Client terminating bc __handle_incoming!')
58 logger.debug('Client terminating bc __handle_incoming got an error!')
3959 raise err
4060
41 ################################
42 # #
43 # ADD CHANNEL BINDING HERE! #
44 ################################
45
61 #print('Incoming message data: %s' % message_data)
4662 if self.bind_ok is True:
4763 if self.__encrypt_messages is True:
48 #print('Encrypted %s' % message_data)
4964 #removing size
5065 message_data = message_data[4:]
5166 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
5371 #print('Decrypted %s' % message_data.hex())
72 #print('Decrypted %s' % message_data)
5473 except:
5574 import traceback
5675 traceback.print_exc()
76 raise
5777
5878 elif self.__sign_messages is True:
5979 #print('Signed %s' % message_data)
6484 except:
6585 import traceback
6686 traceback.print_exc()
87 raise
88
6789
6890 msg_len = calcualte_length(message_data)
6991 msg_total_len = len(message_data)
90112 self.message_table_notify[message_id].set()
91113
92114 except asyncio.CancelledError:
93 #not notifying clients, at this point the client is terminating
115 self.status = MSLDAPClientStatus.STOPPED
94116 return
95117
96118 except Exception as e:
97 import traceback
98 traceback.print_exc()
119 self.status = MSLDAPClientStatus.ERROR
120 self.lasterror = e
99121 for msgid in self.message_table_notify:
100122 self.message_table[msgid] = [e]
101123 self.message_table_notify[msgid].set()
124
125 self.status = MSLDAPClientStatus.STOPPED
102126
103127
104128 async def send_message(self, message):
142166 return messages
143167
144168 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
153198
154199 async def disconnect(self):
200 """
201 Tears down the connection.
202
203 :return: Nothing
204 :rtype: None
205 """
206
155207 logger.debug('Disconnecting!')
156208 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()
159213
160214
161215 def __bind_success(self):
172226 self.network.is_plain_msg = False
173227
174228 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 """
175236 logger.debug('BIND in progress...')
176237 try:
177238 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
180244 auth = {
181245 'sicily_disco' : b''
182246 }
197261 return False, res
198262 res = res.native
199263 if res['protocolOp']['resultCode'] != 'success':
200 return False, Exception(
201 'BIND failed! Result code: "%s" Reason: "%s"' % (
264 return False, LDAPBindException(
202265 res['protocolOp']['resultCode'],
203266 res['protocolOp']['diagnosticMessage']
204 ))
267 )
205268
206269 auth = {
207270 'sicily_nego' : data
223286 return False, res
224287 res = res.native
225288 if res['protocolOp']['resultCode'] != 'success':
226 return False, Exception(
227 'BIND failed! Result code: "%s" Reason: "%s"' % (
289 return False, LDAPBindException(
228290 res['protocolOp']['resultCode'],
229291 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
233297
234298 auth = {
235299 'sicily_resp' : data
251315 return False, res
252316 res = res.native
253317 if res['protocolOp']['resultCode'] != 'success':
254 return False, Exception(
255 'BIND failed! Result code: "%s" Reason: "%s"' % (
318 return False, LDAPBindException(
256319 res['protocolOp']['resultCode'],
257320 res['protocolOp']['diagnosticMessage']
258 ))
321 )
259322
260323
261324 self.__bind_success()
294357 return True, None
295358
296359 else:
297 return False, Exception(
298 'BIND failed! Result code: "%s" Reason: "%s"' % (
360 return False, LDAPBindException(
299361 res['protocolOp']['resultCode'],
300362 res['protocolOp']['diagnosticMessage']
301 ))
363 )
302364
303365 elif self.creds.auth_method in MSLDAP_GSS_METHODS:
304366 challenge = None
305367 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
307374
308375 sasl = {
309376 'mechanism' : 'GSS-SPNEGO'.encode(),
315382
316383 bindreq = {
317384 'version' : 3,
318 'name': ''.encode(),
385 'name': b'',
319386 'authentication': AuthenticationChoice(auth),
320387 }
321388
329396 return False, res
330397 res = res.native
331398 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()
332405 self.__bind_success()
406
333407 return True, None
334408
335409 elif res['protocolOp']['resultCode'] == 'saslBindInProgress':
337411 continue
338412
339413 else:
340 return False, Exception(
341 'BIND failed! Result code: "%s" Reason: "%s"' % (
414 return False, LDAPBindException(
342415 res['protocolOp']['resultCode'],
343416 res['protocolOp']['diagnosticMessage']
344 ))
417 )
345418
346 #print(res)
419 else:
420 raise Exception('Not implemented authentication method: %s' % self.creds.auth_method.name)
347421 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:
349537 return False, e
350538
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
355570 try:
356571 if timeLimit is None:
357572 timeLimit = 600 #not sure
573
574 flt = query_syntax_converter(query)
358575
359576 searchreq = {
360 'baseObject' : base,
577 'baseObject' : base.encode(),
361578 'scope': search_scope,
362579 'derefAliases': derefAliases,
363 'sizeLimit': paged_size,
580 'sizeLimit': size_limit,
364581 'timeLimit': timeLimit,
365 'typesOnly': typesOnly,
366 'filter': filter,
582 'typesOnly': types_only,
583 'filter': flt,
367584 'attributes': attributes,
368585 }
369586
380597 msg_type = message['protocolOp'].name
381598 message = message.native
382599 if msg_type == 'searchResDone':
383 #print(message)
384 #print('BREAKING!')
385600 if return_done is True:
386601 yield (message, None)
387602 break
402617 except Exception as e:
403618 yield (None, e)
404619
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
406651 try:
407652 cookie = b''
408653 while True:
409
654 await asyncio.sleep(rate_limit)
410655 ctrl_list_temp = [
411656 Control({
412657 'controlType' : b'1.2.840.113556.1.4.319',
413658 'controlValue': SearchControlValue({
414 'size' : paged_size,
659 'size' : size_limit,
415660 'cookie': cookie
416661 }).dump()
417662 })
426671
427672 async for res, err in self.search(
428673 base,
429 filter,
674 query,
430675 attributes,
431676 search_scope = search_scope,
432 paged_size=paged_size,
433 typesOnly=typesOnly,
677 size_limit=size_limit,
678 types_only=typesOnly,
434679 derefAliases=derefAliases,
435680 timeLimit=timeLimit,
436681 controls = ctrs,
439684 if err is not None:
440685 yield (None, err)
441686 return
442
687
443688 if 'resultCode' in res['protocolOp']:
444689 for control in res['controls']:
445690 if control['controlType'] == b'1.2.840.113556.1.4.319':
461706
462707
463708 async def get_serverinfo(self):
709 if self.status != MSLDAPClientStatus.RUNNING:
710 return None, Exception('Connection not running! Probably encountered an error')
711
464712 attributes = [
465713 b'subschemaSubentry',
466714 b'dsServiceName',
496744
497745 msg_id = await self.send_message(msg)
498746 res = await self.recv_message(msg_id)
499 res = res[0].native
500
747 res = res[0]
501748 if isinstance(res, Exception):
502749 return None, res
503750
504751 #print('res')
505752 #print(res)
506 return convert_attributes(res['protocolOp']['attributes']), None
753 return convert_attributes(res.native['protocolOp']['attributes']), None
507754
508755
509756 async def amain():
524771 #target.dc_ip = '10.10.10.2'
525772 #target.domain = 'TEST'
526773
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'
528775
529776 dec = MSLDAPURLDecoder(url)
530777 cred = dec.get_credential()
540787 res, err = await client.bind()
541788 if err is not None:
542789 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
549790
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!')
550807
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)
556811
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
568812 await client.disconnect()
569813
570814
577821
578822 logger.setLevel(2)
579823
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
589827
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)
612828
613829
614830
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
44 currently it's not the perfect wrapper, needs to be extended
55 """
66
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
99 try:
1010 from Crypto.Cipher import ARC4 as _pyCryptoRC4
1111 except Exception as e:
00 import hashlib
11 import hmac
22
3 from aiosmb.crypto.BASE import hashBASE, hmacBASE
3 from msldap.crypto.BASE import hashBASE, hmacBASE
4 from msldap.crypto.MD4 import MD4
45
56 class md5(hashBASE):
67 def __init__(self, data = None):
1415 def hexdigest(self):
1516 return self._hash.hexdigest()
1617
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
2831
2932 class hmac_md5(hmacBASE):
3033 def __init__(self, key):
99 import csv
1010 import shlex
1111 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
1516 from tqdm import tqdm
1617
1718 from msldap import logger
2122 from msldap.ldap_objects import MSADUser, MSADMachine, MSADUser_TSV_ATTRS
2223
2324 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
2428
2529
2630 class MSLDAPClientConsole(aiocmd.PromptToolkitCmd):
3236 self.connection = None
3337 self.adinfo = None
3438 self.ldapinfo = None
39 self.domain_name = None
3540
3641 async def do_login(self, url = None):
3742 """Performs connection and login"""
38 try:
39 print('url %s' % repr(url))
40
43 try:
4144 if self.conn_url is None and url is None:
4245 print('Not url was set, cant do logon')
4346 if url is not None:
4447 self.conn_url = MSLDAPURLDecoder(url)
4548
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())
4851
4952
5053 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
5562
5663 async def do_ldapinfo(self, show = True):
5764 """Prints detailed LDAP connection info (DSA)"""
5966 if self.ldapinfo is None:
6067 self.ldapinfo = self.connection.get_server_info()
6168 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
6575
6676 async def do_adinfo(self, show = True):
6777 """Prints detailed Active Driectory info"""
6878 try:
6979 if self.adinfo is None:
7080 self.adinfo = self.connection._ldapinfo
81 self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.')
7182 if show is True:
7283 print(self.adinfo)
73 except:
74 traceback.print_exc()
84 return True
85 except:
86 traceback.print_exc()
87 return False
7588
7689 async def do_spns(self):
7790 """Fetches kerberoastable user accounts"""
7891 try:
7992 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
8196 print(user.sAMAccountName)
82 except:
83 traceback.print_exc()
84
97
98 return True
99 except:
100 traceback.print_exc()
101 return False
102
85103 async def do_asrep(self):
86104 """Fetches ASREP-roastable user accounts"""
87105 try:
88106 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
90110 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
94135
95136 async def do_dump(self):
96137 """Fetches ALL user and machine accounts from the domain with a LOT of attributes"""
101142 users_filename = 'users_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
102143 pbar = tqdm(desc = 'Writing users to file %s' % users_filename)
103144 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
105148 pbar.update()
106149 f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS)))
107150 print('Users dump was written to %s' % users_filename)
109152 users_filename = 'computers_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
110153 pbar = tqdm(desc = 'Writing computers to file %s' % users_filename)
111154 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
113158 pbar.update()
114159 f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS)))
115160 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
119166 async def do_query(self, query, attributes = None):
120167 """Performs a raw LDAP query against the server. Secondary parameter is the requested attributes SEPARATED WITH COMMA (,)"""
121168 try:
126173 attributes = attributes.split(',')
127174 logging.debug('Query: %s' % (query))
128175 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
130179 print(entry)
131 except:
132 traceback.print_exc()
180 return True
181 except:
182 traceback.print_exc()
183 return False
133184
134185 async def do_tree(self, dn = None, level = 1):
135186 """Prints a tree from the given DN (if not set, the top) and with a given depth (default: 1)"""
155206 tr = LeftAligned()
156207 print(tr(tree_data))
157208
158
159 except:
160 traceback.print_exc()
209 return True
210 except:
211 traceback.print_exc()
212 return False
161213
162214 async def do_user(self, samaccountname):
163215 """Feteches a user object based on the sAMAccountName of the user"""
164216 try:
165217 await self.do_ldapinfo(False)
166218 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:
168225 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):
173400 """Feteches security info for a given DN"""
174401 try:
175402 await self.do_ldapinfo(False)
176403 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
181413
182414 async def do_gpos(self):
183415 """Feteches security info for a given DN"""
184416 try:
185417 await self.do_ldapinfo(False)
186418 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
188422 print(gpo)
189 except:
190 traceback.print_exc()
423
424 return True
425 except:
426 traceback.print_exc()
427 return False
191428
192429 async def do_laps(self):
193430 """Feteches all laps passwords"""
194431 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
196435 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']
199438 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
202444
203445 async def do_groupmembership(self, dn):
204446 """Feteches names all groupnames the user is a member of for a given DN"""
206448 await self.do_ldapinfo(False)
207449 await self.do_adinfo(False)
208450 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
210454 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
212458 print('%s - %s' % (group_dn, group_sid))
213459
214460 if len(group_sids) == 0:
215461 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
218467
219468 async def do_bindtree(self, newtree):
220469 """Changes the LDAP TREE for future queries.
225474 async def do_trusts(self):
226475 """Feteches gives back domain trusts"""
227476 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
229480 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
233608 async def do_test(self):
234609 """testing, dontuse"""
235610 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
237615 if entry.objectClass[-1] != 'user':
238616 print(entry.objectClass)
239 except:
240 traceback.print_exc()
617
618 return True
619 except:
620 traceback.print_exc()
621 return False
241622
242623 """
243624 async def do_info(self):
258639 await client.run()
259640 else:
260641 for command in args.commands:
642 if command == 'i':
643 await client.run()
644 return
261645 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
263649
264650 def main():
265651 import argparse
267653 parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked')
268654 parser.add_argument('-n', '--no-interactive', action='store_true')
269655 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.")
271657
272658 args = parser.parse_args()
273659
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()
(New empty file)
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 )
88 from msldap.ldap_objects.adcomp import MSADMachine, MSADMachine_ATTRS, MSADMachine_TSV_ATTRS
99 from msldap.ldap_objects.adsec import MSADSecurityInfo, MSADTokenGroup
1010 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
1313 from msldap.ldap_objects.adgpo import MSADGPO, MSADGPO_ATTRS
1414 from msldap.ldap_objects.adtrust import MSADDomainTrust, MSADDomainTrust_ATTRS
15 from msldap.ldap_objects.adschemaentry import MSADSCHEMAENTRY_ATTRS, MSADSchemaEntry
1516
1617 __all__ = [
1718 'MSADUser',
3132 'MSADGPO_ATTRS',
3233 'MSADDomainTrust',
3334 'MSADDomainTrust_ATTRS',
35 'MSADGroup_ATTRS',
36 'MSADOU_ATTRS',
37 'MSADSCHEMAENTRY_ATTRS',
38 'MSADSchemaEntry',
3439 ]
33 # Tamas Jos (@skelsec)
44 #
55
6 import datetime
67 from msldap.ldap_objects.common import MSLDAP_UAC, vn
78
89 MSADMachine_ATTRS = [
1415 'operatingSystem', 'operatingSystemVersion','primaryGroupID',
1516 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl',
1617 'whenChanged', 'whenCreated', 'servicePrincipalName','msDS-AllowedToDelegateTo',
18 'msDS-AllowedToActOnBehalfOfOtherIdentity'
1719 ]
1820
1921 MSADMachine_TSV_ATTRS = [
6264 self.whenCreated = None
6365 self.servicePrincipalName = None
6466 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
65118
66119 @staticmethod
67120 def from_ldap(entry, adinfo = None):
101154 adi.servicePrincipalName = entry['attributes'].get('servicePrincipalName')
102155
103156 adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo')
157 adi.allowedtoactonbehalfofotheridentity = entry['attributes'].get('msDS-AllowedToActOnBehalfOfOtherIdentity')
104158
105159 temp = entry['attributes'].get('userAccountControl')
106160 if temp:
107161 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
108172 return adi
109173
110174 def to_dict(self):
155219
156220 def get_row(self, attrs):
157221 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
77
88 MSADGPO_ATTRS = [
99 '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',
1213 ]
1314
1415 class MSADGPO:
2021 self.gPCFileSysPath = None #str
2122 self.gPCFunctionalityVersion = None #str
2223 self.gPCMachineExtensionNames = None
24 self.gPCUserExtensionNames = None
2325 self.objectClass = None #str
2426 self.objectGUID = None #uid
2527 self.systemFlags = None #str
2628 self.whenChanged = None #uid
2729 self.whenCreated = None #str
30 self.versionNumber = None
2831
2932
3033 @staticmethod
3740 adi.gPCFileSysPath = entry['attributes'].get('gPCFileSysPath')
3841 adi.gPCFunctionalityVersion = entry['attributes'].get('gPCFunctionalityVersion')
3942 adi.gPCMachineExtensionNames = entry['attributes'].get('gPCMachineExtensionNames')
43 adi.gPCUserExtensionNames = entry['attributes'].get('gPCUserExtensionNames')
4044 adi.objectClass = entry['attributes'].get('objectClass')
4145 adi.objectGUID = entry['attributes'].get('objectGUID')
4246 adi.systemFlags = entry['attributes'].get('systemFlags')
4347 adi.whenChanged = entry['attributes'].get('whenChanged')
4448 adi.whenCreated = entry['attributes'].get('whenCreated')
49 adi.versionNumber = entry['attributes'].get('versionNumber')
4550
4651 return adi
4752
5459 t['gPCFileSysPath'] = vn(self.gPCFileSysPath)
5560 t['gPCFunctionalityVersion'] = vn(self.gPCFunctionalityVersion)
5661 t['gPCMachineExtensionNames'] = vn(self.gPCMachineExtensionNames)
62 t['gPCUserExtensionNames'] = vn(self.gPCUserExtensionNames)
5763 t['systemFlags'] = vn(self.systemFlags)
5864 t['objectClass'] = vn(self.objectClass)
5965 t['objectGUID'] = vn(self.objectGUID)
6066 t['whenChanged'] = vn(self.whenChanged)
6167 t['whenCreated'] = vn(self.whenCreated)
68 t['versionNumber'] = vn(self.versionNumber)
6269 return t
6370
6471 def __str__(self):
55
66 from msldap.wintypes import *
77 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
817
918 class MSADGroup:
1019 def __init__(self):
6877 t.description = ', '.join(t.description)
6978
7079
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)
7483 return t
7584
7685
1010 'name', 'nextRid', 'nTSecurityDescriptor', 'objectCategory', 'objectClass',
1111 'objectGUID', 'objectSid', 'pwdHistoryLength',
1212 'pwdProperties', 'serverState', 'systemFlags', 'uASCompat', 'uSNChanged',
13 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference','msDS-Behavior-Version'
13 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference',
14 'msDS-Behavior-Version'
1415 ]
1516 class MSADInfo:
1617 def __init__(self):
151152 t += 'uSNCreated: %s\n' % self.uSNCreated
152153 t += 'whenChanged: %s\n' % self.whenChanged
153154 t += 'whenCreated: %s\n' % self.whenCreated
155 t += 'domainmodelevel: %s\n' % self.domainmodelevel
154156 return t
22 # Author:
33 # Tamas Jos (@skelsec)
44 #
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 ]
513
614 class MSADOU:
715 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
1313 'objectCategory', 'objectClass', 'objectGUID', 'objectSid', 'primaryGroupID',
1414 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl',
1515 'userPrincipalName', 'whenChanged', 'whenCreated','memberOf','member', 'servicePrincipalName',
16 'msDS-AllowedToDelegateTo',
16 'msDS-AllowedToDelegateTo', 'adminCount'
1717 ]
1818 MSADUser_TSV_ATTRS = [
1919 'sAMAccountName', 'userPrincipalName' ,'canLogon', 'badPasswordTime', 'description',
2121 'whenCreated', 'whenChanged', 'member', 'memberOf', 'servicePrincipalName',
2222 'objectSid', 'cn', 'UAC_SCRIPT', 'UAC_ACCOUNTDISABLE', 'UAC_LOCKOUT', 'UAC_PASSWD_NOTREQD',
2323 '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'
2525 ]
2626
2727 class MSADUser:
6666 self.sAMAccountType = None #int
6767 self.userAccountControl = None #UserAccountControl intflag
6868 self.allowedtodelegateto = None
69 self.admincount = None
6970
7071
7172 ## other
7980 self.canLogon = None #bool
8081
8182 # https://msdn.microsoft.com/en-us/library/cc245739.aspx
82 def calc_PasswordMustChange(self):
83 def calc_PasswordMustChange(self, adinfo):
8384 # Crtieria 1
8485 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]
8586 for flag in flags:
8788 return datetime.datetime.max #never
8889
8990 #criteria 2
90 if self.pwdLastSet == 0:
91 if self.pwdLastSet == datetime.timedelta():
9192 return datetime.datetime.min
9293
93 if (self.when_pw_expires - datetime.datetime.now()).total_seconds() > 0:
94 if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta
9495 return datetime.datetime.max #never
9596
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)
97101
98102
99103 # https://msdn.microsoft.com/en-us/library/cc223991.aspx
102106 for flag in flags:
103107 if flag & self.userAccountControl:
104108 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:
107111 return False
108112
109113 #
155159 adi.countryCode = entry['attributes'].get('countryCode')
156160
157161 adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo')
162 adi.admincount = entry['attributes'].get('adminCount')
158163
159164 temp = entry['attributes'].get('userAccountControl')
160165 if temp:
161166 adi.userAccountControl = MSLDAP_UAC(temp)
162167
163168 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
169176
170177
171178 return adi
207214 t['when_pw_change'] = vn(self.when_pw_change)
208215 t['when_pw_expires'] = vn(self.when_pw_expires)
209216 t['must_change_pw'] = vn(self.must_change_pw)
217 t['admincount'] = self.admincount
210218 t['canLogon'] = vn(self.canLogon)
211219 return t
212220
255263 t += 'when_pw_change: %s\n' % self.when_pw_change
256264 t += 'when_pw_expires: %s\n' % self.when_pw_expires
257265 t += 'must_change_pw: %s\n' % self.must_change_pw
266 t += 'admincount: %s\n' % self.admincount
258267 t += 'canLogon: %s\n' % self.canLogon
259268
260269 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
00
1 from msldap import logger
12 from msldap.network.tcp import MSLDAPTCPNetwork
23 from msldap.network.socks import SocksProxyConnection
4 from msldap.network.multiplexor import MultiplexorProxyConnection
35 from msldap.commons.proxy import MSLDAPProxyType
46
57 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 ]
1016
1117 class MSLDAPNetworkSelector:
1218 def __init__(self):
1319 pass
1420
1521 @staticmethod
16 def select(target):
22 async def select(target):
1723 if target.proxy is not None:
1824 if target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES:
1925 return SocksProxyConnection(target)
2026 else:
21 raise Exception('Multiplexor coming soon!')
27 mpc = MultiplexorProxyConnection(target)
28 socks_proxy = await mpc.connect()
29 return socks_proxy
2230
2331 return MSLDAPTCPNetwork(target)
0
1 #
2 #
3 #
4 #
5 #
6 #
7
80
91 import enum
102 import asyncio
4032 Disconnects from the socket.
4133 Stops the reader and writer streams.
4234 """
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)
4548
4649 def get_one_message(self,data):
4750 if len(data) < 6:
8285 data += temp
8386 continue
8487
85 #except asyncio.CancelledError:
86 # return
88 except asyncio.CancelledError:
89 return
8790 except Exception as e:
8891 logger.exception('handle_in_q')
8992 await self.in_queue.put((None, e))
104107 self.proxy_in_queue = asyncio.Queue()
105108 comms = SocksQueueComms(self.out_queue, self.proxy_in_queue)
106109
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)
111115 self.proxy_task = asyncio.create_task(self.client.run())
112116 self.handle_in_task = asyncio.create_task(self.handle_in_q())
113117 return True, None
2020 async def terminate(self):
2121 self.handle_in_task.cancel()
2222 self.handle_out_task.cancel()
23
23
24 def get_peer_certificate(self):
25 return self.writer.get_extra_info('ssl_object').getpeercert(True)
2426
2527 async def handle_in_q(self):
2628 try:
8183 self.in_queue = asyncio.Queue()
8284 self.out_queue = asyncio.Queue()
8385 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 ),
8591 timeout = self.target.timeout
8692 )
8793
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
00 import re
11 import platform
22
3 import msldap.protocol.ldap_filter.parser as parser
3 from msldap.protocol.ldap_filter import parser
44 from msldap.protocol.ldap_filter.soundex import soundex_compare
55
66
141141 _input_size = None
142142 _actions = None
143143
144 REGEX_1 = re.compile('^[^!*\\x29]')
144 REGEX_1 = re.compile('^[^*\\x29]') #re.compile('^[^!*\\x29]')
145145 REGEX_2 = re.compile('^[a-fA-F0-9]')
146146 REGEX_3 = re.compile('^[\\x20]')
147147 REGEX_4 = re.compile('^[\\x09]')
4949
5050 class resultCode(core.Enumerated):
5151 _map = {
52 0 : 'success',
52 0 : 'success',
5353 1 : 'operationsError',
5454 2 : 'protocolError',
5555 3 : 'timeLimitExceeded',
9090 80 : 'other',
9191 }
9292
93 class changeoperation(core.Enumerated):
93 class ChangeOperation(core.Enumerated):
9494 _map = {
9595 0 : 'add',
9696 1 : 'delete',
287287
288288 class Change(core.Sequence):
289289 _fields = [
290 ('operation', changeoperation),
290 ('operation', ChangeOperation),
291291 ('modification', PartialAttribute),
292292 ]
293293
66
77
88 def equality(attr, value):
9 #print(attr)
10 #print(value)
119 if attr[-1] == ':':
12 #possible OID
1310 name, oid_raw = attr[:-1].split(':')
14 #print(oid_raw)
1511 return Filter({
1612 'extensibleMatch' : MatchingRuleAssertion({
1713 'matchingRule' : oid_raw.encode(),
6056
6157
6258 def query_syntax_converter_inner(ftr):
63 #print(ftr.__dict__)
64 #print(ftr.comp)
65 #print(ftr.type)
6659 if ftr.type == 'filter':
6760 if ftr.comp == '=':
6861 return equality(ftr.attr, ftr.val)
44 from winacl.dtyp.guid import GUID
55 from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR
66 from msldap import logger
7 from msldap.protocol.messages import Attribute, Change, PartialAttribute
78
89 MSLDAP_DT_WIN_EPOCH = datetime.datetime(1601, 1, 1)
910
6162 def list_str(x):
6263 return [e.decode() for e in x ]
6364
65 def list_str_enc(x):
66 return [e.encode() for e in x ]
67
6468 def list_int(x):
6569 return [int(e) for e in x ]
6670
71 def list_int_enc(x):
72 return [str(e).encode() for e in x ]
73
6774 def list_int_one(x):
6875 return int(x[0])
6976
77 def list_int_one_enc(x):
78 return [str(x[0]).encode()]
79
7080 def list_str_one(x):
7181 return x[0].decode()
7282
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
7389 def list_bytes_one(x):
7490 return x[0]
91
92 def list_bytes_one_enc(x):
93 return x
7594
7695 def int2timedelta(x):
7796 x = int(x[0])
146165 t.append(ts2dt((a, None)))
147166 return t
148167
168
149169 LDAP_ATTRIBUTE_TYPES = {
150170 'supportedCapabilities' : list_str,
151171 'serverName' : list_str_one,
157177 'supportedControl' : list_str,
158178 'rootDomainNamingContext' : list_str_one,
159179 'configurationNamingContext' : list_str_one,
180 'schemaIDGUID' : x2guid,
181 'lDAPDisplayName' : list_str_one,
160182 'schemaNamingContext' : list_str_one,
161183 'defaultNamingContext' : list_str_one,
184 'adminDescription' : list_str_one,
185 'adminDisplayName' : list_str_one,
162186 'namingContexts' : list_str,
163187 'dsServiceName' : list_str_one,
164188 'subschemaSubentry' : list_str_one,
203227 'lockoutThreshold' : list_int_one,
204228 'lockOutObservationWindow' : list_int_one,
205229 'lockoutDuration' : list_int_one,
206 'forceLogoff' : list_str_one,
230 'forceLogoff' : int2timedelta,
207231 'creationTime' : int2dt,
208232 'maxPwdAge' : int2timedelta,
209233 'pwdHistoryLength' : list_int_one,
233257 'versionNumber' : list_int_one,
234258 'gPCFunctionalityVersion' : list_int_one,
235259 'gPCMachineExtensionNames' : list_str,
260 'gPCUserExtensionNames' : list_str,
236261 'groupType' : list_int_one,
237262 'member' : list_str,
238263 'adminCount' : list_int_one,
246271 'trustPartner' : list_str_one,
247272 'securityIdentifier' : list_bytes_one,
248273 'versionNumber' : list_int_one,
249
274 'unicodePwd' : list_str_one,
275 'ms-Mcs-AdmPwd' : list_str_one,
276 'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one,
250277 }
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
251312
252313 def convert_attributes(x):
253314 t = {}
269330 return {
270331 'objectName' : x['objectName'].decode(),
271332 '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
00 Metadata-Version: 1.2
11 Name: msldap
2 Version: 0.2.10
2 Version: 0.3.30
33 Summary: Python library to play with MS LDAP
44 Home-page: https://github.com/skelsec/msldap
55 Author: Tamas Jos
6 Author-email: [email protected]
6 Author-email: [email protected]
77 License: UNKNOWN
88 Description: Python library to play with MS LDAP
99 Platform: UNKNOWN
10 Classifier: Programming Language :: Python :: 3.6
10 Classifier: Programming Language :: Python :: 3.7
11 Classifier: Programming Language :: Python :: 3.8
1112 Classifier: License :: OSI Approved :: MIT License
1213 Classifier: Operating System :: OS Independent
13 Requires-Python: >=3.6
14 Requires-Python: >=3.7
1010 msldap.egg-info/SOURCES.txt
1111 msldap.egg-info/dependency_links.txt
1212 msldap.egg-info/entry_points.txt
13 msldap.egg-info/not-zip-safe
1314 msldap.egg-info/requires.txt
1415 msldap.egg-info/top_level.txt
15 msldap.egg-info/zip-safe
1616 msldap/authentication/__init__.py
1717 msldap/authentication/kerberos/__init__.py
18 msldap/authentication/kerberos/gssapi.py
1819 msldap/authentication/kerberos/multiplexor.py
1920 msldap/authentication/kerberos/native.py
2021 msldap/authentication/kerberos/sspi.py
22 msldap/authentication/kerberos/sspiproxyws.py
23 msldap/authentication/kerberos/wsnet.py
2124 msldap/authentication/ntlm/__init__.py
2225 msldap/authentication/ntlm/creds_calc.py
2326 msldap/authentication/ntlm/multiplexor.py
2427 msldap/authentication/ntlm/native.py
2528 msldap/authentication/ntlm/sspi.py
29 msldap/authentication/ntlm/sspiproxy.py
30 msldap/authentication/ntlm/wsnet.py
2631 msldap/authentication/ntlm/messages/__init__.py
2732 msldap/authentication/ntlm/messages/authenticate.py
2833 msldap/authentication/ntlm/messages/challenge.py
4449 msldap/authentication/spnego/sspi.py
4550 msldap/commons/__init__.py
4651 msldap/commons/authbuilder.py
52 msldap/commons/common.py
4753 msldap/commons/credential.py
54 msldap/commons/exceptions.py
4855 msldap/commons/proxy.py
4956 msldap/commons/target.py
5057 msldap/commons/url.py
5259 msldap/crypto/AES.py
5360 msldap/crypto/BASE.py
5461 msldap/crypto/DES.py
62 msldap/crypto/MD4.py
5563 msldap/crypto/RC4.py
5664 msldap/crypto/TDES.py
5765 msldap/crypto/__init__.py
6876 msldap/crypto/pure/RC4/__init__.py
6977 msldap/examples/__init__.py
7078 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
7192 msldap/ldap_objects/__init__.py
7293 msldap/ldap_objects/adcomp.py
7394 msldap/ldap_objects/adgpo.py
7495 msldap/ldap_objects/adgroup.py
7596 msldap/ldap_objects/adinfo.py
7697 msldap/ldap_objects/adou.py
98 msldap/ldap_objects/adschemaentry.py
7799 msldap/ldap_objects/adsec.py
78100 msldap/ldap_objects/adtrust.py
79101 msldap/ldap_objects/aduser.py
80102 msldap/ldap_objects/common.py
81103 msldap/network/__init__.py
104 msldap/network/multiplexor.py
82105 msldap/network/selector.py
83106 msldap/network/socks.py
84107 msldap/network/tcp.py
108 msldap/network/wsnet.py
85109 msldap/network/proxy/__init__.py
86110 msldap/network/proxy/handler.py
87111 msldap/protocol/__init__.py
00 [console_scripts]
11 msldap = msldap.examples.msldapclient:main
2 msldapcompdns = msldap.examples.msldapcompdnslist:main
23
00 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
56
67 [:platform_system == "Windows"]
7 winsspi
8 winsspi>=0.0.9
+0
-1
msldap.egg-info/zip-safe less more
0
1919
2020 # Application author details:
2121 author="Tamas Jos",
22 author_email="[email protected]",
22 author_email="[email protected]",
2323
2424 # Packages
2525 packages=find_packages(),
3131 # Details
3232 url="https://github.com/skelsec/msldap",
3333
34 zip_safe = True,
34 zip_safe = False,
3535 #
3636 # license="LICENSE.txt",
3737 description="Python library to play with MS LDAP",
3838 long_description="Python library to play with MS LDAP",
3939
4040 # long_description=open("README.txt").read(),
41 python_requires='>=3.6',
41 python_requires='>=3.7',
4242 classifiers=(
43 "Programming Language :: Python :: 3.6",
43 "Programming Language :: Python :: 3.7",
44 "Programming Language :: Python :: 3.8",
4445 "License :: OSI Approved :: MIT License",
4546 "Operating System :: OS Independent",
4647 ),
4748 install_requires=[
4849 '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',
5556 ],
5657 entry_points={
5758 'console_scripts': [
5859 'msldap = msldap.examples.msldapclient:main',
60 'msldapcompdns = msldap.examples.msldapcompdnslist:main',
5961 ],
6062 }
6163 )