Codebase list msldap / 4705343
Import upstream version 0.3.22 Kali Janitor 3 years ago
61 changed file(s) with 4388 addition(s) and 1264 deletion(s). Raw diff Collapse all Expand all
00 Metadata-Version: 1.2
11 Name: msldap
2 Version: 0.2.10
2 Version: 0.3.22
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.22"
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
105 is_rpc = False
49106 if self.ksspi is None:
50107 await self.start_remote_kerberos()
51108 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')
109 apreq, res = await self.ksspi.authenticate(self.settings.target.to_target_string(), flags=str(self.flags.value))
110 #print('MULTIPLEXOR KERBEROS SSPI, APREQ: %s ERROR: %s' % (apreq, res))
111 if res is not None:
112 return None, None, res
113
114 # here it seems like we get the full token not just the apreq data...
115 # so we need to discard the layers
116
117 self.session_key, err = await self.ksspi.get_session_key()
118 if err is not None:
119 return None, None, err
120
121 unwrap = KRB5_MECH_INDEP_TOKEN.from_bytes(apreq)
122 aprep = AP_REQ.load(unwrap.data[2:]).native
123 subkey = Key(aprep['ticket']['enc-part']['etype'], self.session_key)
124 self.gssapi = get_gssapi(subkey)
93125
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
126 if aprep['ticket']['enc-part']['etype'] != 23:
127 raw_seq_data, err = await self.ksspi.get_seq_number()
128 if err is not None:
129 return None, None, err
130 self.seq_number = GSSWrapToken.from_bytes(raw_seq_data[16:]).SND_SEQ
131
132 return unwrap.data[2:], False, res
101133 except Exception as e:
102 import traceback
103 traceback.print_exc()
104 return None
134 return None, None, e
105135
106136 async def start_remote_kerberos(self):
107137 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
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
2323
2424 # SMBKerberosCredential
25
26 MSLDAP_SOCKS_PROXY_TYPES = [
27 MSLDAPProxyType.SOCKS4 ,
28 MSLDAPProxyType.SOCKS4_SSL ,
29 MSLDAPProxyType.SOCKS5 ,
30 MSLDAPProxyType.SOCKS5_SSL]
2531
2632 class MSLDAPKerberos:
2733 def __init__(self, settings):
2834 self.settings = settings
35 self.signing_preferred = None
36 self.encryption_preferred = None
2937 self.ccred = None
3038 self.target = None
3139 self.spn = None
3240 self.kc = None
41 self.flags = None
42 self.preferred_etypes = [23,17,18]
3343
3444 self.session_key = None
3545 self.gssapi = None
3646 self.iterations = 0
3747 self.etype = None
48 self.seq_number = 0
49 self.expected_server_seq_number = None
3850
3951 self.setup()
4052
53 def get_seq_number(self):
54 """
55 Returns the initial sequence number. It is 0 by default, but can be adjusted during authentication,
56 by passing the 'seq_number' parameter in the 'authenticate' function
57 """
58 return self.seq_number
59
4160 def signing_needed(self):
42 return False
61 """
62 Checks if integrity protection was negotiated
63 """
64 return ChecksumFlags.GSS_C_INTEG_FLAG in self.flags
4365
4466 def encryption_needed(self):
45 return False #change to true to enable encryption channel binding
67 """
68 Checks if confidentiality flag was negotiated
69 """
70 return ChecksumFlags.GSS_C_CONF_FLAG in self.flags
4671
4772 async def sign(self, data, message_no, direction = 'init'):
73 """
74 Signs a message.
75 """
4876 return self.gssapi.GSS_GetMIC(data, message_no, direction = direction)
4977
5078 async def encrypt(self, data, message_no):
79 """
80 Encrypts a message.
81 """
82
5183 return self.gssapi.GSS_Wrap(data, message_no)
5284
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)
85 async def decrypt(self, data, message_no, direction='init'):
86 """
87 Decrypts message. Also performs integrity checking.
88 """
89
90 return self.gssapi.GSS_Unwrap(data, message_no, direction=direction)
5591
5692 def setup(self):
5793 self.ccred = self.settings.ccred
5894 self.spn = self.settings.spn
5995 self.target = self.settings.target
60
61 self.kc = AIOKerberosClient(self.ccred, self.target)
62
96 if self.settings.enctypes is not None:
97 self.preferred_etypes = self.settings.enctypes
98
99 self.flags = ChecksumFlags.GSS_C_MUTUAL_FLAG
100 if self.settings.encrypt is True:
101 self.flags = \
102 ChecksumFlags.GSS_C_CONF_FLAG |\
103 ChecksumFlags.GSS_C_INTEG_FLAG |\
104 ChecksumFlags.GSS_C_REPLAY_FLAG |\
105 ChecksumFlags.GSS_C_SEQUENCE_FLAG #|\
106 #ChecksumFlags.GSS_C_MUTUAL_FLAG
107
108 #self.kc = AIOKerberosClient(self.ccred, self.target)
63109
64110 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:
111 return self.session_key.contents, None
112
113
114 async def setup_kc(self):
115 try:
116 if self.target.proxy is None:
117 self.kc = AIOKerberosClient(self.ccred, self.target)
118 elif self.target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES:
119 target = AIOKerberosClientSocksSocket(self.target)
120 self.kc = AIOKerberosClient(self.ccred, target)
121
122 elif self.target.proxy.type in [MSLDAPProxyType.MULTIPLEXOR, MSLDAPProxyType.MULTIPLEXOR_SSL]:
123 from msldap.network.multiplexor import MultiplexorProxyConnection
124 mpc = MultiplexorProxyConnection(self.target)
125 socks_proxy = await mpc.connect(is_kerberos = True)
126
127 self.kc = AIOKerberosClient(self.ccred, socks_proxy)
128
129 else:
130 raise Exception('Unknown proxy type %s' % self.target.proxy.type)
131
132 return None, None
133 except Exception as e:
134 return None, e
135
136 async def authenticate(self, authData, flags = None, seq_number = 0, cb_data = None):
137 """
138 This function is called (multiple times depending on the flags) to perform authentication.
139 """
140 try:
141 if self.kc is None:
142 _, err = await self.setup_kc()
143 if err is not None:
144 return None, None, err
145
76146 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)
147 self.seq_number = 0 #int.from_bytes(os.urandom(4), byteorder='big', signed=False)
82148 self.iterations += 1
83 return apreq, False
84
149
150 #tgt = await self.kc.get_TGT()
151 tgt = await self.kc.get_TGT(override_etype = self.preferred_etypes)
152 tgs, encpart, self.session_key = await self.kc.get_TGS(self.spn)#, override_etype = self.preferred_etypes)
153
154 #self.expected_server_seq_number = encpart.get('nonce', seq_number)
155
156 ap_opts = []
157 if ChecksumFlags.GSS_C_MUTUAL_FLAG in self.flags or ChecksumFlags.GSS_C_DCE_STYLE in self.flags:
158 if ChecksumFlags.GSS_C_MUTUAL_FLAG in self.flags:
159 ap_opts.append('mutual-required')
160 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)
161 return apreq, True, None
162
163 else:
164 #no mutual or dce auth will take one step only
165 apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = self.flags, seq_number = self.seq_number, ap_opts=[], cb_data = cb_data)
166 self.gssapi = get_gssapi(self.session_key)
167 return apreq, False, None
168
85169 else:
86 #mutual authentication part here
87 aprep = AP_REP.load(authData).native
170 self.iterations += 1
171 #raise Exception('Not implemented!')
172 if ChecksumFlags.GSS_C_DCE_STYLE in self.flags:
173 # adata = authData[16:]
174 # if ChecksumFlags.GSS_C_DCE_STYLE in self.flags:
175 # adata = authData
176 raise Exception('DCE auth Not implemented!')
177
178 # at this point we are dealing with mutual authentication
179 # This means that the server sent back an AP-rep wrapped in a token
180 # The APREP contains a new session key we'd need to update and a seq-number
181 # that is expected the server will use for future communication.
182 # For mutual auth we dont need to reply anything after this step,
183 # but for DCE auth a reply is expected. TODO
184
185 # converting the token to aprep
186 token = KRB5_MECH_INDEP_TOKEN.from_bytes(authData)
187 if token.data[:2] != b'\x02\x00':
188 raise Exception('Unexpected token type! %s' % token.data[:2].hex() )
189 aprep = AP_REP.load(token.data[2:]).native
190
191 # decrypting aprep
88192 cipher = _enctype_table[int(aprep['enc-part']['etype'])]()
89193 cipher_text = aprep['enc-part']['cipher']
90194 temp = cipher.decrypt(self.session_key, 12, cipher_text)
91
92195 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()
196
197 #updating session key, gssapi
198 self.session_key = Key(int(enc_part['subkey']['keytype']), enc_part['subkey']['keyvalue'])
199 #self.seq_number = enc_part.get('seq-number', 0)
112200 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
201
202 return b'', False, None
203
204 except Exception as e:
205 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
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
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['responseToken'] is None:
199 # https://tools.ietf.org/html/rfc4178#section-5
200 # mechlistmic exchange happening at the end of the authentication
201 return None, True, None
202 #raise Exception('Should not be here....')
203 #print('server mechListMIC: %s' % neg_token['mechListMIC'])
204 #res = await self.verify(self.negtypes_store, neg_token['mechListMIC'])
205 #print('res %s' % res)
206 #print(self.negtypes_store)
207 #print(self.negtypes_store.hex())
208 #ret = await self.sign(self.negtypes_store, 0)
209 #print(ret)
210 #print(ret.hex())
211 #res = {
212 # 'mechListMIC' : ret,
213 # 'negState': NegState('accept-completed')
214 #}
215 #return NegotiationToken({'negTokenResp':NegTokenResp(res)}).dump(), True, None
216
252217 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)
218 response, to_continue, err = await self.process_ctx_authenticate(neg_token['responseToken'], flags = flags, seq_number = seq_number, cb_data = cb_data)
219 if err is not None:
220 return None, None, err
257221 if not response:
258 return None, False
259 return NegotiationToken({'negTokenResp':NegTokenResp(response)}).dump(), to_continue
222 return None, False, None
223
224 if self.selected_mechtype.startswith('NTLM'):
225 response['mechListMIC'] = await self.sign(self.negtypes_store, 0, reset_cipher = True)
226 #self.selected_authentication_context.
227 #print(response)
228 res = NegotiationToken({'negTokenResp':NegTokenResp(response)}).dump()
229
230 return res, to_continue, None
260231
261232 def test():
262233 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)
44 #
55
66 from msldap import logger
7 from msldap.commons.common import MSLDAPClientStatus
78 from msldap.wintypes.asn1.sdflagsrequest import SDFlagsRequest, SDFlagsRequestValue
89 from msldap.protocol.constants import BASE, ALL_ATTRIBUTES, LEVEL
910
10 from msldap.protocol.query import escape_filter_chars, query_syntax_converter
11 from msldap.protocol.query import escape_filter_chars
1112 from msldap.connection import MSLDAPClientConnection
1213 from msldap.protocol.messages import Control
1314 from msldap.ldap_objects import *
1415
1516 class MSLDAPClient:
17 """
18 High level API for LDAP operations.
19
20 target, creds, ldap_query_page_size
21
22 :param target: The target object describing the connection info
23 :type target: :class:`MSLDAPTarget`
24 :param creds: The credential object describing the authentication to be used
25 :type creds: :class:`MSLDAPCredential`
26 :param ldap_query_page_size:
27 :type ldap_query_page_size: int
28 :return: A dictionary representing the LDAP tree
29 :rtype: dict
30
31 """
1632 def __init__(self, target, creds, ldap_query_page_size = 1000):
1733 self.creds = creds
1834 self.target = target
2440
2541
2642 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
43 try:
44 self._con = MSLDAPClientConnection(self.target, self.creds)
45 _, err = await self._con.connect()
46 if err is not None:
47 raise err
48 res, err = await self._con.bind()
49 if err is not None:
50 return False, err
51 res, err = await self._con.get_serverinfo()
52 if err is not None:
53 raise err
54 self._serverinfo = res
55 self._tree = res['defaultNamingContext']
56 self._ldapinfo, err = await self.get_ad_info()
57 if err is not None:
58 raise err
59 return True, None
60 except Exception as e:
61 return False, e
3762
3863 def get_server_info(self):
3964 return self._serverinfo
4065
41 async def pagedsearch(self, ldap_filter, attributes, controls = None):
66 async def pagedsearch(self, query, attributes, controls = None):
4267 """
4368 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)))
69 !The LDAP connection MUST be active before invoking this function!
70
71 :param query: LDAP query filter
72 :type query: str
73 :param attributes: List of requested attributes
74 :type attributes: List[str]
75 :param controls: additional controls to be passed in the query
76 :type controls: dict
77 :param level: Recursion level
78 :type level: int
79
80 :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error
81 :rtype: Iterator[(:class:`dict`, :class:`Exception`)]
82
83 """
84 logger.debug('Paged search, filter: %s attributes: %s' % (query, ','.join(attributes)))
85 if self._con.status != MSLDAPClientStatus.RUNNING:
86 if self._con.status == MSLDAPClientStatus.ERROR:
87 print('There was an error in the connection!')
88 return
89 elif self._con.status == MSLDAPClientStatus.ERROR:
90 print('Theconnection is in stopped state!')
91 return
92
93 if self._tree is None:
94 raise Exception('BIND first!')
5595 t = []
5696 for x in attributes:
5797 t.append(x.encode())
5898 attributes = t
59 ldap_filter = query_syntax_converter(ldap_filter)
6099
61100 t = []
62101 if controls is not None:
70109 controls = t
71110
72111 async for entry, err in self._con.pagedsearch(
73 self._tree.encode(),
74 ldap_filter,
112 self._tree,
113 query,
75114 attributes = attributes,
76 paged_size = self.ldap_query_page_size,
115 size_limit = self.ldap_query_page_size,
77116 controls = controls
78117 ):
79118
80119 if err is not None:
81 raise err
120 yield None, err
121 return
82122 if entry['objectName'] == '' and entry['attributes'] == '':
83123 #searchresref...
84124 continue
85125 #print('et %s ' % entry)
86 yield entry
87
88 async def get_tree_plot(self, dn, level = 2):
126 yield entry, None
127
128 async def get_tree_plot(self, root_dn, level = 2):
89129 """
90130 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))
131
132 :param root_dn: The start DN of the tree
133 :type root_dn: str
134 :param level: Recursion level
135 :type level: int
136
137 :return: A dictionary representing the LDAP tree
138 :rtype: dict
139 """
140
141 logger.debug('Tree, dn: %s level: %s' % (root_dn, level))
98142 tree = {}
99 #entries =
100143 async for entry, err in self._con.pagedsearch(
101 dn.encode(),
102 query_syntax_converter('(distinguishedName=*)'),
144 root_dn,
145 '(distinguishedName=*)',
103146 attributes = [b'distinguishedName'],
104 paged_size = self.ldap_query_page_size,
147 size_limit = self.ldap_query_page_size,
105148 search_scope=LEVEL,
106149 controls = None,
107150 ):
116159 continue
117160 subtree = await self.get_tree_plot(entry['attributes']['distinguishedName'], level = level -1)
118161 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
162 return {root_dn : tree}
163
164 async def get_all_users(self):
165 """
166 Fetches all user objects available in the LDAP tree and yields them as MSADUser object.
167
168 :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error
169 :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)]
170
125171 """
126172 logger.debug('Polling AD for all user objects')
127173 ldap_filter = r'(sAMAccountType=805306368)'
128 async for entry in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
129 yield MSADUser.from_ldap(entry, self._ldapinfo)
174 async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
175 if err is not None:
176 yield None, err
177 return
178 yield MSADUser.from_ldap(entry, self._ldapinfo), None
130179 logger.debug('Finished polling for entries!')
131180
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
181 async def get_all_machines(self, attrs = MSADMachine_ATTRS):
182 """
183 Fetches all machine objects available in the LDAP tree and yields them as MSADMachine object.
184
185 :param attrs: Lists of attributes to request (eg. `['sAMAccountName', 'dNSHostName']`) Default: all attrs.
186 :type attrs: list
187 :return: Async generator which yields (`MSADMachine`, None) tuple on success or (None, `Exception`) on error
188 :rtype: Iterator[(:class:`MSADMachine`, :class:`Exception`)]
189
144190 """
145191 logger.debug('Polling AD for all user objects')
146192 ldap_filter = r'(sAMAccountType=805306369)'
147193
148 async for entry in self.pagedsearch(ldap_filter, MSADMachine_ATTRS):
149 yield MSADMachine.from_ldap(entry, self._ldapinfo)
194 async for entry, err in self.pagedsearch(ldap_filter, attrs):
195 if err is not None:
196 yield None, err
197 return
198 yield MSADMachine.from_ldap(entry, self._ldapinfo), None
150199 logger.debug('Finished polling for entries!')
151200
152201 async def get_all_gpos(self):
202 """
203 Fetches all GPOs available in the LDAP tree and yields them as MSADGPO object.
204
205 :return: Async generator which yields (`MSADGPO`, None) tuple on success or (None, `Exception`) on error
206 :rtype: Iterator[(:class:`MSADGPO`, :class:`Exception`)]
207
208 """
209
153210 ldap_filter = r'(objectCategory=groupPolicyContainer)'
154 async for entry in self.pagedsearch(ldap_filter, MSADGPO_ATTRS):
155 yield MSADGPO.from_ldap(entry)
211 async for entry, err in self.pagedsearch(ldap_filter, MSADGPO_ATTRS):
212 if err is not None:
213 yield None, err
214 return
215 yield MSADGPO.from_ldap(entry), None
156216
157217 async def get_all_laps(self):
218 """
219 Fetches all LAPS passwords for all machines. This functionality is only available to specific high-privileged users.
220
221 :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error
222 :rtype: Iterator[(:class:`dict`, :class:`Exception`)]
223 """
224
158225 ldap_filter = r'(sAMAccountType=805306369)'
159226 attributes = ['cn','ms-mcs-AdmPwd']
160 async for entry in self.pagedsearch(ldap_filter, attributes):
161 yield entry
227 async for entry, err in self.pagedsearch(ldap_filter, attributes):
228 yield entry, err
229
230 async def get_schemaentry(self, dn):
231 """
232 Fetches one Schema entriy identified by dn
233
234 :return: (`MSADSchemaEntry`, None) tuple on success or (None, `Exception`) on error
235 :rtype: (:class:`MSADSchemaEntry`, :class:`Exception`)
236 """
237 logger.debug('Polling Schema entry for %s'% dn)
238
239 async for entry, err in self._con.pagedsearch(
240 dn,
241 r'(distinguishedName=%s)' % escape_filter_chars(dn),
242 attributes = [x.encode() for x in MSADSCHEMAENTRY_ATTRS],
243 size_limit = self.ldap_query_page_size,
244 search_scope=BASE,
245 controls = None,
246 ):
247 if err is not None:
248 raise err
249
250 return MSADSchemaEntry.from_ldap(entry), None
251 else:
252 return None, None
253 logger.debug('Finished polling for entries!')
254
255 async def get_all_schemaentry(self):
256 """
257 Fetches all Schema entries under CN=Schema,CN=Configuration,...
258
259 :return: Async generator which yields (`MSADSchemaEntry`, None) tuple on success or (None, `Exception`) on error
260 :rtype: Iterator[(:class:`MSADSchemaEntry`, :class:`Exception`)]
261 """
262 res = await self.get_tree_plot('CN=Schema,CN=Configuration,' + self._tree, level = 1)
263 for x in res:
264 for dn in res[x]:
265 async for entry, err in self._con.pagedsearch(
266 dn,
267 r'(distinguishedName=%s)' % escape_filter_chars(dn),
268 attributes = [x.encode() for x in MSADSCHEMAENTRY_ATTRS],
269 size_limit = self.ldap_query_page_size,
270 search_scope=BASE,
271 controls = None,
272 ):
273 if err is not None:
274 yield None, err
275 return
276
277 yield MSADSchemaEntry.from_ldap(entry), None
278 break
279 else:
280 yield None, None
281
282 logger.debug('Finished polling for entries!')
162283
163284 async def get_laps(self, sAMAccountName):
285 """
286 Fetches the LAPS password for a machine. This functionality is only available to specific high-privileged users.
287
288 :param sAMAccountName: The username of the machine (eg. `COMP123$`).
289 :type sAMAccountName: str
290 :return: Laps attributes as a `dict`
291 :rtype: (:class:`dict`, :class:`Exception`)
292 """
293
164294 ldap_filter = r'(&(sAMAccountType=805306369)(sAMAccountName=%s))' % sAMAccountName
165295 attributes = ['cn','ms-mcs-AdmPwd']
166 async for entry in self.pagedsearch(ldap_filter, attributes):
167 yield entry
296 async for entry, err in self.pagedsearch(ldap_filter, attributes):
297 return entry, err
168298
169299 async def get_user(self, sAMAccountName):
170300 """
171301 Fetches one user object from the AD, based on the sAMAccountName attribute (read: username)
302
303 :param sAMAccountName: The username of the user.
304 :type sAMAccountName: str
305 :return: A tuple with the user as `MSADUser` and an `Exception` is there was any
306 :rtype: (:class:`MSADUser`, :class:`Exception`)
172307 """
173308 logger.debug('Polling AD for user %s'% sAMAccountName)
174309 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)
310 async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
311 if err is not None:
312 return None, err
313 return MSADUser.from_ldap(entry, self._ldapinfo), None
314 else:
315 return None, None
178316 logger.debug('Finished polling for entries!')
179317
318 async def get_machine(self, sAMAccountName):
319 """
320 Fetches one machine object from the AD, based on the sAMAccountName attribute (read: username)
321
322 :param sAMAccountName: The username of the machine.
323 :type sAMAccountName: str
324 :return: A tuple with the user as `MSADMachine` and an `Exception` is there was any
325 :rtype: (:class:`MSADMachine`, :class:`Exception`)
326 """
327 logger.debug('Polling AD for user %s'% sAMAccountName)
328 ldap_filter = r'(&(sAMAccountType=805306369)(sAMAccountName=%s))' % sAMAccountName
329 async for entry, err in self.pagedsearch(ldap_filter, MSADMachine_ATTRS):
330 if err is not None:
331 return None, err
332 return MSADMachine.from_ldap(entry, self._ldapinfo), None
333 else:
334 return None, None
335 logger.debug('Finished polling for entries!')
336
180337 async def get_ad_info(self):
181338 """
182339 Polls for basic AD information (needed for determine password usage characteristics!)
340
341 :return: A tuple with the domain information as `MSADInfo` and an `Exception` is there was any
342 :rtype: (:class:`MSADInfo`, :class:`Exception`)
183343 """
184344 logger.debug('Polling AD for basic info')
185345 ldap_filter = r'(distinguishedName=%s)' % self._tree
186 async for entry in self.pagedsearch(ldap_filter, MSADInfo_ATTRS):
346 async for entry, err in self.pagedsearch(ldap_filter, MSADInfo_ATTRS):
347 if err is not None:
348 return None, err
187349 self._ldapinfo = MSADInfo.from_ldap(entry)
188 return self._ldapinfo
350 return self._ldapinfo, None
189351
190352 logger.debug('Poll finished!')
191353
192354 async def get_all_spn_entries(self):
355 """
356 Fetches all service user objects from the AD, and returns MSADUser object.
357 Service user refers to an user with SPN (servicePrincipalName) attribute set
358
359 :param include_machine: Specifies wether machine accounts should be included in the query
360 :type include_machine: bool
361 :return: Async generator which yields tuples with a string in SPN format and an Exception if there was any
362 :rtype: Iterator[(:class:`str`, :class:`Exception`)]
363
364 """
365
193366 logger.debug('Polling AD for all SPN entries')
194367 ldap_filter = r'(&(sAMAccountType=805306369))'
195368 attributes = ['objectSid','sAMAccountName', 'servicePrincipalName']
196369
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):
370 async for entry, err in self.pagedsearch(ldap_filter, attributes):
371 yield entry, err
372
373 async def get_all_service_users(self, include_machine = False):
201374 """
202375 Fetches all service user objects from the AD, and returns MSADUser object.
203 Service user refers to an user whith SPN (servicePrincipalName) attribute set
376 Service user refers to an user with SPN (servicePrincipalName) attribute set
377
378 :param include_machine: Specifies wether machine accounts should be included in the query
379 :type include_machine: bool
380
381 :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error
382 :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)]
383
204384 """
205385 logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine)
206386 if include_machine == True:
208388 else:
209389 ldap_filter = r'(&(servicePrincipalName=*)(!(sAMAccountName=*$)))'
210390
211 async for entry in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
212 yield MSADUser.from_ldap(entry, self._ldapinfo)
391 async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
392 if err is not None:
393 yield None, err
394 return
395 yield MSADUser.from_ldap(entry, self._ldapinfo), None
213396 logger.debug('Finished polling for entries!')
214397
215 async def get_all_knoreq_user_objects(self, include_machine = False):
398 async def get_all_knoreq_users(self, include_machine = False):
216399 """
217400 Fetches all user objects with useraccountcontrol DONT_REQ_PREAUTH flag set from the AD, and returns MSADUser object.
218401
402 :param include_machine: Specifies wether machine accounts should be included in the query
403 :type include_machine: bool
404 :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error
405 :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)]
406
219407 """
220408 logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine)
221409 if include_machine == True:
223411 else:
224412 ldap_filter = r'(&(userAccountControl:1.2.840.113556.1.4.803:=4194304)(!(sAMAccountName=*$)))'
225413
226 async for entry in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
227 yield MSADUser.from_ldap(entry, self._ldapinfo)
414 async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
415 if err is not None:
416 yield None, err
417 return
418 yield MSADUser.from_ldap(entry, self._ldapinfo), None
228419 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)
245420
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})
421 async def get_objectacl_by_dn_p(self, dn, flags = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION):
422 """
423 Returns the full or partial Security Descriptor of the object specified by it's DN.
424 The flags indicate which part of the security Descriptor to be returned.
425 By default the full SD info is returned.
426
427 :param object_dn: The object's DN
428 :type object_dn: str
429 :param flags: Flags indicate the data type to be returned.
430 :type flags: :class:`SDFlagsRequest`
431 :return:
432 :rtype: :class:`MSADSecurityInfo`
433
434 """
435
436 req_flags = SDFlagsRequestValue({'Flags' : flags})
253437
254438 ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn)
255439 attributes = MSADSecurityInfo.ATTRS
256440 controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())]
257441
258 async for entry in self.pagedsearch(ldap_filter, attributes, controls = controls):
259 yield MSADSecurityInfo.from_ldap(entry)
260
442 async for entry, err in self.pagedsearch(ldap_filter, attributes, controls = controls):
443 if err is not None:
444 yield None, err
445 return
446 yield MSADSecurityInfo.from_ldap(entry), None
447
448 async def get_objectacl_by_dn(self, dn, flags = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION):
449 """
450 Returns the full or partial Security Descriptor of the object specified by it's DN.
451 The flags indicate which part of the security Descriptor to be returned.
452 By default the full SD info is returned.
453
454 :param object_dn: The object's DN
455 :type object_dn: str
456 :param flags: Flags indicate the data type to be returned.
457 :type flags: :class:`SDFlagsRequest`
458 :return: nTSecurityDescriptor attribute of the object as `bytes` and an `Exception` is there was any
459 :rtype: (:class:`bytes`, :class:`Exception`)
460
461 """
462
463 req_flags = SDFlagsRequestValue({'Flags' : flags})
464
465 ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn)
466 attributes = ['nTSecurityDescriptor']
467 controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())]
468
469 async for entry, err in self.pagedsearch(ldap_filter, attributes, controls = controls):
470 if err is not None:
471 return None, err
472 return entry['attributes'].get('nTSecurityDescriptor'), None
473 return None, None
474
475 async def set_objectacl_by_dn(self, object_dn, data, flags = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION):
476 """
477 Updates the security descriptor of the LDAP object
478
479 :param object_dn: The object's DN
480 :type object_dn: str
481 :param data: The actual data as bytearray to be updated in the Security Descriptor of the specified object
482 :type data: bytes
483 :param flags: Flags indicate the data type to be updated.
484 :type flags: :class:`SDFlagsRequest`
485 :return: A tuple of (True, None) on success or (False, Exception) on error.
486 :rtype: tuple
487
488 """
489
490 req_flags = SDFlagsRequestValue({'Flags' : flags})
491 controls = [
492 Control({
493 'controlType' : b'1.2.840.113556.1.4.801',
494 'controlValue': req_flags.dump(),
495 'criticality' : True,
496 })
497 ]
498
499 changes = {
500 'nTSecurityDescriptor': [('replace', [data])]
501 }
502 return await self._con.modify(object_dn, changes, controls = controls)
503
504 async def get_all_groups(self):
505 """
506 Yields all Groups present in the LDAP tree.
507
508 :return: Async generator which yields (`MSADGroup`, None) tuple on success or (None, `Exception`) on error
509 :rtype: Iterator[(:class:`MSADGroup`, :class:`Exception`)]
510 """
511 ldap_filter = r'(objectClass=group)'
512 async for entry, err in self.pagedsearch(ldap_filter, MSADGroup_ATTRS):
513 if err is not None:
514 yield None, err
515 return
516 yield MSADGroup.from_ldap(entry), None
261517
518 async def get_all_ous(self):
519 """
520 Yields all OUs present in the LDAP tree.
521
522 :return: Async generator which yields (`MSADOU`, None) tuple on success or (None, `Exception`) on error
523 :rtype: Iterator[(:class:`MSADOU`, :class:`Exception`)]
524 """
525 ldap_filter = r'(objectClass=organizationalUnit)'
526 async for entry, err in self.pagedsearch(ldap_filter, MSADOU_ATTRS):
527 if err is not None:
528 yield None, err
529 return
530 yield MSADOU.from_ldap(entry), None
531
532 async def get_group_by_dn(self, group_dn):
533 """
534 Returns an `MSADGroup` object for the group specified by group_dn
535
536 :param group_dn: The user's DN
537 :type group_dn: str
538 :return: tuple of `MSADGroup` and an `Exception` is there was any
539 :rtype: (:class:`MSADGroup`, :class:`Exception`)
540 """
541
542 ldap_filter = r'(&(objectClass=group)(distinguishedName=%s))' % escape_filter_chars(group_dn)
543 async for entry, err in self.pagedsearch(ldap_filter, MSADGroup_ATTRS):
544 if err is not None:
545 return None, err
546 return MSADGroup.from_ldap(entry), None
547
548 async def get_user_by_dn(self, user_dn):
549 """
550 Fetches the DN for an object specified by `objectsid`
551
552 :param user_dn: The user's DN
553 :type user_dn: str
554 :return: The user object
555 :rtype: (:class:`MSADUser`, :class:`Exception`)
556 """
557
558 ldap_filter = r'(&(objectClass=user)(distinguishedName=%s))' % user_dn
559 async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
560 if err is not None:
561 return None, err
562 return MSADUser.from_ldap(entry), None
563
564 async def get_group_members(self, dn, recursive = False):
565 """
566 Fetches the DN for an object specified by `objectsid`
567
568 :param dn: The object's DN
569 :type dn: str
570 :param recursive: Indicates wether the lookup should recursively affect all groups
571 :type recursive: bool
572 :return: Async generator which yields (`MSADUser`, None) tuple on success or (None, `Exception`) on error
573 :rtype: Iterator[(:class:`MSADUser`, :class:`Exception`)]
574 """
575
576 group, err = self.get_group_by_dn(dn)
577 if err is not None:
578 yield None, err
579 return
580 for member in group.member:
581 async for result in self.get_object_by_dn(member):
582 if isinstance(result, MSADGroup) and recursive:
583 async for user, err in self.get_group_members(result.distinguishedName, recursive = True):
584 yield user, err
585 else:
586 yield result, err
587
588 async def get_dn_for_objectsid(self, objectsid):
589 """
590 Fetches the DN for an object specified by `objectsid`
591
592 :param objectsid: The object's SID
593 :type objectsid: str
594 :return: The distinguishedName
595 :rtype: (:class:`str`, :class:`Exception`)
596
597 """
598
599 ldap_filter = r'(objectSid=%s)' % str(objectsid)
600 async for entry, err in self.pagedsearch(ldap_filter, ['distinguishedName']):
601 if err is not None:
602 return None, err
603
604 return entry['attributes']['distinguishedName'], None
605
606 async def get_tokengroups(self, dn):
607 """
608 Yields SIDs of groups that the given DN is a member of.
609
610 :return: Async generator which yields (`str`, None) tuple on success or (None, `Exception`) on error
611 :rtype: Iterator[(:class:`str`, :class:`Exception`)]
612
613 """
614 ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn)
615 attributes=[b'tokenGroups']
616
617 async for entry, err in self._con.pagedsearch(
618 dn,
619 ldap_filter,
620 attributes = attributes,
621 size_limit = self.ldap_query_page_size,
622 search_scope=BASE,
623 ):
624 if err is not None:
625 yield None, err
626 return
627
628 #print(entry['attributes'])
629 if 'tokenGroups' in entry['attributes']:
630 for sid_data in entry['attributes']['tokenGroups']:
631 yield sid_data, None
632
633 async def get_all_tokengroups(self):
634 """
635 Yields all effective group membership information for all objects of the following type:
636 Users, Groups, Computers
637
638 :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error
639 :rtype: Iterator[(:class:`dict`, :class:`Exception`)]
640
641 """
642
643 ldap_filter = r'(|(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))'
644 async for entry, err in self.pagedsearch(
645 ldap_filter,
646 attributes = ['dn', 'cn', 'objectSid','objectClass', 'objectGUID']
647 ):
648 if err is not None:
649 yield None, err
650 return
651 if 'objectName' in entry:
652 #print(entry['objectName'])
653 async for entry2, err in self._con.pagedsearch(
654 entry['objectName'],
655 r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName']),
656 attributes = [b'tokenGroups'],
657 size_limit = self.ldap_query_page_size,
658 search_scope=BASE,
659 ):
660
661 #print(entry2)
662 if err is not None:
663 yield None, err
664 break
665 if 'tokenGroups' in entry2['attributes']:
666 for token in entry2['attributes']['tokenGroups']:
667 yield {
668 'cn' : entry['attributes']['cn'],
669 'dn' : entry['objectName'],
670 'guid' : entry['attributes']['objectGUID'],
671 'sid' : entry['attributes']['objectSid'],
672 'type' : entry['attributes']['objectClass'][-1],
673 'token' : token
674
675 }, None
676
677 async def get_all_objectacl(self):
678 """
679 Yields the security descriptor of all objects in the LDAP tree of the following types:
680 Users, Computers, GPOs, OUs, Groups
681
682 :return: Async generator which yields (`MSADSecurityInfo`, None) tuple on success or (None, `Exception`) on error
683 :rtype: Iterator[(:class:`MSADSecurityInfo`, :class:`Exception`)]
684
685 """
686
687 flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION
688 req_flags = SDFlagsRequestValue({'Flags' : flags_value})
689
690 ldap_filter = r'(|(objectClass=organizationalUnit)(objectCategory=groupPolicyContainer)(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))'
691 async for entry, err in self.pagedsearch(ldap_filter, attributes = ['dn']):
692 if err is not None:
693 yield None, err
694 return
695 ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName'])
696 attributes = MSADSecurityInfo.ATTRS
697 controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())]
698
699 async for entry2, err in self.pagedsearch(ldap_filter, attributes, controls = controls):
700 if err is not None:
701 yield None, err
702 return
703 yield MSADSecurityInfo.from_ldap(entry2), None
704
705
706 async def get_all_trusts(self):
707 """
708 Yields all trusted domains.
709
710 :return: Async generator which yields (`MSADDomainTrust`, None) tuple on success or (None, `Exception`) on error
711 :rtype: Iterator[(:class:`MSADDomainTrust`, :class:`Exception`)]
712
713 """
714
715 ldap_filter = r'(objectClass=trustedDomain)'
716 async for entry, err in self.pagedsearch(ldap_filter, attributes = MSADDomainTrust_ATTRS):
717 if err is not None:
718 yield None, err
719 return
720 yield MSADDomainTrust.from_ldap(entry), None
721
722
723 async def create_user(self, username, password):
724 """
725 Creates a new user object with a password.
726 WARNING: this function only creates the user, but will not enable it! To create a user account to be used immediately, use the `create_user_dn` function!
727
728 :param user_dn: The user's DN
729 :type user_dn: str
730 :param password: The password of the user
731 :type password: str
732 :return: A tuple of (True, None) on success or (False, Exception) on error.
733 :rtype: (:class:`bool`, :class:`Exception`)
734
735 """
736 user_dn = 'CN=%s,CN=Users,%s' % (username, self._tree)
737 return await self.create_user_dn(user_dn, password)
738
739 async def create_user_dn(self, user_dn, password):
740 """
741 Creates a new user object with a password and enables the user so it can be used immediately.
742
743 :param user_dn: The user's DN
744 :type user_dn: str
745 :param password: The password of the user
746 :type password: str
747 :return: A tuple of (True, None) on success or (False, Exception) on error.
748 :rtype: (:class:`bool`, :class:`Exception`)
749
750 """
751 try:
752 sn = user_dn.split(',')[0][3:]
753 domain = self._tree[3:].replace(',DC=','.')
754 attributes = {
755 'objectClass': ['organizationalPerson', 'person', 'top', 'user'],
756 'sn': sn,
757 'sAMAccountName': sn,
758 'displayName': sn,
759 'userPrincipalName' : "{}@{}".format(sn, domain),
760 }
761
762 _, err = await self._con.add(user_dn, attributes)
763 if err is not None:
764 return False, err
765
766 _, err = await self.change_password(user_dn, password)
767 if err is not None:
768 return False, err
769
770 _, err = await self.enable_user(user_dn)
771 if err is not None:
772 return False, err
773
774 return True, None
775 except Exception as e:
776 return False, e
777
778
779 async def unlock_user(self, user_dn):
780 """
781 Unlocks the user by clearing the lockoutTime attribute.
782
783 :param user_dn: The user's DN
784 :type user_dn: str
785 :return: A tuple of (True, None) on success or (False, Exception) on error.
786 :rtype: (:class:`bool`, :class:`Exception`)
787
788 """
789 changes = {
790 'lockoutTime': [('replace', [0])]
791 }
792 return await self._con.modify(user_dn, changes)
793
794 async def enable_user(self, user_dn):
795 """
796 Sets the user object to enabled by modifying the UserAccountControl attribute.
797
798 :param user_dn: The user's DN
799 :type user_dn: str
800 :return: A tuple of (True, None) on success or (False, Exception) on error.
801 :rtype: (:class:`bool`, :class:`Exception`)
802
803 """
804 changes = {
805 'userAccountControl': [('replace', [512])]
806 }
807 return await self._con.modify(user_dn, changes)
808
809 async def disable_user(self, user_dn):
810 """
811 Sets the user object to disabled by modifying the UserAccountControl attribute.
812
813 :param user_dn: The user's DN
814 :type user_dn: str
815 :return: A tuple of (True, None) on success or (False, Exception) on error.
816 :rtype: (:class:`bool`, :class:`Exception`)
817
818 """
819 changes = {
820 'userAccountControl': [('replace', [2])]
821 }
822 return await self._con.modify(user_dn, changes)
823
824 async def add_user_spn(self, user_dn, spn):
825 """
826 Adds an SPN record to the user object.
827
828 :param user_dn: The user's DN
829 :type user_dn: str
830 :param spn: The SPN to be added. It must follow the SPN string format specifications.
831 :type spn: str
832 :return: A tuple of (True, None) on success or (False, Exception) on error.
833 :rtype: (:class:`bool`, :class:`Exception`)
834
835 """
836 changes = {
837 'servicePrincipalName': [('add', [spn])]
838 }
839 return await self._con.modify(user_dn, changes)
840
841 async def add_additional_hostname(self, user_dn, hostname):
842 """
843 Adds additional hostname to the user object.
844
845 :param user_dn: The user's DN
846 :type user_dn: str
847 :return: A tuple of (True, None) on success or (False, Exception) on error.
848 :rtype: (:class:`bool`, :class:`Exception`)
849
850 """
851 changes = {
852 'msds-additionaldnshostname': [('add', [hostname])]
853 }
854 return await self._con.modify(user_dn, changes)
855
856
857 async def delete_user(self, user_dn):
858 """
859 Deletes the user.
860 This action is destructive!
861
862 :param user_dn: The user's DN
863 :type user_dn: str
864 :return: A tuple of (True, None) on success or (False, Exception) on error.
865 :rtype: (:class:`bool`, :class:`Exception`)
866
867 """
868 return await self._con.delete(user_dn)
869
870 async def change_password(self, user_dn: str, newpass: str, oldpass = None):
871 """
872 Changes the password of a user.
873 If used with a high-privileged account (eg. Domain admin, Account operator...), the old password can be `None`
874
875 :param user_dn: The user's DN
876 :type user_dn: str
877 :param newpass: The new password
878 :type newpass: str
879 :param oldpass: The current password
880 :type oldpass: str
881 :return: A tuple of (True, None) on success or (False, Exception) on error.
882 :rtype: (:class:`bool`, :class:`Exception`)
883
884 """
885 changes = {
886 'unicodePwd': []
887 }
888 if oldpass is not None:
889 changes['unicodePwd'].append(('delete', ['"%s"' % oldpass]))
890 changes['unicodePwd'].append(('add', ['"%s"' % newpass]))
891 else:
892 #if you are admin...
893 changes['unicodePwd'].append(('replace', ['"%s"' % newpass]))
894
895 return await self._con.modify(user_dn, changes)
896
897
898 async def add_user_to_group(self, user_dn: str, group_dn: str):
899 """
900 Adds a user to a group
901
902 :param user_dn: The user's DN
903 :type user_dn: str
904 :param group_dn: The groups's DN
905 :type group_dn: str
906 :return: A tuple of (True, None) on success or (False, Exception) on error.
907 :rtype: (:class:`bool`, :class:`Exception`)
908
909
910 """
911 changes = {
912 'member': [('add', [user_dn])]
913 }
914 return await self._con.modify(group_dn, changes)
915
916 async def del_user_from_group(self, user_dn: str, group_dn: str):
917 """
918 Removes user from group
919
920 :param user_dn: The user's DN
921 :type user_dn: str
922 :param group_dn: The groups's DN
923 :type group_dn: str
924 :return: A tuple of (True, None) on success or (False, Exception) on error.
925 :rtype: (:class:`bool`, :class:`Exception`)
926
927
928 """
929 changes = {
930 'member': [('delete', [user_dn])]
931 }
932 return await self._con.modify(group_dn, changes)
933
934
935 async def get_object_by_dn(self, dn, expected_class = None):
936 ldap_filter = r'(distinguishedName=%s)' % dn
937 async for entry, err in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES):
938 if err is not None:
939 yield None, err
940 return
941 temp = entry['attributes'].get('objectClass')
942 if expected_class:
943 yield expected_class.from_ldap(entry), None
944
945 if not temp:
946 yield entry, None
947 elif 'user' in temp:
948 yield MSADUser.from_ldap(entry), None
949 elif 'group' in temp:
950 yield MSADGroup.from_ldap(entry), None
951
952 async def modify(self, dn, changes, controls = None):
953 """
954 Performs the modify operation.
955
956 :param dn: The DN of the object whose attributes are to be modified
957 :type dn: str
958 :param changes: Describes the changes to be made on the object. Must be a dictionary of the following format: {'attribute': [('change_type', [value])]}
959 :type changes: dict
960 :param controls: additional controls to be passed in the query
961 :type controls: dict
962 :return: A tuple of (True, None) on success or (False, Exception) on error.
963 :rtype: (:class:`bool`, :class:`Exception`)
964 """
965 if controls is None:
966 controls = []
967 controls_conv = []
968 for control in controls:
969 controls_conv.append(Control(control))
970 return await self._con.modify(dn, changes, controls=controls_conv)
971
972
973 async def add(self, dn, attributes):
974 """
975 Performs the add operation.
976
977 :param dn: The DN of the object to be added
978 :type dn: str
979 :param attributes: Attributes to be used in the operation
980 :type attributes: dict
981 :return: A tuple of (True, None) on success or (False, Exception) on error.
982 :rtype: (:class:`bool`, :class:`Exception`)
983 """
984
985 return await self._con.add(dn, attributes)
986
987 async def delete(self, dn):
988 """
989 Performs the delete operation.
990
991 :param dn: The DN of the object to be deleted
992 :type dn: str
993 :return: A tuple of (True, None) on success or (False, Exception) on error.
994 :rtype: (:class:`bool`, :class:`Exception`)
995 """
996
997 return await self._con.delete(dn)
998
999 #async def get_permissions_for_dn(self, dn):
1000 # """
1001 # Lists all users who can modify the specified dn
1002 # """
1003 # async for secinfo in self.get_objectacl_by_dn(dn):
1004 # for sdec in secinfo.nTSecurityDescriptor:
1005 # sids_to_lookup = {}
1006 # if not sdec.Dacl:
1007 # continue
1008 #
1009 # for ace in sdec.Dacl.aces:
1010 # sids_to_lookup[str(ace.Sid)] = 1
1011 #
1012 # for sid in sids_to_lookup:
1013 # sids_to_lookup[sid] = self.get_dn_for_objectsid(sid)
1014 #
1015 # print(sids_to_lookup)
1016 #
1017 # for ace in sdec.Dacl.aces:
1018 # if not sids_to_lookup[str(ace.Sid)]:
1019 # print(str(ace.Sid))
1020 # #print('===== %s =====' % sids_to_lookup[str(ace.Sid)])
1021 # #if
1022 # #print(str(ace))
1023
2621024 #async def get_all_tokengroups(self):
2631025 # """
2641026 # returns the tokengroups attribute for all user and machine on the server
2811043 # async for entry, err in self._con.response:
2821044 # #yield MSADTokenGroup.from_ldap(entry)
2831045 # 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']
1046
3041047
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)
1048 #async def get_all_objectacl(self):
1049 # """
1050 # Returns all ACL info for all AD objects
1051 # """
1052 #
1053 # flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION
1054 # req_flags = SDFlagsRequestValue({'Flags' : flags_value})
1055 #
1056 # ldap_filter = r'(objectClass=*)'
1057 # attributes = MSADSecurityInfo.ATTRS
1058 # controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())]
1059 #
1060 # async for entry in self.pagedsearch(ldap_filter, attributes, controls = controls):
1061 # yield MSADSecurityInfo.from_ldap(entry)
1062
1063
1064 #async def get_netdomain(self):
1065 # def nameconvert(x):
1066 # return x.split(',CN=')[1]
1067 # """
1068 # gets the name of the current user's domain
1069 # """
1070 # if not self._ldapinfo:
1071 # self.get_ad_info()
1072 # print(self._ldapinfo)
1073 # dname = self._ldapinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.')
1074 # domain_controllers = ','.join(nameconvert(x) + '.' +dname for x in self._ldapinfo.masteredBy)
1075 #
1076 # ridroleowner = nameconvert(self.get_ridroleowner()) + '.' +dname
1077 # infraowner = nameconvert(self.get_infrastructureowner()) + '.' +dname
1078 # pdcroleowner = nameconvert(self.get_pdcroleowner()) + '.' +dname
1079 #
1080 # print('name : %s' % dname)
1081 # print('Domain Controllers : %s' % domain_controllers)
1082 # print('DomainModeLevel : %s' % self._ldapinfo.domainmodelevel)
1083 # print('PdcRoleOwner : %s' % pdcroleowner)
1084 # print('RidRoleOwner : %s' % ridroleowner)
1085 # print('InfrastructureRoleOwner : %s' % infraowner)
1086 #
1087 #async def get_domaincontroller(self):
1088 # ldap_filter = r'(userAccountControl:1.2.840.113556.1.4.803:=8192)'
1089 # async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES):
1090 # print('Forest: %s' % '')
1091 # print('Name: %s' % entry['attributes'].get('dNSHostName'))
1092 # print('OSVersion: %s' % entry['attributes'].get('operatingSystem'))
1093 # print(entry['attributes'])
1094
1095 #async def get_pdcroleowner(self):
1096 # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/
1097 # #get adinfo -> get ridmanagerreference attr -> look up the dn of ridmanagerreference -> get fsmoroleowner attr (which is a DN)
1098 # if not self._ldapinfo:
1099 # self.get_ad_info()
1100 #
1101 # ldap_filter = r'(distinguishedName=%s)' % self._ldapinfo.rIDManagerReference
1102 # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']):
1103 # return entry['attributes']['fSMORoleOwner']
1104 #
1105 #async def get_infrastructureowner(self):
1106 # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/
1107 # #"CN=Infrastructure,DC=concorp,DC=contoso,DC=com" -l fSMORoleOwner
1108 # if not self._ldapinfo:
1109 # self.get_ad_info()
1110 #
1111 # ldap_filter = r'(distinguishedName=%s)' % ('CN=Infrastructure,' + self._ldapinfo.distinguishedName)
1112 # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']):
1113 # return entry['attributes']['fSMORoleOwner']
1114 #
1115 #async def get_ridroleowner(self):
1116 # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/
1117 # if not self._ldapinfo:
1118 # self.get_ad_info()
1119 #
1120 # ldap_filter = r'(distinguishedName=%s)' % ('CN=RID Manager$,CN=System,' + self._ldapinfo.distinguishedName)
1121 # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']):
1122 # return entry['attributes']['fSMORoleOwner']
1123
1124 #async def get_all_user_raw(self):
1125 # """
1126 # Fetches all user objects from the AD, and returns MSADUser object
1127 # """
1128 # logger.debug('Polling AD for all user objects')
1129 # ldap_filter = r'(sAMAccountType=805306368)'
1130 #
1131 # return self.pagedsearch(ldap_filter, MSADUser_ATTRS)
2626 self.is_guest = False
2727 self.nt_hash = None
2828 self.lm_hash = None
29 self.encrypt = False
2930
3031 class MSLDAPSIMPLECredential:
3132 def __init__(self):
4546 self.target = None #KerberosTarget
4647 self.ksoc = None #KerberosSocketAIO
4748 self.ccred = None
49 self.encrypt = False
50 self.enctypes = None #[23,17,18]
4851
4952 class MSLDAPKerberosSSPICredential:
5053 def __init__(self):
51 self.client = None
52 self.password = None
53 self.target = None
54 self.domain = None
55 self.password = None
56 self.username = None
57 self.encrypt = False
5458
5559 class MSLDAPNTLMSSPICredential:
5660 def __init__(self):
57 self.client = None
58 self.passwrd = None
61 self.username = None
62 self.password = None
63 self.domain = None
64 self.encrypt = False
65
66 class MSLDAPMultiplexorCredential:
67 def __init__(self):
68 self.type = 'NTLM'
69 self.username = '<CURRENT>'
70 self.domain = '<CURRENT>'
71 self.password = '<CURRENT>'
72 self.target = None
73 self.is_guest = False
74 self.is_ssl = False
75 self.mp_host = '127.0.0.1'
76 self.mp_port = 9999
77 self.mp_username = None
78 self.mp_domain = None
79 self.mp_password = None
80 self.agent_id = None
81 self.encrypt = False
82
83 def get_url(self):
84 url_temp = 'ws://%s:%s'
85 if self.is_ssl is True:
86 url_temp = 'wss://%s:%s'
87 url = url_temp % (self.mp_host, self.mp_port)
88 return url
89
90 def parse_settings(self, settings):
91 req = ['agentid']
92 for r in req:
93 if r not in settings:
94 raise Exception('%s parameter missing' % r)
95 self.mp_host = settings.get('host', ['127.0.0.1'])[0]
96 self.mp_port = settings.get('port', ['9999'])[0]
97 if self.mp_port is None:
98 self.mp_port = '9999'
99 if 'user' in settings:
100 self.mp_username = settings.get('user')[0]
101 if 'domain' in settings:
102 self.mp_domain = settings.get('domain')[0]
103 if 'password' in settings:
104 self.mp_password = settings.get('password')[0]
105 self.agent_id = settings['agentid'][0]
59106
60107
61108
81128 ntlmcred.domain = self.creds.domain if self.creds.domain is not None else ''
82129 ntlmcred.workstation = None
83130 ntlmcred.is_guest = False
131 ntlmcred.encrypt = self.creds.encrypt
132
84133
85134 if self.creds.password is None:
86 raise Exception('NTLM authentication requres password!')
87 ntlmcred.password = self.creds.password
135 raise Exception('NTLM authentication requres password/NT hash!')
136
137
138 if len(self.creds.password) == 32:
139 try:
140 bytes.fromhex(self.creds.password)
141 except:
142 ntlmcred.password = self.creds.password
143 else:
144 ntlmcred.nt_hash = self.creds.password
145
146 else:
147 ntlmcred.password = self.creds.password
88148
89149 settings = NTLMHandlerSettings(ntlmcred)
90150 return NTLMAUTHHandler(settings)
109169 ntlmcred.domain = self.creds.domain if self.creds.domain is not None else ''
110170 ntlmcred.workstation = None
111171 ntlmcred.is_guest = False
172 ntlmcred.encrypt = self.creds.encrypt
112173
113174 if self.creds.password is None:
114175 raise Exception('NTLM authentication requres password!')
146207 if self.target.dc_ip is None:
147208 raise Exception('target must have a dc_ip for kerberos!')
148209
149
210 kcred = MSLDAPKerberosCredential()
150211 kc = KerberosCredential()
151212 kc.username = self.creds.username
152213 kc.domain = self.creds.domain
214 kcred.enctypes = []
153215 if self.creds.auth_method == LDAPAuthProtocol.KERBEROS_PASSWORD:
154216 kc.password = self.creds.password
217 kcred.enctypes = [23,17,18]
155218 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_NT:
156219 kc.nt_hash = self.creds.password
220 kcred.enctypes = [23]
157221
158222 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_AES:
159223 if len(self.creds.password) == 32:
160224 kc.kerberos_key_aes_128 = self.creds.password
225 kcred.enctypes = [17]
161226 elif len(self.creds.password) == 64:
162227 kc.kerberos_key_aes_256 = self.creds.password
228 kcred.enctypes = [18]
163229
164230 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_RC4:
165231 kc.kerberos_key_rc4 = self.creds.password
232 kcred.enctypes = [23]
166233
167234 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_CCACHE:
168235 kc.ccache = self.creds.password
236 kcred.enctypes = [23,17,18]
169237 elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KEYTAB:
170238 kc.keytab = self.creds.password
239 kcred.enctypes = [23,17,18]
171240 else:
172241 raise Exception('No suitable secret type found to set up kerberos!')
173
174
175 kcred = MSLDAPKerberosCredential()
242
243 if self.creds.etypes is not None:
244 kcred.enctypes = list(set(self.creds.etypes).intersection(set(kcred.enctypes)))
245
176246 kcred.ccred = kc
177247 kcred.spn = KerberosSPN.from_target_string(self.target.to_target_string())
178248 kcred.target = KerberosTarget(self.target.dc_ip)
249 kcred.encrypt = self.creds.encrypt
250
179251 if self.target.proxy is not None:
180252 kcred.target.proxy = KerberosProxy()
253 kcred.target.proxy.type = self.target.proxy.type
181254 kcred.target.proxy.target = copy.deepcopy(self.target.proxy.target)
182255 kcred.target.proxy.target.endpoint_ip = self.target.dc_ip
183256 kcred.target.proxy.target.endpoint_port = 88
195268 raise Exception('Target must be specified with Kerberos SSPI!')
196269
197270 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()
271 kerbcred.username = self.creds.domain if self.creds.domain is not None else '<CURRENT>'
272 kerbcred.username = self.creds.username if self.creds.username is not None else '<CURRENT>'
273 kerbcred.password = self.creds.password if self.creds.password is not None else '<CURRENT>'
274 kerbcred.spn = self.target.to_target_string()
275 kerbcred.encrypt = self.creds.encrypt
201276
202277 handler = MSLDAPKerberosSSPI(kerbcred)
203278 #setting up SPNEGO
207282
208283 elif self.creds.auth_method == LDAPAuthProtocol.SSPI_NTLM:
209284 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
285 ntlmcred.username = self.creds.domain if self.creds.domain is not None else '<CURRENT>'
286 ntlmcred.username = self.creds.username if self.creds.username is not None else '<CURRENT>'
287 ntlmcred.password = self.creds.password if self.creds.password is not None else '<CURRENT>'
288 ntlmcred.encrypt = self.creds.encrypt
289
213290 handler = MSLDAPNTLMSSPI(ntlmcred)
214291 #setting up SPNEGO
215292 spneg = SPNEGO()
216293 spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler)
217294 return spneg
218295
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()
296 elif self.creds.auth_method.value.startswith('MULTIPLEXOR'):
297 if self.creds.auth_method in [LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM, LDAPAuthProtocol.MULTIPLEXOR_NTLM]:
298 from msldap.authentication.ntlm.multiplexor import MSLDAPNTLMMultiplexor
299 ntlmcred = MSLDAPMultiplexorCredential()
226300 ntlmcred.type = 'NTLM'
227 if creds.username is not None:
301 if self.creds.username is not None:
228302 ntlmcred.username = '<CURRENT>'
229 if creds.domain is not None:
303 if self.creds.domain is not None:
230304 ntlmcred.domain = '<CURRENT>'
231 if creds.secret is not None:
305 if self.creds.password is not None:
232306 ntlmcred.password = '<CURRENT>'
233307 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)
308 ntlmcred.is_ssl = True if self.creds.auth_method == LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM else False
309 ntlmcred.parse_settings(self.creds.settings)
310 ntlmcred.encrypt = self.creds.encrypt
311
312 handler = MSLDAPNTLMMultiplexor(ntlmcred)
238313 #setting up SPNEGO
239314 spneg = SPNEGO()
240315 spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler)
241316 return spneg
242317
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()
318 elif self.creds.auth_method in [LDAPAuthProtocol.MULTIPLEXOR_SSL_KERBEROS, LDAPAuthProtocol.MULTIPLEXOR_KERBEROS]:
319 from msldap.authentication.kerberos.multiplexor import MSLDAPKerberosMultiplexor
320
321 ntlmcred = MSLDAPMultiplexorCredential()
247322 ntlmcred.type = 'KERBEROS'
248 ntlmcred.target = creds.target
249 if creds.username is not None:
323 ntlmcred.target = self.target
324 if self.creds.username is not None:
250325 ntlmcred.username = '<CURRENT>'
251 if creds.domain is not None:
326 if self.creds.domain is not None:
252327 ntlmcred.domain = '<CURRENT>'
253 if creds.secret is not None:
328 if self.creds.password is not None:
254329 ntlmcred.password = '<CURRENT>'
255330 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)
331 ntlmcred.is_ssl = True if self.creds.auth_method == LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM else False
332 ntlmcred.parse_settings(self.creds.settings)
333 ntlmcred.encrypt = self.creds.encrypt
334
335 handler = MSLDAPKerberosMultiplexor(ntlmcred)
260336 #setting up SPNEGO
261337 spneg = SPNEGO()
262338 spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler)
263339 return spneg
264 """
340
0 import enum
1
2 class MSLDAPClientStatus(enum.Enum):
3 RUNNING = 'RUNNING'
4 STOPPED = 'STOPPED'
5 ERROR = 'ERROR'
3434 KERBEROS_PASSWORD = 'KERBEROS_PASSWORD' #actually SASL-GSSAPI-SPNEGO-KERBEROS
3535 KERBEROS_CCACHE = 'KERBEROS_CCACHE' #actually SASL-GSSAPI-SPNEGO-KERBEROS
3636 KERBEROS_KEYTAB = 'KERBEROS_KEYTAB' #actually SASL-GSSAPI-SPNEGO-KERBEROS
37 MULTIPLEXOR = 'MULTIPLEXOR'
38 MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL'
37 MULTIPLEXOR_KERBEROS = 'MULTIPLEXOR_KERBEROS'
38 MULTIPLEXOR_NTLM = 'MULTIPLEXOR_NTLM'
39 MULTIPLEXOR_SSL_KERBEROS = 'MULTIPLEXOR_SSL_KERBEROS'
40 MULTIPLEXOR_SSL_NTLM = 'MULTIPLEXOR_SSL_NTLM'
3941 SSPI_NTLM = 'SSPI_NTLM' #actually SASL-GSSAPI-SPNEGO-NTLM but with integrated SSPI
4042 SSPI_KERBEROS = 'SSPI_KERBEROS' #actually SASL-GSSAPI-SPNEGO-KERBEROS but with integrated SSPI
4143
5052 LDAPAuthProtocol.KERBEROS_KEYTAB ,
5153 LDAPAuthProtocol.SSPI_NTLM ,
5254 LDAPAuthProtocol.SSPI_KERBEROS,
53
55 LDAPAuthProtocol.MULTIPLEXOR_KERBEROS,
56 LDAPAuthProtocol.MULTIPLEXOR_NTLM,
57 LDAPAuthProtocol.MULTIPLEXOR_SSL_KERBEROS,
58 LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM,
59 ]
60
61 MSLDAP_KERBEROS_PROTOCOLS = [
62 LDAPAuthProtocol.KERBEROS_RC4 ,
63 LDAPAuthProtocol.KERBEROS_NT ,
64 LDAPAuthProtocol.KERBEROS_AES ,
65 LDAPAuthProtocol.KERBEROS_PASSWORD ,
66 LDAPAuthProtocol.KERBEROS_CCACHE ,
67 LDAPAuthProtocol.KERBEROS_KEYTAB ,
5468 ]
5569
5670 class MSLDAPCredential:
57 def __init__(self, domain=None, username= None, password = None, auth_method = None, settings = None):
71 """
72 Describes the user's credentials to be used for authentication during the bind operation.
73
74 :param domain: Domain of the user
75 :type domain: str
76 :param username: Username of the user
77 :type username: str
78 :param password: The authentication secret. The actual contents depend on the `auth_method`
79 :type password: str
80 :param auth_method: The ahtentication method to be performed during bind operation
81 :type auth_method: :class:`LDAPAuthProtocol`
82 :param settings: Additional settings
83 :type settings: dict
84 :param etypes: Supported encryption types for Kerberos authentication.
85 :type etypes: List[:class:`int`]
86 :param encrypt: Use protocol-level encryption. Doesnt work on LDAPS
87 :type encrypt: bool
88 """
89 def __init__(self, domain=None, username= None, password = None, auth_method = None, settings = None, etypes = None, encrypt = False):
5890 self.auth_method = auth_method
5991 self.domain = domain
6092 self.username = username
6193 self.password = password
94 self.signing_preferred = False
95 self.encryption_preferred = False
6296 self.settings = settings
97 self.etypes = etypes
98 self.encrypt = encrypt
6399
64100 def get_msuser(self):
65101 if not self.domain:
1818 MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL'
1919
2020 class MSLDAPProxy:
21 def __init__(self):
22 self.type = None
23 self.target = None
24 self.auth = None
21 """
22 Describes the proxy to be used when connecting to the server. Used as a parameter to the `MSLDAPTarget` object
23
24 :param type: Specifies the proxy type
25 :type type: :class:`MSLDAPProxyType`
26 :param target:
27 :type target:
28 :param auth: Specifies the proxy authentication if any
29 :type auth:
30 """
31 def __init__(self, type = None, target = None, auth = None):
32 self.type = type
33 self.target = target
34 self.auth = auth
2535
2636
2737 @staticmethod
2838 def from_params(url_str):
39 """
40 Creates a proxy object from the parameters found in an LDAP URL string
41
42 :param type: url_str
43 :type type: str
44 :return: The proxy object
45 :rtype: :class:`MSLDAPProxy`
46 """
2947 proxy = MSLDAPProxy()
3048 url = urlparse(url_str)
3149 if url.query is None:
3856 proxy.type = MSLDAPProxyType(query['proxytype'][0].upper())
3957 if proxy.type in [MSLDAPProxyType.SOCKS4, MSLDAPProxyType.SOCKS4_SSL, MSLDAPProxyType.SOCKS5, MSLDAPProxyType.SOCKS5_SSL]:
4058 cu = SocksClientURL.from_params(url_str)
59 proxy.target = cu.get_target()
60 proxy.auth = cu.get_creds()
4161 else:
42 raise Exception('Multiplexor not yet implemented as a proxy!')
43 #cu = SocksClientURL.from_params(url_str)
62 proxy.target = MSLDAPMultiplexorProxy.from_params(url_str)
4463
45 proxy.target = cu.get_target()
46 proxy.auth = cu.get_creds()
4764 return proxy
4865
4966 def __str__(self):
5269 t += '%s: %s\r\n' % (k, self.__dict__[k])
5370
5471 return t
55
56
5772
5873
74 class MSLDAPMultiplexorProxy:
75 def __init__(self):
76 self.ip = None
77 self.port = None
78 self.timeout = 10
79 self.type = MSLDAPProxyType.MULTIPLEXOR
80 self.username = None
81 self.password = None
82 self.domain = None
83 self.agent_id = None
84 self.virtual_socks_port = None
85 self.virtual_socks_ip = None
86
87 def sanity_check(self):
88 if self.ip is None:
89 raise Exception('MULTIPLEXOR server IP is missing!')
90 if self.port is None:
91 raise Exception('MULTIPLEXOR server port is missing!')
92 if self.agent_id is None:
93 raise Exception('MULTIPLEXOR proxy requires agentid to be set!')
94
95 def get_server_url(self):
96 con_str = 'ws://%s:%s' % (self.ip, self.port)
97 if self.type == MSLDAPProxyType.MULTIPLEXOR_SSL:
98 con_str = 'wss://%s:%s' % (self.ip, self.port)
99 return con_str
100
101 @staticmethod
102 def from_params(url_str):
103 res = MSLDAPMultiplexorProxy()
104 url = urlparse(url_str)
105 res.endpoint_ip = url.hostname
106 if url.port:
107 res.endpoint_port = int(url.port)
108 if url.query is not None:
109 query = parse_qs(url.query)
110
111 for k in query:
112 if k.startswith('proxy'):
113 if k[5:] in multiplexorproxyurl_param2var:
114
115 data = query[k][0]
116 for c in multiplexorproxyurl_param2var[k[5:]][1]:
117 data = c(data)
118
119 setattr(
120 res,
121 multiplexorproxyurl_param2var[k[5:]][0],
122 data
123 )
124 res.sanity_check()
125
126 return res
127
128 def stru(x):
129 return str(x).upper()
130
131 multiplexorproxyurl_param2var = {
132 'type' : ('version', [stru, MSLDAPProxyType]),
133 'host' : ('ip', [str]),
134 'port' : ('port', [int]),
135 'timeout': ('timeout', [int]),
136 'user' : ('username', [str]),
137 'pass' : ('password', [str]),
138 #'authtype' : ('authtype', [SOCKS5Method]),
139 'agentid' : ('agent_id', [str]),
140 'domain' : ('domain', [str])
141
142 }
143
1414
1515
1616 class MSLDAPTarget:
17 """
18 Describes the connection to the server.
19
20 :param host: IP address or hostname of the server
21 :type host: str
22 :param port: port of the LDAP service running on the server
23 :type port: int
24 :param proto: Connection protocol to be used
25 :type proto: :class:`LDAPProtocol`
26 :param tree: The tree to connect to
27 :type tree: str
28 :param proxy: specifies what kind of proxy to be used
29 :type proxy: :class:`MSLDAPProxy`
30 :param timeout: connection timeout in seconds
31 :type timeout: int
32 """
1733 def __init__(self, host, port = 389, proto = LDAPProtocol.TCP, tree = None, proxy = None, timeout = 10):
1834 self.proto = proto
1935 self.host = host
2238 self.proxy = proxy
2339 self.timeout = timeout
2440 self.dc_ip = None
41 self.serverip = None
2542 self.domain = None
2643 self.sslctx = None
2744
2845 def get_ssl_context(self):
2946 if self.proto == LDAPProtocol.SSL:
3047 if self.sslctx is None:
31 self.sslctx = ssl.create_default_context()
48 # TODO ssl verification :)
49 self.sslctx = ssl._create_unverified_context()
50 #self.sslctx.verify = False
3251 return self.sslctx
3352 return None
3453
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
3471 - proxytimeout: timeout ins ecodns for the proxy connection
72 - dc: the IP address of the domain controller, MUST be used for kerberos authentication
3573
3674 Examples:
3775 ldap://10.10.10.2 (anonymous bind)
3876 ldaps://test.corp (anonymous bind)
39 ldap+sspi:///test.corp
77 ldap+sspi-ntlm://test.corp
78 ldap+sspi-kerberos://test.corp
4079 ldap://TEST\\victim:<password>@10.10.10.2 (defaults to SASL GSSAPI NTLM)
4180 ldap+simple://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth)
4281 ldap+plain://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth)
5796 self.domain = None
5897 self.username = None
5998 self.password = None
99 self.encrypt = False
60100 self.auth_settings = {}
101 self.etypes = None
61102
62103 self.ldap_proto = None
63104 self.ldap_host = None
69110 self.serverip = None
70111 self.proxy = None
71112
113 self.__pwpreprocess = None
114
72115 self.parse()
73116
74117
75118 def get_credential(self):
76 return MSLDAPCredential(
119 """
120 Creates a credential object
121
122 :return: Credential object
123 :rtype: :class:`MSLDAPCredential`
124 """
125 t = MSLDAPCredential(
77126 domain=self.domain,
78127 username=self.username,
79128 password = self.password,
80129 auth_method=self.auth_scheme,
81130 settings = self.auth_settings
82131 )
132 t.encrypt = self.encrypt
133 t.etypes = self.etypes
134
135 return t
83136
84137 def get_target(self):
138 """
139 Creates a target object
140
141 :return: Target object
142 :rtype: :class:`MSLDAPTarget`
143 """
85144 target = MSLDAPTarget(
86145 self.ldap_host,
87146 port = self.ldap_port,
92151 target.domain = self.domain
93152 target.dc_ip = self.dc_ip
94153 target.proxy = self.proxy
154 target.serverip = self.serverip
95155 return target
96156
97157 def get_client(self):
158 """
159 Creates a client that can be used to interface with the server
160
161 :return: LDAP client
162 :rtype: :class:`MSLDAPClient`
163 """
98164 cred = self.get_credential()
99165 target = self.get_target()
100166 return MSLDAPClient(target, cred, ldap_query_page_size = self.target_pagesize)
167
168 def get_connection(self):
169 """
170 Creates a connection that can be used to interface with the server
171
172 :return: LDAP connection
173 :rtype: :class:`MSLDAPClientConnection`
174 """
175 cred = self.get_credential()
176 target = self.get_target()
177 return MSLDAPClientConnection(target, cred)
101178
102179 def scheme_decoder(self, scheme):
103180 schemes = []
126203 return
127204
128205 try:
129 self.auth_scheme = LDAPAuthProtocol(schemes[1])
206 x = PLAINTEXTSCHEME(schemes[1])
207 if x == PLAINTEXTSCHEME.SIMPLE_PROMPT:
208 self.auth_scheme = LDAPAuthProtocol.SIMPLE
209 self.__pwpreprocess = 'PROMPT'
210
211 if x == PLAINTEXTSCHEME.SIMPLE_HEX:
212 self.auth_scheme = LDAPAuthProtocol.SIMPLE
213 self.__pwpreprocess = 'HEX'
214
215 if x == PLAINTEXTSCHEME.SIMPLE_B64:
216 self.auth_scheme = LDAPAuthProtocol.SIMPLE
217 self.__pwpreprocess = 'B64'
218
219 if x == PLAINTEXTSCHEME.PLAIN_PROMPT:
220 self.auth_scheme = LDAPAuthProtocol.PLAIN
221 self.__pwpreprocess = 'PROMPT'
222
223 if x == PLAINTEXTSCHEME.PLAIN_HEX:
224 self.auth_scheme = LDAPAuthProtocol.PLAIN
225 self.__pwpreprocess = 'HEX'
226
227 if x == PLAINTEXTSCHEME.PLAIN_B64:
228 self.auth_scheme = LDAPAuthProtocol.PLAIN
229 self.__pwpreprocess = 'B64'
230
231 if x == PLAINTEXTSCHEME.SICILY_PROMPT:
232 self.auth_scheme = LDAPAuthProtocol.SICILY
233 self.__pwpreprocess = 'PROMPT'
234
235 if x == PLAINTEXTSCHEME.SICILY_HEX:
236 self.auth_scheme = LDAPAuthProtocol.SICILY
237 self.__pwpreprocess = 'HEX'
238
239 if x == PLAINTEXTSCHEME.SICILY_B64:
240 self.auth_scheme = LDAPAuthProtocol.SICILY
241 self.__pwpreprocess = 'B64'
242
243 if x == PLAINTEXTSCHEME.NTLM_PROMPT:
244 self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD
245 self.__pwpreprocess = 'PROMPT'
246
247 if x == PLAINTEXTSCHEME.NTLM_HEX:
248 self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD
249 self.__pwpreprocess = 'HEX'
250
251 if x == PLAINTEXTSCHEME.NTLM_B64:
252 self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD
253 self.__pwpreprocess = 'B64'
130254 except:
131 raise Exception('Uknown scheme!')
255 try:
256 self.auth_scheme = LDAPAuthProtocol(schemes[1])
257 except:
258 raise Exception('Uknown scheme!')
132259
133260 return
134261
137264 self.scheme_decoder(url_e.scheme)
138265
139266 self.password = url_e.password
267 if self.__pwpreprocess is not None:
268 if self.__pwpreprocess == 'PROMPT':
269 self.password = getpass.getpass()
270
271 elif self.__pwpreprocess == 'HEX':
272 self.password = bytes.fromhex(self.password).decode()
273
274 elif self.__pwpreprocess == 'B64':
275 self.password = base64.b64decode(self.password).decode()
276
277 else:
278 raise Exception('Unknown password preprocess directive %s' % self.__pwpreprocess)
279
140280
141281 if url_e.username is not None:
142282 if url_e.username.find('\\') != -1:
172312 proxy_present = False
173313 if url_e.query is not None:
174314 query = parse_qs(url_e.query)
315 if 'etype' in query:
316 self.etypes = []
175317 for k in query:
176318 if k.startswith('proxy') is True:
177319 proxy_present = True
180322 elif k == 'timeout':
181323 self.timeout = int(query[k][0])
182324 elif k == 'serverip':
183 self.server_ip = query[k][0]
325 self.serverip = query[k][0]
184326 elif k == 'dns':
185327 self.dns = query[k] #multiple dns can be set, so not trimming here
328 elif k == 'encrypt':
329 self.encrypt = bool(int(query[k][0]))
330 elif k == 'etype':
331 self.etypes = [int(x) for x in query[k]]
186332 elif k.startswith('auth'):
187333 self.auth_settings[k[len('auth'):]] = query[k]
188334 #elif k.startswith('same'):
201347 if self.domain is None:
202348 self.domain = '<CURRENT>'
203349
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 #
350 if self.auth_scheme in MSLDAP_KERBEROS_PROTOCOLS and self.dc_ip is None:
351 raise Exception('The "dc" parameter MUST be used for kerberos authentication types!')
352
353
293354 # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]:
294355 # if self.proxy_port is None:
295356 # 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')
301357 #
302358 # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]:
303359 # if 'agentid' not in self.proxy_settings:
304360 # raise Exception('multiplexor proxy reuires agentid to be set! Set it via proxyagentid parameter!')
305361 #
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!')
362
317363
318364
319365
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 asn1crypto.x509 import Certificate
20 from hashlib import sha256
21 from minikerberos.gssapi.channelbindings import ChannelBindingsStruct
1422
1523 class MSLDAPClientConnection:
1624 def __init__(self, target, creds):
25 if target is None:
26 raise Exception('Target cant be none!')
1727 self.target = target
1828 self.creds = creds
1929 self.auth = AuthenticatorBuilder(self.creds, self.target).build()
2434 self.network = None
2535
2636 self.handle_incoming_task = None
37 self.status = MSLDAPClientStatus.RUNNING
38 self.lasterror = None
2739
2840 self.message_id = 0
2941 self.message_table = {}
3042 self.message_table_notify = {}
31 self.encryption_sequence_counter = 0 #for whatever reason it's only used during encryption, but decryption always uses 0
43 self.encryption_sequence_counter = 0 # this will be set by the inderlying auth algo
44 self.cb_data = None #for channel binding
3245
3346 async def __handle_incoming(self):
3447 try:
3548 while True:
3649 message_data, err = await self.network.in_queue.get()
3750 if err is not None:
38 logger.debug('Client terminating bc __handle_incoming!')
51 logger.debug('Client terminating bc __handle_incoming got an error!')
3952 raise err
4053
41 ################################
42 # #
43 # ADD CHANNEL BINDING HERE! #
44 ################################
45
54 #print('Incoming message data: %s' % message_data)
4655 if self.bind_ok is True:
4756 if self.__encrypt_messages is True:
48 #print('Encrypted %s' % message_data)
4957 #removing size
5058 message_data = message_data[4:]
5159 try:
52 message_data = await self.auth.decrypt(message_data, 0)
60 # seq number doesnt matter here, a it's in the header
61 message_data, err = await self.auth.decrypt(message_data, 0 )
62 if err is not None:
63 raise err
5364 #print('Decrypted %s' % message_data.hex())
65 #print('Decrypted %s' % message_data)
5466 except:
5567 import traceback
5668 traceback.print_exc()
69 raise
5770
5871 elif self.__sign_messages is True:
5972 #print('Signed %s' % message_data)
6477 except:
6578 import traceback
6679 traceback.print_exc()
80 raise
81
6782
6883 msg_len = calcualte_length(message_data)
6984 msg_total_len = len(message_data)
90105 self.message_table_notify[message_id].set()
91106
92107 except asyncio.CancelledError:
93 #not notifying clients, at this point the client is terminating
108 self.status = MSLDAPClientStatus.STOPPED
94109 return
95110
96111 except Exception as e:
97 import traceback
98 traceback.print_exc()
112 self.status = MSLDAPClientStatus.ERROR
113 self.lasterror = e
99114 for msgid in self.message_table_notify:
100115 self.message_table[msgid] = [e]
101116 self.message_table_notify[msgid].set()
117
118 self.status = MSLDAPClientStatus.STOPPED
102119
103120
104121 async def send_message(self, message):
142159 return messages
143160
144161 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!')
162 """
163 Connects to the remote server. Establishes the session, but doesn't perform binding.
164 This function MUST be called first before the `bind` operation.
165
166 :return: A tuple of (True, None) on success or (False, Exception) on error.
167 :rtype: (:class:`bool`, :class:`Exception`)
168 """
169 try:
170 logger.debug('Connecting!')
171 self.network = await MSLDAPNetworkSelector.select(self.target)
172 res, err = await self.network.run()
173 if res is False:
174 return False, err
175
176 # now processing channel binding options
177 if self.target.proto == LDAPProtocol.SSL:
178 certdata = self.network.get_peer_certificate()
179 #cert = Certificate.load(certdata).native
180 #print(cert)
181 cb_struct = ChannelBindingsStruct()
182 cb_struct.application_data = b'tls-server-end-point:' + sha256(certdata).digest()
183
184 self.cb_data = cb_struct.to_bytes()
185
186 self.handle_incoming_task = asyncio.create_task(self.__handle_incoming())
187 logger.debug('Connection succsessful!')
188 return True, None
189 except Exception as e:
190 return False, e
153191
154192 async def disconnect(self):
193 """
194 Tears down the connection.
195
196 :return: Nothing
197 :rtype: None
198 """
199
155200 logger.debug('Disconnecting!')
156201 self.bind_ok = False
157 self.handle_incoming_task.cancel()
158 await self.network.terminate()
202 if self.handle_incoming_task is not None:
203 self.handle_incoming_task.cancel()
204 if self.network is not None:
205 await self.network.terminate()
159206
160207
161208 def __bind_success(self):
172219 self.network.is_plain_msg = False
173220
174221 async def bind(self):
222 """
223 Performs the bind operation.
224 This is where the authentication happens. Remember to call `connect` before this function!
225
226 :return: A tuple of (True, None) on success or (False, Exception) on error.
227 :rtype: (:class:`bool`, :class:`Exception`)
228 """
175229 logger.debug('BIND in progress...')
176230 try:
177231 if self.creds.auth_method == LDAPAuthProtocol.SICILY:
178 data, _ = await self.auth.authenticate(None)
179
232
233 data, to_continue, err = await self.auth.authenticate(None)
234 if err is not None:
235 return None, err
236
180237 auth = {
181238 'sicily_disco' : b''
182239 }
229286 res['protocolOp']['diagnosticMessage']
230287 ))
231288
232 data, _ = await self.auth.authenticate(res['protocolOp']['matchedDN'])
289 data, to_continue, err = await self.auth.authenticate(res['protocolOp']['matchedDN'])
290 if err is not None:
291 return None, err
233292
234293 auth = {
235294 'sicily_resp' : data
303362 elif self.creds.auth_method in MSLDAP_GSS_METHODS:
304363 challenge = None
305364 while True:
306 data, _ = await self.auth.authenticate(challenge)
365 try:
366 data, to_continue, err = await self.auth.authenticate(challenge, cb_data = self.cb_data)
367 if err is not None:
368 raise err
369 except Exception as e:
370 return False, e
307371
308372 sasl = {
309373 'mechanism' : 'GSS-SPNEGO'.encode(),
315379
316380 bindreq = {
317381 'version' : 3,
318 'name': ''.encode(),
382 'name': b'',
319383 'authentication': AuthenticationChoice(auth),
320384 }
321385
329393 return False, res
330394 res = res.native
331395 if res['protocolOp']['resultCode'] == 'success':
396 if 'serverSaslCreds' in res['protocolOp']:
397 data, _, err = await self.auth.authenticate(res['protocolOp']['serverSaslCreds'], cb_data = self.cb_data)
398 if err is not None:
399 return False, err
400
401 self.encryption_sequence_counter = self.auth.get_seq_number()
332402 self.__bind_success()
403
333404 return True, None
334405
335406 elif res['protocolOp']['resultCode'] == 'saslBindInProgress':
343414 res['protocolOp']['diagnosticMessage']
344415 ))
345416
346 #print(res)
417 else:
418 raise Exception('Not implemented authentication method: %s' % self.creds.auth_method.name)
347419 except Exception as e:
348 print(str(e))
420 return False, e
421
422 async def add(self, entry, attributes):
423 """
424 Performs the add operation.
425
426 :param entry: The DN of the object to be added
427 :type entry: str
428 :param attributes: Attributes to be used in the operation
429 :type attributes: dict
430 :return: A tuple of (True, None) on success or (False, Exception) on error.
431 :rtype: (:class:`bool`, :class:`Exception`)
432 """
433 try:
434 req = {
435 'entry' : entry.encode(),
436 'attributes' : encode_attributes(attributes)
437 }
438 br = { 'addRequest' : AddRequest(req)}
439 msg = { 'protocolOp' : protocolOp(br)}
440
441 msg_id = await self.send_message(msg)
442 results = await self.recv_message(msg_id)
443 if isinstance(results[0], Exception):
444 return False, results[0]
445
446 for message in results:
447 msg_type = message['protocolOp'].name
448 message = message.native
449 if msg_type == 'addResponse':
450 if message['protocolOp']['resultCode'] != 'success':
451 return False, Exception('Failed to add DN! LDAP error! Reason: %s Diag: %s' % (
452 message['protocolOp']['resultCode'],
453 message['protocolOp']['diagnosticMessage'])
454 )
455
456 return True, None
457 except Exception as e:
458 return False, e
459
460 async def modify(self, entry, changes, controls = None):
461 """
462 Performs the modify operation.
463
464 :param entry: The DN of the object whose attributes are to be modified
465 :type entry: str
466 :param changes: Describes the changes to be made on the object. Must be a dictionary of the following format: {'attribute': [('change_type', [value])]}
467 :type changes: dict
468 :param controls: additional controls to be passed in the query
469 :type controls: List[class:`Control`]
470 :return: A tuple of (True, None) on success or (False, Exception) on error.
471 :rtype: (:class:`bool`, :class:`Exception`)
472 """
473 try:
474 req = {
475 'object' : entry.encode(),
476 'changes' : encode_changes(changes)
477 }
478 br = { 'modifyRequest' : ModifyRequest(req)}
479 msg = { 'protocolOp' : protocolOp(br)}
480 if controls is not None:
481 msg['controls'] = controls
482
483 msg_id = await self.send_message(msg)
484 results = await self.recv_message(msg_id)
485 if isinstance(results[0], Exception):
486 return False, results[0]
487
488 for message in results:
489 msg_type = message['protocolOp'].name
490 message = message.native
491 if msg_type == 'modifyResponse':
492 if message['protocolOp']['resultCode'] != 'success':
493 return False, Exception('Failed to add DN! LDAP error! Reason: %s Diag: %s' % (
494 message['protocolOp']['resultCode'],
495 message['protocolOp']['diagnosticMessage'])
496 )
497
498 return True, None
499 except Exception as e:
500 return False, e
501
502 async def delete(self, entry):
503 """
504 Performs the delete operation.
505
506 :param entry: The DN of the object to be deleted
507 :type entry: str
508 :return: A tuple of (True, None) on success or (False, Exception) on error.
509 :rtype: (:class:`bool`, :class:`Exception`)
510 """
511 try:
512 br = { 'delRequest' : DelRequest(entry.encode())}
513 msg = { 'protocolOp' : protocolOp(br)}
514
515 msg_id = await self.send_message(msg)
516 results = await self.recv_message(msg_id)
517 if isinstance(results[0], Exception):
518 return False, results[0]
519
520 for message in results:
521 msg_type = message['protocolOp'].name
522 message = message.native
523 if msg_type == 'delResponse':
524 if message['protocolOp']['resultCode'] != 'success':
525 return False, Exception('Failed to add DN! LDAP error! Reason: %s Diag: %s' % (
526 message['protocolOp']['resultCode'],
527 message['protocolOp']['diagnosticMessage'])
528 )
529
530 return True, None
531 except Exception as e:
349532 return False, e
350533
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 """
534 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):
535 """
536 Performs the search operation.
537
538 :param base: base tree on which the search should be performed
539 :type base: str
540 :param query: filter query that defines what should be searched for
541 :type query: str
542 :param attributes: a list of attributes to be included in the response
543 :type attributes: List[str]
544 :param search_scope: Specifies the search operation's scope. Default: 2 (Subtree)
545 :type search_scope: int
546 :param types_only: indicates whether the entries returned should include attribute types only or both types and values. Default: False (both)
547 :type types_only: bool
548 :param size_limit: Size limit of result elements per query. Default: 1000
549 :type size_limit: int
550 :param derefAliases: Specifies the behavior on how aliases are dereferenced. Default: 0 (never)
551 :type derefAliases: int
552 :param timeLimit: Maximum time the search should take. If time limit reached the server SHOULD return an error
553 :type timeLimit: int
554 :param controls: additional controls to be passed in the query
555 :type controls: List[class:`Control`]
556 :param return_done: Controls wether the final 'done' LDAP message should be returned, or just the actual results
557 :type return_done: bool
558
559 :return: Async generator which yields (`LDAPMessage`, None) tuple on success or (None, `Exception`) on error
560 :rtype: Iterator[(:class:`LDAPMessage`, :class:`Exception`)]
561 """
562 if self.status != MSLDAPClientStatus.RUNNING:
563 yield None, Exception('Connection not running! Probably encountered an error')
564 return
355565 try:
356566 if timeLimit is None:
357567 timeLimit = 600 #not sure
568
569 flt = query_syntax_converter(query)
358570
359571 searchreq = {
360 'baseObject' : base,
572 'baseObject' : base.encode(),
361573 'scope': search_scope,
362574 'derefAliases': derefAliases,
363 'sizeLimit': paged_size,
575 'sizeLimit': size_limit,
364576 'timeLimit': timeLimit,
365 'typesOnly': typesOnly,
366 'filter': filter,
577 'typesOnly': types_only,
578 'filter': flt,
367579 'attributes': attributes,
368580 }
369581
380592 msg_type = message['protocolOp'].name
381593 message = message.native
382594 if msg_type == 'searchResDone':
383 #print(message)
384 #print('BREAKING!')
385595 if return_done is True:
386596 yield (message, None)
387597 break
402612 except Exception as e:
403613 yield (None, e)
404614
405 async def pagedsearch(self, base, filter, attributes, search_scope = 2, paged_size = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None):
615 async def pagedsearch(self, base, query, attributes, search_scope = 2, size_limit = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None):
616 """
617 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.
618
619 :param base: base tree on which the search should be performed
620 :type base: str
621 :param query: filter query that defines what should be searched for
622 :type query: str
623 :param attributes: a list of attributes to be included in the response
624 :type attributes: List[str]
625 :param search_scope: Specifies the search operation's scope. Default: 2 (Subtree)
626 :type search_scope: int
627 :param types_only: indicates whether the entries returned should include attribute types only or both types and values. Default: False (both)
628 :type types_only: bool
629 :param size_limit: Size limit of result elements per query. Default: 1000
630 :type size_limit: int
631 :param derefAliases: Specifies the behavior on how aliases are dereferenced. Default: 0 (never)
632 :type derefAliases: int
633 :param timeLimit: Maximum time the search should take. If time limit reached the server SHOULD return an error
634 :type timeLimit: int
635 :param controls: additional controls to be passed in the query
636 :type controls: dict
637 :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error
638 :rtype: Iterator[(:class:`dict`, :class:`Exception`)]
639 """
640
641 if self.status != MSLDAPClientStatus.RUNNING:
642 yield None, Exception('Connection not running! Probably encountered an error')
643 return
406644 try:
407645 cookie = b''
408646 while True:
411649 Control({
412650 'controlType' : b'1.2.840.113556.1.4.319',
413651 'controlValue': SearchControlValue({
414 'size' : paged_size,
652 'size' : size_limit,
415653 'cookie': cookie
416654 }).dump()
417655 })
426664
427665 async for res, err in self.search(
428666 base,
429 filter,
667 query,
430668 attributes,
431669 search_scope = search_scope,
432 paged_size=paged_size,
433 typesOnly=typesOnly,
670 size_limit=size_limit,
671 types_only=typesOnly,
434672 derefAliases=derefAliases,
435673 timeLimit=timeLimit,
436674 controls = ctrs,
439677 if err is not None:
440678 yield (None, err)
441679 return
442
680
443681 if 'resultCode' in res['protocolOp']:
444682 for control in res['controls']:
445683 if control['controlType'] == b'1.2.840.113556.1.4.319':
461699
462700
463701 async def get_serverinfo(self):
702 if self.status != MSLDAPClientStatus.RUNNING:
703 return None, Exception('Connection not running! Probably encountered an error')
704
464705 attributes = [
465706 b'subschemaSubentry',
466707 b'dsServiceName',
496737
497738 msg_id = await self.send_message(msg)
498739 res = await self.recv_message(msg_id)
499 res = res[0].native
500
740 res = res[0]
501741 if isinstance(res, Exception):
502742 return None, res
503743
504744 #print('res')
505745 #print(res)
506 return convert_attributes(res['protocolOp']['attributes']), None
746 return convert_attributes(res.native['protocolOp']['attributes']), None
507747
508748
509749 async def amain():
524764 #target.dc_ip = '10.10.10.2'
525765 #target.domain = 'TEST'
526766
527 url = 'ldap+kerberos-password://test\\victim:Passw0rd!1@WIN2019AD/?dc=10.10.10.2'
767 url = 'ldaps+ntlm-password://test\\Administrator:QLFbT8zkiFGlJuf0B3Qq@WIN2019AD/?dc=10.10.10.2'
528768
529769 dec = MSLDAPURLDecoder(url)
530770 cred = dec.get_credential()
540780 res, err = await client.bind()
541781 if err is not None:
542782 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
549783
784 user = "CN=ldaptest_2,CN=Users,DC=test,DC=corp"
785 #attributes = {'objectClass': ['inetOrgPerson', 'posixGroup', 'top'], 'sn': 'user_sn', 'gidNumber': 0}
786 #res, err = await client.add(user, attributes)
787 #if err is not None:
788 # print(err)
789
790 #changes = {
791 # 'unicodePwd': [('replace', ['"TESTPassw0rd!1"'])],
792 # #'lockoutTime': [('replace', [0])]
793 #}
794
795 #res, err = await client.modify(user, changes)
796 #if err is not None:
797 # print('ERR! %s' % err)
798 #else:
799 # print('OK!')
550800
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!')
801 res, err = await client.delete(user)
802 if err is not None:
803 print('ERR! %s' % err)
556804
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
568805 await client.disconnect()
569806
570807
577814
578815 logger.setLevel(2)
579816
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)
817
818 asyncio.run(amain())
819
589820
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)
612821
613822
614823
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
44
55 class md5(hashBASE):
66 def __init__(self, data = None):
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.sid import SID
2426
2527
2628 class MSLDAPClientConsole(aiocmd.PromptToolkitCmd):
3234 self.connection = None
3335 self.adinfo = None
3436 self.ldapinfo = None
37 self.domain_name = None
3538
3639 async def do_login(self, url = None):
3740 """Performs connection and login"""
38 try:
39 print('url %s' % repr(url))
40
41 try:
4142 if self.conn_url is None and url is None:
4243 print('Not url was set, cant do logon')
4344 if url is not None:
4445 self.conn_url = MSLDAPURLDecoder(url)
4546
46 print(self.conn_url.get_credential())
47 print(self.conn_url.get_target())
47 logger.debug(self.conn_url.get_credential())
48 logger.debug(self.conn_url.get_target())
4849
4950
5051 self.connection = self.conn_url.get_client()
51 await self.connection.connect()
52
53 except:
54 traceback.print_exc()
52 _, err = await self.connection.connect()
53 if err is not None:
54 raise err
55
56 return True
57 except:
58 traceback.print_exc()
59 return False
5560
5661 async def do_ldapinfo(self, show = True):
5762 """Prints detailed LDAP connection info (DSA)"""
5964 if self.ldapinfo is None:
6065 self.ldapinfo = self.connection.get_server_info()
6166 if show is True:
62 print(self.ldapinfo)
63 except:
64 traceback.print_exc()
67 for k in self.ldapinfo:
68 print('%s : %s' % (k, self.ldapinfo[k]))
69 return True
70 except:
71 traceback.print_exc()
72 return False
6573
6674 async def do_adinfo(self, show = True):
6775 """Prints detailed Active Driectory info"""
6876 try:
6977 if self.adinfo is None:
7078 self.adinfo = self.connection._ldapinfo
79 self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.')
7180 if show is True:
7281 print(self.adinfo)
7382 except:
7786 """Fetches kerberoastable user accounts"""
7887 try:
7988 await self.do_ldapinfo(False)
80 async for user in self.connection.get_all_service_user_objects():
89 async for user, err in self.connection.get_all_service_users():
90 if err is not None:
91 raise err
8192 print(user.sAMAccountName)
8293 except:
8394 traceback.print_exc()
8697 """Fetches ASREP-roastable user accounts"""
8798 try:
8899 await self.do_ldapinfo(False)
89 async for user in self.connection.get_all_knoreq_user_objects():
100 async for user, err in self.connection.get_all_knoreq_users():
101 if err is not None:
102 raise err
90103 print(user.sAMAccountName)
91104 except:
92105 traceback.print_exc()
93106
107 async def do_computeraddr(self):
108 """Fetches all computer accounts"""
109 try:
110 await self.do_adinfo(False)
111 #machine_filename = '%s_computers_%s.txt' % (self.domain_name, datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
112
113 async for machine, err in self.connection.get_all_machines(attrs=['sAMAccountName', 'dNSHostName']):
114 if err is not None:
115 raise err
116
117 dns = machine.dNSHostName
118 if dns is None:
119 dns = '%s.%s' % (machine.sAMAccountName[:-1], self.domain_name)
120
121 print(str(dns))
122 except:
123 traceback.print_exc()
94124
95125 async def do_dump(self):
96126 """Fetches ALL user and machine accounts from the domain with a LOT of attributes"""
101131 users_filename = 'users_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
102132 pbar = tqdm(desc = 'Writing users to file %s' % users_filename)
103133 with open(users_filename, 'w', newline='', encoding = 'utf8') as f:
104 async for user in self.connection.get_all_user_objects():
134 async for user, err in self.connection.get_all_users():
135 if err is not None:
136 raise err
105137 pbar.update()
106138 f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS)))
107139 print('Users dump was written to %s' % users_filename)
109141 users_filename = 'computers_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
110142 pbar = tqdm(desc = 'Writing computers to file %s' % users_filename)
111143 with open(users_filename, 'w', newline='', encoding = 'utf8') as f:
112 async for user in self.connection.get_all_machine_objects():
144 async for user, err in self.connection.get_all_machines():
145 if err is not None:
146 raise err
113147 pbar.update()
114148 f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS)))
115149 print('Computer dump was written to %s' % users_filename)
126160 attributes = attributes.split(',')
127161 logging.debug('Query: %s' % (query))
128162 logging.debug('Attributes: %s' % (attributes))
129 async for entry in self.connection.pagedsearch(query, attributes):
163 async for entry, err in self.connection.pagedsearch(query, attributes):
164 if err is not None:
165 raise err
130166 print(entry)
131167 except:
132168 traceback.print_exc()
164200 try:
165201 await self.do_ldapinfo(False)
166202 await self.do_adinfo(False)
167 async for user in self.connection.get_user(samaccountname):
203 user, err = await self.connection.get_user(samaccountname)
204 if err is not None:
205 raise err
206 if user is None:
207 print('User not found!')
208 else:
168209 print(user)
169210 except:
170211 traceback.print_exc()
171212
172 async def do_acl(self, dn):
213 async def do_machine(self, samaccountname):
214 """Feteches a machine object based on the sAMAccountName of the machine"""
215 try:
216 await self.do_ldapinfo(False)
217 await self.do_adinfo(False)
218 machine, err = await self.connection.get_machine(samaccountname)
219 if err is not None:
220 raise err
221 if machine is None:
222 print('machine not found!')
223 else:
224 print(machine)
225 ####TEST
226 x = SECURITY_DESCRIPTOR.from_bytes(machine.allowedtoactonbehalfofotheridentity)
227 print(x)
228 except:
229 traceback.print_exc()
230
231 async def do_schemaentry(self, cn):
232 """Feteches a schema object entry object based on the DN of the object (must start with CN=)"""
233 try:
234 await self.do_ldapinfo(False)
235 await self.do_adinfo(False)
236 schemaentry, err = await self.connection.get_schemaentry(cn)
237 if err is not None:
238 raise err
239
240 print(str(schemaentry))
241
242 except:
243 traceback.print_exc()
244
245 async def do_allschemaentry(self):
246 """Feteches all schema object entry objects"""
247 try:
248 await self.do_ldapinfo(False)
249 await self.do_adinfo(False)
250 async for schemaentry, err in self.connection.get_all_schemaentry():
251 if err is not None:
252 raise err
253
254 print(str(schemaentry))
255
256 except:
257 traceback.print_exc()
258
259 #async def do_addallowedtoactonbehalfofotheridentity(self, target_name, add_computer_name):
260 # """Adds a SID to the msDS-AllowedToActOnBehalfOfOtherIdentity protperty of target_dn"""
261 # try:
262 # await self.do_ldapinfo(False)
263 # await self.do_adinfo(False)
264 #
265 # try:
266 # new_owner_sid = SID.from_string(sid)
267 # except:
268 # print('Incorrect SID!')
269 # return False, Exception('Incorrect SID')
270 #
271 #
272 # target_sd = None
273 # if target_attribute is None or target_attribute == '':
274 # target_attribute = 'nTSecurityDescriptor'
275 # res, err = await self.connection.get_objectacl_by_dn(target_dn)
276 # if err is not None:
277 # raise err
278 # target_sd = SECURITY_DESCRIPTOR.from_bytes(res)
279 # else:
280 #
281 # query = '(distinguishedName=%s)' % target_dn
282 # async for entry, err in self.connection.pagedsearch(query, [target_attribute]):
283 # if err is not None:
284 # raise err
285 # print(entry['attributes'][target_attribute])
286 # target_sd = SECURITY_DESCRIPTOR.from_bytes(entry['attributes'][target_attribute])
287 # break
288 # else:
289 # print('Target DN not found!')
290 # return False, Exception('Target DN not found!')
291 #
292 # print(target_sd)
293 # new_sd = copy.deepcopy(target_sd)
294 # new_sd.Owner = new_owner_sid
295 # print(new_sd)
296 #
297 # changes = {
298 # target_attribute : [('replace', [new_sd.to_bytes()])]
299 # }
300 # _, err = await self.connection.modify(target_dn, changes)
301 # if err is not None:
302 # raise err
303 #
304 # print('Change OK!')
305 # except:
306 # traceback.print_exc()
307
308 async def do_changeowner(self, new_owner_sid, target_dn, target_attribute = None):
309 """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"""
310 try:
311 await self.do_ldapinfo(False)
312 await self.do_adinfo(False)
313
314 try:
315 new_owner_sid = SID.from_string(new_owner_sid)
316 except:
317 print('Incorrect SID!')
318 return False, Exception('Incorrect SID')
319
320
321 target_sd = None
322 if target_attribute is None or target_attribute == '':
323 target_attribute = 'nTSecurityDescriptor'
324 res, err = await self.connection.get_objectacl_by_dn(target_dn)
325 if err is not None:
326 raise err
327 target_sd = SECURITY_DESCRIPTOR.from_bytes(res)
328 else:
329
330 query = '(distinguishedName=%s)' % target_dn
331 async for entry, err in self.connection.pagedsearch(query, [target_attribute]):
332 if err is not None:
333 raise err
334 print(entry['attributes'][target_attribute])
335 target_sd = SECURITY_DESCRIPTOR.from_bytes(entry['attributes'][target_attribute])
336 break
337 else:
338 print('Target DN not found!')
339 return False, Exception('Target DN not found!')
340
341 new_sd = copy.deepcopy(target_sd)
342 new_sd.Owner = new_owner_sid
343
344 changes = {
345 target_attribute : [('replace', [new_sd.to_bytes()])]
346 }
347 _, err = await self.connection.modify(target_dn, changes)
348 if err is not None:
349 raise err
350
351 print('Change OK!')
352 except:
353 traceback.print_exc()
354
355 async def do_setsd(self, target_dn, sddl):
356 """Updates the security descriptor of an object"""
357 try:
358 await self.do_ldapinfo(False)
359 await self.do_adinfo(False)
360
361 try:
362 new_sd = SECURITY_DESCRIPTOR.from_sddl(sddl)
363 except:
364 print('Incorrect SDDL input!')
365 return False, Exception('Incorrect SDDL input!')
366
367 _, err = await self.connection.set_objectacl_by_dn(target_dn, new_sd.to_bytes())
368 if err is not None:
369 raise err
370 print('Change OK!')
371 except:
372 print('Erro while updating security descriptor!')
373 traceback.print_exc()
374
375 async def do_getsd(self, dn):
173376 """Feteches security info for a given DN"""
174377 try:
175378 await self.do_ldapinfo(False)
176379 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)))
380 sec_info, err = await self.connection.get_objectacl_by_dn(dn)
381 if err is not None:
382 raise err
383 sd = SECURITY_DESCRIPTOR.from_bytes(sec_info)
384 print(sd.to_sddl())
179385 except:
180386 traceback.print_exc()
181387
184390 try:
185391 await self.do_ldapinfo(False)
186392 await self.do_adinfo(False)
187 async for gpo in self.connection.get_all_gpos():
393 async for gpo, err in self.connection.get_all_gpos():
394 if err is not None:
395 raise err
188396 print(gpo)
189397 except:
190398 traceback.print_exc()
192400 async def do_laps(self):
193401 """Feteches all laps passwords"""
194402 try:
195 async for entry in self.connection.get_all_laps():
403 async for entry, err in self.connection.get_all_laps():
404 if err is not None:
405 raise err
196406 pwd = '<MISSING>'
197 if 'ms-mcs-AdmPwd' in entry['attributes']:
198 pwd = entry['attributes']['ms-mcs-AdmPwd']
407 if 'ms-Mcs-AdmPwd' in entry['attributes']:
408 pwd = entry['attributes']['ms-Mcs-AdmPwd']
199409 print('%s : %s' % (entry['attributes']['cn'], pwd))
200410 except:
201411 traceback.print_exc()
206416 await self.do_ldapinfo(False)
207417 await self.do_adinfo(False)
208418 group_sids = []
209 async for group_sid in self.connection.get_tokengroups(dn):
419 async for group_sid, err in self.connection.get_tokengroups(dn):
420 if err is not None:
421 raise err
210422 group_sids.append(group_sids)
211 group_dn = await self.connection.get_dn_for_objectsid(group_sid)
423 group_dn, err = await self.connection.get_dn_for_objectsid(group_sid)
424 if err is not None:
425 raise err
212426 print('%s - %s' % (group_dn, group_sid))
213427
214428 if len(group_sids) == 0:
215429 print('No memberships found')
216 except:
430 except Exception as e:
431 print(e)
217432 traceback.print_exc()
218433
219434 async def do_bindtree(self, newtree):
225440 async def do_trusts(self):
226441 """Feteches gives back domain trusts"""
227442 try:
228 async for entry in self.connection.get_all_trusts():
443 async for entry, err in self.connection.get_all_trusts():
444 if err is not None:
445 raise err
229446 print(entry.get_line())
230447 except:
231448 traceback.print_exc()
232449
450 async def do_adduser(self, username, password):
451 """Creates a new domain user with password"""
452 try:
453 _, err = await self.connection.create_user(username, password)
454 if err is not None:
455 raise err
456 print('User added')
457 except:
458 traceback.print_exc()
459
460
461 async def do_deluser(self, user_dn):
462 """Deletes the user! This action is irrecoverable (actually domain admins can do that but probably will shout with you)"""
463 try:
464 _, err = await self.connection.delete_user(user_dn)
465 if err is not None:
466 raise err
467 print('Goodbye, Caroline.')
468 except:
469 traceback.print_exc()
470
471 async def do_changeuserpw(self, user_dn, newpass, oldpass = None):
472 """Changes user password, if you are admin then old pw doesnt need to be supplied"""
473 try:
474 _, err = await self.connection.change_password(user_dn, newpass, oldpass)
475 if err is not None:
476 raise err
477 print('User password changed')
478 except:
479 traceback.print_exc()
480
481 async def do_unlockuser(self, user_dn):
482 """Unlock user by setting lockoutTime to 0"""
483 try:
484 _, err = await self.connection.unlock_user(user_dn)
485 if err is not None:
486 raise err
487 print('User unlocked')
488 except:
489 traceback.print_exc()
490
491 async def do_enableuser(self, user_dn):
492 """Unlock user by flipping useraccountcontrol bits"""
493 try:
494 _, err = await self.connection.enable_user(user_dn)
495 if err is not None:
496 raise err
497 print('User enabled')
498 except:
499 traceback.print_exc()
500
501 async def do_disableuser(self, user_dn):
502 """Unlock user by flipping useraccountcontrol bits"""
503 try:
504 _, err = await self.connection.disable_user(user_dn)
505 if err is not None:
506 raise err
507 print('User disabled')
508 except:
509 traceback.print_exc()
510
511 async def do_addspn(self, user_dn, spn):
512 """Adds an SPN entry to the users account"""
513 try:
514 _, err = await self.connection.add_user_spn(user_dn, spn)
515 if err is not None:
516 raise err
517 print('SPN added!')
518 except:
519 traceback.print_exc()
520
521 async def do_addhostname(self, user_dn, hostname):
522 """Adds additional hostname to computer account"""
523 try:
524 _, err = await self.connection.add_additional_hostname(user_dn, hostname)
525 if err is not None:
526 raise err
527 print('Hostname added!')
528 except:
529 traceback.print_exc()
530
531 async def do_addusertogroup(self, user_dn, group_dn):
532 """Adds user to specified group. Both user and group must be in DN format!"""
533 try:
534 _, err = await self.connection.add_user_to_group(user_dn, group_dn)
535 if err is not None:
536 raise err
537 print('User added to group!')
538 except:
539 traceback.print_exc()
540
541 async def do_deluserfromgroup(self, user_dn, group_dn):
542 """Removes user from specified group. Both user and group must be in DN format!"""
543 try:
544 _, err = await self.connection.del_user_from_group(user_dn, group_dn)
545 if err is not None:
546 raise err
547 print('User added to group!')
548 except:
549 traceback.print_exc()
550
233551 async def do_test(self):
234552 """testing, dontuse"""
235553 try:
236 async for entry in self.connection.get_all_objectacl():
554 async for entry, err in self.connection.get_all_objectacl():
555 if err is not None:
556 raise err
557
237558 if entry.objectClass[-1] != 'user':
238559 print(entry.objectClass)
239560 except:
258579 await client.run()
259580 else:
260581 for command in args.commands:
582 if command == 'i':
583 await client.run()
584 return
261585 cmd = shlex.split(command)
262 await client._run_single_command(cmd[0], cmd[1:])
586 res = await client._run_single_command(cmd[0], cmd[1:])
587 if res is False:
588 return
263589
264590 def main():
265591 import argparse
267593 parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked')
268594 parser.add_argument('-n', '--no-interactive', action='store_true')
269595 parser.add_argument('url', help='Connection string in URL format.')
270 parser.add_argument('commands', nargs='*')
596 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.")
271597
272598 args = parser.parse_args()
273599
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 await com_func(*args)
98 else:
99 com_func(*args)
100 return
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 = [
1315 pass
1416
1517 @staticmethod
16 def select(target):
18 async def select(target):
1719 if target.proxy is not None:
1820 if target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES:
1921 return SocksProxyConnection(target)
2022 else:
21 raise Exception('Multiplexor coming soon!')
23 mpc = MultiplexorProxyConnection(target)
24 socks_proxy = await mpc.connect()
25 return socks_proxy
2226
2327 return MSLDAPTCPNetwork(target)
4040 Disconnects from the socket.
4141 Stops the reader and writer streams.
4242 """
43 self.proxy_task.cancel()
44 self.handle_in_task.cancel()
43 if self.client is not None:
44 await self.client.terminate()
45 if self.proxy_task is not None:
46 self.proxy_task.cancel()
47 if self.handle_in_q is not None:
48 self.handle_in_task.cancel()
49
50 async def terminate(self):
51 await self.disconnect()
52
53 def get_peer_certificate(self):
54 raise Exception('Not yet implemented! SSL implementation on socks is missing!')
55 return self.writer.get_extra_info('socket').getpeercert(True)
4556
4657 def get_one_message(self,data):
4758 if len(data) < 6:
8293 data += temp
8394 continue
8495
85 #except asyncio.CancelledError:
86 # return
96 except asyncio.CancelledError:
97 return
8798 except Exception as e:
8899 logger.exception('handle_in_q')
89100 await self.in_queue.put((None, e))
106117
107118 self.target.proxy.target.endpoint_ip = self.target.host
108119 self.target.proxy.target.endpoint_port = int(self.target.port)
120 self.target.proxy.target.endpoint_timeout = None #TODO: maybe implement endpoint timeout?
121 self.target.proxy.target.timeout = self.target.timeout
109122
110123 self.client = SOCKSClient(comms, self.target.proxy.target, self.target.proxy.auth)
111124 self.proxy_task = asyncio.create_task(self.client.run())
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
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]')
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.22
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
4445 msldap/authentication/spnego/sspi.py
4546 msldap/commons/__init__.py
4647 msldap/commons/authbuilder.py
48 msldap/commons/common.py
4749 msldap/commons/credential.py
4850 msldap/commons/proxy.py
4951 msldap/commons/target.py
6870 msldap/crypto/pure/RC4/__init__.py
6971 msldap/examples/__init__.py
7072 msldap/examples/msldapclient.py
73 msldap/examples/msldapcompdnslist.py
74 msldap/external/__init__.py
75 msldap/external/aiocmd/__init__.py
76 msldap/external/aiocmd/setup.py
77 msldap/external/aiocmd/aiocmd/__init__.py
78 msldap/external/aiocmd/aiocmd/aiocmd.py
79 msldap/external/aiocmd/aiocmd/nested_completer.py
80 msldap/external/asciitree/__init__.py
81 msldap/external/asciitree/setup.py
82 msldap/external/asciitree/asciitree/__init__.py
83 msldap/external/asciitree/asciitree/drawing.py
84 msldap/external/asciitree/asciitree/traversal.py
85 msldap/external/asciitree/asciitree/util.py
7186 msldap/ldap_objects/__init__.py
7287 msldap/ldap_objects/adcomp.py
7388 msldap/ldap_objects/adgpo.py
7489 msldap/ldap_objects/adgroup.py
7590 msldap/ldap_objects/adinfo.py
7691 msldap/ldap_objects/adou.py
92 msldap/ldap_objects/adschemaentry.py
7793 msldap/ldap_objects/adsec.py
7894 msldap/ldap_objects/adtrust.py
7995 msldap/ldap_objects/aduser.py
8096 msldap/ldap_objects/common.py
8197 msldap/network/__init__.py
98 msldap/network/multiplexor.py
8299 msldap/network/selector.py
83100 msldap/network/socks.py
84101 msldap/network/tcp.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.0.10
2 minikerberos>=0.2.4
3 prompt-toolkit>=3.0.2
4 tqdm
5 winacl>=0.1.0
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.4',
52 'asysocks>=0.0.10',
53 'winacl>=0.1.0',
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 )