Import upstream version 0.3.22
Kali Janitor
3 years ago
0 | 0 | Metadata-Version: 1.2 |
1 | 1 | Name: msldap |
2 | Version: 0.2.10 | |
2 | Version: 0.3.22 | |
3 | 3 | Summary: Python library to play with MS LDAP |
4 | 4 | Home-page: https://github.com/skelsec/msldap |
5 | 5 | Author: Tamas Jos |
6 | Author-email: [email protected] | |
6 | Author-email: [email protected] | |
7 | 7 | License: UNKNOWN |
8 | 8 | Description: Python library to play with MS LDAP |
9 | 9 | Platform: UNKNOWN |
10 | Classifier: Programming Language :: Python :: 3.6 | |
10 | Classifier: Programming Language :: Python :: 3.7 | |
11 | Classifier: Programming Language :: Python :: 3.8 | |
11 | 12 | Classifier: License :: OSI Approved :: MIT License |
12 | 13 | Classifier: Operating System :: OS Independent |
13 | Requires-Python: >=3.6 | |
14 | Requires-Python: >=3.7 |
0 | [![Documentation Status](https://readthedocs.org/projects/msldap/badge/?version=latest)](https://msldap.readthedocs.io/en/latest/?badge=latest) | |
1 | ||
2 | # msldap client | |
3 | ![Documentation Status](https://user-images.githubusercontent.com/19204702/81515211-3761e880-9333-11ea-837f-bcbe2a67ee48.gif ) | |
4 | ||
0 | 5 | # msldap |
1 | 6 | LDAP library for MS AD |
7 | ||
8 | # Documentation | |
9 | [Awesome documentation here!](https://msldap.readthedocs.io/en/latest/) | |
2 | 10 | |
3 | 11 | # Features |
4 | 12 | - Comes with a built-in console LDAP client |
5 | 13 | - All parameters can be conrolled via a conveinent URL (see below) |
6 | - Supports integrated windows authentication | |
14 | - Supports integrated windows authentication (SSPI) both with NTLM and with KERBEROS | |
15 | - Supports channel binding (for ntlm and kerberos not SSPI) | |
16 | - Supports encryption (for NTLM/KERBEROS/SSPI) | |
17 | - Supports LDAPS (TODO: actually verify certificate) | |
7 | 18 | - Supports SOCKS5 proxy withot the need of extra proxifyer |
8 | 19 | - Minimal footprint |
9 | 20 | - A lot of pre-built queries for convenient information polling |
10 | 21 | - Easy to integrate to your project |
11 | - Completely missing documentation | |
12 | 22 | - No testing suite |
13 | 23 | |
14 | 24 | # Installation |
18 | 28 | `pip install msldap` |
19 | 29 | |
20 | 30 | # Prerequisites |
21 | - `ldap3` module. It's pure python so you dont have to compile anything. | |
22 | 31 | - `winsspi` module. For windows only. This supports SSPI based authentication. |
23 | 32 | - `asn1crypto` module. Some LDAP queries incorporate ASN1 strucutres to be sent on top of the ASN1 transport XD |
24 | - `socks5line` module. To support socks5 proxying. | |
33 | - `asysocks` module. To support socks proxying. | |
25 | 34 | - `aiocmd` For the interactive client |
26 | 35 | - `asciitree` For plotting nice trees in the interactive client |
27 | 36 | |
34 | 43 | The new connection string is composed in the following manner: |
35 | 44 | `<protocol>+<auth_method>://<domain>\<username>:<password>@<ip>:<port>/?<param>=<value>&<param>=<value>&...` |
36 | 45 | Detailed explanation with examples: |
37 | ``` | |
38 | MSLDAP URL Format: <protocol>+<auth>://<username>:<password>@<ip_or_host>:<port>/<tree>/?<param>=<value> | |
46 | ``` | |
47 | <protocol>+<auth>://<username>:<password>@<ip_or_host>:<port>/<tree>/?<param>=<value> | |
48 | ||
49 | ||
39 | 50 | <protocol> sets the ldap protocol following values supported: |
40 | 51 | - ldap |
41 | - ldaps (ldap over SSL) << known to be problematic because of the underlying library (ldap3) | |
42 | <auth> can be omitted if plaintext authentication is to be performed, otherwise: | |
43 | - ntlm | |
44 | - sspi (windows only!) | |
52 | - ldaps | |
53 | ||
54 | <auth> can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: | |
55 | - ntlm-password | |
56 | - ntlm-nt | |
57 | - kerberos-password (dc option param must be used) | |
58 | - kerberos-rc4 / kerberos-nt (dc option param must be used) | |
59 | - kerberos-aes (dc option param must be used) | |
60 | - kerberos-keytab (dc option param must be used) | |
61 | - kerberos-ccache (dc option param must be used) | |
62 | - sspi-ntlm (windows only!) | |
63 | - sspi-kerberos (windows only!) | |
45 | 64 | - anonymous |
46 | 65 | - plain |
66 | - simple | |
67 | - sicily (same format as ntlm-nt but using the SICILY authentication) | |
68 | ||
69 | <tree>: | |
70 | OPTIONAL. Specifies the root tree of all queries | |
71 | ||
47 | 72 | <param> can be: |
48 | 73 | - timeout : connction timeout in seconds |
49 | 74 | - proxytype: currently only socks5 proxy is supported |
50 | 75 | - proxyhost: Ip or hostname of the proxy server |
51 | 76 | - proxyport: port of the proxy server |
52 | 77 | - proxytimeout: timeout ins ecodns for the proxy connection |
78 | - dc: the IP address of the domain controller, MUST be used for kerberos authentication | |
53 | 79 | |
54 | 80 | Examples: |
55 | ldap://10.10.10.2 | |
56 | ldaps://test.corp | |
57 | ldap+sspi:///test.corp | |
58 | ldap+ntlm://TEST\\victim:[email protected] | |
81 | ldap://10.10.10.2 (anonymous bind) | |
82 | ldaps://test.corp (anonymous bind) | |
83 | ldap+sspi-ntlm://test.corp | |
84 | ldap+sspi-kerberos://test.corp | |
85 | ldap://TEST\\victim:<password>@10.10.10.2 (defaults to SASL GSSAPI NTLM) | |
86 | ldap+simple://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) | |
87 | ldap+plain://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) | |
88 | ldap+ntlm-password://TEST\\victim:<password>@10.10.10.2 | |
89 | ldap+ntlm-nt://TEST\\victim:<nthash>@10.10.10.2 | |
90 | ldap+kerberos-password://TEST\\victim:<password>@10.10.10.2 | |
91 | ldap+kerberos-rc4://TEST\\victim:<rc4key>@10.10.10.2 | |
92 | ldap+kerberos-aes://TEST\\victim:<aes>@10.10.10.2 | |
59 | 93 | ldap://TEST\\victim:[email protected]/DC=test,DC=corp/ |
60 | 94 | ldap://TEST\\victim:[email protected]/DC=test,DC=corp/?timeout=99&proxytype=socks5&proxyhost=127.0.0.1&proxyport=1080&proxytimeout=44 |
61 | 95 | ``` |
62 | 96 | |
63 | 97 | # Kudos |
64 | This project is built on top of the [ldap3](https://github.com/cannatag/ldap3) project. | |
98 |
65 | 65 | |
66 | 66 | elif args.command == 'spn': |
67 | 67 | connection.connect() |
68 | adinfo = connection.get_ad_info() | |
68 | adinfo, err = connection.get_ad_info() | |
69 | 69 | with open(args.outfile, 'w', newline='', encoding = 'utf8') as f: |
70 | 70 | for user in connection.get_all_service_user_objects(): |
71 | 71 | f.write(user.sAMAccountName + '\r\n') |
0 | import enum | |
1 | import io | |
2 | import os | |
3 | ||
4 | from asn1crypto.core import ObjectIdentifier | |
5 | ||
6 | from minikerberos.protocol.constants import EncryptionType | |
7 | from minikerberos.protocol import encryption | |
8 | from minikerberos.crypto.hashing import md5, hmac_md5 | |
9 | from minikerberos.crypto.RC4 import RC4 | |
10 | ||
11 | #TODO: RC4 support! | |
12 | ||
13 | # https://tools.ietf.org/html/draft-raeburn-krb-rijndael-krb-05 | |
14 | # https://tools.ietf.org/html/rfc2478 | |
15 | # https://tools.ietf.org/html/draft-ietf-krb-wg-gssapi-cfx-02 | |
16 | # https://tools.ietf.org/html/rfc4757 | |
17 | # https://www.rfc-editor.org/errata/rfc4757 | |
18 | ||
19 | GSS_WRAP_HEADER = b'\x60\x2b\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' | |
20 | GSS_WRAP_HEADER_OID = b'\x60\x2b\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' | |
21 | ||
22 | class KRB5_MECH_INDEP_TOKEN: | |
23 | # https://tools.ietf.org/html/rfc2743#page-81 | |
24 | # Mechanism-Independent Token Format | |
25 | ||
26 | def __init__(self, data, oid, remlen = None): | |
27 | self.oid = oid | |
28 | self.data = data | |
29 | ||
30 | #dont set this | |
31 | self.length = remlen | |
32 | ||
33 | @staticmethod | |
34 | def from_bytes(data): | |
35 | return KRB5_MECH_INDEP_TOKEN.from_buffer(io.BytesIO(data)) | |
36 | ||
37 | @staticmethod | |
38 | def from_buffer(buff): | |
39 | ||
40 | start = buff.read(1) | |
41 | if start != b'\x60': | |
42 | raise Exception('Incorrect token data!') | |
43 | remaining_length = KRB5_MECH_INDEP_TOKEN.decode_length_buffer(buff) | |
44 | token_data = buff.read(remaining_length) | |
45 | ||
46 | buff = io.BytesIO(token_data) | |
47 | pos = buff.tell() | |
48 | buff.read(1) | |
49 | oid_length = KRB5_MECH_INDEP_TOKEN.decode_length_buffer(buff) | |
50 | buff.seek(pos) | |
51 | token_oid = ObjectIdentifier.load(buff.read(oid_length+2)) | |
52 | ||
53 | return KRB5_MECH_INDEP_TOKEN(buff.read(), str(token_oid), remlen = remaining_length) | |
54 | ||
55 | @staticmethod | |
56 | def decode_length_buffer(buff): | |
57 | lf = buff.read(1)[0] | |
58 | if lf <= 127: | |
59 | length = lf | |
60 | else: | |
61 | bcount = lf - 128 | |
62 | length = int.from_bytes(buff.read(bcount), byteorder = 'big', signed = False) | |
63 | return length | |
64 | ||
65 | @staticmethod | |
66 | def encode_length(length): | |
67 | if length <= 127: | |
68 | return length.to_bytes(1, byteorder = 'big', signed = False) | |
69 | else: | |
70 | lb = length.to_bytes((length.bit_length() + 7) // 8, 'big') | |
71 | return (128+len(lb)).to_bytes(1, byteorder = 'big', signed = False) + lb | |
72 | ||
73 | ||
74 | def to_bytes(self): | |
75 | t = ObjectIdentifier(self.oid).dump() + self.data | |
76 | t = b'\x60' + KRB5_MECH_INDEP_TOKEN.encode_length(len(t)) + t | |
77 | return t[:-len(self.data)] , self.data | |
78 | ||
79 | ||
80 | class GSSAPIFlags(enum.IntFlag): | |
81 | GSS_C_DCE_STYLE = 0x1000 | |
82 | GSS_C_DELEG_FLAG = 1 | |
83 | GSS_C_MUTUAL_FLAG = 2 | |
84 | GSS_C_REPLAY_FLAG = 4 | |
85 | GSS_C_SEQUENCE_FLAG = 8 | |
86 | GSS_C_CONF_FLAG = 0x10 | |
87 | GSS_C_INTEG_FLAG = 0x20 | |
88 | ||
89 | class KG_USAGE(enum.Enum): | |
90 | ACCEPTOR_SEAL = 22 | |
91 | ACCEPTOR_SIGN = 23 | |
92 | INITIATOR_SEAL = 24 | |
93 | INITIATOR_SIGN = 25 | |
94 | ||
95 | class FlagsField(enum.IntFlag): | |
96 | SentByAcceptor = 0 | |
97 | Sealed = 2 | |
98 | AcceptorSubkey = 4 | |
99 | ||
100 | # https://tools.ietf.org/html/rfc4757 (7.2) | |
101 | class GSSMIC_RC4: | |
102 | def __init__(self): | |
103 | self.TOK_ID = b'\x01\x01' | |
104 | self.SGN_ALG = b'\x11\x00' #HMAC | |
105 | self.Filler = b'\xff'*4 | |
106 | self.SND_SEQ = None | |
107 | self.SGN_CKSUM = None | |
108 | ||
109 | @staticmethod | |
110 | def from_bytes(data): | |
111 | return GSSMIC_RC4.from_buffer(io.BytesIO(data)) | |
112 | ||
113 | @staticmethod | |
114 | def from_buffer(buff): | |
115 | mic = GSSMIC_RC4() | |
116 | mic.TOK_ID = buff.read(2) | |
117 | mic.SGN_ALG = buff.read(2) | |
118 | mic.Filler = buff.read(4) | |
119 | mic.SND_SEQ = buff.read(8) | |
120 | mic.SGN_CKSUM = buff.read(8) | |
121 | ||
122 | return mic | |
123 | ||
124 | def to_bytes(self): | |
125 | t = self.TOK_ID | |
126 | t += self.SGN_ALG | |
127 | t += self.Filler | |
128 | t += self.SND_SEQ | |
129 | if self.SGN_CKSUM is not None: | |
130 | t += self.SGN_CKSUM | |
131 | ||
132 | return t | |
133 | ||
134 | class GSSWRAP_RC4: | |
135 | def __init__(self): | |
136 | self.TOK_ID = b'\x02\x01' | |
137 | self.SGN_ALG = b'\x11\x00' #HMAC | |
138 | self.SEAL_ALG = None | |
139 | self.Filler = b'\xFF' * 2 | |
140 | self.SND_SEQ = None | |
141 | self.SGN_CKSUM = None | |
142 | self.Confounder = None | |
143 | ||
144 | def __str__(self): | |
145 | t = 'GSSWRAP_RC4\r\n' | |
146 | t += 'TOK_ID : %s\r\n' % self.TOK_ID.hex() | |
147 | t += 'SGN_ALG : %s\r\n' % self.SGN_ALG.hex() | |
148 | t += 'SEAL_ALG : %s\r\n' % self.SEAL_ALG.hex() | |
149 | t += 'Filler : %s\r\n' % self.Filler.hex() | |
150 | t += 'SND_SEQ : %s\r\n' % self.SND_SEQ.hex() | |
151 | t += 'SGN_CKSUM : %s\r\n' % self.SGN_CKSUM.hex() | |
152 | t += 'Confounder : %s\r\n' % self.Confounder.hex() | |
153 | return t | |
154 | ||
155 | @staticmethod | |
156 | def from_bytes(data): | |
157 | return GSSWRAP_RC4.from_buffer(io.BytesIO(data)) | |
158 | ||
159 | @staticmethod | |
160 | def from_buffer(buff): | |
161 | wrap = GSSWRAP_RC4() | |
162 | wrap.TOK_ID = buff.read(2) | |
163 | wrap.SGN_ALG = buff.read(2) | |
164 | wrap.SEAL_ALG = buff.read(2) | |
165 | wrap.Filler = buff.read(2) | |
166 | wrap.SND_SEQ = buff.read(8) | |
167 | wrap.SGN_CKSUM = buff.read(8) | |
168 | wrap.Confounder = buff.read(8) | |
169 | ||
170 | return wrap | |
171 | ||
172 | def to_bytes(self): | |
173 | t = self.TOK_ID | |
174 | t += self.SGN_ALG | |
175 | t += self.SEAL_ALG | |
176 | t += self.Filler | |
177 | t += self.SND_SEQ | |
178 | ||
179 | if self.SGN_CKSUM: | |
180 | t += self.SGN_CKSUM | |
181 | if self.Confounder: | |
182 | t += self.Confounder | |
183 | ||
184 | ||
185 | return t | |
186 | ||
187 | class GSSAPI_RC4: | |
188 | def __init__(self, session_key): | |
189 | self.session_key = session_key | |
190 | ||
191 | def GSS_GetMIC(self, data, sequenceNumber, direction = 'init'): | |
192 | raise Exception('Not tested! Sure it needs some changes') | |
193 | GSS_GETMIC_HEADER = b'\x60\x23\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' | |
194 | ||
195 | # Let's pad the data | |
196 | pad = (4 - (len(data) % 4)) & 0x3 | |
197 | padStr = bytes([pad]) * pad | |
198 | data += padStr | |
199 | ||
200 | mic = GSSMIC_RC4() | |
201 | ||
202 | if direction == 'init': | |
203 | mic.SND_SEQ = sequenceNumber.to_bytes(4, 'big', signed = False) + b'\x00'*4 | |
204 | else: | |
205 | mic.SND_SEQ = sequenceNumber.to_bytes(4, 'big', signed = False) + b'\xff'*4 | |
206 | ||
207 | Ksign_ctx = hmac_md5(self.session_key.contents) | |
208 | Ksign_ctx.update(b'signaturekey\0') | |
209 | Ksign = Ksign_ctx.digest() | |
210 | ||
211 | id = 15 | |
212 | temp = md5( id.to_bytes(4, 'little', signed = False) + mic.to_bytes()[:8] ).digest() | |
213 | chksum_ctx = hmac_md5(Ksign) | |
214 | chksum_ctx.update(temp) | |
215 | mic.SGN_CKSUM = chksum_ctx.digest()[:8] | |
216 | ||
217 | id = 0 | |
218 | temp = hmac_md5(self.session_key.contents) | |
219 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
220 | ||
221 | Kseq_ctx = hmac_md5(temp.digest()) | |
222 | Kseq_ctx.update(mic.SGN_CKSUM) | |
223 | Kseq = Kseq_ctx.digest() | |
224 | ||
225 | mic.SGN_CKSUM = RC4(Kseq).encrypt(mic.SND_SEQ) | |
226 | ||
227 | return GSS_GETMIC_HEADER + mic.to_bytes() | |
228 | ||
229 | ||
230 | def GSS_Wrap(self, data, seq_num, direction = 'init', encrypt=True, cofounder = None): | |
231 | #direction = 'a' | |
232 | #seq_num = 0 | |
233 | #print('[GSS_Wrap] data: %s' % data) | |
234 | #print('[GSS_Wrap] seq_num: %s' % seq_num.to_bytes(4, 'big', signed = False).hex()) | |
235 | #print('[GSS_Wrap] direction: %s' % direction) | |
236 | #print('[GSS_Wrap] encrypt: %s' % encrypt) | |
237 | # | |
238 | #print('[GSS_Wrap] auth_data: %s' % auth_data) | |
239 | ||
240 | #pad = 0 | |
241 | if encrypt is True: | |
242 | data += b'\x01' | |
243 | #pad = (8 - (len(data) % 8)) & 0x7 | |
244 | #padStr = bytes([pad]) * pad | |
245 | #data += padStr | |
246 | # | |
247 | ##data += b'\x08' * 8 | |
248 | #print('[GSS_Wrap] pad: %s' % pad) | |
249 | #print('[GSS_Wrap] data padded: %s' % data) | |
250 | ||
251 | ||
252 | token = GSSWRAP_RC4() | |
253 | token.SEAL_ALG = b'\x10\x00' # RC4 | |
254 | ||
255 | if direction == 'init': | |
256 | token.SND_SEQ = seq_num.to_bytes(4, 'big', signed = False) + b'\x00'*4 | |
257 | else: | |
258 | token.SND_SEQ = seq_num.to_bytes(4, 'big', signed = False) + b'\xff'*4 | |
259 | ||
260 | token.Confounder = os.urandom(8) | |
261 | #if cofounder is not None: | |
262 | # token.Confounder = cofounder | |
263 | # #testing purposes only, pls remove | |
264 | ||
265 | ||
266 | temp = hmac_md5(self.session_key.contents) | |
267 | temp.update(b'signaturekey\0') | |
268 | Ksign = temp.digest() | |
269 | ||
270 | id = 13 | |
271 | Sgn_Cksum = md5(id.to_bytes(4, 'little', signed = False) + token.to_bytes()[:8] + token.Confounder + data).digest() | |
272 | ||
273 | klocal = b'' | |
274 | for b in self.session_key.contents: | |
275 | klocal += bytes([b ^ 0xf0]) | |
276 | ||
277 | id = 0 | |
278 | temp = hmac_md5(klocal) | |
279 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
280 | temp = hmac_md5(temp.digest()) | |
281 | temp.update(seq_num.to_bytes(4, 'big', signed = False)) | |
282 | Kcrypt = temp.digest() | |
283 | ||
284 | temp = hmac_md5(Ksign) | |
285 | temp.update(Sgn_Cksum) | |
286 | token.SGN_CKSUM = temp.digest()[:8] | |
287 | ||
288 | id = 0 | |
289 | temp = hmac_md5(self.session_key.contents) | |
290 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
291 | temp = hmac_md5(temp.digest()) | |
292 | temp.update(token.SGN_CKSUM) | |
293 | Kseq = temp.digest() | |
294 | ||
295 | token.SND_SEQ = RC4(Kseq).encrypt(token.SND_SEQ) | |
296 | ||
297 | ||
298 | #if auth_data is not None: | |
299 | if encrypt is False: | |
300 | #print('Unwrap sessionkey: %s' % self.session_key.contents.hex()) | |
301 | #print('Unwrap data : %s' % data.hex()) | |
302 | ||
303 | sspi_wrap = KRB5_MECH_INDEP_TOKEN.from_bytes(data) | |
304 | ||
305 | hdr = sspi_wrap.data[:32] | |
306 | data = sspi_wrap.data[32:] | |
307 | ||
308 | wrap = GSSWRAP_RC4.from_bytes(hdr) | |
309 | ||
310 | id = 0 | |
311 | temp = hmac_md5(self.session_key.contents) | |
312 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
313 | temp = hmac_md5(temp.digest()) | |
314 | temp.update(wrap.SGN_CKSUM) | |
315 | Kseq = temp.digest() | |
316 | ||
317 | snd_seq = RC4(Kseq).encrypt(wrap.SND_SEQ) | |
318 | ||
319 | id = 0 | |
320 | temp = hmac_md5(klocal) | |
321 | temp.update(id.to_bytes(4, 'little', signed = False)) | |
322 | temp = hmac_md5(temp.digest()) | |
323 | temp.update(snd_seq[:4]) | |
324 | Kcrypt = temp.digest() | |
325 | ||
326 | rc4 = RC4(Kcrypt) | |
327 | dec_cofounder = rc4.decrypt(wrap.Confounder) | |
328 | dec_data = rc4.decrypt(data) | |
329 | ||
330 | id = 13 | |
331 | Sgn_Cksum_calc = md5(id.to_bytes(4, 'little', signed = False) + wrap.to_bytes()[:8] + dec_cofounder + dec_data).digest() | |
332 | ||
333 | temp = hmac_md5(Ksign) | |
334 | temp.update(Sgn_Cksum_calc) | |
335 | Sgn_Cksum_calc = temp.digest()[:8] | |
336 | ||
337 | if wrap.SGN_CKSUM != Sgn_Cksum_calc[:8]: | |
338 | return None, Exception('Integrity verification failed') | |
339 | ||
340 | pad = 1 | |
341 | return dec_data[:-pad], None | |
342 | ||
343 | elif encrypt is True: | |
344 | rc4 = RC4(Kcrypt) | |
345 | token.Confounder = rc4.encrypt(token.Confounder) | |
346 | cipherText = rc4.encrypt(data) | |
347 | finalData, cipherText = KRB5_MECH_INDEP_TOKEN( token.to_bytes() + cipherText, '1.2.840.113554.1.2.2' ).to_bytes() | |
348 | ||
349 | ||
350 | #print('cipherText %s' % cipherText.hex()) | |
351 | #print('finalData %s' % finalData.hex()) | |
352 | #print('sessionkey %s' % self.session_key.contents.hex()) | |
353 | return cipherText, finalData | |
354 | ||
355 | ||
356 | def GSS_Unwrap(self, data, seq_num, direction='init'): | |
357 | #print('GSS_Unwrap data : %s' % data) | |
358 | dec_data, err = self.GSS_Wrap(data, seq_num, direction=direction, encrypt = False) | |
359 | #print('GSS_Unwrap decrypted data : %s' % dec_data) | |
360 | return dec_data, err | |
361 | ||
362 | # 4.2.6.1. MIC Tokens | |
363 | class GSSMIC: | |
364 | def __init__(self): | |
365 | self.TOK_ID = b'\x04\x04' | |
366 | self.Flags = None | |
367 | self.Filler = b'\xFF' * 5 | |
368 | self.SND_SEQ = None | |
369 | self.SGN_CKSUM = None | |
370 | ||
371 | @staticmethod | |
372 | def from_bytes(data): | |
373 | return GSSMIC.from_buffer(io.BytesIO(data)) | |
374 | ||
375 | @staticmethod | |
376 | def from_buffer(buff): | |
377 | m = GSSMIC() | |
378 | m.TOK_ID = buff.read(2) | |
379 | m.Flags = FlagsField(int.from_bytes(buff.read(1), 'big', signed = False)) | |
380 | m.Filler = buff.read(5) | |
381 | m.SND_SEQ = int.from_bytes(buff.read(8), 'big', signed = False) | |
382 | m.SGN_CKSUM = buff.read() #should know the size based on the algo! | |
383 | return m | |
384 | ||
385 | def to_bytes(self): | |
386 | t = self.TOK_ID | |
387 | t += self.Flags.to_bytes(1, 'big', signed = False) | |
388 | t += self.Filler | |
389 | t += self.SND_SEQ.to_bytes(8, 'big', signed = False) | |
390 | if self.SGN_CKSUM is not None: | |
391 | t += self.SGN_CKSUM | |
392 | ||
393 | return t | |
394 | ||
395 | # 4.2.6.2. Wrap Tokens | |
396 | class GSSWrapToken: | |
397 | def __init__(self): | |
398 | self.TOK_ID = b'\x05\x04' | |
399 | self.Flags = None | |
400 | self.Filler = b'\xFF' | |
401 | self.EC = None | |
402 | self.RRC = None | |
403 | self.SND_SEQ = None | |
404 | self.Data = None | |
405 | ||
406 | @staticmethod | |
407 | def from_bytes(data): | |
408 | return GSSWrapToken.from_buffer(io.BytesIO(data)) | |
409 | ||
410 | @staticmethod | |
411 | def from_buffer(buff): | |
412 | m = GSSWrapToken() | |
413 | m.TOK_ID = buff.read(2) | |
414 | m.Flags = FlagsField(int.from_bytes(buff.read(1), 'big', signed = False)) | |
415 | m.Filler = buff.read(1) | |
416 | m.EC = int.from_bytes(buff.read(2), 'big', signed = False) | |
417 | m.RRC = int.from_bytes(buff.read(2), 'big', signed = False) | |
418 | m.SND_SEQ = int.from_bytes(buff.read(8), 'big', signed = False) | |
419 | return m | |
420 | ||
421 | def to_bytes(self): | |
422 | t = self.TOK_ID | |
423 | t += self.Flags.to_bytes(1, 'big', signed = False) | |
424 | t += self.Filler | |
425 | t += self.EC.to_bytes(2, 'big', signed = False) | |
426 | t += self.RRC.to_bytes(2, 'big', signed = False) | |
427 | t += self.SND_SEQ.to_bytes(8, 'big', signed = False) | |
428 | if self.Data is not None: | |
429 | t += self.Data | |
430 | ||
431 | return t | |
432 | ||
433 | class GSSAPI_AES: | |
434 | def __init__(self, session_key, cipher_type, checksum_profile): | |
435 | self.session_key = session_key | |
436 | self.checksum_profile = checksum_profile | |
437 | self.cipher_type = cipher_type | |
438 | self.cipher = None | |
439 | ||
440 | def rotate(self, data, numBytes): | |
441 | numBytes %= len(data) | |
442 | left = len(data) - numBytes | |
443 | result = data[left:] + data[:left] | |
444 | return result | |
445 | ||
446 | def unrotate(self, data, numBytes): | |
447 | numBytes %= len(data) | |
448 | result = data[numBytes:] + data[:numBytes] | |
449 | return result | |
450 | ||
451 | def GSS_GetMIC(self, data, seq_num): | |
452 | pad = (4 - (len(data) % 4)) & 0x3 | |
453 | padStr = bytes([pad]) * pad | |
454 | data += padStr | |
455 | ||
456 | m = GSSMIC() | |
457 | m.Flags = FlagsField.AcceptorSubkey | |
458 | m.SND_SEQ = seq_num | |
459 | checksum_profile = self.checksum_profile() | |
460 | m.checksum = checksum_profile.checksum(self.session_key, KG_USAGE.INITIATOR_SIGN.value, data + m.to_bytes()[:16]) | |
461 | ||
462 | return m.to_bytes() | |
463 | ||
464 | def GSS_Wrap(self, data, seq_num, use_padding = False): | |
465 | #print('[GSS_Wrap] seq_num: %s' % seq_num.to_bytes(4, 'big', signed = False).hex()) | |
466 | cipher = self.cipher_type() | |
467 | pad = 0 | |
468 | if use_padding is True: | |
469 | pad = ((cipher.blocksize - len(data)) % cipher.blocksize) #(cipher.blocksize - (len(data) % cipher.blocksize)) & 15 | |
470 | padStr = b'\xFF' * pad | |
471 | data += padStr | |
472 | ||
473 | t = GSSWrapToken() | |
474 | t.Flags = FlagsField.AcceptorSubkey | FlagsField.Sealed | |
475 | t.EC = pad | |
476 | t.RRC = 0 | |
477 | t.SND_SEQ = seq_num | |
478 | ||
479 | #print('Wrap data: %s' % (data + t.to_bytes())) | |
480 | cipher_text = cipher.encrypt(self.session_key, KG_USAGE.INITIATOR_SEAL.value, data + t.to_bytes(), None) | |
481 | t.RRC = 28 #[RFC4121] section 4.2.5 | |
482 | cipher_text = self.rotate(cipher_text, t.RRC + t.EC) | |
483 | ||
484 | ret1 = cipher_text | |
485 | ret2 = t.to_bytes() | |
486 | ||
487 | return ret1, ret2 | |
488 | ||
489 | def GSS_Unwrap(self, data, seq_num, direction='init', auth_data = None, use_padding = False): | |
490 | #print('') | |
491 | #print('Unwrap data %s' % data[16:]) | |
492 | #print('Unwrap hdr %s' % data[:16]) | |
493 | ||
494 | cipher = self.cipher_type() | |
495 | original_hdr = GSSWrapToken.from_bytes(data[:16]) | |
496 | rotated = data[16:] | |
497 | ||
498 | cipher_text = self.unrotate(rotated, original_hdr.RRC + original_hdr.EC) | |
499 | plain_text = cipher.decrypt(self.session_key, KG_USAGE.ACCEPTOR_SEAL.value, cipher_text) | |
500 | new_hdr = GSSWrapToken.from_bytes(plain_text[-16:]) | |
501 | ||
502 | #signature checking | |
503 | new_hdr.RRC = 28 | |
504 | if data[:16] != new_hdr.to_bytes(): | |
505 | return None, Exception('GSS_Unwrap signature mismatch!') | |
506 | ||
507 | ||
508 | #print('Unwrap checksum: %s' % plain_text[-(original_hdr.EC + 16):]) | |
509 | #print('Unwrap orig chk: %s' % original_hdr.to_bytes()) | |
510 | #print('Unwrap result 1: %s' % plain_text) | |
511 | #print('Unwrap result : %s' % plain_text[:-(original_hdr.EC + 16)]) | |
512 | return plain_text[:-(original_hdr.EC + 16)], None | |
513 | ||
514 | def get_gssapi(session_key): | |
515 | if session_key.enctype == encryption.Enctype.AES256: | |
516 | return GSSAPI_AES(session_key, encryption._AES256CTS, encryption._SHA1AES256) | |
517 | if session_key.enctype == encryption.Enctype.AES128: | |
518 | return GSSAPI_AES(session_key, encryption._AES128CTS, encryption._SHA1AES128) | |
519 | elif session_key.enctype == encryption.Enctype.RC4: | |
520 | return GSSAPI_RC4(session_key) | |
521 | else: | |
522 | raise Exception('Unsupported etype %s' % session_key.enctype) | |
523 | ||
524 | ||
525 | def test(): | |
526 | data = b'\xAF' * 1024 | |
527 | session_key = encryption.Key( encryption.Enctype.AES256 , bytes.fromhex('3e242e91996aadd513ecb1bc2369e44183e08e08c51550fa4b681e77f75ed8e1')) | |
528 | sequenceNumber = 0 | |
529 | gssapi = get_gssapi(session_key) | |
530 | ||
531 | r1, r2 = gssapi.GSS_Wrap(data, sequenceNumber) | |
532 | print(len(r2)) | |
533 | sent = r2 + r1 | |
534 | print(r1) | |
535 | ret1, ret2 = gssapi.GSS_Unwrap(sent, sequenceNumber) | |
536 | ||
537 | print(r1.hex()) | |
538 | print(ret1.hex()) | |
539 | ||
540 | ||
541 | if __name__ == '__main__': | |
542 | test()⏎ |
9 | 9 | ## TODO: RPC auth type is not implemented or tested!!!! |
10 | 10 | |
11 | 11 | from msldap.authentication.spnego.asn1_structs import KRB5Token |
12 | from minikerberos.gssapi.gssapi import get_gssapi | |
12 | from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken, KRB5_MECH_INDEP_TOKEN | |
13 | 13 | from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP |
14 | 14 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table |
15 | 15 | |
16 | 16 | from multiplexor.operator.external.sspi import KerberosSSPIClient |
17 | 17 | from multiplexor.operator import MultiplexorOperator |
18 | import enum | |
18 | 19 | |
19 | # SMBKerberosSSPICredential: | |
20 | # mutual auth not supported | |
21 | # encryption is always on | |
22 | # we dont get the output flags back (lack of time to do the multiplexor protocol... TODO | |
20 | 23 | |
21 | class SMBKerberosMultiplexor: | |
24 | class ISC_REQ(enum.IntFlag): | |
25 | DELEGATE = 1 | |
26 | MUTUAL_AUTH = 2 | |
27 | REPLAY_DETECT = 4 | |
28 | SEQUENCE_DETECT = 8 | |
29 | CONFIDENTIALITY = 16 | |
30 | USE_SESSION_KEY = 32 | |
31 | PROMPT_FOR_CREDS = 64 | |
32 | USE_SUPPLIED_CREDS = 128 | |
33 | ALLOCATE_MEMORY = 256 | |
34 | USE_DCE_STYLE = 512 | |
35 | DATAGRAM = 1024 | |
36 | CONNECTION = 2048 | |
37 | CALL_LEVEL = 4096 | |
38 | FRAGMENT_SUPPLIED = 8192 | |
39 | EXTENDED_ERROR = 16384 | |
40 | STREAM = 32768 | |
41 | INTEGRITY = 65536 | |
42 | IDENTIFY = 131072 | |
43 | NULL_SESSION = 262144 | |
44 | MANUAL_CRED_VALIDATION = 524288 | |
45 | RESERVED1 = 1048576 | |
46 | FRAGMENT_TO_FIT = 2097152 | |
47 | HTTP = 0x10000000 | |
48 | ||
49 | class MSLDAPKerberosMultiplexor: | |
22 | 50 | def __init__(self, settings): |
23 | 51 | self.iterations = 0 |
24 | 52 | self.settings = settings |
29 | 57 | self.gssapi = None |
30 | 58 | self.etype = None |
31 | 59 | self.session_key = None |
60 | self.seq_number = 0 | |
61 | self.flags = ISC_REQ.CONNECTION | |
32 | 62 | |
33 | 63 | self.setup() |
34 | 64 | |
35 | 65 | def setup(self): |
36 | return | |
66 | if self.settings.encrypt is True: | |
67 | self.flags = \ | |
68 | ISC_REQ.CONFIDENTIALITY |\ | |
69 | ISC_REQ.INTEGRITY |\ | |
70 | ISC_REQ.REPLAY_DETECT |\ | |
71 | ISC_REQ.SEQUENCE_DETECT | |
72 | ||
73 | def get_seq_number(self): | |
74 | """ | |
75 | Fetches the starting sequence number. This is either zero or can be found in the authenticator field of the | |
76 | AP_REQ structure. As windows uses a random seq number AND a subkey as well, we can't obtain it by decrypting the | |
77 | AP_REQ structure. Insead under the hood we perform an encryption operation via EncryptMessage API which will | |
78 | yield the start sequence number | |
79 | """ | |
80 | return self.seq_number | |
37 | 81 | |
38 | 82 | async def encrypt(self, data, message_no): |
39 | 83 | return self.gssapi.GSS_Wrap(data, message_no) |
41 | 85 | async def decrypt(self, data, message_no, direction='init', auth_data=None): |
42 | 86 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) |
43 | 87 | |
88 | def signing_needed(self): | |
89 | """ | |
90 | Checks if integrity protection was negotiated | |
91 | """ | |
92 | return ISC_REQ.INTEGRITY in self.flags | |
93 | ||
94 | def encryption_needed(self): | |
95 | """ | |
96 | Checks if confidentiality flag was negotiated | |
97 | """ | |
98 | return ISC_REQ.CONFIDENTIALITY in self.flags | |
99 | ||
44 | 100 | def get_session_key(self): |
45 | 101 | return self.session_key |
46 | 102 | |
47 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
103 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): | |
48 | 104 | #authdata is only for api compatibility reasons |
105 | is_rpc = False | |
49 | 106 | if self.ksspi is None: |
50 | 107 | await self.start_remote_kerberos() |
51 | 108 | 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) | |
93 | 125 | |
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 | |
101 | 133 | except Exception as e: |
102 | import traceback | |
103 | traceback.print_exc() | |
104 | return None | |
134 | return None, None, e | |
105 | 135 | |
106 | 136 | async def start_remote_kerberos(self): |
107 | 137 | try: |
2 | 2 | # This is just a simple interface to the minikerberos library to support SPNEGO |
3 | 3 | # |
4 | 4 | # |
5 | # - Hardships - | |
6 | # 1. DCERPC kerberos authentication requires a complete different approach and flags, | |
7 | # also requires mutual authentication | |
8 | # | |
9 | 5 | # - Links - |
10 | # 1. Most of the idea was taken from impacket | |
11 | # 2. See minikerberos library | |
6 | # 1. See minikerberos library | |
12 | 7 | |
13 | 8 | import datetime |
14 | 9 | |
10 | import os | |
15 | 11 | from minikerberos.common import * |
16 | 12 | |
17 | from minikerberos.protocol.asn1_structs import AP_REP, EncAPRepPart, EncryptedData | |
18 | from minikerberos.gssapi.gssapi import get_gssapi | |
13 | ||
14 | from minikerberos.protocol.asn1_structs import AP_REP, EncAPRepPart, EncryptedData, AP_REQ | |
15 | from msldap.authentication.kerberos.gssapi import get_gssapi, KRB5_MECH_INDEP_TOKEN | |
16 | from msldap.commons.proxy import MSLDAPProxyType | |
19 | 17 | from minikerberos.protocol.structures import ChecksumFlags |
20 | 18 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table |
21 | 19 | from minikerberos.protocol.constants import MESSAGE_TYPE |
22 | 20 | from minikerberos.aioclient import AIOKerberosClient |
21 | from minikerberos.network.aioclientsockssocket import AIOKerberosClientSocksSocket | |
22 | ||
23 | 23 | |
24 | 24 | # SMBKerberosCredential |
25 | ||
26 | MSLDAP_SOCKS_PROXY_TYPES = [ | |
27 | MSLDAPProxyType.SOCKS4 , | |
28 | MSLDAPProxyType.SOCKS4_SSL , | |
29 | MSLDAPProxyType.SOCKS5 , | |
30 | MSLDAPProxyType.SOCKS5_SSL] | |
25 | 31 | |
26 | 32 | class MSLDAPKerberos: |
27 | 33 | def __init__(self, settings): |
28 | 34 | self.settings = settings |
35 | self.signing_preferred = None | |
36 | self.encryption_preferred = None | |
29 | 37 | self.ccred = None |
30 | 38 | self.target = None |
31 | 39 | self.spn = None |
32 | 40 | self.kc = None |
41 | self.flags = None | |
42 | self.preferred_etypes = [23,17,18] | |
33 | 43 | |
34 | 44 | self.session_key = None |
35 | 45 | self.gssapi = None |
36 | 46 | self.iterations = 0 |
37 | 47 | self.etype = None |
48 | self.seq_number = 0 | |
49 | self.expected_server_seq_number = None | |
38 | 50 | |
39 | 51 | self.setup() |
40 | 52 | |
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 | ||
41 | 60 | 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 | |
43 | 65 | |
44 | 66 | 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 | |
46 | 71 | |
47 | 72 | async def sign(self, data, message_no, direction = 'init'): |
73 | """ | |
74 | Signs a message. | |
75 | """ | |
48 | 76 | return self.gssapi.GSS_GetMIC(data, message_no, direction = direction) |
49 | 77 | |
50 | 78 | async def encrypt(self, data, message_no): |
79 | """ | |
80 | Encrypts a message. | |
81 | """ | |
82 | ||
51 | 83 | return self.gssapi.GSS_Wrap(data, message_no) |
52 | 84 | |
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) | |
55 | 91 | |
56 | 92 | def setup(self): |
57 | 93 | self.ccred = self.settings.ccred |
58 | 94 | self.spn = self.settings.spn |
59 | 95 | 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) | |
63 | 109 | |
64 | 110 | 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 | ||
76 | 146 | 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) | |
82 | 148 | 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 | ||
85 | 169 | 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 | |
88 | 192 | cipher = _enctype_table[int(aprep['enc-part']['etype'])]() |
89 | 193 | cipher_text = aprep['enc-part']['cipher'] |
90 | 194 | temp = cipher.decrypt(self.session_key, 12, cipher_text) |
91 | ||
92 | 195 | 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) | |
112 | 200 | 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⏎ |
5 | 5 | # |
6 | 6 | |
7 | 7 | from msldap.authentication.spnego.asn1_structs import KRB5Token |
8 | from winsspi.sspi import KerberosSMBSSPI | |
9 | from winsspi.common.function_defs import ISC_REQ | |
10 | from minikerberos.gssapi.gssapi import get_gssapi | |
8 | from winsspi.sspi import KerberosMSLDAPSSPI | |
9 | from winsspi.common.function_defs import ISC_REQ, GetSequenceNumberFromEncryptdataKerberos | |
10 | from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken | |
11 | 11 | from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP |
12 | 12 | from minikerberos.protocol.encryption import Enctype, Key, _enctype_table |
13 | 13 | |
15 | 15 | def __init__(self, settings): |
16 | 16 | self.iterations = 0 |
17 | 17 | self.settings = settings |
18 | self.mode = 'CLIENT' | |
19 | self.ksspi = KerberosSMBSSPI() | |
20 | self.client = None | |
21 | self.target = None | |
18 | self.username = settings.username | |
19 | self.password = settings.password | |
20 | self.domain = settings.domain | |
21 | self.actual_ctx_flags = None #this will be popilated by the output of get_ticket_for_spn | |
22 | self.flags = ISC_REQ.CONNECTION | |
23 | if settings.encrypt is True: | |
24 | self.flags = ISC_REQ.CONFIDENTIALITY| ISC_REQ.INTEGRITY | ISC_REQ.CONNECTION #| ISC_REQ.MUTUAL_AUTH #| ISC_REQ.USE_DCE_STYLE | |
25 | self.ksspi = None | |
26 | self.spn = settings.spn | |
22 | 27 | self.gssapi = None |
23 | 28 | self.etype = None |
24 | ||
25 | self.setup() | |
26 | ||
27 | def setup(self): | |
28 | self.mode = self.settings.mode | |
29 | self.client = self.settings.client | |
30 | self.target = self.settings.target | |
31 | ||
29 | self.session_key = None | |
30 | self.seq_number = None | |
31 | ||
32 | def get_seq_number(self): | |
33 | """ | |
34 | Fetches the starting sequence number. This is either zero or can be found in the authenticator field of the | |
35 | AP_REQ structure. As windows uses a random seq number AND a subkey as well, we can't obtain it by decrypting the | |
36 | AP_REQ structure. Insead under the hood we perform an encryption operation via EncryptMessage API which will | |
37 | yield the start sequence number | |
38 | """ | |
39 | if self.seq_number is not None: | |
40 | return self.seq_number | |
41 | if ISC_REQ.CONFIDENTIALITY in self.actual_ctx_flags: | |
42 | self.seq_number = GetSequenceNumberFromEncryptdataKerberos(self.ksspi.context) | |
43 | if self.seq_number is None: | |
44 | self.seq_number = 0 | |
45 | ||
46 | return self.seq_number | |
47 | ||
48 | def signing_needed(self): | |
49 | """ | |
50 | Checks if integrity protection was enabled | |
51 | """ | |
52 | return ISC_REQ.INTEGRITY in self.actual_ctx_flags | |
53 | ||
54 | def encryption_needed(self): | |
55 | """ | |
56 | Checks if confidentiality was enabled | |
57 | """ | |
58 | return ISC_REQ.CONFIDENTIALITY in self.actual_ctx_flags | |
59 | ||
60 | async def sign(self, data, message_no, direction = 'init'): | |
61 | """ | |
62 | Signs a message. | |
63 | """ | |
64 | return self.gssapi.GSS_GetMIC(data, message_no, direction = direction) | |
65 | ||
32 | 66 | async def encrypt(self, data, message_no): |
67 | """ | |
68 | Encrypts a message. | |
69 | """ | |
33 | 70 | return self.gssapi.GSS_Wrap(data, message_no) |
34 | 71 | |
35 | async def decrypt(self, data, message_no, direction='init', auth_data=None): | |
36 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) | |
72 | async def decrypt(self, data, message_no, direction='init'): | |
73 | """ | |
74 | Decrypts message. Also performs integrity checking. | |
75 | """ | |
76 | return self.gssapi.GSS_Unwrap(data, message_no, direction=direction) | |
37 | 77 | |
38 | 78 | def get_session_key(self): |
39 | return self.ksspi.get_session_key() | |
79 | """ | |
80 | Fetches the session key. Under the hood this uses QueryContextAttributes API call. | |
81 | This will fail if the authentication is not yet finished! | |
82 | """ | |
83 | err = None | |
84 | if self.session_key is None: | |
85 | self.session_key, err = self.ksspi.get_session_key() | |
86 | return self.session_key, err | |
40 | 87 | |
41 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
42 | #authdata is only for api compatibility reasons | |
43 | if is_rpc == True: | |
88 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data = None): | |
89 | """ | |
90 | This function is called (multiple times depending on the flags) to perform authentication. | |
91 | """ | |
92 | try: | |
44 | 93 | if self.iterations == 0: |
45 | flags = ISC_REQ.CONFIDENTIALITY | \ | |
46 | ISC_REQ.INTEGRITY | \ | |
47 | ISC_REQ.MUTUAL_AUTH | \ | |
48 | ISC_REQ.REPLAY_DETECT | \ | |
49 | ISC_REQ.SEQUENCE_DETECT|\ | |
50 | ISC_REQ.USE_DCE_STYLE | |
94 | self.ksspi = KerberosMSLDAPSSPI(domain = self.domain, username=self.username, password=self.password) | |
95 | token, self.actual_ctx_flags = self.ksspi.get_ticket_for_spn(self.spn, ctx_flags = self.flags) | |
96 | self.iterations += 1 | |
97 | ||
98 | ||
99 | if ISC_REQ.MUTUAL_AUTH in self.actual_ctx_flags or ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: | |
100 | #in these cases continuation is needed | |
101 | return token, True, None | |
102 | ||
103 | else: | |
104 | #no mutual or dce auth will take one step only | |
105 | _, err = self.get_session_key() | |
106 | if err is not None: | |
107 | return None, None, err | |
108 | apreq = AP_REQ.load(token).native | |
109 | subkey = Key(apreq['ticket']['enc-part']['etype'], self.session_key) | |
110 | self.gssapi = get_gssapi(subkey) | |
111 | self.get_seq_number() | |
51 | 112 | |
113 | return token, False, None | |
52 | 114 | |
53 | token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) | |
54 | #print(token.hex()) | |
55 | self.iterations += 1 | |
56 | return token, True | |
57 | 115 | |
58 | elif self.iterations == 1: | |
59 | flags = ISC_REQ.USE_DCE_STYLE | |
60 | ||
61 | token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) | |
62 | #print(token.hex()) | |
116 | else: | |
117 | adata = authData[16:] | |
118 | if ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: | |
119 | adata = authData | |
120 | token, self.actual_ctx_flags = self.ksspi.get_ticket_for_spn(self.spn, ctx_flags = self.actual_ctx_flags, token_data = adata) | |
63 | 121 | |
64 | 122 | |
65 | aprep = AP_REP.load(token).native | |
123 | ||
124 | if ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: | |
125 | #Using DCE style 3-legged auth | |
126 | aprep = AP_REP.load(token).native | |
127 | else: | |
128 | aprep = AP_REP.load(adata).native | |
129 | subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) | |
130 | ||
131 | _, err = self.get_session_key() | |
132 | if err is not None: | |
133 | return None, None, err | |
66 | 134 | |
67 | subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) | |
68 | ||
69 | cipher_text = aprep['enc-part']['cipher'] | |
70 | cipher = _enctype_table[aprep['enc-part']['etype']]() | |
71 | ||
72 | plaintext = cipher.decrypt(subkey, 12, cipher_text) | |
73 | ||
135 | _, err = self.get_seq_number() | |
136 | if err is not None: | |
137 | return None, None, err | |
138 | ||
139 | subkey = Key(token['enc-part']['etype'], self.session_key) | |
74 | 140 | self.gssapi = get_gssapi(subkey) |
75 | 141 | |
76 | 142 | self.iterations += 1 |
77 | return token, False | |
78 | ||
79 | else: | |
80 | raise Exception('SSPI Kerberos -RPC - auth encountered too many calls for authenticate.') | |
143 | return token, False, None | |
81 | 144 | |
82 | else: | |
83 | apreq = self.ksspi.get_ticket_for_spn(self.target) | |
84 | return apreq, False | |
145 | except Exception as e: | |
146 | return None, None, e | |
85 | 147 | ⏎ |
40 | 40 | FRAGMENT_TO_FIT = 2097152 |
41 | 41 | HTTP = 0x10000000 |
42 | 42 | |
43 | class SMBNTLMMultiplexor: | |
43 | # | |
44 | # | |
45 | # Interface to support remote authentication via multiplexor | |
46 | # | |
47 | # Connects to the multiplexor server, and starts an SSPI server locally for the specific agentid | |
48 | # SSPI server will be used to perform NTLM authentication remotely, | |
49 | # while constructing a local NTLM authentication object | |
50 | # After the auth finishes, it also grabs the sessionkey. | |
51 | # The NTLM object can be used in future operations (encrypt/decrypt/sign) locally | |
52 | # without the need of future remote calls | |
53 | # | |
54 | ||
55 | class MSLDAPNTLMMultiplexor: | |
44 | 56 | def __init__(self, settings): |
45 | 57 | self.settings = settings |
46 | 58 | self.mode = None #'CLIENT' |
48 | 60 | self.operator = None |
49 | 61 | self.client = None |
50 | 62 | self.target = None |
51 | #self.ntlmChallenge = None | |
63 | self.seq_number = 0 | |
52 | 64 | |
53 | 65 | self.session_key = None |
54 | 66 | self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL')) |
65 | 77 | |
66 | 78 | def get_signkey(self, mode = 'Client'): |
67 | 79 | return self.ntlm_ctx.get_signkey(mode = mode) |
68 | ||
69 | ||
70 | def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt): | |
71 | return self.ntlm_ctx.SEAL(signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt) | |
72 | ||
73 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): | |
74 | return self.ntlm_ctx.SIGN(signingKey, message, seqNum, cipher_encrypt) | |
75 | 80 | |
76 | 81 | def get_session_key(self): |
77 | 82 | return self.session_key |
78 | 83 | |
79 | def get_extra_info(self): | |
80 | return self.ntlm_ctx.get_extra_info() | |
81 | ||
82 | 84 | def is_extended_security(self): |
83 | 85 | return self.ntlm_ctx.is_extended_security() |
86 | ||
87 | def get_seq_number(self): | |
88 | return self.seq_number | |
89 | ||
90 | def signing_needed(self): | |
91 | return self.ntlm_ctx.signing_needed() | |
92 | ||
93 | def encryption_needed(self): | |
94 | return self.ntlm_ctx.encryption_needed() | |
84 | 95 | |
85 | #async def encrypt(self, data, message_no): | |
86 | # return self.sspi.encrypt(data, message_no) | |
87 | # | |
88 | #async def decrypt(self, data, message_no): | |
89 | # return self.sspi.decrypt(data, message_no) | |
96 | async def encrypt(self, data, message_no): | |
97 | return await self.ntlm_ctx.encrypt(data, message_no) | |
98 | ||
99 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
100 | return await self.ntlm_ctx.decrypt(data, sequence_no, direction=direction, auth_data=auth_data) | |
101 | ||
102 | async def sign(self, data, message_no, direction=None, reset_cipher = False): | |
103 | return await self.ntlm_ctx.sign(data, message_no, direction=None, reset_cipher = reset_cipher) | |
90 | 104 | |
91 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
105 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): | |
106 | is_rpc = False | |
92 | 107 | if self.sspi is None: |
93 | res = await self.start_remote_sspi() | |
94 | if res is None: | |
95 | raise Exception('Failed to start remote SSPI') | |
108 | res, err = await self.start_remote_sspi() | |
109 | if err is not None: | |
110 | return None, None, err | |
96 | 111 | |
97 | 112 | if is_rpc is True and flags is None: |
98 | 113 | flags = ISC_REQ.REPLAY_DETECT | ISC_REQ.CONFIDENTIALITY| ISC_REQ.USE_SESSION_KEY| ISC_REQ.INTEGRITY| ISC_REQ.SEQUENCE_DETECT| ISC_REQ.CONNECTION |
99 | 114 | flags = int(flags) |
100 | 115 | |
101 | if self.settings.mode == 'CLIENT': | |
102 | if authData is None: | |
103 | data, res = await self.sspi.authenticate(flags = flags) | |
116 | if authData is None: | |
117 | data, res = await self.sspi.authenticate(flags = flags) | |
118 | if res is None: | |
119 | self.ntlm_ctx.load_negotiate(data) | |
120 | return data, res, None | |
121 | else: | |
122 | self.ntlm_ctx.load_challenge( authData) | |
123 | data, res = await self.sspi.challenge(authData, flags = flags) | |
124 | if res is None: | |
125 | self.ntlm_ctx.load_authenticate( data) | |
126 | self.session_key, res = await self.sspi.get_session_key() | |
104 | 127 | if res is None: |
105 | self.ntlm_ctx.load_negotiate(data) | |
106 | return data, res | |
107 | else: | |
108 | self.ntlm_ctx.load_challenge( authData) | |
109 | data, res = await self.sspi.challenge(authData, flags = flags) | |
110 | if res is None: | |
111 | self.ntlm_ctx.load_authenticate( data) | |
112 | self.session_key, res = await self.sspi.get_session_key() | |
113 | if res is None: | |
114 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
128 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
115 | 129 | |
116 | return data, res | |
117 | ||
118 | else: | |
119 | raise Exception('Server mode not implemented!') | |
130 | return data, res, None | |
120 | 131 | |
121 | 132 | |
122 | 133 | async def start_remote_sspi(self): |
133 | 144 | #print(sspi_url) |
134 | 145 | self.sspi = SSPINTLMClient(sspi_url) |
135 | 146 | await self.sspi.connect() |
136 | return True | |
147 | return True, None | |
137 | 148 | except Exception as e: |
138 | 149 | import traceback |
139 | 150 | traceback.print_exc() |
140 | return None | |
151 | return None, e | |
141 | 152 | |
142 | 153 | ⏎ |
0 | ||
1 | 0 | import os |
2 | 1 | import struct |
3 | 2 | import hmac |
4 | 3 | import copy |
4 | import hashlib | |
5 | 5 | |
6 | 6 | #from aiosmb.commons.connection.credential import SMBNTLMCredential |
7 | 7 | #from aiosmb.commons.serverinfo import NTLMServerInfo |
24 | 24 | self.mode = mode |
25 | 25 | self.template_name = template_name |
26 | 26 | self.custom_template = custom_template #for custom templates, must be dict |
27 | ||
28 | self.encrypt = False | |
27 | 29 | |
28 | 30 | self.template = None |
29 | 31 | self.ntlm_downgrade = False |
40 | 42 | |
41 | 43 | self.template = self.custom_template |
42 | 44 | |
45 | self.encrypt = self.credential.encrypt | |
46 | ||
47 | if self.encrypt is True: | |
48 | self.template_name = 'Windows10_15063_channel' | |
49 | ||
43 | 50 | if self.mode.upper() == 'SERVER': |
44 | 51 | if self.template_name in NTLMServerTemplates: |
45 | 52 | self.template = NTLMServerTemplates[self.template_name] |
46 | 53 | else: |
47 | 54 | raise Exception('No NTLM server template found with name %s' % self.template_name) |
48 | 55 | |
49 | else: | |
56 | else: | |
50 | 57 | if self.template_name in NTLMClientTemplates: |
51 | 58 | self.template = NTLMClientTemplates[self.template_name] |
52 | 59 | if 'ntlm_downgrade' in self.template: |
84 | 91 | |
85 | 92 | self.crypthandle_client = None |
86 | 93 | self.crypthandle_server = None |
87 | self.signhandle_server = None | |
88 | self.signhandle_client = None | |
89 | ||
90 | ||
94 | #self.signhandle_server = None doesnt exists, only crypthandle | |
95 | #self.signhandle_client = None doesnt exists, only crypthandle | |
96 | ||
97 | self.seq_number = 0 | |
91 | 98 | self.iteration_cnt = 0 |
92 | 99 | self.ntlm_credentials = None |
93 | 100 | self.timestamp = None #used in unittest only! |
109 | 116 | self.RandomSessionKey = self.settings.template['session_key'] |
110 | 117 | |
111 | 118 | self.timestamp = self.settings.template.get('timestamp') #used in unittest only! |
112 | ||
113 | ||
114 | if self.mode.upper() == 'SERVER': | |
115 | version = self.settings.template['version'] | |
116 | targetName = self.settings.template['targetname'] | |
117 | targetInfo = self.settings.template['targetinfo'] | |
118 | ||
119 | self.ntlmChallenge = NTLMChallenge.construct(challenge = self.challenge, targetName = targetName, targetInfo = targetInfo, version = version, flags = self.flags) | |
120 | ||
121 | #else: | |
122 | # domainname = self.settings.template['domain_name'] | |
123 | # workstationname = self.settings.template['workstation_name'] | |
124 | # version = self.settings.template.get('version') | |
125 | 119 | |
126 | 120 | def load_negotiate(self, data): |
127 | 121 | self.ntlmNegotiate = NTLMNegotiate.from_bytes(data) |
135 | 129 | def load_sessionkey(self, data): |
136 | 130 | self.RandomSessionKey = data |
137 | 131 | self.setup_crypto() |
132 | ||
133 | def get_seq_number(self): | |
134 | return self.seq_number | |
138 | 135 | |
139 | 136 | def set_sign(self, tf = True): |
140 | 137 | if tf == True: |
188 | 185 | #msg.Checksum = struct.unpack('<I',handle(messageSignature['Checksum']))[0] |
189 | 186 | |
190 | 187 | return msg.to_bytes() |
191 | ||
192 | #async def sign(self, data, message_no, direction=None): | |
193 | # return self.SIGN(self.SignKey_client, data, message_no, RC4(self.SignKey_client).encrypt ) | |
194 | 188 | |
195 | 189 | async def encrypt(self, data, sequence_no): |
190 | """ | |
191 | This function is to support SSPI encryption. | |
192 | """ | |
196 | 193 | return self.SEAL( |
197 | self.SignKey_client, | |
194 | #self.SignKey_client, | |
195 | self.SignKey_client, | |
198 | 196 | self.SealKey_client, |
199 | data, | |
200 | data, | |
197 | data, | |
198 | data, | |
201 | 199 | sequence_no, |
202 | 200 | self.crypthandle_client.encrypt |
203 | 201 | ) |
204 | 202 | |
205 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
206 | data = data[16:] | |
207 | msg_struct, signature = self.SEAL( | |
208 | self.SignKey_server, | |
209 | self.SealKey_server, | |
210 | data, | |
211 | data, | |
212 | sequence_no, | |
213 | self.crypthandle_server.encrypt | |
214 | ) | |
215 | print(data[:16]) | |
216 | print(signature) | |
217 | return msg_struct | |
218 | ||
219 | async def sign(self, data, message_no, direction=None): | |
220 | return self.SIGN( | |
221 | self.SealKey_client, | |
222 | data, | |
223 | message_no, | |
224 | self.signhandle_client.encrypt | |
225 | ) | |
203 | async def decrypt(self, data, sequence_no, direction='init', auth_data=None): | |
204 | """ | |
205 | This function is to support SSPI decryption. | |
206 | """ | |
207 | edata = data[16:] | |
208 | srv_sig = NTLMSSP_MESSAGE_SIGNATURE.from_bytes(data[:16]) | |
209 | sealedMessage = self.crypthandle_server.encrypt(edata) | |
210 | signature = self.MAC(self.crypthandle_server.encrypt, self.SignKey_server, srv_sig.SeqNum, sealedMessage) | |
211 | #print('seqno %s' % sequence_no) | |
212 | #print('Srv sig: %s' % data[:16]) | |
213 | #print('Calc sig: %s' % signature) | |
214 | ||
215 | return sealedMessage, None | |
216 | ||
217 | async def sign(self, data, message_no, direction=None, reset_cipher = False): | |
218 | """ | |
219 | Singing outgoing messages. The reset_cipher parameter is needed for calculating mechListMIC. | |
220 | """ | |
221 | #print('sign data : %s' % data) | |
222 | #print('sign message_no : %s' % message_no) | |
223 | #print('sign direction : %s' % direction) | |
224 | signature = self.MAC(self.crypthandle_client.encrypt, self.SignKey_client, message_no, data) | |
225 | if reset_cipher is True: | |
226 | self.crypthandle_client = RC4(self.SealKey_client) | |
227 | self.crypthandle_server = RC4(self.SealKey_server) | |
228 | self.seq_number += 1 | |
229 | return signature | |
230 | ||
231 | async def verify(self, data, signature): | |
232 | """ | |
233 | Verifying incoming server message | |
234 | """ | |
235 | signature_struct = NTLMSSP_MESSAGE_SIGNATURE.from_bytes(signature) | |
236 | calc_sig = self.MAC(self.crypthandle_server.encrypt, self.SignKey_server, signature_struct.SeqNum, data) | |
237 | #print('server signature : %s' % signature) | |
238 | #print('calculates signature: %s' % calc_sig) | |
239 | return signature == calc_sig | |
226 | 240 | |
227 | 241 | def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt): |
242 | """ | |
243 | This is the official SEAL function. | |
244 | """ | |
228 | 245 | sealedMessage = cipher_encrypt(messageToEncrypt) |
229 | 246 | signature = self.MAC(cipher_encrypt, signingKey, seqNum, messageToSign) |
230 | 247 | return sealedMessage, signature |
231 | 248 | |
232 | 249 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): |
250 | """ | |
251 | This is the official SIGN function. | |
252 | """ | |
233 | 253 | return self.MAC(cipher_encrypt, signingKey, seqNum, message) |
234 | 254 | |
235 | 255 | def signing_needed(self): |
290 | 310 | |
291 | 311 | if mode == 'Client': |
292 | 312 | self.SignKey_client = signkey |
293 | if signkey is not None: | |
294 | self.signhandle_client = RC4(self.SealKey_client) | |
295 | 313 | |
296 | 314 | else: |
297 | 315 | self.SignKey_server = signkey |
298 | if signkey is not None: | |
299 | self.signhandle_server = RC4(self.SignKey_server) | |
300 | 316 | |
301 | 317 | return signkey |
302 | 318 | |
332 | 348 | self.calc_signkey('Client') |
333 | 349 | self.calc_signkey('Server') |
334 | 350 | |
335 | async def authenticate(self, authData, flags = None, seq_number = 0, is_rpc = False): | |
336 | if self.mode.upper() == 'SERVER': | |
337 | if self.ntlmNegotiate is None: | |
338 | ###parse client NTLMNegotiate message | |
339 | self.ntlmNegotiate = NTLMNegotiate.from_bytes(authData) | |
340 | return self.ntlmChallenge.to_bytes(), True | |
341 | ||
342 | elif self.ntlmAuthenticate is None: | |
343 | self.ntlmAuthenticate = NTLMAuthenticate.from_bytes(authData, self.use_NTLMv2) | |
344 | creds = NTLMcredential.construct(self.ntlmNegotiate, self.ntlmChallenge, self.ntlmAuthenticate) | |
345 | print(creds) | |
346 | ||
347 | # TODO: check when is sessionkey needed and check when is singing needed, and calculate the keys! | |
348 | # self.calc_SessionBaseKey() | |
349 | # self.calc_KeyExchangeKey() | |
350 | auth_credential = creds[0] | |
351 | #self.SessionBaseKey = auth_credential.calc_session_base_key() | |
352 | #self.calc_key_exchange_key() | |
353 | ||
354 | if auth_credential.verify(self.credential): | |
355 | return False, auth_credential | |
356 | else: | |
357 | return False, auth_credential | |
358 | ||
359 | else: | |
360 | raise Exception('Too many calls to do_AUTH function!') | |
361 | ||
362 | elif self.mode.upper() == 'CLIENT': | |
351 | async def authenticate(self, authData, flags = None, seq_number = 0, cb_data = None): | |
352 | if self.mode.upper() == 'CLIENT': | |
363 | 353 | if self.iteration_cnt == 0: |
364 | 354 | if authData is not None: |
365 | 355 | raise Exception('First call as client MUST be with empty data!') |
366 | ||
367 | if is_rpc == True: | |
368 | #rpc (unknow reason) reqauires seal to be set, otherwise it will fail to authenticate | |
369 | self.set_seal() | |
370 | 356 | |
371 | 357 | self.iteration_cnt += 1 |
372 | 358 | #negotiate message was already calulcated in setup |
373 | 359 | self.ntlmNegotiate = NTLMNegotiate.construct(self.flags, domainname = self.settings.template['domain_name'], workstationname = self.settings.template['workstation_name'], version = self.settings.template.get('version')) |
374 | 360 | self.ntlmNegotiate_raw = self.ntlmNegotiate.to_bytes() |
375 | return self.ntlmNegotiate_raw, True | |
361 | return self.ntlmNegotiate_raw, True, None | |
376 | 362 | |
377 | 363 | else: |
378 | 364 | #server challenge incoming |
379 | 365 | self.ntlmChallenge_raw = authData |
380 | 366 | self.ntlmChallenge = NTLMChallenge.from_bytes(authData) |
381 | ||
382 | ||
383 | if is_rpc == True: | |
384 | #rpc (unknow reason) reqauires seal to be set, otherwise it will fail to authenticate | |
385 | self.set_seal() | |
386 | 367 | |
387 | 368 | ##################self.flags = self.ntlmChallenge.NegotiateFlags |
388 | 369 | |
396 | 377 | lmresp = LMResponse() |
397 | 378 | lmresp.Response = b'\x00' |
398 | 379 | self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, lm_response= lmresp) |
399 | return self.ntlmAuthenticate.to_bytes(), False | |
380 | return self.ntlmAuthenticate.to_bytes(), False, None | |
400 | 381 | |
401 | 382 | if self.flags & NegotiateFlags.NEGOTIATE_EXTENDED_SESSIONSECURITY: |
402 | 383 | #Extended auth! |
422 | 403 | lmresp = LMResponse() |
423 | 404 | lmresp.Response = b'\x00' |
424 | 405 | self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, lm_response= lmresp) |
425 | return self.ntlmAuthenticate.to_bytes(), False | |
406 | return self.ntlmAuthenticate.to_bytes(), False, None | |
426 | 407 | |
427 | 408 | else: |
428 | 409 | #comment this out for testing! |
429 | 410 | ti = self.ntlmChallenge.TargetInfo |
430 | ti[AVPAIRType.MsvAvTargetName] = 'cifs/%s' % ti[AVPAIRType.MsvAvNbComputerName] | |
411 | ti[AVPAIRType.MsvAvTargetName] = 'ldaps/%s' % ti[AVPAIRType.MsvAvDnsComputerName] | |
412 | if cb_data is not None: | |
413 | md5_ctx = hashlib.new('md5') | |
414 | md5_ctx.update(cb_data) | |
415 | ti[AVPAIRType.MsvChannelBindings] = md5_ctx.digest() | |
431 | 416 | ### |
432 | 417 | |
433 | 418 | self.ntlm_credentials = netntlmv2.construct(self.ntlmChallenge.ServerChallenge, self.challenge, ti, self.settings.credential, timestamp = self.timestamp) |
438 | 423 | mic = None |
439 | 424 | |
440 | 425 | self.ntlmAuthenticate = NTLMAuthenticate.construct(self.flags, domainname= self.settings.credential.domain, workstationname= self.settings.credential.workstation, username= self.settings.credential.username, lm_response= self.ntlm_credentials.LMResponse, nt_response= self.ntlm_credentials.NTResponse, version = self.ntlmNegotiate.Version, encrypted_session = self.EncryptedRandomSessionKey, mic = mic) |
441 | ||
426 | ||
427 | ||
442 | 428 | self.ntlmAuthenticate_raw = self.ntlmAuthenticate.to_bytes() |
443 | return self.ntlmAuthenticate_raw, False | |
429 | return self.ntlmAuthenticate_raw, False, None | |
444 | 430 | |
445 | 431 | elif self.mode.upper() == 'RELAY': |
446 | 432 | if self.iteration_cnt == 0: |
460 | 446 | |
461 | 447 | else: |
462 | 448 | raise Exception('Too many iterations for relay mode!') |
463 | ||
464 | ||
465 | ||
466 | #def test_msdn(): | |
467 | # credential = Credential() | |
468 | # credential.username = 'User' | |
469 | # credential.domain = 'Domain' | |
470 | # credential.password = 'Password' | |
471 | # | |
472 | # template = { | |
473 | # 'flags' : NegotiateFlags.NEGOTIATE_56| | |
474 | # NegotiateFlags.REQUEST_NON_NT_SESSION_KEY| | |
475 | # NegotiateFlags.NEGOTIATE_KEY_EXCH| | |
476 | # NegotiateFlags.NEGOTIATE_128| | |
477 | # NegotiateFlags.NEGOTIATE_VERSION| | |
478 | # NegotiateFlags.TARGET_TYPE_SERVER| | |
479 | # NegotiateFlags.NEGOTIATE_ALWAYS_SIGN| | |
480 | # NegotiateFlags.NEGOTIATE_NTLM| | |
481 | # NegotiateFlags.NEGOTIATE_SIGN| | |
482 | # NegotiateFlags.NEGOTIATE_SEAL| | |
483 | # NegotiateFlags.NTLM_NEGOTIATE_OEM| | |
484 | # NegotiateFlags.NEGOTIATE_UNICODE, | |
485 | # 'version' : Version.construct(WindowsMajorVersion.WINDOWS_MAJOR_VERSION_10, minor = WindowsMinorVersion.WINDOWS_MINOR_VERSION_0, build = 15063 ), | |
486 | # 'domain_name' : 'Domain', | |
487 | # 'workstation_name' : 'COMPUTER', | |
488 | # 'ntlm_downgrade' : True, | |
489 | # 'extended_security': False | |
490 | # } | |
491 | # settings = NTLMHandlerSettings(credential, mode = 'CLIENT', template_name = None, ntlm_downgrade = True, extended_security = False, custom_template = template) | |
492 | # handler = NTLMAUTHHandler(settings) | |
493 | # #assert handler.flags == int.from_bytes(b'\x33\x82\x02\xe2', "little", signed = False) | |
494 | # data, is_res = handler.authenticate(None) | |
495 | # print(data) | |
496 | # print(is_res) | |
497 | # | |
498 | # details = AVPairs({AVPAIRType.MsvAvNbDomainName: 'TEST', AVPAIRType.MsvAvNbComputerName: 'WIN2019AD', AVPAIRType.MsvAvDnsDomainName: 'test.corp', AVPAIRType.MsvAvDnsComputerName: 'WIN2019AD.test.corp', AVPAIRType.MsvAvTimestamp: b'\xae\xc6\x00\xbf\xc5\xfd\xd4\x01', AVPAIRType.MsvAvFlags: b'\x02\x00\x00\x00', AVPAIRType.MsvAvSingleHost: b"0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00R}'\xf24\xdet7`\x96c\x84\xd3oa\xae*\xa4\xfc*8\x06\x99\xf8\xca\xa6\x00\x01\x1bHm\x89", AVPAIRType.MsvChannelBindings: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', AVPAIRType.MsvAvTargetName: 'cifs/10.10.10.2'}) | |
499 | # | |
500 | # challenge = NTLMChallenge.construct(challenge=b'\x01\x23\x45\x67\x89\xab\xcd\xef', targetName = 'Domain', targetInfo = details, version = handler.ntlmNegotiate.Version, flags= handler.flags) | |
501 | # data, is_res = handler.authenticate(challenge.to_bytes()) | |
502 | # print(data) | |
503 | # print(is_res) | |
504 | # | |
505 | # print(handler.ntlmAuthenticate.LMChallenge.to_bytes().hex()) | |
506 | # print(handler.ntlmAuthenticate.NTChallenge.to_bytes().hex()) | |
507 | # | |
508 | # | |
509 | #def test(): | |
510 | # template_name = 'Windows10_15063' | |
511 | # credential = Credential() | |
512 | # credential.username = 'test' | |
513 | # credential.password = 'test' | |
514 | # | |
515 | # settings = NTLMHandlerSettings(credential, mode = 'CLIENT', template_name = template_name, ntlm_downgrade = False, extended_security = True) | |
516 | # handler = NTLMAUTHHandler(settings) | |
517 | # data, is_res = handler.authenticate(None) | |
518 | # print(data) | |
519 | # print(is_res) | |
520 | # | |
521 | #if __name__ == '__main__': | |
522 | # from aiosmb.ntlm.structures.version import Version, WindowsMajorVersion, WindowsMinorVersion | |
523 | # test_msdn()⏎ | |
449 | ⏎ |
1 | 1 | # |
2 | 2 | # This is just a simple interface to the winsspi library to support NTLM |
3 | 3 | # |
4 | from winsspi.sspi import NTLMSMBSSPI | |
4 | from winsspi.sspi import NTLMMSLDAPSSPI | |
5 | from winsspi.common.function_defs import ISC_REQ | |
5 | 6 | from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings |
6 | 7 | |
7 | 8 | class MSLDAPNTLMSSPI: |
8 | 9 | def __init__(self, settings): |
9 | 10 | self.settings = settings |
10 | self.mode = None #'CLIENT' | |
11 | self.sspi = NTLMSMBSSPI() | |
12 | self.client = None | |
13 | self.target = None | |
14 | #self.ntlmChallenge = None | |
15 | ||
11 | self.mode = 'CLIENT' | |
12 | self.username = settings.username | |
13 | self.password = settings.password | |
14 | self.domain = settings.domain | |
15 | self.actual_ctx_flags = None | |
16 | self.flags = ISC_REQ.CONNECTION | |
17 | if settings.encrypt is True: | |
18 | #self.flags = ISC_REQ.REPLAY_DETECT | ISC_REQ.CONFIDENTIALITY| ISC_REQ.USE_SESSION_KEY| ISC_REQ.INTEGRITY| ISC_REQ.SEQUENCE_DETECT| ISC_REQ.CONNECTION | |
19 | self.flags = ISC_REQ.CONNECTION | ISC_REQ.CONFIDENTIALITY | |
20 | self.sspi = NTLMMSLDAPSSPI() | |
21 | ||
22 | self.seq_number = 0 | |
16 | 23 | self.session_key = None |
17 | 24 | self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL')) |
18 | ||
19 | self.setup() | |
20 | 25 | |
21 | 26 | @property |
22 | 27 | def ntlmChallenge(self): |
23 | 28 | return self.ntlm_ctx.ntlmChallenge |
29 | ||
30 | def get_seq_number(self): | |
31 | return self.ntlm_ctx.get_seq_number() | |
24 | 32 | |
25 | def setup(self): | |
26 | self.mode = self.settings.mode.upper() | |
27 | self.client = self.settings.client | |
28 | self.password = self.settings.password | |
29 | ||
33 | def signing_needed(self): | |
34 | return self.ntlm_ctx.signing_needed() | |
35 | ||
36 | def encryption_needed(self): | |
37 | return self.ntlm_ctx.encryption_needed() | |
38 | ||
30 | 39 | def get_sealkey(self, mode = 'Client'): |
31 | 40 | return self.ntlm_ctx.get_sealkey(mode = mode) |
32 | 41 | |
44 | 53 | |
45 | 54 | def SIGN(self, signingKey, message, seqNum, cipher_encrypt): |
46 | 55 | return self.ntlm_ctx.SIGN(signingKey, message, seqNum, cipher_encrypt) |
56 | ||
57 | def sign(self, data, message_no = 0, direction = 'init', reset_cipher = False): | |
58 | return self.ntlm_ctx.sign(data, message_no = message_no, reset_cipher = reset_cipher) | |
59 | ||
60 | def verify(self, data, signature): | |
61 | return self.ntlm_ctx.verify(data, signature) | |
47 | 62 | |
48 | 63 | def get_session_key(self): |
49 | 64 | if not self.session_key: |
57 | 72 | def is_extended_security(self): |
58 | 73 | return self.ntlm_ctx.is_extended_security() |
59 | 74 | |
60 | async def encrypt(self, data, message_no): | |
61 | return self.sspi.encrypt(data, message_no) | |
75 | def encrypt(self, data, message_no): | |
76 | return self.ntlm_ctx.encrypt(data, message_no) | |
62 | 77 | |
63 | async def decrypt(self, data, message_no): | |
64 | return self.sspi.decrypt(data, message_no) | |
78 | def decrypt(self, data, message_no, direction='init', auth_data=None): | |
79 | return self.ntlm_ctx.decrypt(data, message_no, direction=direction, auth_data=auth_data) | |
65 | 80 | |
66 | async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): | |
67 | if self.mode == 'CLIENT': | |
68 | if authData is None: | |
69 | data, res = self.sspi.negotiate(is_rpc = is_rpc) | |
81 | async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data = None): | |
82 | if authData is None: | |
83 | try: | |
84 | data, res = self.sspi.negotiate(ctx_flags = self.flags) | |
85 | self.actual_ctx_flags = self.sspi.ctx_outflags | |
70 | 86 | self.ntlm_ctx.load_negotiate(data) |
71 | return data, res | |
72 | else: | |
73 | self.ntlm_ctx.load_challenge( authData) | |
74 | data, res = self.sspi.authenticate(authData, is_rpc = is_rpc) | |
75 | self.ntlm_ctx.load_authenticate( data) | |
76 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
87 | return data, res, None | |
88 | except Exception as e: | |
89 | return None, None, e | |
90 | else: | |
91 | self.ntlm_ctx.load_challenge(authData) | |
92 | data, res = self.sspi.authenticate(authData, ctx_flags = self.flags) | |
93 | self.ntlm_ctx.load_authenticate( data) | |
94 | self.ntlm_ctx.load_sessionkey(self.get_session_key()) | |
77 | 95 | |
78 | return data, res | |
96 | return data, res, None | |
79 | 97 | |
80 | 98 | ⏎ |
5 | 5 | |
6 | 6 | import copy |
7 | 7 | from msldap.authentication.spnego.asn1_structs import * |
8 | from asn1crypto.core import OctetString | |
8 | 9 | |
9 | 10 | # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/d4f2b41c-5f9e-4e11-98d0-ade76467095d |
10 | 11 | |
46 | 47 | def encryption_needed(self): |
47 | 48 | return self.selected_authentication_context.encryption_needed() |
48 | 49 | |
50 | def get_seq_number(self): | |
51 | return self.selected_authentication_context.get_seq_number() | |
52 | ||
49 | 53 | async def unsign(self, data): |
50 | 54 | #TODO: IMPLEMENT THIS |
51 | 55 | return data |
52 | 56 | |
53 | async def sign(self, data, message_no, direction='init'): | |
54 | return await self.selected_authentication_context.sign(data, message_no, direction=direction) | |
57 | async def verify(self, data, signature): | |
58 | return await self.selected_authentication_context.verify(data, signature) | |
59 | ||
60 | async def sign(self, data, message_no, direction='init', reset_cipher = False): | |
61 | return await self.selected_authentication_context.sign(data, message_no, direction=direction, reset_cipher = reset_cipher) | |
55 | 62 | |
56 | 63 | async def encrypt(self, data, message_no): |
57 | 64 | return await self.selected_authentication_context.encrypt(data, message_no) |
58 | 65 | |
59 | 66 | async def decrypt(self, data, message_no, direction='init', auth_data=None): |
60 | return await self.selected_authentication_context.decrypt(data, message_no, direction=direction, auth_data=auth_data) | |
67 | return await self.selected_authentication_context.decrypt(data, message_no, direction=direction) | |
61 | 68 | |
62 | 69 | def add_auth_context(self, name, ctx): |
63 | 70 | """ |
77 | 84 | def select_common_athentication_type(self, mech_types): |
78 | 85 | for auth_type_name in self.authentication_contexts: |
79 | 86 | if auth_type_name in mech_types: |
80 | print(auth_type_name) | |
81 | 87 | return auth_type_name, self.authentication_contexts[auth_type_name] |
82 | 88 | |
83 | 89 | return None, None |
84 | 90 | |
85 | async def process_ctx_authenticate(self, token_data, include_negstate = False, flags = None, seq_number = 0, is_rpc = False): | |
86 | result, to_continue = await self.selected_authentication_context.authenticate(token_data, flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
91 | async def process_ctx_authenticate(self, token_data, include_negstate = False, flags = None, seq_number = 0, cb_data = None): | |
92 | result, to_continue, err = await self.selected_authentication_context.authenticate(token_data, flags = flags, seq_number = seq_number, cb_data = cb_data) | |
93 | if err is not None: | |
94 | return None, None, err | |
87 | 95 | if not result: |
88 | return None, False | |
96 | return None, False, None | |
89 | 97 | response = {} |
90 | 98 | if include_negstate == True: |
91 | 99 | if to_continue == True: |
94 | 102 | response['negState'] = NegState('accept-completed') |
95 | 103 | |
96 | 104 | response['responseToken'] = result |
97 | return response, to_continue | |
105 | return response, to_continue, None | |
98 | 106 | |
99 | 107 | def get_extra_info(self): |
100 | 108 | if hasattr(self.selected_authentication_context, 'get_extra_info'): |
115 | 123 | #spnego = GSS_SPNEGO({'NegotiationToken':negtoken}) |
116 | 124 | return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump() |
117 | 125 | |
118 | async def authenticate(self, token, flags = None, seq_number = 0, is_rpc = False): | |
126 | async def authenticate(self, token, flags = None, seq_number = 0, cb_data = None): | |
119 | 127 | """ |
120 | 128 | This function is called (multiple times) during negotiation phase of a protocol to determine hich auth mechanism to be used |
121 | 129 | Token is a byte array that is an ASN1 NegotiationToken structure. |
122 | 130 | """ |
123 | ||
124 | if self.mode == 'SERVER': | |
125 | if self.selected_authentication_context is None: | |
126 | gss = GSSAPI.load(token).native | |
127 | negtoken = gss['value'] | |
128 | if len(negtoken['mechTypes']) == 1: | |
129 | self.selected_mechtype = negtoken['mechTypes'][0] | |
130 | if negtoken['mechTypes'][0] == 'NTLMSSP - Microsoft NTLM Security Support Provider': | |
131 | self.selected_authentication_context = self.authentication_contexts[negtoken['mechTypes'][0]] | |
132 | ||
133 | ||
134 | else: | |
135 | raise Exception('This path is not yet implemented') | |
136 | #self.selected_mechtype, self.selected_authentication_context = self.select_common_athentication_type(neg_token.mechTypes) | |
137 | #if self.selected_mechtype is None: | |
138 | # raise Exception('Failed to select common authentication mechanism! Client sent: %s We have %s' % ()) | |
139 | # | |
140 | # ##server offered multiple auth types, we must choose one | |
141 | # #response = {} | |
142 | # #response['negState'] = NegState('accept-incomplete') | |
143 | # #response['supportedMech'] = MechType(self.selected_mechtype) | |
144 | # # | |
145 | #return NegTokenResp(response).dump(), True | |
146 | ||
147 | ||
148 | if self.selected_authentication_context is not None: | |
149 | response, to_continue = await self.process_ctx_authenticate(negtoken['mechToken'], flags = flags, seq_number = seq_number, is_rpc = is_rpc, include_negstate = True) | |
150 | if self.iteration_ctr == 0: | |
151 | response['supportedMech'] = MechType(self.selected_mechtype) | |
152 | negtoken = NegotiationToken({'negTokenResp':NegTokenResp(response)}) | |
131 | if self.selected_mechtype is None: | |
132 | if token is None: | |
133 | #first call to auth, we need to create NegTokenInit2 | |
134 | #we must list all available auth types, if only one is present then generate initial auth data with it | |
135 | ||
136 | selected_name = None | |
137 | mechtypes = [] | |
138 | for mechname in self.authentication_contexts: | |
139 | selected_name = mechname #only used if there is one! | |
140 | mechtypes.append(MechType(mechname)) | |
141 | ||
142 | response = {} | |
143 | response['mechTypes'] = MechTypes(mechtypes) | |
144 | ||
145 | self.negtypes_store = MechTypes(mechtypes).dump() | |
146 | ||
147 | ||
148 | if len(mechtypes) == 1: | |
149 | self.selected_authentication_context = self.authentication_contexts[selected_name] | |
150 | self.selected_mechtype = selected_name | |
151 | result, to_continue, err = await self.selected_authentication_context.authenticate(None, cb_data=cb_data) | |
152 | if err is not None: | |
153 | return None, None, err | |
154 | ||
155 | if not result: | |
156 | return None, False, None | |
157 | if str(response['mechTypes'][0]) == '1.2.840.48018.1.2.2': | |
158 | response['mechToken'] = KRB5Token(result).to_bytes() | |
159 | ||
160 | #response['mechToken'] = bytes.fromhex('2a864886f712010202') + #??????? | |
161 | else: | |
162 | response['mechToken'] = result | |
163 | #raise Exception('NTLM as RPC GSSAPI not implemented!') | |
164 | ||
165 | ### First message and ONLY the first message goes out with additional wrapping | |
166 | ||
167 | negtoken = NegotiationToken({'negTokenInit':NegTokenInit2(response)}) | |
153 | 168 | |
154 | 169 | |
155 | 170 | #spnego = GSS_SPNEGO({'NegotiationToken':negtoken}) |
156 | ||
157 | self.iteration_ctr += 1 | |
158 | #return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump(), to_continue | |
159 | return negtoken.dump(), to_continue | |
160 | ||
161 | #neg_token_raw = NegotiationToken.load(token) | |
162 | #neg_token = neg_token_raw.native | |
163 | #if isinstance(neg_token_raw, NegTokenInit2): | |
164 | # if selected_authentication_context is not None: | |
165 | # raise Exception('Authentication context already selected, but Client sent NegTokenInit2') | |
166 | # | |
167 | # if len(neg_token.mechTypes) == 1: | |
168 | # #client only sent 1 negotiation token type, we either support it or raise exception | |
169 | # if neg_token.mechTypes[0] not in self.authentication_contexts: | |
170 | # raise Exception('Client sent %s auth mechanism but we dont have that set up!' % neg_token.mechTypes[0]) | |
171 | # | |
172 | # self.selected_mechtype = neg_token.mechTypes[0] | |
173 | # self.selected_authentication_context = self.authentication_contexts[neg_token.mechTypes[0]] | |
174 | # #there is an option if onyl one auth type is set to have the auth token already in this message | |
175 | # if neg_token.mechToken is not None: | |
176 | # response, to_continue = await self.process_ctx_authenticate(neg_token.mechToken, flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
177 | # if not response: | |
178 | # return None, False | |
179 | # response['supportedMech'] = MechType(self.selected_mechtype) | |
180 | # return NegTokenResp(response).dump(), to_continue | |
181 | # | |
182 | # else: | |
183 | # response = {} | |
184 | # response['negState'] = NegState('accept-incomplete') | |
185 | # response['supportedMech'] = MechType(self.selected_mechtype) | |
186 | # return NegTokenResp(response).dump(), True | |
187 | ||
188 | ||
189 | ||
190 | #elif isinstance(neg_token_raw, NegTokenResp): | |
191 | # if selected_authentication_context is None: | |
192 | # raise Exception('NegTokenResp got, but no authentication context selected!') | |
193 | # | |
194 | # response, to_continue = await self.process_ctx_authenticate(neg_token.mechToken, flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
195 | # return NegTokenResp(response.dump()), to_continue | |
196 | ||
171 | return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump(), True, None | |
172 | ||
173 | ||
174 | else: | |
175 | #we have already send the NegTokenInit2, but it contained multiple auth types, | |
176 | #at this point server is replying which auth type to use | |
177 | neg_token_raw = NegotiationToken.load(token) | |
178 | ||
179 | neg_token = neg_token_raw.native | |
180 | ||
181 | if not isinstance(neg_token_raw, NegTokenResp): | |
182 | raise Exception('Server send init???') | |
183 | ||
184 | ||
185 | self.selected_authentication_context = self.authentication_contexts[neg_token.mechTypes[0]] | |
186 | self.selected_mechtype = neg_token['supportedMech'] | |
187 | ||
188 | ||
189 | response, to_continue, err = await self.process_ctx_authenticate(neg_token['responseToken'], flags = flags, seq_number = seq_number, cb_data = cb_data) | |
190 | if err is not None: | |
191 | return None, None, err | |
192 | return NegTokenResp(response).dump(), to_continue, None | |
193 | ||
197 | 194 | else: |
198 | if self.selected_mechtype is None: | |
199 | if token is None: | |
200 | #first call to auth, we need to create NegTokenInit2 | |
201 | #we must list all available auth types, if only one is present then generate initial auth data with it | |
202 | ||
203 | selected_name = None | |
204 | mechtypes = [] | |
205 | for mechname in self.authentication_contexts: | |
206 | selected_name = mechname #only used if there is one! | |
207 | mechtypes.append(MechType(mechname)) | |
208 | ||
209 | response = {} | |
210 | response['mechTypes'] = MechTypes(mechtypes) | |
211 | ||
212 | if len(mechtypes) == 1: | |
213 | self.selected_authentication_context = self.authentication_contexts[selected_name] | |
214 | self.selected_mechtype = selected_name | |
215 | result, to_continue = await self.selected_authentication_context.authenticate(None, is_rpc = is_rpc) | |
216 | if is_rpc == False: | |
217 | response['mechToken'] = result | |
218 | else: | |
219 | if not result: | |
220 | return None, False | |
221 | if str(response['mechTypes'][0]) == '1.2.840.48018.1.2.2': | |
222 | response['mechToken'] = KRB5Token(result).to_bytes() | |
223 | ||
224 | #response['mechToken'] = bytes.fromhex('2a864886f712010202') + #??????? | |
225 | else: | |
226 | raise Exception('NTLM as RPC GSSAPI not implemented!') | |
227 | ||
228 | ### First message and ONLY the first message goes out with additional wrapping | |
229 | ||
230 | negtoken = NegotiationToken({'negTokenInit':NegTokenInit2(response)}) | |
231 | ||
232 | ||
233 | #spnego = GSS_SPNEGO({'NegotiationToken':negtoken}) | |
234 | return GSSAPI({'type': GSSType('1.3.6.1.5.5.2'), 'value':negtoken}).dump(), True | |
235 | ||
236 | ||
237 | else: | |
238 | #we have already send the NegTokenInit2, but it contained multiple auth types, | |
239 | #at this point server is replying which auth type to use | |
240 | neg_token_raw = NegotiationToken.load(token) | |
241 | neg_token = neg_token_raw.native | |
242 | ||
243 | if not isinstance(neg_token_raw, NegTokenResp): | |
244 | raise Exception('Server send init???') | |
245 | ||
246 | self.selected_authentication_context = self.authentication_contexts[neg_token.mechTypes[0]] | |
247 | self.selected_mechtype = neg_token['supportedMech'] | |
248 | ||
249 | response, to_continue = await self.process_ctx_authenticate(neg_token['responseToken'], flags = flags, seq_number = seq_number, is_rpc = is_rpc) | |
250 | return NegTokenResp(response).dump(), to_continue | |
251 | ||
195 | #everything is netotiated, but authentication needs more setps | |
196 | neg_token_raw = NegotiationToken.load(token) | |
197 | neg_token = neg_token_raw.native | |
198 | if neg_token['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 | ||
252 | 217 | 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 | |
257 | 221 | 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 | |
260 | 231 | |
261 | 232 | def test(): |
262 | 233 | test_data = bytes.fromhex('a03e303ca00e300c060a2b06010401823702020aa22a04284e544c4d5353500001000000978208e2000000000000000000000000000000000a00d73a0000000f') |
43 | 43 | async def decrypt(self, data, message_no): |
44 | 44 | return await self.sspi.decrypt(data, message_no) |
45 | 45 | |
46 | async def authenticate(self, token, flags = None, seq_number = 0, is_rpc = False): | |
46 | async def authenticate(self, token, flags = None, seq_number = 0): | |
47 | 47 | try: |
48 | 48 | if self.mode.upper() == 'CLIENT': |
49 | 49 | res, data = self.sspi.authGSSClientStep(token) |
4 | 4 | # |
5 | 5 | |
6 | 6 | from msldap import logger |
7 | from msldap.commons.common import MSLDAPClientStatus | |
7 | 8 | from msldap.wintypes.asn1.sdflagsrequest import SDFlagsRequest, SDFlagsRequestValue |
8 | 9 | from msldap.protocol.constants import BASE, ALL_ATTRIBUTES, LEVEL |
9 | 10 | |
10 | from msldap.protocol.query import escape_filter_chars, query_syntax_converter | |
11 | from msldap.protocol.query import escape_filter_chars | |
11 | 12 | from msldap.connection import MSLDAPClientConnection |
12 | 13 | from msldap.protocol.messages import Control |
13 | 14 | from msldap.ldap_objects import * |
14 | 15 | |
15 | 16 | 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 | """ | |
16 | 32 | def __init__(self, target, creds, ldap_query_page_size = 1000): |
17 | 33 | self.creds = creds |
18 | 34 | self.target = target |
24 | 40 | |
25 | 41 | |
26 | 42 | 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 | |
37 | 62 | |
38 | 63 | def get_server_info(self): |
39 | 64 | return self._serverinfo |
40 | 65 | |
41 | async def pagedsearch(self, ldap_filter, attributes, controls = None): | |
66 | async def pagedsearch(self, query, attributes, controls = None): | |
42 | 67 | """ |
43 | 68 | 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!') | |
55 | 95 | t = [] |
56 | 96 | for x in attributes: |
57 | 97 | t.append(x.encode()) |
58 | 98 | attributes = t |
59 | ldap_filter = query_syntax_converter(ldap_filter) | |
60 | 99 | |
61 | 100 | t = [] |
62 | 101 | if controls is not None: |
70 | 109 | controls = t |
71 | 110 | |
72 | 111 | async for entry, err in self._con.pagedsearch( |
73 | self._tree.encode(), | |
74 | ldap_filter, | |
112 | self._tree, | |
113 | query, | |
75 | 114 | attributes = attributes, |
76 | paged_size = self.ldap_query_page_size, | |
115 | size_limit = self.ldap_query_page_size, | |
77 | 116 | controls = controls |
78 | 117 | ): |
79 | 118 | |
80 | 119 | if err is not None: |
81 | raise err | |
120 | yield None, err | |
121 | return | |
82 | 122 | if entry['objectName'] == '' and entry['attributes'] == '': |
83 | 123 | #searchresref... |
84 | 124 | continue |
85 | 125 | #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): | |
89 | 129 | """ |
90 | 130 | 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)) | |
98 | 142 | tree = {} |
99 | #entries = | |
100 | 143 | async for entry, err in self._con.pagedsearch( |
101 | dn.encode(), | |
102 | query_syntax_converter('(distinguishedName=*)'), | |
144 | root_dn, | |
145 | '(distinguishedName=*)', | |
103 | 146 | attributes = [b'distinguishedName'], |
104 | paged_size = self.ldap_query_page_size, | |
147 | size_limit = self.ldap_query_page_size, | |
105 | 148 | search_scope=LEVEL, |
106 | 149 | controls = None, |
107 | 150 | ): |
116 | 159 | continue |
117 | 160 | subtree = await self.get_tree_plot(entry['attributes']['distinguishedName'], level = level -1) |
118 | 161 | 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 | ||
125 | 171 | """ |
126 | 172 | logger.debug('Polling AD for all user objects') |
127 | 173 | 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 | |
130 | 179 | logger.debug('Finished polling for entries!') |
131 | 180 | |
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 | ||
144 | 190 | """ |
145 | 191 | logger.debug('Polling AD for all user objects') |
146 | 192 | ldap_filter = r'(sAMAccountType=805306369)' |
147 | 193 | |
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 | |
150 | 199 | logger.debug('Finished polling for entries!') |
151 | 200 | |
152 | 201 | 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 | ||
153 | 210 | 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 | |
156 | 216 | |
157 | 217 | 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 | ||
158 | 225 | ldap_filter = r'(sAMAccountType=805306369)' |
159 | 226 | 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!') | |
162 | 283 | |
163 | 284 | 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 | ||
164 | 294 | ldap_filter = r'(&(sAMAccountType=805306369)(sAMAccountName=%s))' % sAMAccountName |
165 | 295 | 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 | |
168 | 298 | |
169 | 299 | async def get_user(self, sAMAccountName): |
170 | 300 | """ |
171 | 301 | 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`) | |
172 | 307 | """ |
173 | 308 | logger.debug('Polling AD for user %s'% sAMAccountName) |
174 | 309 | 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 | |
178 | 316 | logger.debug('Finished polling for entries!') |
179 | 317 | |
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 | ||
180 | 337 | async def get_ad_info(self): |
181 | 338 | """ |
182 | 339 | 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`) | |
183 | 343 | """ |
184 | 344 | logger.debug('Polling AD for basic info') |
185 | 345 | 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 | |
187 | 349 | self._ldapinfo = MSADInfo.from_ldap(entry) |
188 | return self._ldapinfo | |
350 | return self._ldapinfo, None | |
189 | 351 | |
190 | 352 | logger.debug('Poll finished!') |
191 | 353 | |
192 | 354 | 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 | ||
193 | 366 | logger.debug('Polling AD for all SPN entries') |
194 | 367 | ldap_filter = r'(&(sAMAccountType=805306369))' |
195 | 368 | attributes = ['objectSid','sAMAccountName', 'servicePrincipalName'] |
196 | 369 | |
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): | |
201 | 374 | """ |
202 | 375 | 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 | ||
204 | 384 | """ |
205 | 385 | logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine) |
206 | 386 | if include_machine == True: |
208 | 388 | else: |
209 | 389 | ldap_filter = r'(&(servicePrincipalName=*)(!(sAMAccountName=*$)))' |
210 | 390 | |
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 | |
213 | 396 | logger.debug('Finished polling for entries!') |
214 | 397 | |
215 | async def get_all_knoreq_user_objects(self, include_machine = False): | |
398 | async def get_all_knoreq_users(self, include_machine = False): | |
216 | 399 | """ |
217 | 400 | Fetches all user objects with useraccountcontrol DONT_REQ_PREAUTH flag set from the AD, and returns MSADUser object. |
218 | 401 | |
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 | ||
219 | 407 | """ |
220 | 408 | logger.debug('Polling AD for all user objects, machine accounts included: %s'% include_machine) |
221 | 409 | if include_machine == True: |
223 | 411 | else: |
224 | 412 | ldap_filter = r'(&(userAccountControl:1.2.840.113556.1.4.803:=4194304)(!(sAMAccountName=*$)))' |
225 | 413 | |
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 | |
228 | 419 | logger.debug('Finished polling for entries!') |
229 | ||
230 | ||
231 | #async def get_all_objectacl(self): | |
232 | # """ | |
233 | # Returns all ACL info for all AD objects | |
234 | # """ | |
235 | # | |
236 | # flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION | |
237 | # req_flags = SDFlagsRequestValue({'Flags' : flags_value}) | |
238 | # | |
239 | # ldap_filter = r'(objectClass=*)' | |
240 | # attributes = MSADSecurityInfo.ATTRS | |
241 | # controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] | |
242 | # | |
243 | # async for entry in self.pagedsearch(ldap_filter, attributes, controls = controls): | |
244 | # yield MSADSecurityInfo.from_ldap(entry) | |
245 | 420 | |
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}) | |
253 | 437 | |
254 | 438 | ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(dn) |
255 | 439 | attributes = MSADSecurityInfo.ATTRS |
256 | 440 | controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] |
257 | 441 | |
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 | |
261 | 517 | |
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 | ||
262 | 1024 | #async def get_all_tokengroups(self): |
263 | 1025 | # """ |
264 | 1026 | # returns the tokengroups attribute for all user and machine on the server |
281 | 1043 | # async for entry, err in self._con.response: |
282 | 1044 | # #yield MSADTokenGroup.from_ldap(entry) |
283 | 1045 | # 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 | ||
304 | 1047 | |
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) |
26 | 26 | self.is_guest = False |
27 | 27 | self.nt_hash = None |
28 | 28 | self.lm_hash = None |
29 | self.encrypt = False | |
29 | 30 | |
30 | 31 | class MSLDAPSIMPLECredential: |
31 | 32 | def __init__(self): |
45 | 46 | self.target = None #KerberosTarget |
46 | 47 | self.ksoc = None #KerberosSocketAIO |
47 | 48 | self.ccred = None |
49 | self.encrypt = False | |
50 | self.enctypes = None #[23,17,18] | |
48 | 51 | |
49 | 52 | class MSLDAPKerberosSSPICredential: |
50 | 53 | 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 | |
54 | 58 | |
55 | 59 | class MSLDAPNTLMSSPICredential: |
56 | 60 | 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] | |
59 | 106 | |
60 | 107 | |
61 | 108 | |
81 | 128 | ntlmcred.domain = self.creds.domain if self.creds.domain is not None else '' |
82 | 129 | ntlmcred.workstation = None |
83 | 130 | ntlmcred.is_guest = False |
131 | ntlmcred.encrypt = self.creds.encrypt | |
132 | ||
84 | 133 | |
85 | 134 | 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 | |
88 | 148 | |
89 | 149 | settings = NTLMHandlerSettings(ntlmcred) |
90 | 150 | return NTLMAUTHHandler(settings) |
109 | 169 | ntlmcred.domain = self.creds.domain if self.creds.domain is not None else '' |
110 | 170 | ntlmcred.workstation = None |
111 | 171 | ntlmcred.is_guest = False |
172 | ntlmcred.encrypt = self.creds.encrypt | |
112 | 173 | |
113 | 174 | if self.creds.password is None: |
114 | 175 | raise Exception('NTLM authentication requres password!') |
146 | 207 | if self.target.dc_ip is None: |
147 | 208 | raise Exception('target must have a dc_ip for kerberos!') |
148 | 209 | |
149 | ||
210 | kcred = MSLDAPKerberosCredential() | |
150 | 211 | kc = KerberosCredential() |
151 | 212 | kc.username = self.creds.username |
152 | 213 | kc.domain = self.creds.domain |
214 | kcred.enctypes = [] | |
153 | 215 | if self.creds.auth_method == LDAPAuthProtocol.KERBEROS_PASSWORD: |
154 | 216 | kc.password = self.creds.password |
217 | kcred.enctypes = [23,17,18] | |
155 | 218 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_NT: |
156 | 219 | kc.nt_hash = self.creds.password |
220 | kcred.enctypes = [23] | |
157 | 221 | |
158 | 222 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_AES: |
159 | 223 | if len(self.creds.password) == 32: |
160 | 224 | kc.kerberos_key_aes_128 = self.creds.password |
225 | kcred.enctypes = [17] | |
161 | 226 | elif len(self.creds.password) == 64: |
162 | 227 | kc.kerberos_key_aes_256 = self.creds.password |
228 | kcred.enctypes = [18] | |
163 | 229 | |
164 | 230 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_RC4: |
165 | 231 | kc.kerberos_key_rc4 = self.creds.password |
232 | kcred.enctypes = [23] | |
166 | 233 | |
167 | 234 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_CCACHE: |
168 | 235 | kc.ccache = self.creds.password |
236 | kcred.enctypes = [23,17,18] | |
169 | 237 | elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KEYTAB: |
170 | 238 | kc.keytab = self.creds.password |
239 | kcred.enctypes = [23,17,18] | |
171 | 240 | else: |
172 | 241 | 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 | ||
176 | 246 | kcred.ccred = kc |
177 | 247 | kcred.spn = KerberosSPN.from_target_string(self.target.to_target_string()) |
178 | 248 | kcred.target = KerberosTarget(self.target.dc_ip) |
249 | kcred.encrypt = self.creds.encrypt | |
250 | ||
179 | 251 | if self.target.proxy is not None: |
180 | 252 | kcred.target.proxy = KerberosProxy() |
253 | kcred.target.proxy.type = self.target.proxy.type | |
181 | 254 | kcred.target.proxy.target = copy.deepcopy(self.target.proxy.target) |
182 | 255 | kcred.target.proxy.target.endpoint_ip = self.target.dc_ip |
183 | 256 | kcred.target.proxy.target.endpoint_port = 88 |
195 | 268 | raise Exception('Target must be specified with Kerberos SSPI!') |
196 | 269 | |
197 | 270 | 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 | |
201 | 276 | |
202 | 277 | handler = MSLDAPKerberosSSPI(kerbcred) |
203 | 278 | #setting up SPNEGO |
207 | 282 | |
208 | 283 | elif self.creds.auth_method == LDAPAuthProtocol.SSPI_NTLM: |
209 | 284 | 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 | ||
213 | 290 | handler = MSLDAPNTLMSSPI(ntlmcred) |
214 | 291 | #setting up SPNEGO |
215 | 292 | spneg = SPNEGO() |
216 | 293 | spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) |
217 | 294 | return spneg |
218 | 295 | |
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() | |
226 | 300 | ntlmcred.type = 'NTLM' |
227 | if creds.username is not None: | |
301 | if self.creds.username is not None: | |
228 | 302 | ntlmcred.username = '<CURRENT>' |
229 | if creds.domain is not None: | |
303 | if self.creds.domain is not None: | |
230 | 304 | ntlmcred.domain = '<CURRENT>' |
231 | if creds.secret is not None: | |
305 | if self.creds.password is not None: | |
232 | 306 | ntlmcred.password = '<CURRENT>' |
233 | 307 | 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) | |
238 | 313 | #setting up SPNEGO |
239 | 314 | spneg = SPNEGO() |
240 | 315 | spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) |
241 | 316 | return spneg |
242 | 317 | |
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() | |
247 | 322 | 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: | |
250 | 325 | ntlmcred.username = '<CURRENT>' |
251 | if creds.domain is not None: | |
326 | if self.creds.domain is not None: | |
252 | 327 | ntlmcred.domain = '<CURRENT>' |
253 | if creds.secret is not None: | |
328 | if self.creds.password is not None: | |
254 | 329 | ntlmcred.password = '<CURRENT>' |
255 | 330 | 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) | |
260 | 336 | #setting up SPNEGO |
261 | 337 | spneg = SPNEGO() |
262 | 338 | spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler) |
263 | 339 | return spneg |
264 | """ | |
340 |
0 | import enum | |
1 | ||
2 | class MSLDAPClientStatus(enum.Enum): | |
3 | RUNNING = 'RUNNING' | |
4 | STOPPED = 'STOPPED' | |
5 | ERROR = 'ERROR' |
34 | 34 | KERBEROS_PASSWORD = 'KERBEROS_PASSWORD' #actually SASL-GSSAPI-SPNEGO-KERBEROS |
35 | 35 | KERBEROS_CCACHE = 'KERBEROS_CCACHE' #actually SASL-GSSAPI-SPNEGO-KERBEROS |
36 | 36 | 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' | |
39 | 41 | SSPI_NTLM = 'SSPI_NTLM' #actually SASL-GSSAPI-SPNEGO-NTLM but with integrated SSPI |
40 | 42 | SSPI_KERBEROS = 'SSPI_KERBEROS' #actually SASL-GSSAPI-SPNEGO-KERBEROS but with integrated SSPI |
41 | 43 | |
50 | 52 | LDAPAuthProtocol.KERBEROS_KEYTAB , |
51 | 53 | LDAPAuthProtocol.SSPI_NTLM , |
52 | 54 | 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 , | |
54 | 68 | ] |
55 | 69 | |
56 | 70 | 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): | |
58 | 90 | self.auth_method = auth_method |
59 | 91 | self.domain = domain |
60 | 92 | self.username = username |
61 | 93 | self.password = password |
94 | self.signing_preferred = False | |
95 | self.encryption_preferred = False | |
62 | 96 | self.settings = settings |
97 | self.etypes = etypes | |
98 | self.encrypt = encrypt | |
63 | 99 | |
64 | 100 | def get_msuser(self): |
65 | 101 | if not self.domain: |
18 | 18 | MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL' |
19 | 19 | |
20 | 20 | 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 | |
25 | 35 | |
26 | 36 | |
27 | 37 | @staticmethod |
28 | 38 | 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 | """ | |
29 | 47 | proxy = MSLDAPProxy() |
30 | 48 | url = urlparse(url_str) |
31 | 49 | if url.query is None: |
38 | 56 | proxy.type = MSLDAPProxyType(query['proxytype'][0].upper()) |
39 | 57 | if proxy.type in [MSLDAPProxyType.SOCKS4, MSLDAPProxyType.SOCKS4_SSL, MSLDAPProxyType.SOCKS5, MSLDAPProxyType.SOCKS5_SSL]: |
40 | 58 | cu = SocksClientURL.from_params(url_str) |
59 | proxy.target = cu.get_target() | |
60 | proxy.auth = cu.get_creds() | |
41 | 61 | 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) | |
44 | 63 | |
45 | proxy.target = cu.get_target() | |
46 | proxy.auth = cu.get_creds() | |
47 | 64 | return proxy |
48 | 65 | |
49 | 66 | def __str__(self): |
52 | 69 | t += '%s: %s\r\n' % (k, self.__dict__[k]) |
53 | 70 | |
54 | 71 | return t |
55 | ||
56 | ||
57 | 72 | |
58 | 73 | |
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 |
14 | 14 | |
15 | 15 | |
16 | 16 | 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 | """ | |
17 | 33 | def __init__(self, host, port = 389, proto = LDAPProtocol.TCP, tree = None, proxy = None, timeout = 10): |
18 | 34 | self.proto = proto |
19 | 35 | self.host = host |
22 | 38 | self.proxy = proxy |
23 | 39 | self.timeout = timeout |
24 | 40 | self.dc_ip = None |
41 | self.serverip = None | |
25 | 42 | self.domain = None |
26 | 43 | self.sslctx = None |
27 | 44 | |
28 | 45 | def get_ssl_context(self): |
29 | 46 | if self.proto == LDAPProtocol.SSL: |
30 | 47 | 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 | |
32 | 51 | return self.sslctx |
33 | 52 | return None |
34 | 53 |
6 | 6 | |
7 | 7 | import platform |
8 | 8 | import hashlib |
9 | import getpass | |
10 | import base64 | |
11 | import enum | |
9 | 12 | from urllib.parse import urlparse, parse_qs |
10 | 13 | |
11 | from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol | |
14 | from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol, MSLDAP_KERBEROS_PROTOCOLS | |
12 | 15 | from msldap.commons.target import MSLDAPTarget, LDAPProtocol |
13 | 16 | from msldap.commons.proxy import MSLDAPProxy, MSLDAPProxyType |
14 | 17 | from msldap.client import MSLDAPClient |
15 | ||
18 | from msldap.connection import MSLDAPClientConnection | |
19 | ||
20 | class PLAINTEXTSCHEME(enum.Enum): | |
21 | """ | |
22 | Additional conveinence functions. | |
23 | """ | |
24 | SIMPLE_PROMPT = 'SIMPLE_PROMPT' | |
25 | SIMPLE_HEX = 'SIMPLE_HEX' | |
26 | SIMPLE_B64 = 'SIMPLE_B64' | |
27 | PLAIN_PROMPT = 'PLAIN_PROMPT' | |
28 | PLAIN_HEX = 'PLAIN_HEX' | |
29 | PLAIN_B64 = 'PLAIN_B64' | |
30 | SICILY_PROMPT = 'SICILY_PROMPT' | |
31 | SICILY_HEX = 'SICILY_HEX' | |
32 | SICILY_B64 = 'SICILY_B64' | |
33 | NTLM_PROMPT = 'NTLM_PROMPT' | |
34 | NTLM_HEX = 'NTLM_HEX' | |
35 | NTLM_B64 = 'NTLM_B64' | |
16 | 36 | |
17 | 37 | class MSLDAPURLDecoder: |
38 | """ | |
39 | The URL describes both the connection target and the credentials. This class creates all necessary objects to set up the client. | |
40 | ||
41 | :param url: | |
42 | :type url: str | |
43 | """ | |
18 | 44 | |
19 | 45 | help_epilog = """ |
20 | 46 | MSLDAP URL Format: <protocol>+<auth>://<username>:<password>@<ip_or_host>:<port>/<tree>/?<param>=<value> |
21 | 47 | <protocol> sets the ldap protocol following values supported: |
22 | 48 | - ldap |
23 | 49 | - ldaps |
24 | <auth> can be omitted if plaintext authentication is to be performed, otherwise: | |
25 | - ntlm | |
26 | - sspi (windows only!) | |
50 | <auth> can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: | |
51 | - ntlm-password | |
52 | - ntlm-nt | |
53 | - kerberos-password (dc option param must be used) | |
54 | - kerberos-rc4 / kerberos-nt (dc option param must be used) | |
55 | - kerberos-aes (dc option param must be used) | |
56 | - kerberos-keytab (dc option param must be used) | |
57 | - kerberos-ccache (dc option param must be used) | |
58 | - sspi-ntlm (windows only!) | |
59 | - sspi-kerberos (windows only!) | |
27 | 60 | - anonymous |
28 | 61 | - plain |
62 | - simple | |
63 | - sicily (same format as ntlm-nt but using the SICILY authentication) | |
64 | <tree>: | |
65 | OPTIONAL. Specifies the root tree of all queries | |
29 | 66 | <param> can be: |
30 | 67 | - timeout : connction timeout in seconds |
31 | 68 | - proxytype: currently only socks5 proxy is supported |
32 | 69 | - proxyhost: Ip or hostname of the proxy server |
33 | 70 | - proxyport: port of the proxy server |
34 | 71 | - proxytimeout: timeout ins ecodns for the proxy connection |
72 | - dc: the IP address of the domain controller, MUST be used for kerberos authentication | |
35 | 73 | |
36 | 74 | Examples: |
37 | 75 | ldap://10.10.10.2 (anonymous bind) |
38 | 76 | ldaps://test.corp (anonymous bind) |
39 | ldap+sspi:///test.corp | |
77 | ldap+sspi-ntlm://test.corp | |
78 | ldap+sspi-kerberos://test.corp | |
40 | 79 | ldap://TEST\\victim:<password>@10.10.10.2 (defaults to SASL GSSAPI NTLM) |
41 | 80 | ldap+simple://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) |
42 | 81 | ldap+plain://TEST\\victim:<password>@10.10.10.2 (SASL SIMPLE auth) |
57 | 96 | self.domain = None |
58 | 97 | self.username = None |
59 | 98 | self.password = None |
99 | self.encrypt = False | |
60 | 100 | self.auth_settings = {} |
101 | self.etypes = None | |
61 | 102 | |
62 | 103 | self.ldap_proto = None |
63 | 104 | self.ldap_host = None |
69 | 110 | self.serverip = None |
70 | 111 | self.proxy = None |
71 | 112 | |
113 | self.__pwpreprocess = None | |
114 | ||
72 | 115 | self.parse() |
73 | 116 | |
74 | 117 | |
75 | 118 | 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( | |
77 | 126 | domain=self.domain, |
78 | 127 | username=self.username, |
79 | 128 | password = self.password, |
80 | 129 | auth_method=self.auth_scheme, |
81 | 130 | settings = self.auth_settings |
82 | 131 | ) |
132 | t.encrypt = self.encrypt | |
133 | t.etypes = self.etypes | |
134 | ||
135 | return t | |
83 | 136 | |
84 | 137 | def get_target(self): |
138 | """ | |
139 | Creates a target object | |
140 | ||
141 | :return: Target object | |
142 | :rtype: :class:`MSLDAPTarget` | |
143 | """ | |
85 | 144 | target = MSLDAPTarget( |
86 | 145 | self.ldap_host, |
87 | 146 | port = self.ldap_port, |
92 | 151 | target.domain = self.domain |
93 | 152 | target.dc_ip = self.dc_ip |
94 | 153 | target.proxy = self.proxy |
154 | target.serverip = self.serverip | |
95 | 155 | return target |
96 | 156 | |
97 | 157 | 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 | """ | |
98 | 164 | cred = self.get_credential() |
99 | 165 | target = self.get_target() |
100 | 166 | 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) | |
101 | 178 | |
102 | 179 | def scheme_decoder(self, scheme): |
103 | 180 | schemes = [] |
126 | 203 | return |
127 | 204 | |
128 | 205 | 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' | |
130 | 254 | except: |
131 | raise Exception('Uknown scheme!') | |
255 | try: | |
256 | self.auth_scheme = LDAPAuthProtocol(schemes[1]) | |
257 | except: | |
258 | raise Exception('Uknown scheme!') | |
132 | 259 | |
133 | 260 | return |
134 | 261 | |
137 | 264 | self.scheme_decoder(url_e.scheme) |
138 | 265 | |
139 | 266 | 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 | ||
140 | 280 | |
141 | 281 | if url_e.username is not None: |
142 | 282 | if url_e.username.find('\\') != -1: |
172 | 312 | proxy_present = False |
173 | 313 | if url_e.query is not None: |
174 | 314 | query = parse_qs(url_e.query) |
315 | if 'etype' in query: | |
316 | self.etypes = [] | |
175 | 317 | for k in query: |
176 | 318 | if k.startswith('proxy') is True: |
177 | 319 | proxy_present = True |
180 | 322 | elif k == 'timeout': |
181 | 323 | self.timeout = int(query[k][0]) |
182 | 324 | elif k == 'serverip': |
183 | self.server_ip = query[k][0] | |
325 | self.serverip = query[k][0] | |
184 | 326 | elif k == 'dns': |
185 | 327 | 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]] | |
186 | 332 | elif k.startswith('auth'): |
187 | 333 | self.auth_settings[k[len('auth'):]] = query[k] |
188 | 334 | #elif k.startswith('same'): |
201 | 347 | if self.domain is None: |
202 | 348 | self.domain = '<CURRENT>' |
203 | 349 | |
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 | ||
293 | 354 | # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]: |
294 | 355 | # if self.proxy_port is None: |
295 | 356 | # self.proxy_port = 9999 |
296 | # | |
297 | # #sanity checks... | |
298 | # if self.proxy_scheme is not None: | |
299 | # if self.proxy_ip is None: | |
300 | # raise Exception('proxyserver MUST be provided if using proxy') | |
301 | 357 | # |
302 | 358 | # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]: |
303 | 359 | # if 'agentid' not in self.proxy_settings: |
304 | 360 | # raise Exception('multiplexor proxy reuires agentid to be set! Set it via proxyagentid parameter!') |
305 | 361 | # |
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 | ||
317 | 363 | |
318 | 364 | |
319 | 365 |
0 | 0 | import asyncio |
1 | 1 | |
2 | ||
2 | 3 | from msldap import logger |
4 | from msldap.commons.common import MSLDAPClientStatus | |
3 | 5 | from msldap.protocol.messages import LDAPMessage, BindRequest, \ |
4 | 6 | protocolOp, AuthenticationChoice, SaslCredentials, \ |
5 | 7 | SearchRequest, AttributeDescription, Filter, Filters, \ |
6 | Controls, Control, SearchControlValue | |
8 | Controls, Control, SearchControlValue, AddRequest, \ | |
9 | ModifyRequest, DelRequest | |
7 | 10 | |
8 | 11 | from msldap.protocol.utils import calcualte_length |
9 | from msldap.protocol.typeconversion import convert_result, convert_attributes | |
12 | from msldap.protocol.typeconversion import convert_result, convert_attributes, encode_attributes, encode_changes | |
13 | from msldap.protocol.query import escape_filter_chars, query_syntax_converter | |
10 | 14 | from msldap.commons.authbuilder import AuthenticatorBuilder |
11 | 15 | from msldap.commons.credential import MSLDAP_GSS_METHODS |
12 | 16 | from msldap.network.selector import MSLDAPNetworkSelector |
13 | 17 | from msldap.commons.credential import LDAPAuthProtocol |
18 | from msldap.commons.target import LDAPProtocol | |
19 | from asn1crypto.x509 import Certificate | |
20 | from hashlib import sha256 | |
21 | from minikerberos.gssapi.channelbindings import ChannelBindingsStruct | |
14 | 22 | |
15 | 23 | class MSLDAPClientConnection: |
16 | 24 | def __init__(self, target, creds): |
25 | if target is None: | |
26 | raise Exception('Target cant be none!') | |
17 | 27 | self.target = target |
18 | 28 | self.creds = creds |
19 | 29 | self.auth = AuthenticatorBuilder(self.creds, self.target).build() |
24 | 34 | self.network = None |
25 | 35 | |
26 | 36 | self.handle_incoming_task = None |
37 | self.status = MSLDAPClientStatus.RUNNING | |
38 | self.lasterror = None | |
27 | 39 | |
28 | 40 | self.message_id = 0 |
29 | 41 | self.message_table = {} |
30 | 42 | 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 | |
32 | 45 | |
33 | 46 | async def __handle_incoming(self): |
34 | 47 | try: |
35 | 48 | while True: |
36 | 49 | message_data, err = await self.network.in_queue.get() |
37 | 50 | if err is not None: |
38 | logger.debug('Client terminating bc __handle_incoming!') | |
51 | logger.debug('Client terminating bc __handle_incoming got an error!') | |
39 | 52 | raise err |
40 | 53 | |
41 | ################################ | |
42 | # # | |
43 | # ADD CHANNEL BINDING HERE! # | |
44 | ################################ | |
45 | ||
54 | #print('Incoming message data: %s' % message_data) | |
46 | 55 | if self.bind_ok is True: |
47 | 56 | if self.__encrypt_messages is True: |
48 | #print('Encrypted %s' % message_data) | |
49 | 57 | #removing size |
50 | 58 | message_data = message_data[4:] |
51 | 59 | 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 | |
53 | 64 | #print('Decrypted %s' % message_data.hex()) |
65 | #print('Decrypted %s' % message_data) | |
54 | 66 | except: |
55 | 67 | import traceback |
56 | 68 | traceback.print_exc() |
69 | raise | |
57 | 70 | |
58 | 71 | elif self.__sign_messages is True: |
59 | 72 | #print('Signed %s' % message_data) |
64 | 77 | except: |
65 | 78 | import traceback |
66 | 79 | traceback.print_exc() |
80 | raise | |
81 | ||
67 | 82 | |
68 | 83 | msg_len = calcualte_length(message_data) |
69 | 84 | msg_total_len = len(message_data) |
90 | 105 | self.message_table_notify[message_id].set() |
91 | 106 | |
92 | 107 | except asyncio.CancelledError: |
93 | #not notifying clients, at this point the client is terminating | |
108 | self.status = MSLDAPClientStatus.STOPPED | |
94 | 109 | return |
95 | 110 | |
96 | 111 | except Exception as e: |
97 | import traceback | |
98 | traceback.print_exc() | |
112 | self.status = MSLDAPClientStatus.ERROR | |
113 | self.lasterror = e | |
99 | 114 | for msgid in self.message_table_notify: |
100 | 115 | self.message_table[msgid] = [e] |
101 | 116 | self.message_table_notify[msgid].set() |
117 | ||
118 | self.status = MSLDAPClientStatus.STOPPED | |
102 | 119 | |
103 | 120 | |
104 | 121 | async def send_message(self, message): |
142 | 159 | return messages |
143 | 160 | |
144 | 161 | 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 | |
153 | 191 | |
154 | 192 | async def disconnect(self): |
193 | """ | |
194 | Tears down the connection. | |
195 | ||
196 | :return: Nothing | |
197 | :rtype: None | |
198 | """ | |
199 | ||
155 | 200 | logger.debug('Disconnecting!') |
156 | 201 | 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() | |
159 | 206 | |
160 | 207 | |
161 | 208 | def __bind_success(self): |
172 | 219 | self.network.is_plain_msg = False |
173 | 220 | |
174 | 221 | 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 | """ | |
175 | 229 | logger.debug('BIND in progress...') |
176 | 230 | try: |
177 | 231 | 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 | ||
180 | 237 | auth = { |
181 | 238 | 'sicily_disco' : b'' |
182 | 239 | } |
229 | 286 | res['protocolOp']['diagnosticMessage'] |
230 | 287 | )) |
231 | 288 | |
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 | |
233 | 292 | |
234 | 293 | auth = { |
235 | 294 | 'sicily_resp' : data |
303 | 362 | elif self.creds.auth_method in MSLDAP_GSS_METHODS: |
304 | 363 | challenge = None |
305 | 364 | 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 | |
307 | 371 | |
308 | 372 | sasl = { |
309 | 373 | 'mechanism' : 'GSS-SPNEGO'.encode(), |
315 | 379 | |
316 | 380 | bindreq = { |
317 | 381 | 'version' : 3, |
318 | 'name': ''.encode(), | |
382 | 'name': b'', | |
319 | 383 | 'authentication': AuthenticationChoice(auth), |
320 | 384 | } |
321 | 385 | |
329 | 393 | return False, res |
330 | 394 | res = res.native |
331 | 395 | 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() | |
332 | 402 | self.__bind_success() |
403 | ||
333 | 404 | return True, None |
334 | 405 | |
335 | 406 | elif res['protocolOp']['resultCode'] == 'saslBindInProgress': |
343 | 414 | res['protocolOp']['diagnosticMessage'] |
344 | 415 | )) |
345 | 416 | |
346 | #print(res) | |
417 | else: | |
418 | raise Exception('Not implemented authentication method: %s' % self.creds.auth_method.name) | |
347 | 419 | 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: | |
349 | 532 | return False, e |
350 | 533 | |
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 | |
355 | 565 | try: |
356 | 566 | if timeLimit is None: |
357 | 567 | timeLimit = 600 #not sure |
568 | ||
569 | flt = query_syntax_converter(query) | |
358 | 570 | |
359 | 571 | searchreq = { |
360 | 'baseObject' : base, | |
572 | 'baseObject' : base.encode(), | |
361 | 573 | 'scope': search_scope, |
362 | 574 | 'derefAliases': derefAliases, |
363 | 'sizeLimit': paged_size, | |
575 | 'sizeLimit': size_limit, | |
364 | 576 | 'timeLimit': timeLimit, |
365 | 'typesOnly': typesOnly, | |
366 | 'filter': filter, | |
577 | 'typesOnly': types_only, | |
578 | 'filter': flt, | |
367 | 579 | 'attributes': attributes, |
368 | 580 | } |
369 | 581 | |
380 | 592 | msg_type = message['protocolOp'].name |
381 | 593 | message = message.native |
382 | 594 | if msg_type == 'searchResDone': |
383 | #print(message) | |
384 | #print('BREAKING!') | |
385 | 595 | if return_done is True: |
386 | 596 | yield (message, None) |
387 | 597 | break |
402 | 612 | except Exception as e: |
403 | 613 | yield (None, e) |
404 | 614 | |
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 | |
406 | 644 | try: |
407 | 645 | cookie = b'' |
408 | 646 | while True: |
411 | 649 | Control({ |
412 | 650 | 'controlType' : b'1.2.840.113556.1.4.319', |
413 | 651 | 'controlValue': SearchControlValue({ |
414 | 'size' : paged_size, | |
652 | 'size' : size_limit, | |
415 | 653 | 'cookie': cookie |
416 | 654 | }).dump() |
417 | 655 | }) |
426 | 664 | |
427 | 665 | async for res, err in self.search( |
428 | 666 | base, |
429 | filter, | |
667 | query, | |
430 | 668 | attributes, |
431 | 669 | search_scope = search_scope, |
432 | paged_size=paged_size, | |
433 | typesOnly=typesOnly, | |
670 | size_limit=size_limit, | |
671 | types_only=typesOnly, | |
434 | 672 | derefAliases=derefAliases, |
435 | 673 | timeLimit=timeLimit, |
436 | 674 | controls = ctrs, |
439 | 677 | if err is not None: |
440 | 678 | yield (None, err) |
441 | 679 | return |
442 | ||
680 | ||
443 | 681 | if 'resultCode' in res['protocolOp']: |
444 | 682 | for control in res['controls']: |
445 | 683 | if control['controlType'] == b'1.2.840.113556.1.4.319': |
461 | 699 | |
462 | 700 | |
463 | 701 | async def get_serverinfo(self): |
702 | if self.status != MSLDAPClientStatus.RUNNING: | |
703 | return None, Exception('Connection not running! Probably encountered an error') | |
704 | ||
464 | 705 | attributes = [ |
465 | 706 | b'subschemaSubentry', |
466 | 707 | b'dsServiceName', |
496 | 737 | |
497 | 738 | msg_id = await self.send_message(msg) |
498 | 739 | res = await self.recv_message(msg_id) |
499 | res = res[0].native | |
500 | ||
740 | res = res[0] | |
501 | 741 | if isinstance(res, Exception): |
502 | 742 | return None, res |
503 | 743 | |
504 | 744 | #print('res') |
505 | 745 | #print(res) |
506 | return convert_attributes(res['protocolOp']['attributes']), None | |
746 | return convert_attributes(res.native['protocolOp']['attributes']), None | |
507 | 747 | |
508 | 748 | |
509 | 749 | async def amain(): |
524 | 764 | #target.dc_ip = '10.10.10.2' |
525 | 765 | #target.domain = 'TEST' |
526 | 766 | |
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' | |
528 | 768 | |
529 | 769 | dec = MSLDAPURLDecoder(url) |
530 | 770 | cred = dec.get_credential() |
540 | 780 | res, err = await client.bind() |
541 | 781 | if err is not None: |
542 | 782 | raise err |
543 | ||
544 | #res = await client.search_test_2() | |
545 | #pprint.pprint(res) | |
546 | #search = bytes.fromhex('30840000007702012663840000006e043c434e3d3430392c434e3d446973706c6179537065636966696572732c434e3d436f6e66696775726174696f6e2c44433d746573742c44433d636f72700a01000a010002010002020258010100870b6f626a656374436c61737330840000000d040b6f626a656374436c617373') | |
547 | #msg = LDAPMessage.load(search) | |
548 | ||
549 | 783 | |
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!') | |
550 | 800 | |
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) | |
556 | 804 | |
557 | flt = query_syntax_converter(qry) | |
558 | i = 0 | |
559 | async for res, err in client.pagedsearch(base.encode(), flt, ['*'.encode()], derefAliases=3, typesOnly=False): | |
560 | if err is not None: | |
561 | print('Error!') | |
562 | raise err | |
563 | i += 1 | |
564 | if i % 1000 == 0: | |
565 | print(i) | |
566 | #pprint.pprint(res) | |
567 | ||
568 | 805 | await client.disconnect() |
569 | 806 | |
570 | 807 | |
577 | 814 | |
578 | 815 | logger.setLevel(2) |
579 | 816 | |
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 | ||
589 | 820 | |
590 | #input() | |
591 | ||
592 | asyncio.run(amain()) | |
593 | ||
594 | ||
595 | #qry = '(&(sAMAccountType=805306369)(sAMAccountName=test))' | |
596 | #qry = '(sAMAccountName=*)' | |
597 | #flt = LF.parse(qry) | |
598 | #print(flt) | |
599 | #print(flt.__dict__) | |
600 | #for f in flt.filters: | |
601 | # print(f.__dict__) | |
602 | ||
603 | #x = convert(flt) | |
604 | #print(x) | |
605 | #print(x.native) | |
606 | ||
607 | #qry = '(sAMAccountType=0x100)' | |
608 | #flt = Filter.parse(qry) | |
609 | #print(flt) | |
610 | #print(flt.__dict__) | |
611 | #print(flt.filters) | |
612 | 821 | |
613 | 822 | |
614 | 823 |
4 | 4 | currently it's not the perfect wrapper, needs to be extended |
5 | 5 | """ |
6 | 6 | |
7 | from aiosmb.crypto.BASE import symmetricBASE, cipherMODE | |
8 | from aiosmb.crypto.pure.RC4.RC4 import RC4 as _pureRC4 | |
7 | from msldap.crypto.BASE import symmetricBASE, cipherMODE | |
8 | from msldap.crypto.pure.RC4.RC4 import RC4 as _pureRC4 | |
9 | 9 | try: |
10 | 10 | from Crypto.Cipher import ARC4 as _pyCryptoRC4 |
11 | 11 | except Exception as e: |
0 | 0 | import hashlib |
1 | 1 | import hmac |
2 | 2 | |
3 | from aiosmb.crypto.BASE import hashBASE, hmacBASE | |
3 | from msldap.crypto.BASE import hashBASE, hmacBASE | |
4 | 4 | |
5 | 5 | class md5(hashBASE): |
6 | 6 | def __init__(self, data = None): |
9 | 9 | import csv |
10 | 10 | import shlex |
11 | 11 | import datetime |
12 | ||
13 | from aiocmd import aiocmd | |
14 | from asciitree import LeftAligned | |
12 | import copy | |
13 | ||
14 | from msldap.external.aiocmd.aiocmd import aiocmd | |
15 | from msldap.external.asciitree.asciitree import LeftAligned | |
15 | 16 | from tqdm import tqdm |
16 | 17 | |
17 | 18 | from msldap import logger |
21 | 22 | from msldap.ldap_objects import MSADUser, MSADMachine, MSADUser_TSV_ATTRS |
22 | 23 | |
23 | 24 | from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR |
25 | from winacl.dtyp.sid import SID | |
24 | 26 | |
25 | 27 | |
26 | 28 | class MSLDAPClientConsole(aiocmd.PromptToolkitCmd): |
32 | 34 | self.connection = None |
33 | 35 | self.adinfo = None |
34 | 36 | self.ldapinfo = None |
37 | self.domain_name = None | |
35 | 38 | |
36 | 39 | async def do_login(self, url = None): |
37 | 40 | """Performs connection and login""" |
38 | try: | |
39 | print('url %s' % repr(url)) | |
40 | ||
41 | try: | |
41 | 42 | if self.conn_url is None and url is None: |
42 | 43 | print('Not url was set, cant do logon') |
43 | 44 | if url is not None: |
44 | 45 | self.conn_url = MSLDAPURLDecoder(url) |
45 | 46 | |
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()) | |
48 | 49 | |
49 | 50 | |
50 | 51 | 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 | |
55 | 60 | |
56 | 61 | async def do_ldapinfo(self, show = True): |
57 | 62 | """Prints detailed LDAP connection info (DSA)""" |
59 | 64 | if self.ldapinfo is None: |
60 | 65 | self.ldapinfo = self.connection.get_server_info() |
61 | 66 | 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 | |
65 | 73 | |
66 | 74 | async def do_adinfo(self, show = True): |
67 | 75 | """Prints detailed Active Driectory info""" |
68 | 76 | try: |
69 | 77 | if self.adinfo is None: |
70 | 78 | self.adinfo = self.connection._ldapinfo |
79 | self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') | |
71 | 80 | if show is True: |
72 | 81 | print(self.adinfo) |
73 | 82 | except: |
77 | 86 | """Fetches kerberoastable user accounts""" |
78 | 87 | try: |
79 | 88 | 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 | |
81 | 92 | print(user.sAMAccountName) |
82 | 93 | except: |
83 | 94 | traceback.print_exc() |
86 | 97 | """Fetches ASREP-roastable user accounts""" |
87 | 98 | try: |
88 | 99 | 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 | |
90 | 103 | print(user.sAMAccountName) |
91 | 104 | except: |
92 | 105 | traceback.print_exc() |
93 | 106 | |
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() | |
94 | 124 | |
95 | 125 | async def do_dump(self): |
96 | 126 | """Fetches ALL user and machine accounts from the domain with a LOT of attributes""" |
101 | 131 | users_filename = 'users_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") |
102 | 132 | pbar = tqdm(desc = 'Writing users to file %s' % users_filename) |
103 | 133 | 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 | |
105 | 137 | pbar.update() |
106 | 138 | f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS))) |
107 | 139 | print('Users dump was written to %s' % users_filename) |
109 | 141 | users_filename = 'computers_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") |
110 | 142 | pbar = tqdm(desc = 'Writing computers to file %s' % users_filename) |
111 | 143 | 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 | |
113 | 147 | pbar.update() |
114 | 148 | f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS))) |
115 | 149 | print('Computer dump was written to %s' % users_filename) |
126 | 160 | attributes = attributes.split(',') |
127 | 161 | logging.debug('Query: %s' % (query)) |
128 | 162 | 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 | |
130 | 166 | print(entry) |
131 | 167 | except: |
132 | 168 | traceback.print_exc() |
164 | 200 | try: |
165 | 201 | await self.do_ldapinfo(False) |
166 | 202 | 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: | |
168 | 209 | print(user) |
169 | 210 | except: |
170 | 211 | traceback.print_exc() |
171 | 212 | |
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): | |
173 | 376 | """Feteches security info for a given DN""" |
174 | 377 | try: |
175 | 378 | await self.do_ldapinfo(False) |
176 | 379 | 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()) | |
179 | 385 | except: |
180 | 386 | traceback.print_exc() |
181 | 387 | |
184 | 390 | try: |
185 | 391 | await self.do_ldapinfo(False) |
186 | 392 | 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 | |
188 | 396 | print(gpo) |
189 | 397 | except: |
190 | 398 | traceback.print_exc() |
192 | 400 | async def do_laps(self): |
193 | 401 | """Feteches all laps passwords""" |
194 | 402 | 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 | |
196 | 406 | 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'] | |
199 | 409 | print('%s : %s' % (entry['attributes']['cn'], pwd)) |
200 | 410 | except: |
201 | 411 | traceback.print_exc() |
206 | 416 | await self.do_ldapinfo(False) |
207 | 417 | await self.do_adinfo(False) |
208 | 418 | 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 | |
210 | 422 | 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 | |
212 | 426 | print('%s - %s' % (group_dn, group_sid)) |
213 | 427 | |
214 | 428 | if len(group_sids) == 0: |
215 | 429 | print('No memberships found') |
216 | except: | |
430 | except Exception as e: | |
431 | print(e) | |
217 | 432 | traceback.print_exc() |
218 | 433 | |
219 | 434 | async def do_bindtree(self, newtree): |
225 | 440 | async def do_trusts(self): |
226 | 441 | """Feteches gives back domain trusts""" |
227 | 442 | 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 | |
229 | 446 | print(entry.get_line()) |
230 | 447 | except: |
231 | 448 | traceback.print_exc() |
232 | 449 | |
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 | ||
233 | 551 | async def do_test(self): |
234 | 552 | """testing, dontuse""" |
235 | 553 | 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 | ||
237 | 558 | if entry.objectClass[-1] != 'user': |
238 | 559 | print(entry.objectClass) |
239 | 560 | except: |
258 | 579 | await client.run() |
259 | 580 | else: |
260 | 581 | for command in args.commands: |
582 | if command == 'i': | |
583 | await client.run() | |
584 | return | |
261 | 585 | 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 | |
263 | 589 | |
264 | 590 | def main(): |
265 | 591 | import argparse |
267 | 593 | parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') |
268 | 594 | parser.add_argument('-n', '--no-interactive', action='store_true') |
269 | 595 | 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.") | |
271 | 597 | |
272 | 598 | args = parser.parse_args() |
273 | 599 |
0 | #!/usr/bin/env python3 | |
1 | # | |
2 | # Author: | |
3 | # Tamas Jos (@skelsec) | |
4 | # | |
5 | ||
6 | import asyncio | |
7 | import traceback | |
8 | import logging | |
9 | import csv | |
10 | import shlex | |
11 | import datetime | |
12 | ||
13 | from msldap.external.aiocmd.aiocmd import aiocmd | |
14 | from msldap.external.asciitree.asciitree import LeftAligned | |
15 | from tqdm import tqdm | |
16 | ||
17 | from msldap import logger | |
18 | from asysocks import logger as sockslogger | |
19 | from msldap.client import MSLDAPClient | |
20 | from msldap.commons.url import MSLDAPURLDecoder | |
21 | from msldap.ldap_objects import MSADUser, MSADMachine, MSADUser_TSV_ATTRS | |
22 | ||
23 | from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR | |
24 | ||
25 | ||
26 | class MSLDAPCompDomainList: | |
27 | def __init__(self, ldap_url): | |
28 | self.conn_url = ldap_url | |
29 | self.connection = None | |
30 | self.adinfo = None | |
31 | self.ldapinfo = None | |
32 | self.domain_name = None | |
33 | ||
34 | async def login(self): | |
35 | """Performs connection and login""" | |
36 | try: | |
37 | logger.debug(self.conn_url.get_credential()) | |
38 | logger.debug(self.conn_url.get_target()) | |
39 | ||
40 | ||
41 | self.connection = self.conn_url.get_client() | |
42 | _, err = await self.connection.connect() | |
43 | if err is not None: | |
44 | raise err | |
45 | ||
46 | return True, None | |
47 | except Exception as e: | |
48 | return False, e | |
49 | ||
50 | async def do_adinfo(self, show = True): | |
51 | """Prints detailed Active Driectory info""" | |
52 | try: | |
53 | if self.adinfo is None: | |
54 | self.adinfo = self.connection._ldapinfo | |
55 | self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') | |
56 | if show is True: | |
57 | print(self.adinfo) | |
58 | ||
59 | return True, None | |
60 | except Exception as e: | |
61 | return False, e | |
62 | ||
63 | async def run(self): | |
64 | try: | |
65 | _, err = await self.login() | |
66 | if err is not None: | |
67 | raise err | |
68 | _, err = await self.do_adinfo(False) | |
69 | if err is not None: | |
70 | raise err | |
71 | #machine_filename = '%s_computers_%s.txt' % (self.domain_name, datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) | |
72 | ||
73 | async for machine, err in self.connection.get_all_machines(attrs=['sAMAccountName', 'dNSHostName']): | |
74 | if err is not None: | |
75 | raise err | |
76 | ||
77 | dns = machine.dNSHostName | |
78 | if dns is None: | |
79 | dns = '%s.%s' % (machine.sAMAccountName[:-1], self.domain_name) | |
80 | ||
81 | print(str(dns)) | |
82 | except: | |
83 | traceback.print_exc() | |
84 | ||
85 | ||
86 | def main(): | |
87 | import argparse | |
88 | parser = argparse.ArgumentParser(description='MS LDAP library') | |
89 | parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') | |
90 | parser.add_argument('-n', '--no-interactive', action='store_true') | |
91 | parser.add_argument('url', help='Connection string in URL format.') | |
92 | ||
93 | args = parser.parse_args() | |
94 | ||
95 | ||
96 | ###### VERBOSITY | |
97 | if args.verbose == 0: | |
98 | logging.basicConfig(level=logging.INFO) | |
99 | else: | |
100 | sockslogger.setLevel(logging.DEBUG) | |
101 | logger.setLevel(logging.DEBUG) | |
102 | logging.basicConfig(level=logging.DEBUG) | |
103 | ||
104 | ldap_url = MSLDAPURLDecoder(args.url) | |
105 | compdomlist = MSLDAPCompDomainList(ldap_url) | |
106 | ||
107 | ||
108 | asyncio.run(compdomlist.run()) | |
109 | ||
110 | if __name__ == '__main__': | |
111 | main()⏎ |
0 | import asyncio | |
1 | import inspect | |
2 | import shlex | |
3 | import signal | |
4 | import sys | |
5 | ||
6 | from prompt_toolkit import PromptSession | |
7 | from prompt_toolkit.completion import WordCompleter | |
8 | from prompt_toolkit.key_binding import KeyBindings | |
9 | from prompt_toolkit.patch_stdout import patch_stdout | |
10 | ||
11 | try: | |
12 | from prompt_toolkit.completion.nested import NestedCompleter | |
13 | except ImportError: | |
14 | from aiocmd.nested_completer import NestedCompleter | |
15 | ||
16 | ||
17 | class ExitPromptException(Exception): | |
18 | pass | |
19 | ||
20 | ||
21 | class PromptToolkitCmd: | |
22 | """Baseclass for custom CLIs | |
23 | ||
24 | Works similarly to the built-in Cmd class. You can inherit from this class and implement: | |
25 | - do_<action> - This will add the "<action>" command to the cli. | |
26 | The method may receive arguments (required) and keyword arguments (optional). | |
27 | - _<action>_completions - Returns a custom Completer class to use as a completer for this action. | |
28 | Additionally, the user cant change the "prompt" variable to change how the prompt looks, and add | |
29 | command aliases to the 'aliases' dict. | |
30 | """ | |
31 | ATTR_START = "do_" | |
32 | prompt = "$ " | |
33 | doc_header = "Documented commands:" | |
34 | aliases = {"?": "help", "exit": "quit"} | |
35 | ||
36 | def __init__(self, ignore_sigint=True): | |
37 | self.completer = self._make_completer() | |
38 | self.session = None | |
39 | self._ignore_sigint = ignore_sigint | |
40 | self._currently_running_task = None | |
41 | ||
42 | async def run(self): | |
43 | if self._ignore_sigint and sys.platform != "win32": | |
44 | asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self._sigint_handler) | |
45 | self.session = PromptSession(enable_history_search=True, key_bindings=self._get_bindings()) | |
46 | try: | |
47 | with patch_stdout(): | |
48 | await self._run_prompt_forever() | |
49 | finally: | |
50 | if self._ignore_sigint and sys.platform != "win32": | |
51 | asyncio.get_event_loop().remove_signal_handler(signal.SIGINT) | |
52 | self._on_close() | |
53 | ||
54 | async def _run_prompt_forever(self): | |
55 | while True: | |
56 | try: | |
57 | result = await self.session.prompt_async(self.prompt, completer=self.completer) | |
58 | except EOFError: | |
59 | return | |
60 | ||
61 | if not result: | |
62 | continue | |
63 | args = shlex.split(result) | |
64 | if args[0] in self.command_list: | |
65 | try: | |
66 | self._currently_running_task = asyncio.ensure_future( | |
67 | self._run_single_command(args[0], args[1:])) | |
68 | await self._currently_running_task | |
69 | except asyncio.CancelledError: | |
70 | print() | |
71 | continue | |
72 | except ExitPromptException: | |
73 | return | |
74 | else: | |
75 | print("Command %s not found!" % args[0]) | |
76 | ||
77 | def _sigint_handler(self): | |
78 | if self._currently_running_task: | |
79 | self._currently_running_task.cancel() | |
80 | ||
81 | def _get_bindings(self): | |
82 | bindings = KeyBindings() | |
83 | bindings.add("c-c")(lambda event: self._interrupt_handler(event)) | |
84 | return bindings | |
85 | ||
86 | async def _run_single_command(self, command, args): | |
87 | command_real_args, command_real_kwargs = self._get_command_args(command) | |
88 | if len(args) < len(command_real_args) or len(args) > (len(command_real_args) | |
89 | + len(command_real_kwargs)): | |
90 | print("Bad command args. Usage: %s" % self._get_command_usage(command, command_real_args, | |
91 | command_real_kwargs)) | |
92 | return | |
93 | ||
94 | try: | |
95 | com_func = self._get_command(command) | |
96 | if asyncio.iscoroutinefunction(com_func): | |
97 | 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 | ) |
8 | 8 | from msldap.ldap_objects.adcomp import MSADMachine, MSADMachine_ATTRS, MSADMachine_TSV_ATTRS |
9 | 9 | from msldap.ldap_objects.adsec import MSADSecurityInfo, MSADTokenGroup |
10 | 10 | from msldap.ldap_objects.common import MSLDAP_UAC |
11 | from msldap.ldap_objects.adgroup import MSADGroup | |
12 | from msldap.ldap_objects.adou import MSADOU | |
11 | from msldap.ldap_objects.adgroup import MSADGroup, MSADGroup_ATTRS | |
12 | from msldap.ldap_objects.adou import MSADOU, MSADOU_ATTRS | |
13 | 13 | from msldap.ldap_objects.adgpo import MSADGPO, MSADGPO_ATTRS |
14 | 14 | from msldap.ldap_objects.adtrust import MSADDomainTrust, MSADDomainTrust_ATTRS |
15 | from msldap.ldap_objects.adschemaentry import MSADSCHEMAENTRY_ATTRS, MSADSchemaEntry | |
15 | 16 | |
16 | 17 | __all__ = [ |
17 | 18 | 'MSADUser', |
31 | 32 | 'MSADGPO_ATTRS', |
32 | 33 | 'MSADDomainTrust', |
33 | 34 | 'MSADDomainTrust_ATTRS', |
35 | 'MSADGroup_ATTRS', | |
36 | 'MSADOU_ATTRS', | |
37 | 'MSADSCHEMAENTRY_ATTRS', | |
38 | 'MSADSchemaEntry', | |
34 | 39 | ]⏎ |
3 | 3 | # Tamas Jos (@skelsec) |
4 | 4 | # |
5 | 5 | |
6 | import datetime | |
6 | 7 | from msldap.ldap_objects.common import MSLDAP_UAC, vn |
7 | 8 | |
8 | 9 | MSADMachine_ATTRS = [ |
14 | 15 | 'operatingSystem', 'operatingSystemVersion','primaryGroupID', |
15 | 16 | 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl', |
16 | 17 | 'whenChanged', 'whenCreated', 'servicePrincipalName','msDS-AllowedToDelegateTo', |
18 | 'msDS-AllowedToActOnBehalfOfOtherIdentity' | |
17 | 19 | ] |
18 | 20 | |
19 | 21 | MSADMachine_TSV_ATTRS = [ |
62 | 64 | self.whenCreated = None |
63 | 65 | self.servicePrincipalName = None |
64 | 66 | self.allowedtodelegateto = None |
67 | self.allowedtoactonbehalfofotheridentity = None | |
68 | ||
69 | ## calculated properties | |
70 | self.when_pw_change = None #datetime | |
71 | self.when_pw_expires = None #datetime | |
72 | self.must_change_pw = None #datetime | |
73 | self.canLogon = None #bool | |
74 | ||
75 | # https://msdn.microsoft.com/en-us/library/cc245739.aspx | |
76 | def calc_PasswordMustChange(self, adinfo): | |
77 | # Crtieria 1 | |
78 | flags = [MSLDAP_UAC.DONT_EXPIRE_PASSWD, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT] | |
79 | for flag in flags: | |
80 | if flag & self.userAccountControl: | |
81 | return datetime.datetime.max #never | |
82 | ||
83 | #criteria 2 | |
84 | if self.pwdLastSet == datetime.timedelta(): | |
85 | return datetime.datetime.min | |
86 | ||
87 | if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta | |
88 | return datetime.datetime.max #never | |
89 | ||
90 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
91 | return datetime.datetime.max #never | |
92 | ||
93 | return (self.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) | |
94 | ||
95 | ||
96 | # https://msdn.microsoft.com/en-us/library/cc223991.aspx | |
97 | def calc_CanLogon(self): | |
98 | flags = [MSLDAP_UAC.ACCOUNTDISABLE, MSLDAP_UAC.LOCKOUT, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT] | |
99 | for flag in flags: | |
100 | if flag & self.userAccountControl: | |
101 | return False | |
102 | ||
103 | if (not (MSLDAP_UAC.DONT_EXPIRE_PASSWD & self.userAccountControl)) and (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: | |
104 | return False | |
105 | ||
106 | # | |
107 | # TODO: logonHours check! | |
108 | # | |
109 | ||
110 | if self.must_change_pw == datetime.datetime.min: | |
111 | #can logon, but must change the password! | |
112 | return True | |
113 | ||
114 | if (self.must_change_pw - datetime.datetime.now()).total_seconds() < 0: | |
115 | return False | |
116 | ||
117 | return True | |
65 | 118 | |
66 | 119 | @staticmethod |
67 | 120 | def from_ldap(entry, adinfo = None): |
101 | 154 | adi.servicePrincipalName = entry['attributes'].get('servicePrincipalName') |
102 | 155 | |
103 | 156 | adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo') |
157 | adi.allowedtoactonbehalfofotheridentity = entry['attributes'].get('msDS-AllowedToActOnBehalfOfOtherIdentity') | |
104 | 158 | |
105 | 159 | temp = entry['attributes'].get('userAccountControl') |
106 | 160 | if temp: |
107 | 161 | adi.userAccountControl = MSLDAP_UAC(temp) |
162 | ||
163 | if adinfo: | |
164 | adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge).replace(tzinfo=None) | |
165 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
166 | adi.when_pw_expires = datetime.datetime.max | |
167 | else: | |
168 | adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) if adinfo.maxPwdAge != 0 else adi.pwdLastSet | |
169 | adi.must_change_pw = adi.calc_PasswordMustChange(adinfo) #datetime | |
170 | adi.canLogon = adi.calc_CanLogon() #bool | |
171 | ||
108 | 172 | return adi |
109 | 173 | |
110 | 174 | def to_dict(self): |
155 | 219 | |
156 | 220 | def get_row(self, attrs): |
157 | 221 | t = self.to_dict() |
158 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs]⏎ | |
222 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] | |
223 | ||
224 | ||
225 | def __str__(self): | |
226 | t = '' | |
227 | for k in self.__dict__: | |
228 | t += '%s : %s\r\n' % (k, self.__dict__[k]) | |
229 | return t⏎ |
7 | 7 | |
8 | 8 | MSADGPO_ATTRS = [ |
9 | 9 | 'cn', 'displayName', 'distinguishedName', 'flags', 'gPCFileSysPath', |
10 | 'gPCFunctionalityVersion', 'gPCMachineExtensionNames', 'objectClass', | |
11 | 'objectGUID', 'systemFlags', 'versionNumber', 'whenChanged', 'whenCreated', | |
10 | 'gPCFunctionalityVersion', 'gPCMachineExtensionNames', 'gPCUserExtensionNames', | |
11 | 'objectClass', 'objectGUID', 'systemFlags', 'versionNumber', 'whenChanged', | |
12 | 'whenCreated', | |
12 | 13 | ] |
13 | 14 | |
14 | 15 | class MSADGPO: |
20 | 21 | self.gPCFileSysPath = None #str |
21 | 22 | self.gPCFunctionalityVersion = None #str |
22 | 23 | self.gPCMachineExtensionNames = None |
24 | self.gPCUserExtensionNames = None | |
23 | 25 | self.objectClass = None #str |
24 | 26 | self.objectGUID = None #uid |
25 | 27 | self.systemFlags = None #str |
26 | 28 | self.whenChanged = None #uid |
27 | 29 | self.whenCreated = None #str |
30 | self.versionNumber = None | |
28 | 31 | |
29 | 32 | |
30 | 33 | @staticmethod |
37 | 40 | adi.gPCFileSysPath = entry['attributes'].get('gPCFileSysPath') |
38 | 41 | adi.gPCFunctionalityVersion = entry['attributes'].get('gPCFunctionalityVersion') |
39 | 42 | adi.gPCMachineExtensionNames = entry['attributes'].get('gPCMachineExtensionNames') |
43 | adi.gPCUserExtensionNames = entry['attributes'].get('gPCUserExtensionNames') | |
40 | 44 | adi.objectClass = entry['attributes'].get('objectClass') |
41 | 45 | adi.objectGUID = entry['attributes'].get('objectGUID') |
42 | 46 | adi.systemFlags = entry['attributes'].get('systemFlags') |
43 | 47 | adi.whenChanged = entry['attributes'].get('whenChanged') |
44 | 48 | adi.whenCreated = entry['attributes'].get('whenCreated') |
49 | adi.versionNumber = entry['attributes'].get('versionNumber') | |
45 | 50 | |
46 | 51 | return adi |
47 | 52 | |
54 | 59 | t['gPCFileSysPath'] = vn(self.gPCFileSysPath) |
55 | 60 | t['gPCFunctionalityVersion'] = vn(self.gPCFunctionalityVersion) |
56 | 61 | t['gPCMachineExtensionNames'] = vn(self.gPCMachineExtensionNames) |
62 | t['gPCUserExtensionNames'] = vn(self.gPCUserExtensionNames) | |
57 | 63 | t['systemFlags'] = vn(self.systemFlags) |
58 | 64 | t['objectClass'] = vn(self.objectClass) |
59 | 65 | t['objectGUID'] = vn(self.objectGUID) |
60 | 66 | t['whenChanged'] = vn(self.whenChanged) |
61 | 67 | t['whenCreated'] = vn(self.whenCreated) |
68 | t['versionNumber'] = vn(self.versionNumber) | |
62 | 69 | return t |
63 | 70 | |
64 | 71 | def __str__(self): |
5 | 5 | |
6 | 6 | from msldap.wintypes import * |
7 | 7 | from msldap.ldap_objects.common import MSLDAP_UAC, vn |
8 | from winacl.dtyp.sid import SID | |
9 | ||
10 | MSADGroup_ATTRS = [ | |
11 | 'cn', 'distinguishedName', 'objectGUID', 'objectSid', 'groupType', | |
12 | 'instanceType', 'name', 'member', 'sAMAccountName', 'systemFlags', | |
13 | 'whenChanged', 'whenCreated', 'description', 'nTSecurityDescriptor', | |
14 | 'sAMAccountType', | |
15 | ] | |
16 | ||
8 | 17 | |
9 | 18 | class MSADGroup: |
10 | 19 | def __init__(self): |
68 | 77 | t.description = ', '.join(t.description) |
69 | 78 | |
70 | 79 | |
71 | temp = entry['attributes'].get('nTSecurityDescriptor') | |
72 | if temp: | |
73 | t.nTSecurityDescriptor = SID.from_bytes(temp) | |
80 | #temp = entry['attributes'].get('nTSecurityDescriptor') | |
81 | #if temp: | |
82 | # t.nTSecurityDescriptor = SID.from_bytes(temp) | |
74 | 83 | return t |
75 | 84 | |
76 | 85 |
10 | 10 | 'name', 'nextRid', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', |
11 | 11 | 'objectGUID', 'objectSid', 'pwdHistoryLength', |
12 | 12 | 'pwdProperties', 'serverState', 'systemFlags', 'uASCompat', 'uSNChanged', |
13 | 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference','msDS-Behavior-Version' | |
13 | 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference', | |
14 | 'msDS-Behavior-Version' | |
14 | 15 | ] |
15 | 16 | class MSADInfo: |
16 | 17 | def __init__(self): |
151 | 152 | t += 'uSNCreated: %s\n' % self.uSNCreated |
152 | 153 | t += 'whenChanged: %s\n' % self.whenChanged |
153 | 154 | t += 'whenCreated: %s\n' % self.whenCreated |
155 | t += 'domainmodelevel: %s\n' % self.domainmodelevel | |
154 | 156 | return t ⏎ |
2 | 2 | # Author: |
3 | 3 | # Tamas Jos (@skelsec) |
4 | 4 | # |
5 | ||
6 | ||
7 | MSADOU_ATTRS = [ | |
8 | 'description', 'distinguishedName', 'dSCorePropagationData', 'gPLink', 'instanceType', | |
9 | 'isCriticalSystemObject', 'name', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', | |
10 | 'objectGUID', 'ou', 'showInAdvancedViewOnly', 'systemFlags', 'uSNChanged', 'uSNCreated', | |
11 | 'whenChanged', 'whenCreated', | |
12 | ] | |
5 | 13 | |
6 | 14 | class MSADOU: |
7 | 15 | def __init__(self): |
0 | #!/usr/bin/env python3 | |
1 | # | |
2 | # Author: | |
3 | # Tamas Jos (@skelsec) | |
4 | # | |
5 | ||
6 | ||
7 | MSADSCHEMAENTRY_ATTRS = [ | |
8 | 'cn', 'distinguishedName', 'adminDescription', | |
9 | 'adminDisplayName', 'objectGUID', 'schemaIDGUID', | |
10 | 'lDAPDisplayName', 'name', | |
11 | ] | |
12 | ||
13 | class MSADSchemaEntry: | |
14 | def __init__(self): | |
15 | self.cn = None #str | |
16 | self.distinguishedName = None #dn | |
17 | self.adminDescription = None #dunno | |
18 | self.adminDisplayName = None #datetime | |
19 | self.objectGUID = None #int | |
20 | self.schemaIDGUID = None | |
21 | self.lDAPDisplayName = None | |
22 | self.name = None #int | |
23 | ||
24 | ||
25 | @staticmethod | |
26 | def from_ldap(entry): | |
27 | adi = MSADSchemaEntry() | |
28 | adi.cn = entry['attributes'].get('cn') | |
29 | adi.distinguishedName = entry['attributes'].get('distinguishedName') | |
30 | adi.adminDescription = entry['attributes'].get('adminDescription') | |
31 | adi.adminDisplayName = entry['attributes'].get('adminDisplayName') | |
32 | adi.objectGUID = entry['attributes'].get('objectGUID') #str | |
33 | adi.schemaIDGUID = entry['attributes'].get('schemaIDGUID') #list | |
34 | adi.lDAPDisplayName = entry['attributes'].get('lDAPDisplayName') #int | |
35 | adi.name = entry['attributes'].get('name') #int | |
36 | return adi | |
37 | ||
38 | def to_dict(self): | |
39 | d = {} | |
40 | d['cn'] = self.cn | |
41 | d['distinguishedName'] = self.distinguishedName | |
42 | d['adminDescription'] = self.adminDescription | |
43 | d['adminDisplayName'] = self.adminDisplayName | |
44 | d['objectGUID'] = self.objectGUID | |
45 | d['schemaIDGUID'] = self.schemaIDGUID | |
46 | d['lDAPDisplayName'] = self.lDAPDisplayName | |
47 | d['name'] = self.name | |
48 | return d | |
49 | ||
50 | ||
51 | def __str__(self): | |
52 | t = 'MSADSchemaEntry\r\n' | |
53 | d = self.to_dict() | |
54 | for k in d: | |
55 | t += '%s: %s\r\n' % (k, d[k]) | |
56 | return t ⏎ |
13 | 13 | 'objectCategory', 'objectClass', 'objectGUID', 'objectSid', 'primaryGroupID', |
14 | 14 | 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl', |
15 | 15 | 'userPrincipalName', 'whenChanged', 'whenCreated','memberOf','member', 'servicePrincipalName', |
16 | 'msDS-AllowedToDelegateTo', | |
16 | 'msDS-AllowedToDelegateTo', 'adminCount' | |
17 | 17 | ] |
18 | 18 | MSADUser_TSV_ATTRS = [ |
19 | 19 | 'sAMAccountName', 'userPrincipalName' ,'canLogon', 'badPasswordTime', 'description', |
21 | 21 | 'whenCreated', 'whenChanged', 'member', 'memberOf', 'servicePrincipalName', |
22 | 22 | 'objectSid', 'cn', 'UAC_SCRIPT', 'UAC_ACCOUNTDISABLE', 'UAC_LOCKOUT', 'UAC_PASSWD_NOTREQD', |
23 | 23 | 'UAC_PASSWD_CANT_CHANGE', 'UAC_ENCRYPTED_TEXT_PASSWORD_ALLOWED', 'UAC_DONT_EXPIRE_PASSWD', |
24 | 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED' | |
24 | 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED', 'adminCount' | |
25 | 25 | ] |
26 | 26 | |
27 | 27 | class MSADUser: |
66 | 66 | self.sAMAccountType = None #int |
67 | 67 | self.userAccountControl = None #UserAccountControl intflag |
68 | 68 | self.allowedtodelegateto = None |
69 | self.admincount = None | |
69 | 70 | |
70 | 71 | |
71 | 72 | ## other |
79 | 80 | self.canLogon = None #bool |
80 | 81 | |
81 | 82 | # https://msdn.microsoft.com/en-us/library/cc245739.aspx |
82 | def calc_PasswordMustChange(self): | |
83 | def calc_PasswordMustChange(self, adinfo): | |
83 | 84 | # Crtieria 1 |
84 | 85 | flags = [MSLDAP_UAC.DONT_EXPIRE_PASSWD, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT] |
85 | 86 | for flag in flags: |
87 | 88 | return datetime.datetime.max #never |
88 | 89 | |
89 | 90 | #criteria 2 |
90 | if self.pwdLastSet == 0: | |
91 | if self.pwdLastSet == datetime.timedelta(): | |
91 | 92 | return datetime.datetime.min |
92 | 93 | |
93 | if (self.when_pw_expires - datetime.datetime.now()).total_seconds() > 0: | |
94 | if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta | |
94 | 95 | return datetime.datetime.max #never |
95 | 96 | |
96 | return self.pwdLastSet.replace(tzinfo=None) | |
97 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
98 | return datetime.datetime.max #never | |
99 | ||
100 | return (self.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) | |
97 | 101 | |
98 | 102 | |
99 | 103 | # https://msdn.microsoft.com/en-us/library/cc223991.aspx |
102 | 106 | for flag in flags: |
103 | 107 | if flag & self.userAccountControl: |
104 | 108 | return False |
105 | ||
106 | if (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: | |
109 | ||
110 | if (not (MSLDAP_UAC.DONT_EXPIRE_PASSWD & self.userAccountControl)) and (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: | |
107 | 111 | return False |
108 | 112 | |
109 | 113 | # |
155 | 159 | adi.countryCode = entry['attributes'].get('countryCode') |
156 | 160 | |
157 | 161 | adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo') |
162 | adi.admincount = entry['attributes'].get('adminCount') | |
158 | 163 | |
159 | 164 | temp = entry['attributes'].get('userAccountControl') |
160 | 165 | if temp: |
161 | 166 | adi.userAccountControl = MSLDAP_UAC(temp) |
162 | 167 | |
163 | 168 | if adinfo: |
164 | adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge/10000000).replace(tzinfo=None) | |
165 | adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge/10000000).replace(tzinfo=None) | |
166 | adi.must_change_pw = adi.calc_PasswordMustChange() #datetime | |
167 | if adi.sAMAccountName[-1] != '$': | |
168 | adi.canLogon = adi.calc_CanLogon() #bool | |
169 | adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge).replace(tzinfo=None) | |
170 | if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum | |
171 | adi.when_pw_expires = datetime.datetime.max | |
172 | else: | |
173 | adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) if adinfo.maxPwdAge != 0 else adi.pwdLastSet | |
174 | adi.must_change_pw = adi.calc_PasswordMustChange(adinfo) #datetime | |
175 | adi.canLogon = adi.calc_CanLogon() #bool | |
169 | 176 | |
170 | 177 | |
171 | 178 | return adi |
207 | 214 | t['when_pw_change'] = vn(self.when_pw_change) |
208 | 215 | t['when_pw_expires'] = vn(self.when_pw_expires) |
209 | 216 | t['must_change_pw'] = vn(self.must_change_pw) |
217 | t['admincount'] = self.admincount | |
210 | 218 | t['canLogon'] = vn(self.canLogon) |
211 | 219 | return t |
212 | 220 | |
255 | 263 | t += 'when_pw_change: %s\n' % self.when_pw_change |
256 | 264 | t += 'when_pw_expires: %s\n' % self.when_pw_expires |
257 | 265 | t += 'must_change_pw: %s\n' % self.must_change_pw |
266 | t += 'admincount: %s\n' % self.admincount | |
258 | 267 | t += 'canLogon: %s\n' % self.canLogon |
259 | 268 | |
260 | 269 | return t |
0 | import enum | |
1 | import asyncio | |
2 | import ipaddress | |
3 | import copy | |
4 | ||
5 | from asysocks.common.clienturl import SocksClientURL | |
6 | from asysocks.common.constants import SocksServerVersion, SocksProtocol, SOCKS5Method | |
7 | from asysocks.common.target import SocksTarget | |
8 | ||
9 | from msldap import logger | |
10 | from msldap.network.socks import SocksProxyConnection | |
11 | from msldap.commons.proxy import MSLDAPProxy, MSLDAPProxyType | |
12 | from minikerberos.common.target import KerberosTarget | |
13 | from minikerberos.common.proxy import KerberosProxy | |
14 | ||
15 | ||
16 | ||
17 | class MultiplexorProxyConnection: | |
18 | """ | |
19 | """ | |
20 | def __init__(self, target): | |
21 | self.target = target | |
22 | ||
23 | async def connect(self, is_kerberos = False): | |
24 | """ | |
25 | ||
26 | """ | |
27 | #hiding the import, so you'll only need to install multiplexor only when actually using it | |
28 | from multiplexor.operator import MultiplexorOperator | |
29 | ||
30 | con_str = self.target.proxy.target.get_server_url() | |
31 | #creating operator and connecting to multiplexor server | |
32 | self.operator = MultiplexorOperator(con_str, logging_sink = logger) | |
33 | await self.operator.connect() | |
34 | #creating socks5 proxy | |
35 | server_info = await self.operator.start_socks5(self.target.proxy.target.agent_id) | |
36 | await self.operator.terminate() | |
37 | #print(server_info) | |
38 | if is_kerberos is False: | |
39 | ||
40 | #copying the original target, then feeding it to socks5proxy object. it will hold the actual socks5 proxy server address we created before | |
41 | tp = MSLDAPProxy() | |
42 | tp.target = SocksTarget() | |
43 | tp.target.version = SocksServerVersion.SOCKS5 | |
44 | tp.target.server_ip = server_info['listen_ip'] | |
45 | tp.target.server_port = server_info['listen_port'] | |
46 | tp.target.is_bind = False | |
47 | tp.target.proto = SocksProtocol.TCP | |
48 | tp.target.timeout = self.target.timeout | |
49 | tp.target.buffer_size = 4096 | |
50 | ||
51 | tp.target.endpoint_ip = self.target.host | |
52 | tp.target.endpoint_port = self.target.port | |
53 | tp.target.endpoint_timeout = None # TODO: maybe implement endpoint timeout in the msldap target? | |
54 | tp.type = MSLDAPProxyType.SOCKS5 | |
55 | ||
56 | newtarget = copy.deepcopy(self.target) | |
57 | newtarget.proxy = tp | |
58 | ||
59 | ||
60 | ||
61 | return SocksProxyConnection(target = newtarget) | |
62 | ||
63 | else: | |
64 | kt = copy.deepcopy(self.target) | |
65 | kt.proxy = KerberosProxy() | |
66 | kt.proxy.target = SocksTarget() | |
67 | kt.proxy.target.version = SocksServerVersion.SOCKS5 | |
68 | kt.proxy.target.server_ip = server_info['listen_ip'] | |
69 | kt.proxy.target.server_port = server_info['listen_port'] | |
70 | kt.proxy.target.is_bind = False | |
71 | kt.proxy.target.proto = SocksProtocol.TCP | |
72 | kt.proxy.target.timeout = 10 | |
73 | kt.proxy.target.buffer_size = 4096 | |
74 | ||
75 | kt.proxy.target.endpoint_ip = self.target.ip | |
76 | kt.proxy.target.endpoint_port = self.target.port | |
77 | #kt.proxy.creds = copy.deepcopy(self.target.proxy.auth) | |
78 | ||
79 | return kt | |
80 |
0 | 0 | |
1 | from msldap import logger | |
1 | 2 | from msldap.network.tcp import MSLDAPTCPNetwork |
2 | 3 | from msldap.network.socks import SocksProxyConnection |
4 | from msldap.network.multiplexor import MultiplexorProxyConnection | |
3 | 5 | from msldap.commons.proxy import MSLDAPProxyType |
4 | 6 | |
5 | 7 | MSLDAP_SOCKS_PROXY_TYPES = [ |
13 | 15 | pass |
14 | 16 | |
15 | 17 | @staticmethod |
16 | def select(target): | |
18 | async def select(target): | |
17 | 19 | if target.proxy is not None: |
18 | 20 | if target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES: |
19 | 21 | return SocksProxyConnection(target) |
20 | 22 | else: |
21 | raise Exception('Multiplexor coming soon!') | |
23 | mpc = MultiplexorProxyConnection(target) | |
24 | socks_proxy = await mpc.connect() | |
25 | return socks_proxy | |
22 | 26 | |
23 | 27 | return MSLDAPTCPNetwork(target)⏎ |
40 | 40 | Disconnects from the socket. |
41 | 41 | Stops the reader and writer streams. |
42 | 42 | """ |
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) | |
45 | 56 | |
46 | 57 | def get_one_message(self,data): |
47 | 58 | if len(data) < 6: |
82 | 93 | data += temp |
83 | 94 | continue |
84 | 95 | |
85 | #except asyncio.CancelledError: | |
86 | # return | |
96 | except asyncio.CancelledError: | |
97 | return | |
87 | 98 | except Exception as e: |
88 | 99 | logger.exception('handle_in_q') |
89 | 100 | await self.in_queue.put((None, e)) |
106 | 117 | |
107 | 118 | self.target.proxy.target.endpoint_ip = self.target.host |
108 | 119 | 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 | |
109 | 122 | |
110 | 123 | self.client = SOCKSClient(comms, self.target.proxy.target, self.target.proxy.auth) |
111 | 124 | self.proxy_task = asyncio.create_task(self.client.run()) |
20 | 20 | async def terminate(self): |
21 | 21 | self.handle_in_task.cancel() |
22 | 22 | self.handle_out_task.cancel() |
23 | ||
23 | ||
24 | def get_peer_certificate(self): | |
25 | return self.writer.get_extra_info('ssl_object').getpeercert(True) | |
24 | 26 | |
25 | 27 | async def handle_in_q(self): |
26 | 28 | try: |
81 | 83 | self.in_queue = asyncio.Queue() |
82 | 84 | self.out_queue = asyncio.Queue() |
83 | 85 | self.reader, self.writer = await asyncio.wait_for( |
84 | asyncio.open_connection(self.target.host, self.target.port, ssl=self.target.get_ssl_context()), | |
86 | asyncio.open_connection( | |
87 | self.target.serverip if self.target.serverip is not None else self.target.host, | |
88 | self.target.port, | |
89 | ssl=self.target.get_ssl_context() | |
90 | ), | |
85 | 91 | timeout = self.target.timeout |
86 | 92 | ) |
87 | 93 |
0 | 0 | import re |
1 | 1 | import platform |
2 | 2 | |
3 | import msldap.protocol.ldap_filter.parser as parser | |
3 | from msldap.protocol.ldap_filter import parser | |
4 | 4 | from msldap.protocol.ldap_filter.soundex import soundex_compare |
5 | 5 | |
6 | 6 |
141 | 141 | _input_size = None |
142 | 142 | _actions = None |
143 | 143 | |
144 | REGEX_1 = re.compile('^[^!*\\x29]') | |
144 | REGEX_1 = re.compile('^[^*\\x29]') #re.compile('^[^!*\\x29]') | |
145 | 145 | REGEX_2 = re.compile('^[a-fA-F0-9]') |
146 | 146 | REGEX_3 = re.compile('^[\\x20]') |
147 | 147 | REGEX_4 = re.compile('^[\\x09]') |
90 | 90 | 80 : 'other', |
91 | 91 | } |
92 | 92 | |
93 | class changeoperation(core.Enumerated): | |
93 | class ChangeOperation(core.Enumerated): | |
94 | 94 | _map = { |
95 | 95 | 0 : 'add', |
96 | 96 | 1 : 'delete', |
287 | 287 | |
288 | 288 | class Change(core.Sequence): |
289 | 289 | _fields = [ |
290 | ('operation', changeoperation), | |
290 | ('operation', ChangeOperation), | |
291 | 291 | ('modification', PartialAttribute), |
292 | 292 | ] |
293 | 293 |
6 | 6 | |
7 | 7 | |
8 | 8 | def equality(attr, value): |
9 | #print(attr) | |
10 | #print(value) | |
11 | 9 | if attr[-1] == ':': |
12 | #possible OID | |
13 | 10 | name, oid_raw = attr[:-1].split(':') |
14 | #print(oid_raw) | |
15 | 11 | return Filter({ |
16 | 12 | 'extensibleMatch' : MatchingRuleAssertion({ |
17 | 13 | 'matchingRule' : oid_raw.encode(), |
60 | 56 | |
61 | 57 | |
62 | 58 | def query_syntax_converter_inner(ftr): |
63 | #print(ftr.__dict__) | |
64 | #print(ftr.comp) | |
65 | #print(ftr.type) | |
66 | 59 | if ftr.type == 'filter': |
67 | 60 | if ftr.comp == '=': |
68 | 61 | return equality(ftr.attr, ftr.val) |
4 | 4 | from winacl.dtyp.guid import GUID |
5 | 5 | from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR |
6 | 6 | from msldap import logger |
7 | from msldap.protocol.messages import Attribute, Change, PartialAttribute | |
7 | 8 | |
8 | 9 | MSLDAP_DT_WIN_EPOCH = datetime.datetime(1601, 1, 1) |
9 | 10 | |
61 | 62 | def list_str(x): |
62 | 63 | return [e.decode() for e in x ] |
63 | 64 | |
65 | def list_str_enc(x): | |
66 | return [e.encode() for e in x ] | |
67 | ||
64 | 68 | def list_int(x): |
65 | 69 | return [int(e) for e in x ] |
66 | 70 | |
71 | def list_int_enc(x): | |
72 | return [str(e).encode() for e in x ] | |
73 | ||
67 | 74 | def list_int_one(x): |
68 | 75 | return int(x[0]) |
69 | 76 | |
77 | def list_int_one_enc(x): | |
78 | return [str(x[0]).encode()] | |
79 | ||
70 | 80 | def list_str_one(x): |
71 | 81 | return x[0].decode() |
72 | 82 | |
83 | def list_str_one_enc(x): | |
84 | return [x[0].encode()] | |
85 | ||
86 | def list_str_one_utf16le_enc(x): | |
87 | return [x[0].encode('utf-16-le')] | |
88 | ||
73 | 89 | def list_bytes_one(x): |
74 | 90 | return x[0] |
91 | ||
92 | def list_bytes_one_enc(x): | |
93 | return x | |
75 | 94 | |
76 | 95 | def int2timedelta(x): |
77 | 96 | x = int(x[0]) |
146 | 165 | t.append(ts2dt((a, None))) |
147 | 166 | return t |
148 | 167 | |
168 | ||
149 | 169 | LDAP_ATTRIBUTE_TYPES = { |
150 | 170 | 'supportedCapabilities' : list_str, |
151 | 171 | 'serverName' : list_str_one, |
157 | 177 | 'supportedControl' : list_str, |
158 | 178 | 'rootDomainNamingContext' : list_str_one, |
159 | 179 | 'configurationNamingContext' : list_str_one, |
180 | 'schemaIDGUID' : x2guid, | |
181 | 'lDAPDisplayName' : list_str_one, | |
160 | 182 | 'schemaNamingContext' : list_str_one, |
161 | 183 | 'defaultNamingContext' : list_str_one, |
184 | 'adminDescription' : list_str_one, | |
185 | 'adminDisplayName' : list_str_one, | |
162 | 186 | 'namingContexts' : list_str, |
163 | 187 | 'dsServiceName' : list_str_one, |
164 | 188 | 'subschemaSubentry' : list_str_one, |
203 | 227 | 'lockoutThreshold' : list_int_one, |
204 | 228 | 'lockOutObservationWindow' : list_int_one, |
205 | 229 | 'lockoutDuration' : list_int_one, |
206 | 'forceLogoff' : list_str_one, | |
230 | 'forceLogoff' : int2timedelta, | |
207 | 231 | 'creationTime' : int2dt, |
208 | 232 | 'maxPwdAge' : int2timedelta, |
209 | 233 | 'pwdHistoryLength' : list_int_one, |
233 | 257 | 'versionNumber' : list_int_one, |
234 | 258 | 'gPCFunctionalityVersion' : list_int_one, |
235 | 259 | 'gPCMachineExtensionNames' : list_str, |
260 | 'gPCUserExtensionNames' : list_str, | |
236 | 261 | 'groupType' : list_int_one, |
237 | 262 | 'member' : list_str, |
238 | 263 | 'adminCount' : list_int_one, |
246 | 271 | 'trustPartner' : list_str_one, |
247 | 272 | 'securityIdentifier' : list_bytes_one, |
248 | 273 | 'versionNumber' : list_int_one, |
249 | ||
274 | 'unicodePwd' : list_str_one, | |
275 | 'ms-Mcs-AdmPwd' : list_str_one, | |
276 | 'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one, | |
250 | 277 | } |
278 | ||
279 | LDAP_ATTRIBUTE_TYPES_ENC = { | |
280 | 'objectClass' : list_str_enc, | |
281 | 'sn' : list_str_one_enc, | |
282 | 'gidNumber' : list_int_one_enc, | |
283 | 'unicodePwd' : list_str_one_utf16le_enc, | |
284 | 'lockoutTime' : list_int_one_enc, | |
285 | 'sAMAccountName' : list_str_one_enc, | |
286 | 'userAccountControl' : list_int_one_enc, | |
287 | 'displayName' : list_str_one_enc, | |
288 | 'userPrincipalName' : list_str_one_enc, | |
289 | 'servicePrincipalName' : list_str_enc, | |
290 | 'msds-additionaldnshostname' : list_str_enc, | |
291 | 'gPCMachineExtensionNames' : list_str_enc, | |
292 | 'gPCUserExtensionNames' : list_str_enc, | |
293 | 'versionNumber' : list_int_one_enc, | |
294 | 'member' : list_str_enc, | |
295 | 'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one_enc, | |
296 | 'nTSecurityDescriptor' : list_bytes_one_enc, | |
297 | } | |
298 | ||
299 | def encode_attributes(x): | |
300 | """converts a dict to attributelist""" | |
301 | res = [] | |
302 | for k in x: | |
303 | if k not in LDAP_ATTRIBUTE_TYPES_ENC: | |
304 | raise Exception('Unknown conversion type for key "%s"' % k) | |
305 | ||
306 | res.append(Attribute({ | |
307 | 'type' : k.encode(), | |
308 | 'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](x[k]) | |
309 | })) | |
310 | ||
311 | return res | |
251 | 312 | |
252 | 313 | def convert_attributes(x): |
253 | 314 | t = {} |
269 | 330 | return { |
270 | 331 | 'objectName' : x['objectName'].decode(), |
271 | 332 | 'attributes' : convert_attributes(x['attributes']) |
272 | }⏎ | |
333 | } | |
334 | ||
335 | ||
336 | def encode_changes(x): | |
337 | res = [] | |
338 | for k in x: | |
339 | if k not in LDAP_ATTRIBUTE_TYPES_ENC: | |
340 | raise Exception('Unknown conversion type for key "%s"' % k) | |
341 | ||
342 | for mod, value in x[k]: | |
343 | res.append(Change({ | |
344 | 'operation' : mod, | |
345 | 'modification' : PartialAttribute({ | |
346 | 'type' : k.encode(), | |
347 | 'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](value) | |
348 | }) | |
349 | })) | |
350 | return res⏎ |
0 | 0 | Metadata-Version: 1.2 |
1 | 1 | Name: msldap |
2 | Version: 0.2.10 | |
2 | Version: 0.3.22 | |
3 | 3 | Summary: Python library to play with MS LDAP |
4 | 4 | Home-page: https://github.com/skelsec/msldap |
5 | 5 | Author: Tamas Jos |
6 | Author-email: [email protected] | |
6 | Author-email: [email protected] | |
7 | 7 | License: UNKNOWN |
8 | 8 | Description: Python library to play with MS LDAP |
9 | 9 | Platform: UNKNOWN |
10 | Classifier: Programming Language :: Python :: 3.6 | |
10 | Classifier: Programming Language :: Python :: 3.7 | |
11 | Classifier: Programming Language :: Python :: 3.8 | |
11 | 12 | Classifier: License :: OSI Approved :: MIT License |
12 | 13 | Classifier: Operating System :: OS Independent |
13 | Requires-Python: >=3.6 | |
14 | Requires-Python: >=3.7 |
10 | 10 | msldap.egg-info/SOURCES.txt |
11 | 11 | msldap.egg-info/dependency_links.txt |
12 | 12 | msldap.egg-info/entry_points.txt |
13 | msldap.egg-info/not-zip-safe | |
13 | 14 | msldap.egg-info/requires.txt |
14 | 15 | msldap.egg-info/top_level.txt |
15 | msldap.egg-info/zip-safe | |
16 | 16 | msldap/authentication/__init__.py |
17 | 17 | msldap/authentication/kerberos/__init__.py |
18 | msldap/authentication/kerberos/gssapi.py | |
18 | 19 | msldap/authentication/kerberos/multiplexor.py |
19 | 20 | msldap/authentication/kerberos/native.py |
20 | 21 | msldap/authentication/kerberos/sspi.py |
44 | 45 | msldap/authentication/spnego/sspi.py |
45 | 46 | msldap/commons/__init__.py |
46 | 47 | msldap/commons/authbuilder.py |
48 | msldap/commons/common.py | |
47 | 49 | msldap/commons/credential.py |
48 | 50 | msldap/commons/proxy.py |
49 | 51 | msldap/commons/target.py |
68 | 70 | msldap/crypto/pure/RC4/__init__.py |
69 | 71 | msldap/examples/__init__.py |
70 | 72 | 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 | |
71 | 86 | msldap/ldap_objects/__init__.py |
72 | 87 | msldap/ldap_objects/adcomp.py |
73 | 88 | msldap/ldap_objects/adgpo.py |
74 | 89 | msldap/ldap_objects/adgroup.py |
75 | 90 | msldap/ldap_objects/adinfo.py |
76 | 91 | msldap/ldap_objects/adou.py |
92 | msldap/ldap_objects/adschemaentry.py | |
77 | 93 | msldap/ldap_objects/adsec.py |
78 | 94 | msldap/ldap_objects/adtrust.py |
79 | 95 | msldap/ldap_objects/aduser.py |
80 | 96 | msldap/ldap_objects/common.py |
81 | 97 | msldap/network/__init__.py |
98 | msldap/network/multiplexor.py | |
82 | 99 | msldap/network/selector.py |
83 | 100 | msldap/network/socks.py |
84 | 101 | msldap/network/tcp.py |
0 | 0 | [console_scripts] |
1 | 1 | msldap = msldap.examples.msldapclient:main |
2 | msldapcompdns = msldap.examples.msldapcompdnslist:main | |
2 | 3 |
0 | 0 | asn1crypto |
1 | aiocmd | |
2 | asciitree | |
3 | asysocks | |
4 | winacl>=0.0.2 | |
1 | asysocks>=0.0.10 | |
2 | minikerberos>=0.2.4 | |
3 | prompt-toolkit>=3.0.2 | |
4 | tqdm | |
5 | winacl>=0.1.0 | |
5 | 6 | |
6 | 7 | [:platform_system == "Windows"] |
7 | winsspi | |
8 | winsspi>=0.0.9 |
19 | 19 | |
20 | 20 | # Application author details: |
21 | 21 | author="Tamas Jos", |
22 | author_email="[email protected]", | |
22 | author_email="[email protected]", | |
23 | 23 | |
24 | 24 | # Packages |
25 | 25 | packages=find_packages(), |
31 | 31 | # Details |
32 | 32 | url="https://github.com/skelsec/msldap", |
33 | 33 | |
34 | zip_safe = True, | |
34 | zip_safe = False, | |
35 | 35 | # |
36 | 36 | # license="LICENSE.txt", |
37 | 37 | description="Python library to play with MS LDAP", |
38 | 38 | long_description="Python library to play with MS LDAP", |
39 | 39 | |
40 | 40 | # long_description=open("README.txt").read(), |
41 | python_requires='>=3.6', | |
41 | python_requires='>=3.7', | |
42 | 42 | classifiers=( |
43 | "Programming Language :: Python :: 3.6", | |
43 | "Programming Language :: Python :: 3.7", | |
44 | "Programming Language :: Python :: 3.8", | |
44 | 45 | "License :: OSI Approved :: MIT License", |
45 | 46 | "Operating System :: OS Independent", |
46 | 47 | ), |
47 | 48 | install_requires=[ |
48 | 49 | 'asn1crypto', |
49 | 'winsspi;platform_system=="Windows"', | |
50 | 'aiocmd', | |
51 | 'asciitree', | |
52 | #'ldap_filter', | |
53 | 'asysocks', | |
54 | 'winacl>=0.0.2' | |
50 | 'winsspi>=0.0.9;platform_system=="Windows"', | |
51 | 'minikerberos>=0.2.4', | |
52 | 'asysocks>=0.0.10', | |
53 | 'winacl>=0.1.0', | |
54 | 'prompt-toolkit>=3.0.2', | |
55 | 'tqdm', | |
55 | 56 | ], |
56 | 57 | entry_points={ |
57 | 58 | 'console_scripts': [ |
58 | 59 | 'msldap = msldap.examples.msldapclient:main', |
60 | 'msldapcompdns = msldap.examples.msldapcompdnslist:main', | |
59 | 61 | ], |
60 | 62 | } |
61 | 63 | )⏎ |