diff --git a/LICENSE b/LICENSE index ab60297..9814146 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,10 @@ +This projects contains two other project written by a 3rd party. +All code is licensed under MIT. + +License for MSLDAP: MIT License -Copyright (c) 2018 +Copyright (c) 2018 Tamas Jos Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +23,51 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +License for "asciitree": + +Copyright (c) 2015 Marc Brinkmann + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +License for "aiocmd": + +MIT License + +Copyright (c) 2019 Dor Green + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PKG-INFO b/PKG-INFO index 5cce983..49648ad 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,14 +1,15 @@ Metadata-Version: 1.2 Name: msldap -Version: 0.2.10 +Version: 0.3.30 Summary: Python library to play with MS LDAP Home-page: https://github.com/skelsec/msldap Author: Tamas Jos -Author-email: info@skelsec.com +Author-email: info@skelsecprojects.com License: UNKNOWN Description: Python library to play with MS LDAP Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent -Requires-Python: >=3.6 +Requires-Python: >=3.7 diff --git a/README.md b/README.md index 90df437..b1ed9ff 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,25 @@ +[![Documentation Status](https://readthedocs.org/projects/msldap/badge/?version=latest)](https://msldap.readthedocs.io/en/latest/?badge=latest) + +# msldap client +![Documentation Status](https://user-images.githubusercontent.com/19204702/81515211-3761e880-9333-11ea-837f-bcbe2a67ee48.gif ) + # msldap LDAP library for MS AD + +# Documentation +[Awesome documentation here!](https://msldap.readthedocs.io/en/latest/) # Features - Comes with a built-in console LDAP client - All parameters can be conrolled via a conveinent URL (see below) - - Supports integrated windows authentication + - Supports integrated windows authentication (SSPI) both with NTLM and with KERBEROS + - Supports channel binding (for ntlm and kerberos not SSPI) + - Supports encryption (for NTLM/KERBEROS/SSPI) + - Supports LDAPS (TODO: actually verify certificate) - Supports SOCKS5 proxy withot the need of extra proxifyer - Minimal footprint - A lot of pre-built queries for convenient information polling - Easy to integrate to your project - - Completely missing documentation - No testing suite # Installation @@ -19,10 +29,9 @@ `pip install msldap` # Prerequisites - - `ldap3` module. It's pure python so you dont have to compile anything. - `winsspi` module. For windows only. This supports SSPI based authentication. - `asn1crypto` module. Some LDAP queries incorporate ASN1 strucutres to be sent on top of the ASN1 transport XD - - `socks5line` module. To support socks5 proxying. + - `asysocks` module. To support socks proxying. - `aiocmd` For the interactive client - `asciitree` For plotting nice trees in the interactive client @@ -35,31 +44,56 @@ The new connection string is composed in the following manner: `+://\:@:/?=&=&...` Detailed explanation with examples: -``` - MSLDAP URL Format: +://:@://?= +``` ++://:@://?= + + sets the ldap protocol following values supported: - ldap - - ldaps (ldap over SSL) << known to be problematic because of the underlying library (ldap3) - can be omitted if plaintext authentication is to be performed, otherwise: - - ntlm - - sspi (windows only!) + - ldaps + + can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: + - ntlm-password + - ntlm-nt + - kerberos-password (dc option param must be used) + - kerberos-rc4 / kerberos-nt (dc option param must be used) + - kerberos-aes (dc option param must be used) + - kerberos-keytab (dc option param must be used) + - kerberos-ccache (dc option param must be used) + - sspi-ntlm (windows only!) + - sspi-kerberos (windows only!) - anonymous - plain + - simple + - sicily (same format as ntlm-nt but using the SICILY authentication) + + : + OPTIONAL. Specifies the root tree of all queries + can be: - timeout : connction timeout in seconds - proxytype: currently only socks5 proxy is supported - proxyhost: Ip or hostname of the proxy server - proxyport: port of the proxy server - proxytimeout: timeout ins ecodns for the proxy connection + - dc: the IP address of the domain controller, MUST be used for kerberos authentication Examples: - ldap://10.10.10.2 - ldaps://test.corp - ldap+sspi:///test.corp - ldap+ntlm://TEST\\victim:password@10.10.10.2 + ldap://10.10.10.2 (anonymous bind) + ldaps://test.corp (anonymous bind) + ldap+sspi-ntlm://test.corp + ldap+sspi-kerberos://test.corp + ldap://TEST\\victim:@10.10.10.2 (defaults to SASL GSSAPI NTLM) + ldap+simple://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) + ldap+plain://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) + ldap+ntlm-password://TEST\\victim:@10.10.10.2 + ldap+ntlm-nt://TEST\\victim:@10.10.10.2 + ldap+kerberos-password://TEST\\victim:@10.10.10.2 + ldap+kerberos-rc4://TEST\\victim:@10.10.10.2 + ldap+kerberos-aes://TEST\\victim:@10.10.10.2 ldap://TEST\\victim:password@10.10.10.2/DC=test,DC=corp/ ldap://TEST\\victim:password@10.10.10.2/DC=test,DC=corp/?timeout=99&proxytype=socks5&proxyhost=127.0.0.1&proxyport=1080&proxytimeout=44 ``` # Kudos -This project is built on top of the [ldap3](https://github.com/cannatag/ldap3) project. + diff --git a/msldap/__main__.py b/msldap/__main__.py index 598a3fa..03faa57 100644 --- a/msldap/__main__.py +++ b/msldap/__main__.py @@ -66,7 +66,7 @@ elif args.command == 'spn': connection.connect() - adinfo = connection.get_ad_info() + adinfo, err = connection.get_ad_info() with open(args.outfile, 'w', newline='', encoding = 'utf8') as f: for user in connection.get_all_service_user_objects(): f.write(user.sAMAccountName + '\r\n') diff --git a/msldap/_version.py b/msldap/_version.py index 9817873..3ae59a4 100644 --- a/msldap/_version.py +++ b/msldap/_version.py @@ -1,5 +1,5 @@ -__version__ = "0.2.10" +__version__ = "0.3.30" __banner__ = \ """ # msldap %s diff --git a/msldap/authentication/kerberos/gssapi.py b/msldap/authentication/kerberos/gssapi.py new file mode 100644 index 0000000..d005997 --- /dev/null +++ b/msldap/authentication/kerberos/gssapi.py @@ -0,0 +1,543 @@ +import enum +import io +import os + +from asn1crypto.core import ObjectIdentifier + +from minikerberos.protocol.constants import EncryptionType +from minikerberos.protocol import encryption +from minikerberos.crypto.hashing import md5, hmac_md5 +from minikerberos.crypto.RC4 import RC4 + +#TODO: RC4 support! + +# https://tools.ietf.org/html/draft-raeburn-krb-rijndael-krb-05 +# https://tools.ietf.org/html/rfc2478 +# https://tools.ietf.org/html/draft-ietf-krb-wg-gssapi-cfx-02 +# https://tools.ietf.org/html/rfc4757 +# https://www.rfc-editor.org/errata/rfc4757 + +GSS_WRAP_HEADER = b'\x60\x2b\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' +GSS_WRAP_HEADER_OID = b'\x60\x2b\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' + +class KRB5_MECH_INDEP_TOKEN: + # https://tools.ietf.org/html/rfc2743#page-81 + # Mechanism-Independent Token Format + + def __init__(self, data, oid, remlen = None): + self.oid = oid + self.data = data + + #dont set this + self.length = remlen + + @staticmethod + def from_bytes(data): + return KRB5_MECH_INDEP_TOKEN.from_buffer(io.BytesIO(data)) + + @staticmethod + def from_buffer(buff): + + start = buff.read(1) + if start != b'\x60': + raise Exception('Incorrect token data!') + remaining_length = KRB5_MECH_INDEP_TOKEN.decode_length_buffer(buff) + token_data = buff.read(remaining_length) + + buff = io.BytesIO(token_data) + pos = buff.tell() + buff.read(1) + oid_length = KRB5_MECH_INDEP_TOKEN.decode_length_buffer(buff) + buff.seek(pos) + token_oid = ObjectIdentifier.load(buff.read(oid_length+2)) + + return KRB5_MECH_INDEP_TOKEN(buff.read(), str(token_oid), remlen = remaining_length) + + @staticmethod + def decode_length_buffer(buff): + lf = buff.read(1)[0] + if lf <= 127: + length = lf + else: + bcount = lf - 128 + length = int.from_bytes(buff.read(bcount), byteorder = 'big', signed = False) + return length + + @staticmethod + def encode_length(length): + if length <= 127: + return length.to_bytes(1, byteorder = 'big', signed = False) + else: + lb = length.to_bytes((length.bit_length() + 7) // 8, 'big') + return (128+len(lb)).to_bytes(1, byteorder = 'big', signed = False) + lb + + + def to_bytes(self): + t = ObjectIdentifier(self.oid).dump() + self.data + t = b'\x60' + KRB5_MECH_INDEP_TOKEN.encode_length(len(t)) + t + return t[:-len(self.data)] , self.data + + +class GSSAPIFlags(enum.IntFlag): + GSS_C_DCE_STYLE = 0x1000 + GSS_C_DELEG_FLAG = 1 + GSS_C_MUTUAL_FLAG = 2 + GSS_C_REPLAY_FLAG = 4 + GSS_C_SEQUENCE_FLAG = 8 + GSS_C_CONF_FLAG = 0x10 + GSS_C_INTEG_FLAG = 0x20 + +class KG_USAGE(enum.Enum): + ACCEPTOR_SEAL = 22 + ACCEPTOR_SIGN = 23 + INITIATOR_SEAL = 24 + INITIATOR_SIGN = 25 + +class FlagsField(enum.IntFlag): + SentByAcceptor = 0 + Sealed = 2 + AcceptorSubkey = 4 + +# https://tools.ietf.org/html/rfc4757 (7.2) +class GSSMIC_RC4: + def __init__(self): + self.TOK_ID = b'\x01\x01' + self.SGN_ALG = b'\x11\x00' #HMAC + self.Filler = b'\xff'*4 + self.SND_SEQ = None + self.SGN_CKSUM = None + + @staticmethod + def from_bytes(data): + return GSSMIC_RC4.from_buffer(io.BytesIO(data)) + + @staticmethod + def from_buffer(buff): + mic = GSSMIC_RC4() + mic.TOK_ID = buff.read(2) + mic.SGN_ALG = buff.read(2) + mic.Filler = buff.read(4) + mic.SND_SEQ = buff.read(8) + mic.SGN_CKSUM = buff.read(8) + + return mic + + def to_bytes(self): + t = self.TOK_ID + t += self.SGN_ALG + t += self.Filler + t += self.SND_SEQ + if self.SGN_CKSUM is not None: + t += self.SGN_CKSUM + + return t + +class GSSWRAP_RC4: + def __init__(self): + self.TOK_ID = b'\x02\x01' + self.SGN_ALG = b'\x11\x00' #HMAC + self.SEAL_ALG = None + self.Filler = b'\xFF' * 2 + self.SND_SEQ = None + self.SGN_CKSUM = None + self.Confounder = None + + def __str__(self): + t = 'GSSWRAP_RC4\r\n' + t += 'TOK_ID : %s\r\n' % self.TOK_ID.hex() + t += 'SGN_ALG : %s\r\n' % self.SGN_ALG.hex() + t += 'SEAL_ALG : %s\r\n' % self.SEAL_ALG.hex() + t += 'Filler : %s\r\n' % self.Filler.hex() + t += 'SND_SEQ : %s\r\n' % self.SND_SEQ.hex() + t += 'SGN_CKSUM : %s\r\n' % self.SGN_CKSUM.hex() + t += 'Confounder : %s\r\n' % self.Confounder.hex() + return t + + @staticmethod + def from_bytes(data): + return GSSWRAP_RC4.from_buffer(io.BytesIO(data)) + + @staticmethod + def from_buffer(buff): + wrap = GSSWRAP_RC4() + wrap.TOK_ID = buff.read(2) + wrap.SGN_ALG = buff.read(2) + wrap.SEAL_ALG = buff.read(2) + wrap.Filler = buff.read(2) + wrap.SND_SEQ = buff.read(8) + wrap.SGN_CKSUM = buff.read(8) + wrap.Confounder = buff.read(8) + + return wrap + + def to_bytes(self): + t = self.TOK_ID + t += self.SGN_ALG + t += self.SEAL_ALG + t += self.Filler + t += self.SND_SEQ + + if self.SGN_CKSUM: + t += self.SGN_CKSUM + if self.Confounder: + t += self.Confounder + + + return t + +class GSSAPI_RC4: + def __init__(self, session_key): + self.session_key = session_key + + def GSS_GetMIC(self, data, sequenceNumber, direction = 'init'): + raise Exception('Not tested! Sure it needs some changes') + GSS_GETMIC_HEADER = b'\x60\x23\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' + + # Let's pad the data + pad = (4 - (len(data) % 4)) & 0x3 + padStr = bytes([pad]) * pad + data += padStr + + mic = GSSMIC_RC4() + + if direction == 'init': + mic.SND_SEQ = sequenceNumber.to_bytes(4, 'big', signed = False) + b'\x00'*4 + else: + mic.SND_SEQ = sequenceNumber.to_bytes(4, 'big', signed = False) + b'\xff'*4 + + Ksign_ctx = hmac_md5(self.session_key.contents) + Ksign_ctx.update(b'signaturekey\0') + Ksign = Ksign_ctx.digest() + + id = 15 + temp = md5( id.to_bytes(4, 'little', signed = False) + mic.to_bytes()[:8] ).digest() + chksum_ctx = hmac_md5(Ksign) + chksum_ctx.update(temp) + mic.SGN_CKSUM = chksum_ctx.digest()[:8] + + id = 0 + temp = hmac_md5(self.session_key.contents) + temp.update(id.to_bytes(4, 'little', signed = False)) + + Kseq_ctx = hmac_md5(temp.digest()) + Kseq_ctx.update(mic.SGN_CKSUM) + Kseq = Kseq_ctx.digest() + + mic.SGN_CKSUM = RC4(Kseq).encrypt(mic.SND_SEQ) + + return GSS_GETMIC_HEADER + mic.to_bytes() + + + def GSS_Wrap(self, data, seq_num, direction = 'init', encrypt=True, cofounder = None): + #direction = 'a' + #seq_num = 0 + #print('[GSS_Wrap] data: %s' % data) + #print('[GSS_Wrap] seq_num: %s' % seq_num.to_bytes(4, 'big', signed = False).hex()) + #print('[GSS_Wrap] direction: %s' % direction) + #print('[GSS_Wrap] encrypt: %s' % encrypt) + # + #print('[GSS_Wrap] auth_data: %s' % auth_data) + + #pad = 0 + if encrypt is True: + data += b'\x01' + #pad = (8 - (len(data) % 8)) & 0x7 + #padStr = bytes([pad]) * pad + #data += padStr + # + ##data += b'\x08' * 8 + #print('[GSS_Wrap] pad: %s' % pad) + #print('[GSS_Wrap] data padded: %s' % data) + + + token = GSSWRAP_RC4() + token.SEAL_ALG = b'\x10\x00' # RC4 + + if direction == 'init': + token.SND_SEQ = seq_num.to_bytes(4, 'big', signed = False) + b'\x00'*4 + else: + token.SND_SEQ = seq_num.to_bytes(4, 'big', signed = False) + b'\xff'*4 + + token.Confounder = os.urandom(8) + #if cofounder is not None: + # token.Confounder = cofounder + # #testing purposes only, pls remove + + + temp = hmac_md5(self.session_key.contents) + temp.update(b'signaturekey\0') + Ksign = temp.digest() + + id = 13 + Sgn_Cksum = md5(id.to_bytes(4, 'little', signed = False) + token.to_bytes()[:8] + token.Confounder + data).digest() + + klocal = b'' + for b in self.session_key.contents: + klocal += bytes([b ^ 0xf0]) + + id = 0 + temp = hmac_md5(klocal) + temp.update(id.to_bytes(4, 'little', signed = False)) + temp = hmac_md5(temp.digest()) + temp.update(seq_num.to_bytes(4, 'big', signed = False)) + Kcrypt = temp.digest() + + temp = hmac_md5(Ksign) + temp.update(Sgn_Cksum) + token.SGN_CKSUM = temp.digest()[:8] + + id = 0 + temp = hmac_md5(self.session_key.contents) + temp.update(id.to_bytes(4, 'little', signed = False)) + temp = hmac_md5(temp.digest()) + temp.update(token.SGN_CKSUM) + Kseq = temp.digest() + + token.SND_SEQ = RC4(Kseq).encrypt(token.SND_SEQ) + + + #if auth_data is not None: + if encrypt is False: + #print('Unwrap sessionkey: %s' % self.session_key.contents.hex()) + #print('Unwrap data : %s' % data.hex()) + + sspi_wrap = KRB5_MECH_INDEP_TOKEN.from_bytes(data) + + hdr = sspi_wrap.data[:32] + data = sspi_wrap.data[32:] + + wrap = GSSWRAP_RC4.from_bytes(hdr) + + id = 0 + temp = hmac_md5(self.session_key.contents) + temp.update(id.to_bytes(4, 'little', signed = False)) + temp = hmac_md5(temp.digest()) + temp.update(wrap.SGN_CKSUM) + Kseq = temp.digest() + + snd_seq = RC4(Kseq).encrypt(wrap.SND_SEQ) + + id = 0 + temp = hmac_md5(klocal) + temp.update(id.to_bytes(4, 'little', signed = False)) + temp = hmac_md5(temp.digest()) + temp.update(snd_seq[:4]) + Kcrypt = temp.digest() + + rc4 = RC4(Kcrypt) + dec_cofounder = rc4.decrypt(wrap.Confounder) + dec_data = rc4.decrypt(data) + + id = 13 + Sgn_Cksum_calc = md5(id.to_bytes(4, 'little', signed = False) + wrap.to_bytes()[:8] + dec_cofounder + dec_data).digest() + + temp = hmac_md5(Ksign) + temp.update(Sgn_Cksum_calc) + Sgn_Cksum_calc = temp.digest()[:8] + + if wrap.SGN_CKSUM != Sgn_Cksum_calc[:8]: + return None, Exception('Integrity verification failed') + + pad = 1 + return dec_data[:-pad], None + + elif encrypt is True: + rc4 = RC4(Kcrypt) + token.Confounder = rc4.encrypt(token.Confounder) + cipherText = rc4.encrypt(data) + finalData, cipherText = KRB5_MECH_INDEP_TOKEN( token.to_bytes() + cipherText, '1.2.840.113554.1.2.2' ).to_bytes() + + + #print('cipherText %s' % cipherText.hex()) + #print('finalData %s' % finalData.hex()) + #print('sessionkey %s' % self.session_key.contents.hex()) + return cipherText, finalData + + + def GSS_Unwrap(self, data, seq_num, direction='init'): + #print('GSS_Unwrap data : %s' % data) + dec_data, err = self.GSS_Wrap(data, seq_num, direction=direction, encrypt = False) + #print('GSS_Unwrap decrypted data : %s' % dec_data) + return dec_data, err + +# 4.2.6.1. MIC Tokens +class GSSMIC: + def __init__(self): + self.TOK_ID = b'\x04\x04' + self.Flags = None + self.Filler = b'\xFF' * 5 + self.SND_SEQ = None + self.SGN_CKSUM = None + + @staticmethod + def from_bytes(data): + return GSSMIC.from_buffer(io.BytesIO(data)) + + @staticmethod + def from_buffer(buff): + m = GSSMIC() + m.TOK_ID = buff.read(2) + m.Flags = FlagsField(int.from_bytes(buff.read(1), 'big', signed = False)) + m.Filler = buff.read(5) + m.SND_SEQ = int.from_bytes(buff.read(8), 'big', signed = False) + m.SGN_CKSUM = buff.read() #should know the size based on the algo! + return m + + def to_bytes(self): + t = self.TOK_ID + t += self.Flags.to_bytes(1, 'big', signed = False) + t += self.Filler + t += self.SND_SEQ.to_bytes(8, 'big', signed = False) + if self.SGN_CKSUM is not None: + t += self.SGN_CKSUM + + return t + +# 4.2.6.2. Wrap Tokens +class GSSWrapToken: + def __init__(self): + self.TOK_ID = b'\x05\x04' + self.Flags = None + self.Filler = b'\xFF' + self.EC = None + self.RRC = None + self.SND_SEQ = None + self.Data = None + + @staticmethod + def from_bytes(data): + return GSSWrapToken.from_buffer(io.BytesIO(data)) + + @staticmethod + def from_buffer(buff): + m = GSSWrapToken() + m.TOK_ID = buff.read(2) + m.Flags = FlagsField(int.from_bytes(buff.read(1), 'big', signed = False)) + m.Filler = buff.read(1) + m.EC = int.from_bytes(buff.read(2), 'big', signed = False) + m.RRC = int.from_bytes(buff.read(2), 'big', signed = False) + m.SND_SEQ = int.from_bytes(buff.read(8), 'big', signed = False) + return m + + def to_bytes(self): + t = self.TOK_ID + t += self.Flags.to_bytes(1, 'big', signed = False) + t += self.Filler + t += self.EC.to_bytes(2, 'big', signed = False) + t += self.RRC.to_bytes(2, 'big', signed = False) + t += self.SND_SEQ.to_bytes(8, 'big', signed = False) + if self.Data is not None: + t += self.Data + + return t + +class GSSAPI_AES: + def __init__(self, session_key, cipher_type, checksum_profile): + self.session_key = session_key + self.checksum_profile = checksum_profile + self.cipher_type = cipher_type + self.cipher = None + + def rotate(self, data, numBytes): + numBytes %= len(data) + left = len(data) - numBytes + result = data[left:] + data[:left] + return result + + def unrotate(self, data, numBytes): + numBytes %= len(data) + result = data[numBytes:] + data[:numBytes] + return result + + def GSS_GetMIC(self, data, seq_num): + pad = (4 - (len(data) % 4)) & 0x3 + padStr = bytes([pad]) * pad + data += padStr + + m = GSSMIC() + m.Flags = FlagsField.AcceptorSubkey + m.SND_SEQ = seq_num + checksum_profile = self.checksum_profile() + m.checksum = checksum_profile.checksum(self.session_key, KG_USAGE.INITIATOR_SIGN.value, data + m.to_bytes()[:16]) + + return m.to_bytes() + + def GSS_Wrap(self, data, seq_num, use_padding = False): + #print('[GSS_Wrap] seq_num: %s' % seq_num.to_bytes(4, 'big', signed = False).hex()) + cipher = self.cipher_type() + pad = 0 + if use_padding is True: + pad = ((cipher.blocksize - len(data)) % cipher.blocksize) #(cipher.blocksize - (len(data) % cipher.blocksize)) & 15 + padStr = b'\xFF' * pad + data += padStr + + t = GSSWrapToken() + t.Flags = FlagsField.AcceptorSubkey | FlagsField.Sealed + t.EC = pad + t.RRC = 0 + t.SND_SEQ = seq_num + + #print('Wrap data: %s' % (data + t.to_bytes())) + cipher_text = cipher.encrypt(self.session_key, KG_USAGE.INITIATOR_SEAL.value, data + t.to_bytes(), None) + t.RRC = 28 #[RFC4121] section 4.2.5 + cipher_text = self.rotate(cipher_text, t.RRC + t.EC) + + ret1 = cipher_text + ret2 = t.to_bytes() + + return ret1, ret2 + + def GSS_Unwrap(self, data, seq_num, direction='init', auth_data = None, use_padding = False): + #print('') + #print('Unwrap data %s' % data[16:]) + #print('Unwrap hdr %s' % data[:16]) + + cipher = self.cipher_type() + original_hdr = GSSWrapToken.from_bytes(data[:16]) + rotated = data[16:] + + cipher_text = self.unrotate(rotated, original_hdr.RRC + original_hdr.EC) + plain_text = cipher.decrypt(self.session_key, KG_USAGE.ACCEPTOR_SEAL.value, cipher_text) + new_hdr = GSSWrapToken.from_bytes(plain_text[-16:]) + + #signature checking + new_hdr.RRC = 28 + if data[:16] != new_hdr.to_bytes(): + return None, Exception('GSS_Unwrap signature mismatch!') + + + #print('Unwrap checksum: %s' % plain_text[-(original_hdr.EC + 16):]) + #print('Unwrap orig chk: %s' % original_hdr.to_bytes()) + #print('Unwrap result 1: %s' % plain_text) + #print('Unwrap result : %s' % plain_text[:-(original_hdr.EC + 16)]) + return plain_text[:-(original_hdr.EC + 16)], None + +def get_gssapi(session_key): + if session_key.enctype == encryption.Enctype.AES256: + return GSSAPI_AES(session_key, encryption._AES256CTS, encryption._SHA1AES256) + if session_key.enctype == encryption.Enctype.AES128: + return GSSAPI_AES(session_key, encryption._AES128CTS, encryption._SHA1AES128) + elif session_key.enctype == encryption.Enctype.RC4: + return GSSAPI_RC4(session_key) + else: + raise Exception('Unsupported etype %s' % session_key.enctype) + + +def test(): + data = b'\xAF' * 1024 + session_key = encryption.Key( encryption.Enctype.AES256 , bytes.fromhex('3e242e91996aadd513ecb1bc2369e44183e08e08c51550fa4b681e77f75ed8e1')) + sequenceNumber = 0 + gssapi = get_gssapi(session_key) + + r1, r2 = gssapi.GSS_Wrap(data, sequenceNumber) + print(len(r2)) + sent = r2 + r1 + print(r1) + ret1, ret2 = gssapi.GSS_Unwrap(sent, sequenceNumber) + + print(r1.hex()) + print(ret1.hex()) + + +if __name__ == '__main__': + test() \ No newline at end of file diff --git a/msldap/authentication/kerberos/multiplexor.py b/msldap/authentication/kerberos/multiplexor.py index f80b6da..1d78b7a 100644 --- a/msldap/authentication/kerberos/multiplexor.py +++ b/msldap/authentication/kerberos/multiplexor.py @@ -10,16 +10,44 @@ ## TODO: RPC auth type is not implemented or tested!!!! from msldap.authentication.spnego.asn1_structs import KRB5Token -from minikerberos.gssapi.gssapi import get_gssapi +from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken, KRB5_MECH_INDEP_TOKEN from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP from minikerberos.protocol.encryption import Enctype, Key, _enctype_table from multiplexor.operator.external.sspi import KerberosSSPIClient from multiplexor.operator import MultiplexorOperator +import enum -# SMBKerberosSSPICredential: +# mutual auth not supported +# encryption is always on +# we dont get the output flags back (lack of time to do the multiplexor protocol... TODO -class SMBKerberosMultiplexor: +class ISC_REQ(enum.IntFlag): + DELEGATE = 1 + MUTUAL_AUTH = 2 + REPLAY_DETECT = 4 + SEQUENCE_DETECT = 8 + CONFIDENTIALITY = 16 + USE_SESSION_KEY = 32 + PROMPT_FOR_CREDS = 64 + USE_SUPPLIED_CREDS = 128 + ALLOCATE_MEMORY = 256 + USE_DCE_STYLE = 512 + DATAGRAM = 1024 + CONNECTION = 2048 + CALL_LEVEL = 4096 + FRAGMENT_SUPPLIED = 8192 + EXTENDED_ERROR = 16384 + STREAM = 32768 + INTEGRITY = 65536 + IDENTIFY = 131072 + NULL_SESSION = 262144 + MANUAL_CRED_VALIDATION = 524288 + RESERVED1 = 1048576 + FRAGMENT_TO_FIT = 2097152 + HTTP = 0x10000000 + +class MSLDAPKerberosMultiplexor: def __init__(self, settings): self.iterations = 0 self.settings = settings @@ -30,11 +58,27 @@ self.gssapi = None self.etype = None self.session_key = None + self.seq_number = 0 + self.flags = ISC_REQ.CONNECTION self.setup() def setup(self): - return + if self.settings.encrypt is True: + self.flags = \ + ISC_REQ.CONFIDENTIALITY |\ + ISC_REQ.INTEGRITY |\ + ISC_REQ.REPLAY_DETECT |\ + ISC_REQ.SEQUENCE_DETECT + + def get_seq_number(self): + """ + Fetches the starting sequence number. This is either zero or can be found in the authenticator field of the + AP_REQ structure. As windows uses a random seq number AND a subkey as well, we can't obtain it by decrypting the + AP_REQ structure. Insead under the hood we perform an encryption operation via EncryptMessage API which will + yield the start sequence number + """ + return self.seq_number async def encrypt(self, data, message_no): return self.gssapi.GSS_Wrap(data, message_no) @@ -42,67 +86,52 @@ async def decrypt(self, data, message_no, direction='init', auth_data=None): return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) + def signing_needed(self): + """ + Checks if integrity protection was negotiated + """ + return ISC_REQ.INTEGRITY in self.flags + + def encryption_needed(self): + """ + Checks if confidentiality flag was negotiated + """ + return ISC_REQ.CONFIDENTIALITY in self.flags + def get_session_key(self): return self.session_key - async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): + async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): #authdata is only for api compatibility reasons if self.ksspi is None: await self.start_remote_kerberos() try: - if is_rpc == True: - raise Exception('Multiplexor kerberos for RPC is not yet implemented!') - #if self.iterations == 0: - # flags = ISC_REQ.CONFIDENTIALITY | \ - # ISC_REQ.INTEGRITY | \ - # ISC_REQ.MUTUAL_AUTH | \ - # ISC_REQ.REPLAY_DETECT | \ - # ISC_REQ.SEQUENCE_DETECT|\ - # ISC_REQ.USE_DCE_STYLE - # - # - # #token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) - # token = await self.ksspi.authenticate(self.settings.target, flags = flags, token_data = authData) - # print(token.hex()) - # self.iterations += 1 - # return token, True - # - #elif self.iterations == 1: - # flags = ISC_REQ.USE_DCE_STYLE - # - # #token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) - # token = await self.ksspi.get_ticket_for_spn(self.settings.target, flags = flags, token_data = authData) - # print(token.hex()) - # - # - # aprep = AP_REP.load(token).native - # - # subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) - # - # cipher_text = aprep['enc-part']['cipher'] - # cipher = _enctype_table[aprep['enc-part']['etype']]() - # - # plaintext = cipher.decrypt(subkey, 12, cipher_text) - # - # self.gssapi = get_gssapi(subkey) - # - # self.iterations += 1 - # return token, False - # - #else: - # raise Exception('Multiplexor Kerberos authentication exceeded maximum iteration counts') + apreq, res = await self.ksspi.authenticate(self.settings.target.to_target_string(), flags=str(self.flags.value)) + #print('MULTIPLEXOR KERBEROS SSPI, APREQ: %s ERROR: %s' % (apreq, res)) + if res is not None: + return None, None, res + + # here it seems like we get the full token not just the apreq data... + # so we need to discard the layers + + self.session_key, err = await self.ksspi.get_session_key() + if err is not None: + return None, None, err + + unwrap = KRB5_MECH_INDEP_TOKEN.from_bytes(apreq) + aprep = AP_REQ.load(unwrap.data[2:]).native + subkey = Key(aprep['ticket']['enc-part']['etype'], self.session_key) + self.gssapi = get_gssapi(subkey) - else: - apreq, res = await self.ksspi.authenticate(self.settings.target) - print('MULTIPLEXOR KERBEROS SSPI, APREQ: %s ERROR: %s' % (apreq, res)) - if res is None: - self.session_key, res = await self.ksspi.get_session_key() - - return apreq, res + if aprep['ticket']['enc-part']['etype'] != 23: + raw_seq_data, err = await self.ksspi.get_seq_number() + if err is not None: + return None, None, err + self.seq_number = GSSWrapToken.from_bytes(raw_seq_data[16:]).SND_SEQ + + return unwrap.data[2:], False, res except Exception as e: - import traceback - traceback.print_exc() - return None + return None, None, e async def start_remote_kerberos(self): try: diff --git a/msldap/authentication/kerberos/native.py b/msldap/authentication/kerberos/native.py index 1e10079..6d51adb 100644 --- a/msldap/authentication/kerberos/native.py +++ b/msldap/authentication/kerberos/native.py @@ -3,117 +3,218 @@ # This is just a simple interface to the minikerberos library to support SPNEGO # # -# - Hardships - -# 1. DCERPC kerberos authentication requires a complete different approach and flags, -# also requires mutual authentication -# # - Links - -# 1. Most of the idea was taken from impacket -# 2. See minikerberos library +# 1. See minikerberos library import datetime +import os from minikerberos.common import * -from minikerberos.protocol.asn1_structs import AP_REP, EncAPRepPart, EncryptedData -from minikerberos.gssapi.gssapi import get_gssapi + +from minikerberos.protocol.asn1_structs import AP_REP, EncAPRepPart, EncryptedData, AP_REQ, Ticket +from msldap.authentication.kerberos.gssapi import get_gssapi, KRB5_MECH_INDEP_TOKEN +from msldap.commons.proxy import MSLDAPProxyType from minikerberos.protocol.structures import ChecksumFlags from minikerberos.protocol.encryption import Enctype, Key, _enctype_table from minikerberos.protocol.constants import MESSAGE_TYPE from minikerberos.aioclient import AIOKerberosClient +from minikerberos.network.aioclientsockssocket import AIOKerberosClientSocksSocket +from msldap import logger # SMBKerberosCredential + +MSLDAP_SOCKS_PROXY_TYPES = [ + MSLDAPProxyType.SOCKS4, + MSLDAPProxyType.SOCKS4_SSL, + MSLDAPProxyType.SOCKS5, + MSLDAPProxyType.SOCKS5_SSL, + MSLDAPProxyType.WSNET, +] class MSLDAPKerberos: def __init__(self, settings): self.settings = settings + self.signing_preferred = None + self.encryption_preferred = None self.ccred = None self.target = None self.spn = None self.kc = None + self.flags = None + self.preferred_etypes = [23,17,18] self.session_key = None self.gssapi = None self.iterations = 0 self.etype = None + self.seq_number = 0 + self.expected_server_seq_number = None + self.from_ccache = False self.setup() + def get_seq_number(self): + """ + Returns the initial sequence number. It is 0 by default, but can be adjusted during authentication, + by passing the 'seq_number' parameter in the 'authenticate' function + """ + return self.seq_number + def signing_needed(self): - return False + """ + Checks if integrity protection was negotiated + """ + return ChecksumFlags.GSS_C_INTEG_FLAG in self.flags def encryption_needed(self): - return False #change to true to enable encryption channel binding + """ + Checks if confidentiality flag was negotiated + """ + return ChecksumFlags.GSS_C_CONF_FLAG in self.flags async def sign(self, data, message_no, direction = 'init'): + """ + Signs a message. + """ return self.gssapi.GSS_GetMIC(data, message_no, direction = direction) async def encrypt(self, data, message_no): + """ + Encrypts a message. + """ + return self.gssapi.GSS_Wrap(data, message_no) - async def decrypt(self, data, message_no, direction='init', auth_data=None): - return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) + async def decrypt(self, data, message_no, direction='init'): + """ + Decrypts message. Also performs integrity checking. + """ + + return self.gssapi.GSS_Unwrap(data, message_no, direction=direction) def setup(self): self.ccred = self.settings.ccred self.spn = self.settings.spn self.target = self.settings.target - - self.kc = AIOKerberosClient(self.ccred, self.target) + if self.settings.enctypes is not None: + self.preferred_etypes = self.settings.enctypes + + self.flags = ChecksumFlags.GSS_C_MUTUAL_FLAG + if self.settings.encrypt is True: + self.flags = \ + ChecksumFlags.GSS_C_CONF_FLAG |\ + ChecksumFlags.GSS_C_INTEG_FLAG |\ + ChecksumFlags.GSS_C_REPLAY_FLAG |\ + ChecksumFlags.GSS_C_SEQUENCE_FLAG def get_session_key(self): - return self.session_key.contents - - async def authenticate(self, authData, flags = None, seq_number = 0, is_rpc = False): - - if self.iterations == 0: - #tgt = await self.kc.get_TGT(override_etype=[18]) - tgt = await self.kc.get_TGT() - tgs, encpart, self.session_key = await self.kc.get_TGS(self.spn) - self.gssapi = get_gssapi(self.session_key) - ap_opts = [] - if is_rpc == True: + return self.session_key.contents, None + + + async def setup_kc(self): + try: + # sockst/wsnet proxying is handled by the minikerberos&asysocks modules + if self.target.proxy is None or self.target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES: + self.kc = AIOKerberosClient(self.ccred, self.target) + + elif self.target.proxy.type in [MSLDAPProxyType.MULTIPLEXOR, MSLDAPProxyType.MULTIPLEXOR_SSL]: + from msldap.network.multiplexor import MultiplexorProxyConnection + mpc = MultiplexorProxyConnection(self.target) + socks_proxy = await mpc.connect(is_kerberos = True) + + self.kc = AIOKerberosClient(self.ccred, socks_proxy) + + else: + raise Exception('Unknown proxy type %s' % self.target.proxy.type) + + return None, None + except Exception as e: + return None, e + + async def authenticate(self, authData, flags = None, seq_number = 0, cb_data = None): + """ + This function is called (multiple times depending on the flags) to perform authentication. + """ + try: + if self.kc is None: + _, err = await self.setup_kc() + if err is not None: + return None, None, err + if self.iterations == 0: - ap_opts.append('mutual-required') - flags = ChecksumFlags.GSS_C_CONF_FLAG | ChecksumFlags.GSS_C_INTEG_FLAG | ChecksumFlags.GSS_C_SEQUENCE_FLAG|\ - ChecksumFlags.GSS_C_REPLAY_FLAG | ChecksumFlags.GSS_C_MUTUAL_FLAG | ChecksumFlags.GSS_C_DCE_STYLE - - apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = flags, seq_number = seq_number, ap_opts=ap_opts) + self.seq_number = 0 self.iterations += 1 - return apreq, False - + + try: + #check TGS first, maybe ccache already has what we need + for target in self.ccred.ccache.list_targets(): + # just printing this to debug... + logger.debug('CCACHE SPN record: %s' % target) + tgs, encpart, self.session_key = await self.kc.get_TGS(self.spn) + + self.from_ccache = True + except: + tgt = await self.kc.get_TGT(override_etype = self.preferred_etypes) + tgs, encpart, self.session_key = await self.kc.get_TGS(self.spn)#, override_etype = self.preferred_etypes) + + #self.expected_server_seq_number = encpart.get('nonce', seq_number) + + ap_opts = [] + if ChecksumFlags.GSS_C_MUTUAL_FLAG in self.flags or ChecksumFlags.GSS_C_DCE_STYLE in self.flags: + if ChecksumFlags.GSS_C_MUTUAL_FLAG in self.flags: + ap_opts.append('mutual-required') + if self.from_ccache is False: + 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) + else: + apreq = self.kc.construct_apreq_from_ticket(Ticket(tgs['ticket']).dump(), self.session_key, tgs['crealm'], tgs['cname']['name-string'][0], flags = self.flags, seq_number = self.seq_number, ap_opts = ap_opts, cb_data = cb_data) + return apreq, True, None + + else: + #no mutual or dce auth will take one step only + if self.from_ccache is False: + apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = self.flags, seq_number = self.seq_number, ap_opts=[], cb_data = cb_data) + else: + apreq = self.kc.construct_apreq_from_ticket(Ticket(tgs['ticket']).dump(), self.session_key, tgs['crealm'], tgs['cname']['name-string'][0], flags = self.flags, seq_number = self.seq_number, ap_opts = ap_opts, cb_data = cb_data) + + + self.gssapi = get_gssapi(self.session_key) + return apreq, False, None + else: - #mutual authentication part here - aprep = AP_REP.load(authData).native + self.iterations += 1 + if ChecksumFlags.GSS_C_DCE_STYLE in self.flags: + # adata = authData[16:] + # if ChecksumFlags.GSS_C_DCE_STYLE in self.flags: + # adata = authData + raise Exception('DCE auth Not implemented!') + + # at this point we are dealing with mutual authentication + # This means that the server sent back an AP-rep wrapped in a token + # The APREP contains a new session key we'd need to update and a seq-number + # that is expected the server will use for future communication. + # For mutual auth we dont need to reply anything after this step, + # but for DCE auth a reply is expected. TODO + + # converting the token to aprep + token = KRB5_MECH_INDEP_TOKEN.from_bytes(authData) + if token.data[:2] != b'\x02\x00': + raise Exception('Unexpected token type! %s' % token.data[:2].hex() ) + aprep = AP_REP.load(token.data[2:]).native + + # decrypting aprep cipher = _enctype_table[int(aprep['enc-part']['etype'])]() cipher_text = aprep['enc-part']['cipher'] temp = cipher.decrypt(self.session_key, 12, cipher_text) - enc_part = EncAPRepPart.load(temp).native - cipher = _enctype_table[int(enc_part['subkey']['keytype'])]() - - now = datetime.datetime.now(datetime.timezone.utc) - apreppart_data = {} - apreppart_data['cusec'] = now.microsecond - apreppart_data['ctime'] = now.replace(microsecond=0) - apreppart_data['seq-number'] = enc_part['seq-number'] - - apreppart_data_enc = cipher.encrypt(self.session_key, 12, EncAPRepPart(apreppart_data).dump(), None) - - #overriding current session key - self.session_key = Key(cipher.enctype, enc_part['subkey']['keyvalue']) - - ap_rep = {} - ap_rep['pvno'] = 5 - ap_rep['msg-type'] = MESSAGE_TYPE.KRB_AP_REP.value - ap_rep['enc-part'] = EncryptedData({'etype': self.session_key.enctype, 'cipher': apreppart_data_enc}) - - token = AP_REP(ap_rep).dump() + + #updating session key, gssapi + self.session_key = Key(int(enc_part['subkey']['keytype']), enc_part['subkey']['keyvalue']) + #self.seq_number = enc_part.get('seq-number', 0) self.gssapi = get_gssapi(self.session_key) - self.iterations += 1 - - return token, False - else: - apreq = self.kc.construct_apreq(tgs, encpart, self.session_key, flags = flags, seq_number = seq_number, ap_opts=ap_opts) - return apreq, False \ No newline at end of file + + return b'', False, None + + except Exception as e: + return None, None, e \ No newline at end of file diff --git a/msldap/authentication/kerberos/sspi.py b/msldap/authentication/kerberos/sspi.py index bfe2207..35abbef 100644 --- a/msldap/authentication/kerberos/sspi.py +++ b/msldap/authentication/kerberos/sspi.py @@ -6,9 +6,9 @@ # from msldap.authentication.spnego.asn1_structs import KRB5Token -from winsspi.sspi import KerberosSMBSSPI -from winsspi.common.function_defs import ISC_REQ -from minikerberos.gssapi.gssapi import get_gssapi +from winsspi.sspi import KerberosMSLDAPSSPI +from winsspi.common.function_defs import ISC_REQ, GetSequenceNumberFromEncryptdataKerberos +from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP from minikerberos.protocol.encryption import Enctype, Key, _enctype_table @@ -16,71 +16,133 @@ def __init__(self, settings): self.iterations = 0 self.settings = settings - self.mode = 'CLIENT' - self.ksspi = KerberosSMBSSPI() - self.client = None - self.target = None + self.username = settings.username + self.password = settings.password + self.domain = settings.domain + self.actual_ctx_flags = None #this will be popilated by the output of get_ticket_for_spn + self.flags = ISC_REQ.CONNECTION + if settings.encrypt is True: + self.flags = ISC_REQ.CONFIDENTIALITY| ISC_REQ.INTEGRITY | ISC_REQ.CONNECTION #| ISC_REQ.MUTUAL_AUTH #| ISC_REQ.USE_DCE_STYLE + self.ksspi = None + self.spn = settings.spn self.gssapi = None self.etype = None - - self.setup() - - def setup(self): - self.mode = self.settings.mode - self.client = self.settings.client - self.target = self.settings.target - + self.session_key = None + self.seq_number = None + + def get_seq_number(self): + """ + Fetches the starting sequence number. This is either zero or can be found in the authenticator field of the + AP_REQ structure. As windows uses a random seq number AND a subkey as well, we can't obtain it by decrypting the + AP_REQ structure. Insead under the hood we perform an encryption operation via EncryptMessage API which will + yield the start sequence number + """ + if self.seq_number is not None: + return self.seq_number + if ISC_REQ.CONFIDENTIALITY in self.actual_ctx_flags: + self.seq_number = GetSequenceNumberFromEncryptdataKerberos(self.ksspi.context) + if self.seq_number is None: + self.seq_number = 0 + + return self.seq_number + + def signing_needed(self): + """ + Checks if integrity protection was enabled + """ + return ISC_REQ.INTEGRITY in self.actual_ctx_flags + + def encryption_needed(self): + """ + Checks if confidentiality was enabled + """ + return ISC_REQ.CONFIDENTIALITY in self.actual_ctx_flags + + async def sign(self, data, message_no, direction = 'init'): + """ + Signs a message. + """ + return self.gssapi.GSS_GetMIC(data, message_no, direction = direction) + async def encrypt(self, data, message_no): + """ + Encrypts a message. + """ return self.gssapi.GSS_Wrap(data, message_no) - async def decrypt(self, data, message_no, direction='init', auth_data=None): - return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) + async def decrypt(self, data, message_no, direction='init'): + """ + Decrypts message. Also performs integrity checking. + """ + return self.gssapi.GSS_Unwrap(data, message_no, direction=direction) def get_session_key(self): - return self.ksspi.get_session_key() + """ + Fetches the session key. Under the hood this uses QueryContextAttributes API call. + This will fail if the authentication is not yet finished! + """ + err = None + if self.session_key is None: + self.session_key, err = self.ksspi.get_session_key() + return self.session_key, err - async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): - #authdata is only for api compatibility reasons - if is_rpc == True: + async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data = None): + """ + This function is called (multiple times depending on the flags) to perform authentication. + """ + try: if self.iterations == 0: - flags = ISC_REQ.CONFIDENTIALITY | \ - ISC_REQ.INTEGRITY | \ - ISC_REQ.MUTUAL_AUTH | \ - ISC_REQ.REPLAY_DETECT | \ - ISC_REQ.SEQUENCE_DETECT|\ - ISC_REQ.USE_DCE_STYLE + self.ksspi = KerberosMSLDAPSSPI(domain = self.domain, username=self.username, password=self.password) + token, self.actual_ctx_flags = self.ksspi.get_ticket_for_spn(self.spn, ctx_flags = self.flags) + self.iterations += 1 + + + if ISC_REQ.MUTUAL_AUTH in self.actual_ctx_flags or ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: + #in these cases continuation is needed + return token, True, None + + else: + #no mutual or dce auth will take one step only + _, err = self.get_session_key() + if err is not None: + return None, None, err + apreq = AP_REQ.load(token).native + subkey = Key(apreq['ticket']['enc-part']['etype'], self.session_key) + self.gssapi = get_gssapi(subkey) + self.get_seq_number() + return token, False, None - token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) - #print(token.hex()) - self.iterations += 1 - return token, True - elif self.iterations == 1: - flags = ISC_REQ.USE_DCE_STYLE - - token = self.ksspi.get_ticket_for_spn(self.target, flags = flags, is_rpc = True, token_data = authData) - #print(token.hex()) + else: + adata = authData[16:] + if ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: + adata = authData + token, self.actual_ctx_flags = self.ksspi.get_ticket_for_spn(self.spn, ctx_flags = self.actual_ctx_flags, token_data = adata) - aprep = AP_REP.load(token).native + + if ISC_REQ.USE_DCE_STYLE in self.actual_ctx_flags: + #Using DCE style 3-legged auth + aprep = AP_REP.load(token).native + else: + aprep = AP_REP.load(adata).native + subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) + + _, err = self.get_session_key() + if err is not None: + return None, None, err - subkey = Key(aprep['enc-part']['etype'], self.get_session_key()) - - cipher_text = aprep['enc-part']['cipher'] - cipher = _enctype_table[aprep['enc-part']['etype']]() - - plaintext = cipher.decrypt(subkey, 12, cipher_text) - + _, err = self.get_seq_number() + if err is not None: + return None, None, err + + subkey = Key(token['enc-part']['etype'], self.session_key) self.gssapi = get_gssapi(subkey) self.iterations += 1 - return token, False - - else: - raise Exception('SSPI Kerberos -RPC - auth encountered too many calls for authenticate.') + return token, False, None - else: - apreq = self.ksspi.get_ticket_for_spn(self.target) - return apreq, False + except Exception as e: + return None, None, e diff --git a/msldap/authentication/kerberos/sspiproxyws.py b/msldap/authentication/kerberos/sspiproxyws.py new file mode 100644 index 0000000..cf086b7 --- /dev/null +++ b/msldap/authentication/kerberos/sspiproxyws.py @@ -0,0 +1,126 @@ + +## +## +## Interface to allow remote kerberos authentication via Multiplexor +## +## +## +## +## +## TODO: RPC auth type is not implemented or tested!!!! + +import enum + +from msldap.authentication.spnego.asn1_structs import KRB5Token +from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken, KRB5_MECH_INDEP_TOKEN +from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP +from minikerberos.protocol.encryption import Enctype, Key, _enctype_table +from pyodidewsnet.sspiproxyws import SSPIProxyWS + + +# mutual auth not supported +# encryption is always on + +class ISC_REQ(enum.IntFlag): + DELEGATE = 1 + MUTUAL_AUTH = 2 + REPLAY_DETECT = 4 + SEQUENCE_DETECT = 8 + CONFIDENTIALITY = 16 + USE_SESSION_KEY = 32 + PROMPT_FOR_CREDS = 64 + USE_SUPPLIED_CREDS = 128 + ALLOCATE_MEMORY = 256 + USE_DCE_STYLE = 512 + DATAGRAM = 1024 + CONNECTION = 2048 + CALL_LEVEL = 4096 + FRAGMENT_SUPPLIED = 8192 + EXTENDED_ERROR = 16384 + STREAM = 32768 + INTEGRITY = 65536 + IDENTIFY = 131072 + NULL_SESSION = 262144 + MANUAL_CRED_VALIDATION = 524288 + RESERVED1 = 1048576 + FRAGMENT_TO_FIT = 2097152 + HTTP = 0x10000000 + +class MSLDAPSSPIProxyKerberosAuth: + def __init__(self, settings): + self.iterations = 0 + self.settings = settings + self.mode = 'CLIENT' + url = '%s://%s:%s' % (self.settings.proto, self.settings.host, self.settings.port) + self.sspi = SSPIProxyWS(url, self.settings.agent_id) + self.client = None + self.target = None + self.gssapi = None + self.etype = None + self.session_key = None + self.seq_number = 0 + self.flags = ISC_REQ.CONNECTION + + self.setup() + + def setup(self): + if self.settings.encrypt is True: + self.flags = \ + ISC_REQ.CONFIDENTIALITY |\ + ISC_REQ.INTEGRITY |\ + ISC_REQ.REPLAY_DETECT |\ + ISC_REQ.SEQUENCE_DETECT + + def get_seq_number(self): + return self.seq_number + + async def encrypt(self, data, message_no): + return self.gssapi.GSS_Wrap(data, message_no) + + async def decrypt(self, data, message_no, direction='init', auth_data=None): + return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) + + def signing_needed(self): + """ + Checks if integrity protection was negotiated + """ + return ISC_REQ.INTEGRITY in self.flags + + def encryption_needed(self): + """ + Checks if confidentiality flag was negotiated + """ + return ISC_REQ.CONFIDENTIALITY in self.flags + + def get_session_key(self): + return self.session_key + + async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): + try: + status, ctxattr, apreq, err = await self.sspi.authenticate('KERBEROS', '', self.settings.target.to_target_string(), 3, self.flags.value, authdata = b'') + if err is not None: + raise err + + self.flags = ISC_REQ(ctxattr) + + self.session_key, err = await self.sspi.get_sessionkey() + if err is not None: + return None, None, err + + unwrap = KRB5_MECH_INDEP_TOKEN.from_bytes(apreq) + aprep = AP_REQ.load(unwrap.data[2:]).native + subkey = Key(aprep['ticket']['enc-part']['etype'], self.session_key) + self.gssapi = get_gssapi(subkey) + + if aprep['ticket']['enc-part']['etype'] != 23: + if ISC_REQ.CONFIDENTIALITY in self.flags: + raw_seq_data, err = await self.sspi.get_sequenceno() + if err is not None: + return None, None, err + self.seq_number = GSSWrapToken.from_bytes(raw_seq_data[16:]).SND_SEQ + + return unwrap.data[2:], False, None + except Exception as e: + return None, None, e + + \ No newline at end of file diff --git a/msldap/authentication/kerberos/wsnet.py b/msldap/authentication/kerberos/wsnet.py new file mode 100644 index 0000000..0fdf595 --- /dev/null +++ b/msldap/authentication/kerberos/wsnet.py @@ -0,0 +1,125 @@ + +## +## +## Interface to allow remote kerberos authentication via Multiplexor +## +## +## +## +## +## TODO: RPC auth type is not implemented or tested!!!! + +import enum + +from msldap.authentication.spnego.asn1_structs import KRB5Token +from msldap.authentication.kerberos.gssapi import get_gssapi, GSSWrapToken, KRB5_MECH_INDEP_TOKEN +from minikerberos.protocol.asn1_structs import AP_REQ, AP_REP, TGS_REP +from minikerberos.protocol.encryption import Enctype, Key, _enctype_table +from pyodidewsnet.clientauth import WSNETAuth + + +# mutual auth not supported +# encryption is always on + +class ISC_REQ(enum.IntFlag): + DELEGATE = 1 + MUTUAL_AUTH = 2 + REPLAY_DETECT = 4 + SEQUENCE_DETECT = 8 + CONFIDENTIALITY = 16 + USE_SESSION_KEY = 32 + PROMPT_FOR_CREDS = 64 + USE_SUPPLIED_CREDS = 128 + ALLOCATE_MEMORY = 256 + USE_DCE_STYLE = 512 + DATAGRAM = 1024 + CONNECTION = 2048 + CALL_LEVEL = 4096 + FRAGMENT_SUPPLIED = 8192 + EXTENDED_ERROR = 16384 + STREAM = 32768 + INTEGRITY = 65536 + IDENTIFY = 131072 + NULL_SESSION = 262144 + MANUAL_CRED_VALIDATION = 524288 + RESERVED1 = 1048576 + FRAGMENT_TO_FIT = 2097152 + HTTP = 0x10000000 + +class MSLDAPWSNetKerberosAuth: + def __init__(self, settings): + self.iterations = 0 + self.settings = settings + self.mode = 'CLIENT' + self.sspi = WSNETAuth() + self.client = None + self.target = None + self.gssapi = None + self.etype = None + self.session_key = None + self.seq_number = 0 + self.flags = ISC_REQ.CONNECTION + + self.setup() + + def setup(self): + if self.settings.encrypt is True: + self.flags = \ + ISC_REQ.CONFIDENTIALITY |\ + ISC_REQ.INTEGRITY |\ + ISC_REQ.REPLAY_DETECT |\ + ISC_REQ.SEQUENCE_DETECT + + def get_seq_number(self): + return self.seq_number + + async def encrypt(self, data, message_no): + return self.gssapi.GSS_Wrap(data, message_no) + + async def decrypt(self, data, message_no, direction='init', auth_data=None): + return self.gssapi.GSS_Unwrap(data, message_no, direction=direction, auth_data=auth_data) + + def signing_needed(self): + """ + Checks if integrity protection was negotiated + """ + return ISC_REQ.INTEGRITY in self.flags + + def encryption_needed(self): + """ + Checks if confidentiality flag was negotiated + """ + return ISC_REQ.CONFIDENTIALITY in self.flags + + def get_session_key(self): + return self.session_key + + async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): + try: + status, ctxattr, apreq, err = await self.sspi.authenticate('KERBEROS', '', self.settings.target.to_target_string(), 3, self.flags.value, authdata = b'') + if err is not None: + raise err + + self.flags = ISC_REQ(ctxattr) + + self.session_key, err = await self.sspi.get_sessionkey() + if err is not None: + return None, None, err + + unwrap = KRB5_MECH_INDEP_TOKEN.from_bytes(apreq) + aprep = AP_REQ.load(unwrap.data[2:]).native + subkey = Key(aprep['ticket']['enc-part']['etype'], self.session_key) + self.gssapi = get_gssapi(subkey) + + if aprep['ticket']['enc-part']['etype'] != 23: + if ISC_REQ.CONFIDENTIALITY in self.flags: + raw_seq_data, err = await self.sspi.get_sequenceno() + if err is not None: + return None, None, err + self.seq_number = GSSWrapToken.from_bytes(raw_seq_data[16:]).SND_SEQ + + return unwrap.data[2:], False, None + except Exception as e: + return None, None, e + + \ No newline at end of file diff --git a/msldap/authentication/ntlm/messages/authenticate.py b/msldap/authentication/ntlm/messages/authenticate.py index 892c96b..d6e8797 100644 --- a/msldap/authentication/ntlm/messages/authenticate.py +++ b/msldap/authentication/ntlm/messages/authenticate.py @@ -229,7 +229,3 @@ def test_construct(): pass -if __name__ == '__main__': - from aiosmb.utils.hexdump import hexdump - - test() \ No newline at end of file diff --git a/msldap/authentication/ntlm/messages/challenge.py b/msldap/authentication/ntlm/messages/challenge.py index dd73f9b..8d2c382 100644 --- a/msldap/authentication/ntlm/messages/challenge.py +++ b/msldap/authentication/ntlm/messages/challenge.py @@ -160,7 +160,3 @@ def test_construct(): pass -if __name__ == '__main__': - from aiosmb.utils.hexdump import hexdump - - test() \ No newline at end of file diff --git a/msldap/authentication/ntlm/multiplexor.py b/msldap/authentication/ntlm/multiplexor.py index a7821df..902784d 100644 --- a/msldap/authentication/ntlm/multiplexor.py +++ b/msldap/authentication/ntlm/multiplexor.py @@ -41,7 +41,19 @@ FRAGMENT_TO_FIT = 2097152 HTTP = 0x10000000 -class SMBNTLMMultiplexor: +# +# +# Interface to support remote authentication via multiplexor +# +# Connects to the multiplexor server, and starts an SSPI server locally for the specific agentid +# SSPI server will be used to perform NTLM authentication remotely, +# while constructing a local NTLM authentication object +# After the auth finishes, it also grabs the sessionkey. +# The NTLM object can be used in future operations (encrypt/decrypt/sign) locally +# without the need of future remote calls +# + +class MSLDAPNTLMMultiplexor: def __init__(self, settings): self.settings = settings self.mode = None #'CLIENT' @@ -49,7 +61,7 @@ self.operator = None self.client = None self.target = None - #self.ntlmChallenge = None + self.seq_number = 0 self.session_key = None self.ntlm_ctx = NTLMAUTHHandler(NTLMHandlerSettings(None, 'MANUAL')) @@ -66,58 +78,57 @@ def get_signkey(self, mode = 'Client'): return self.ntlm_ctx.get_signkey(mode = mode) - - - def SEAL(self, signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt): - return self.ntlm_ctx.SEAL(signingKey, sealingKey, messageToSign, messageToEncrypt, seqNum, cipher_encrypt) - - def SIGN(self, signingKey, message, seqNum, cipher_encrypt): - return self.ntlm_ctx.SIGN(signingKey, message, seqNum, cipher_encrypt) def get_session_key(self): return self.session_key - def get_extra_info(self): - return self.ntlm_ctx.get_extra_info() - def is_extended_security(self): return self.ntlm_ctx.is_extended_security() + + def get_seq_number(self): + return self.seq_number + + def signing_needed(self): + return self.ntlm_ctx.signing_needed() + + def encryption_needed(self): + return self.ntlm_ctx.encryption_needed() - #async def encrypt(self, data, message_no): - # return self.sspi.encrypt(data, message_no) - # - #async def decrypt(self, data, message_no): - # return self.sspi.decrypt(data, message_no) + async def encrypt(self, data, message_no): + return await self.ntlm_ctx.encrypt(data, message_no) + + async def decrypt(self, data, sequence_no, direction='init', auth_data=None): + return await self.ntlm_ctx.decrypt(data, sequence_no, direction=direction, auth_data=auth_data) + + async def sign(self, data, message_no, direction=None, reset_cipher = False): + return await self.ntlm_ctx.sign(data, message_no, direction=None, reset_cipher = reset_cipher) - async def authenticate(self, authData = None, flags = None, seq_number = 0, is_rpc = False): + async def authenticate(self, authData = None, flags = None, seq_number = 0, cb_data=None): + is_rpc = False if self.sspi is None: - res = await self.start_remote_sspi() - if res is None: - raise Exception('Failed to start remote SSPI') + res, err = await self.start_remote_sspi() + if err is not None: + return None, None, err if is_rpc is True and flags is None: flags = ISC_REQ.REPLAY_DETECT | ISC_REQ.CONFIDENTIALITY| ISC_REQ.USE_SESSION_KEY| ISC_REQ.INTEGRITY| ISC_REQ.SEQUENCE_DETECT| ISC_REQ.CONNECTION flags = int(flags) - if self.settings.mode == 'CLIENT': - if authData is None: - data, res = await self.sspi.authenticate(flags = flags) + if authData is None: + data, res = await self.sspi.authenticate(flags = flags) + if res is None: + self.ntlm_ctx.load_negotiate(data) + return data, res, None + else: + self.ntlm_ctx.load_challenge( authData) + data, res = await self.sspi.challenge(authData, flags = flags) + if res is None: + self.ntlm_ctx.load_authenticate( data) + self.session_key, res = await self.sspi.get_session_key() if res is None: - self.ntlm_ctx.load_negotiate(data) - return data, res - else: - self.ntlm_ctx.load_challenge( authData) - data, res = await self.sspi.challenge(authData, flags = flags) - if res is None: - self.ntlm_ctx.load_authenticate( data) - self.session_key, res = await self.sspi.get_session_key() - if res is None: - self.ntlm_ctx.load_sessionkey(self.get_session_key()) + self.ntlm_ctx.load_sessionkey(self.get_session_key()) - return data, res - - else: - raise Exception('Server mode not implemented!') + return data, res, None async def start_remote_sspi(self): @@ -134,10 +145,10 @@ #print(sspi_url) self.sspi = SSPINTLMClient(sspi_url) await self.sspi.connect() - return True + return True, None except Exception as e: import traceback traceback.print_exc() - return None + return None, e diff --git a/msldap/authentication/ntlm/native.py b/msldap/authentication/ntlm/native.py index 64e446b..53aa537 100644 --- a/msldap/authentication/ntlm/native.py +++ b/msldap/authentication/ntlm/native.py @@ -1,8 +1,8 @@ - import os import struct import hmac import copy +import hashlib #from aiosmb.commons.connection.credential import SMBNTLMCredential #from aiosmb.commons.serverinfo import NTLMServerInfo @@ -25,6 +25,8 @@ self.mode = mode self.template_name = template_name self.custom_template = custom_template #for custom templates, must be dict + + self.encrypt = False self.template = None self.ntlm_downgrade = False @@ -41,13 +43,18 @@ self.template = self.custom_template + self.encrypt = self.credential.encrypt + + if self.encrypt is True: + self.template_name = 'Windows10_15063_channel' + if self.mode.upper() == 'SERVER': if self.template_name in NTLMServerTemplates: self.template = NTLMServerTemplates[self.template_name] else: raise Exception('No NTLM server template found with name %s' % self.template_name) - else: + else: if self.template_name in NTLMClientTemplates: self.template = NTLMClientTemplates[self.template_name] if 'ntlm_downgrade' in self.template: @@ -85,10 +92,10 @@ self.crypthandle_client = None self.crypthandle_server = None - self.signhandle_server = None - self.signhandle_client = None - - + #self.signhandle_server = None doesnt exists, only crypthandle + #self.signhandle_client = None doesnt exists, only crypthandle + + self.seq_number = 0 self.iteration_cnt = 0 self.ntlm_credentials = None self.timestamp = None #used in unittest only! @@ -110,19 +117,6 @@ self.RandomSessionKey = self.settings.template['session_key'] self.timestamp = self.settings.template.get('timestamp') #used in unittest only! - - - if self.mode.upper() == 'SERVER': - version = self.settings.template['version'] - targetName = self.settings.template['targetname'] - targetInfo = self.settings.template['targetinfo'] - - self.ntlmChallenge = NTLMChallenge.construct(challenge = self.challenge, targetName = targetName, targetInfo = targetInfo, version = version, flags = self.flags) - - #else: - # domainname = self.settings.template['domain_name'] - # workstationname = self.settings.template['workstation_name'] - # version = self.settings.template.get('version') def load_negotiate(self, data): self.ntlmNegotiate = NTLMNegotiate.from_bytes(data) @@ -136,6 +130,9 @@ def load_sessionkey(self, data): self.RandomSessionKey = data self.setup_crypto() + + def get_seq_number(self): + return self.seq_number def set_sign(self, tf = True): if tf == True: @@ -189,48 +186,71 @@ #msg.Checksum = struct.unpack(' get ridmanagerreference attr -> look up the dn of ridmanagerreference -> get fsmoroleowner attr (which is a DN) - if not self._ldapinfo: - self.get_ad_info() - - ldap_filter = r'(distinguishedName=%s)' % self._ldapinfo.rIDManagerReference - async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): - return entry['attributes']['fSMORoleOwner'] - - async def get_infrastructureowner(self): - #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ - #"CN=Infrastructure,DC=concorp,DC=contoso,DC=com" -l fSMORoleOwner - if not self._ldapinfo: - self.get_ad_info() - - ldap_filter = r'(distinguishedName=%s)' % ('CN=Infrastructure,' + self._ldapinfo.distinguishedName) - async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): - return entry['attributes']['fSMORoleOwner'] - - async def get_ridroleowner(self): - #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ - if not self._ldapinfo: - self.get_ad_info() - - ldap_filter = r'(distinguishedName=%s)' % ('CN=RID Manager$,CN=System,' + self._ldapinfo.distinguishedName) - async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): - return entry['attributes']['fSMORoleOwner'] - - - async def get_netdomain(self): - def nameconvert(x): - return x.split(',CN=')[1] - """ - gets the name of the current user's domain - """ - if not self._ldapinfo: - self.get_ad_info() - print(self._ldapinfo) - dname = self._ldapinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') - domain_controllers = ','.join(nameconvert(x) + '.' +dname for x in self._ldapinfo.masteredBy) - - ridroleowner = nameconvert(self.get_ridroleowner()) + '.' +dname - infraowner = nameconvert(self.get_infrastructureowner()) + '.' +dname - pdcroleowner = nameconvert(self.get_pdcroleowner()) + '.' +dname - - print('name : %s' % dname) - print('Domain Controllers : %s' % domain_controllers) - print('DomainModeLevel : %s' % self._ldapinfo.domainmodelevel) - print('PdcRoleOwner : %s' % pdcroleowner) - print('RidRoleOwner : %s' % ridroleowner) - print('InfrastructureRoleOwner : %s' % infraowner) - - async def get_domaincontroller(self): - ldap_filter = r'(userAccountControl:1.2.840.113556.1.4.803:=8192)' - async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): - print('Forest: %s' % '') - print('Name: %s' % entry['attributes'].get('dNSHostName')) - print('OSVersion: %s' % entry['attributes'].get('operatingSystem')) - print(entry['attributes']) - - - async def get_all_groups(self): - ldap_filter = r'(objectClass=group)' - async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): - yield MSADGroup.from_ldap(entry) - - async def get_all_ous(self): - ldap_filter = r'(objectClass=organizationalUnit)' - async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): - yield MSADOU.from_ldap(entry) - - async def get_group_by_dn(self, dn): - ldap_filter = r'(&(objectClass=group)(distinguishedName=%s))' % escape_filter_chars(dn) - async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): - yield MSADGroup.from_ldap(entry) - - async def get_object_by_dn(self, dn, expected_class = None): - ldap_filter = r'(distinguishedName=%s)' % dn - async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): - temp = entry['attributes'].get('objectClass') - if expected_class: - yield expected_class.from_ldap(entry) - - if not temp: - yield entry - elif 'user' in temp: - yield MSADUser.from_ldap(entry) - elif 'group' in temp: - yield MSADGroup.from_ldap(entry) - - async def get_user_by_dn(self, dn): - ldap_filter = r'(&(objectClass=user)(distinguishedName=%s))' % dn - async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): - yield MSADUser.from_ldap(entry) - - async def get_group_members(self, dn, recursive = False): - async for group in self.get_group_by_dn(dn): - for member in group.member: - async for result in self.get_object_by_dn(member): - if isinstance(result, MSADGroup) and recursive: - async for user in self.get_group_members(result.distinguishedName, recursive = True): - yield(user) - else: - yield(result) - - async def get_dn_for_objectsid(self, objectsid): - ldap_filter = r'(objectSid=%s)' % str(objectsid) - async for entry in self.pagedsearch(ldap_filter, ['distinguishedName']): - return entry['attributes']['distinguishedName'] - - async def get_permissions_for_dn(self, dn): - """ - Lists all users who can modify the specified dn - """ - async for secinfo in self.get_objectacl_by_dn(dn): - for sdec in secinfo.nTSecurityDescriptor: - sids_to_lookup = {} - if not sdec.Dacl: - continue - - for ace in sdec.Dacl.aces: - sids_to_lookup[str(ace.Sid)] = 1 - - for sid in sids_to_lookup: - sids_to_lookup[sid] = self.get_dn_for_objectsid(sid) - - print(sids_to_lookup) - - for ace in sdec.Dacl.aces: - if not sids_to_lookup[str(ace.Sid)]: - print(str(ace.Sid)) - #print('===== %s =====' % sids_to_lookup[str(ace.Sid)]) - #if - #print(str(ace)) - - - - async def get_tokengroups(self, dn): - """ - returns the tokengroups attribute for a given DN - """ - ldap_filter = query_syntax_converter( r'(distinguishedName=%s)' % escape_filter_chars(dn) ) - attributes=[b'tokenGroups'] - - async for entry, err in self._con.pagedsearch( - dn.encode(), - ldap_filter, - attributes = attributes, - paged_size = self.ldap_query_page_size, - search_scope=BASE, - ): - if err is not None: - yield None, err - break - - #print(entry['attributes']) - if 'tokenGroups' in entry: - for sid_data in entry['tokenGroups']: - yield sid_data - - async def get_all_tokengroups(self): - """ - returns the tokengroups attribute for a given DN - """ - ldap_filter = r'(|(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))' - async for entry in self.pagedsearch( - ldap_filter, - attributes = ['dn', 'cn', 'objectSid','objectClass', 'objectGUID'] - ): - - if 'objectName' in entry: - #print(entry['objectName']) - async for entry2, err in self._con.pagedsearch( - entry['objectName'].encode(), - query_syntax_converter( r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName']) ), - attributes = [b'tokenGroups'], - paged_size = self.ldap_query_page_size, - search_scope=BASE, - ): - - #print(entry2) - if err is not None: - yield None, err - break - if 'tokenGroups' in entry2['attributes']: - for token in entry2['attributes']['tokenGroups']: - yield { - 'cn' : entry['attributes']['cn'], - 'dn' : entry['objectName'], - 'guid' : entry['attributes']['objectGUID'], - 'sid' : entry['attributes']['objectSid'], - 'type' : entry['attributes']['objectClass'][-1], - 'token' : token - - } - - async def get_all_objectacl(self): - """ - bbbbbb - """ - - flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION - req_flags = SDFlagsRequestValue({'Flags' : flags_value}) - - ldap_filter = r'(|(objectClass=organizationalUnit)(objectCategory=groupPolicyContainer)(sAMAccountType=805306369)(objectClass=group)(sAMAccountType=805306368))' - async for entry in self.pagedsearch(ldap_filter, attributes = ['dn']): - ldap_filter = r'(distinguishedName=%s)' % escape_filter_chars(entry['objectName']) - attributes = MSADSecurityInfo.ATTRS - controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] - - async for entry2 in self.pagedsearch(ldap_filter, attributes, controls = controls): - yield MSADSecurityInfo.from_ldap(entry2) - - - async def get_all_trusts(self): - ldap_filter = r'(objectClass=trustedDomain)' - async for entry in self.pagedsearch(ldap_filter, attributes = MSADDomainTrust_ATTRS): - yield MSADDomainTrust.from_ldap(entry) \ No newline at end of file + + + #async def get_all_objectacl(self): + # """ + # Returns all ACL info for all AD objects + # """ + # + # flags_value = SDFlagsRequest.DACL_SECURITY_INFORMATION|SDFlagsRequest.GROUP_SECURITY_INFORMATION|SDFlagsRequest.OWNER_SECURITY_INFORMATION + # req_flags = SDFlagsRequestValue({'Flags' : flags_value}) + # + # ldap_filter = r'(objectClass=*)' + # attributes = MSADSecurityInfo.ATTRS + # controls = [('1.2.840.113556.1.4.801', True, req_flags.dump())] + # + # async for entry in self.pagedsearch(ldap_filter, attributes, controls = controls): + # yield MSADSecurityInfo.from_ldap(entry) + + + #async def get_netdomain(self): + # def nameconvert(x): + # return x.split(',CN=')[1] + # """ + # gets the name of the current user's domain + # """ + # if not self._ldapinfo: + # self.get_ad_info() + # print(self._ldapinfo) + # dname = self._ldapinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') + # domain_controllers = ','.join(nameconvert(x) + '.' +dname for x in self._ldapinfo.masteredBy) + # + # ridroleowner = nameconvert(self.get_ridroleowner()) + '.' +dname + # infraowner = nameconvert(self.get_infrastructureowner()) + '.' +dname + # pdcroleowner = nameconvert(self.get_pdcroleowner()) + '.' +dname + # + # print('name : %s' % dname) + # print('Domain Controllers : %s' % domain_controllers) + # print('DomainModeLevel : %s' % self._ldapinfo.domainmodelevel) + # print('PdcRoleOwner : %s' % pdcroleowner) + # print('RidRoleOwner : %s' % ridroleowner) + # print('InfrastructureRoleOwner : %s' % infraowner) + # + #async def get_domaincontroller(self): + # ldap_filter = r'(userAccountControl:1.2.840.113556.1.4.803:=8192)' + # async for entry in self.pagedsearch(ldap_filter, ALL_ATTRIBUTES): + # print('Forest: %s' % '') + # print('Name: %s' % entry['attributes'].get('dNSHostName')) + # print('OSVersion: %s' % entry['attributes'].get('operatingSystem')) + # print(entry['attributes']) + + #async def get_pdcroleowner(self): + # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ + # #get adinfo -> get ridmanagerreference attr -> look up the dn of ridmanagerreference -> get fsmoroleowner attr (which is a DN) + # if not self._ldapinfo: + # self.get_ad_info() + # + # ldap_filter = r'(distinguishedName=%s)' % self._ldapinfo.rIDManagerReference + # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): + # return entry['attributes']['fSMORoleOwner'] + # + #async def get_infrastructureowner(self): + # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ + # #"CN=Infrastructure,DC=concorp,DC=contoso,DC=com" -l fSMORoleOwner + # if not self._ldapinfo: + # self.get_ad_info() + # + # ldap_filter = r'(distinguishedName=%s)' % ('CN=Infrastructure,' + self._ldapinfo.distinguishedName) + # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): + # return entry['attributes']['fSMORoleOwner'] + # + #async def get_ridroleowner(self): + # #http://adcoding.com/how-to-determine-the-fsmo-role-holder-fsmoroleowner-attribute/ + # if not self._ldapinfo: + # self.get_ad_info() + # + # ldap_filter = r'(distinguishedName=%s)' % ('CN=RID Manager$,CN=System,' + self._ldapinfo.distinguishedName) + # async for entry in self.pagedsearch(ldap_filter, ['fSMORoleOwner']): + # return entry['attributes']['fSMORoleOwner'] + + #async def get_all_user_raw(self): + # """ + # Fetches all user objects from the AD, and returns MSADUser object + # """ + # logger.debug('Polling AD for all user objects') + # ldap_filter = r'(sAMAccountType=805306368)' + # + # return self.pagedsearch(ldap_filter, MSADUser_ATTRS) diff --git a/msldap/commons/authbuilder.py b/msldap/commons/authbuilder.py index 84be987..1f229cf 100644 --- a/msldap/commons/authbuilder.py +++ b/msldap/commons/authbuilder.py @@ -6,6 +6,7 @@ from msldap.authentication.spnego.native import SPNEGO from msldap.authentication.ntlm.native import NTLMAUTHHandler, NTLMHandlerSettings from msldap.authentication.kerberos.native import MSLDAPKerberos +from msldap.commons.proxy import MSLDAPProxyType from minikerberos.common.target import KerberosTarget from minikerberos.common.proxy import KerberosProxy from minikerberos.common.creds import KerberosCredential @@ -27,6 +28,7 @@ self.is_guest = False self.nt_hash = None self.lm_hash = None + self.encrypt = False class MSLDAPSIMPLECredential: def __init__(self): @@ -46,17 +48,90 @@ self.target = None #KerberosTarget self.ksoc = None #KerberosSocketAIO self.ccred = None + self.encrypt = False + self.enctypes = None #[23,17,18] class MSLDAPKerberosSSPICredential: def __init__(self): - self.client = None + self.domain = None self.password = None - self.target = None + self.username = None + self.encrypt = False class MSLDAPNTLMSSPICredential: def __init__(self): - self.client = None - self.passwrd = None + self.username = None + self.password = None + self.domain = None + self.encrypt = False + +class MSLDAPWSNETCredential: + def __init__(self): + self.type = 'NTLM' + self.username = '' + self.domain = '' + self.password = '' + self.target = None + self.is_guest = False + self.agent_id = None + self.encrypt = False + +class MSLDAPSSPIProxyCredential: + def __init__(self): + self.type = 'NTLM' + self.username = '' + self.domain = '' + self.password = '' + self.target = None + self.is_guest = False + self.agent_id = None + self.encrypt = False + self.host = '127.0.0.1' + self.port = 9999 + self.proto = 'ws' + + + +class MSLDAPMultiplexorCredential: + def __init__(self): + self.type = 'NTLM' + self.username = '' + self.domain = '' + self.password = '' + self.target = None + self.is_guest = False + self.is_ssl = False + self.mp_host = '127.0.0.1' + self.mp_port = 9999 + self.mp_username = None + self.mp_domain = None + self.mp_password = None + self.agent_id = None + self.encrypt = False + + def get_url(self): + url_temp = 'ws://%s:%s' + if self.is_ssl is True: + url_temp = 'wss://%s:%s' + url = url_temp % (self.mp_host, self.mp_port) + return url + + def parse_settings(self, settings): + req = ['agentid'] + for r in req: + if r not in settings: + raise Exception('%s parameter missing' % r) + self.mp_host = settings.get('host', ['127.0.0.1'])[0] + self.mp_port = settings.get('port', ['9999'])[0] + if self.mp_port is None: + self.mp_port = '9999' + if 'user' in settings: + self.mp_username = settings.get('user')[0] + if 'domain' in settings: + self.mp_domain = settings.get('domain')[0] + if 'password' in settings: + self.mp_password = settings.get('password')[0] + self.agent_id = settings['agentid'][0] @@ -82,10 +157,23 @@ ntlmcred.domain = self.creds.domain if self.creds.domain is not None else '' ntlmcred.workstation = None ntlmcred.is_guest = False + ntlmcred.encrypt = self.creds.encrypt + if self.creds.password is None: - raise Exception('NTLM authentication requres password!') - ntlmcred.password = self.creds.password + raise Exception('NTLM authentication requres password/NT hash!') + + + if len(self.creds.password) == 32: + try: + bytes.fromhex(self.creds.password) + except: + ntlmcred.password = self.creds.password + else: + ntlmcred.nt_hash = self.creds.password + + else: + ntlmcred.password = self.creds.password settings = NTLMHandlerSettings(ntlmcred) return NTLMAUTHHandler(settings) @@ -110,6 +198,7 @@ ntlmcred.domain = self.creds.domain if self.creds.domain is not None else '' ntlmcred.workstation = None ntlmcred.is_guest = False + ntlmcred.encrypt = self.creds.encrypt if self.creds.password is None: raise Exception('NTLM authentication requres password!') @@ -136,7 +225,8 @@ LDAPAuthProtocol.KERBEROS_AES, LDAPAuthProtocol.KERBEROS_PASSWORD, LDAPAuthProtocol.KERBEROS_CCACHE, - LDAPAuthProtocol.KERBEROS_KEYTAB]: + LDAPAuthProtocol.KERBEROS_KEYTAB, + LDAPAuthProtocol.KERBEROS_KIRBI]: if self.target is None: raise Exception('Target must be specified with Kerberos!') @@ -147,42 +237,62 @@ if self.target.dc_ip is None: raise Exception('target must have a dc_ip for kerberos!') - - kc = KerberosCredential() - kc.username = self.creds.username - kc.domain = self.creds.domain + kcred = MSLDAPKerberosCredential() + if self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KIRBI: + kc = KerberosCredential.from_kirbi(self.creds.password, self.creds.username, self.creds.domain) + elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_CCACHE: + kc = KerberosCredential.from_ccache_file(self.creds.password, self.creds.username, self.creds.domain) + elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KEYTAB: + kc = KerberosCredential.from_kirbi(self.creds.password, self.creds.username, self.creds.domain) + else: + kc = KerberosCredential() + kc.username = self.creds.username + kc.domain = self.creds.domain + kcred.enctypes = [] if self.creds.auth_method == LDAPAuthProtocol.KERBEROS_PASSWORD: kc.password = self.creds.password + kcred.enctypes = [23,17,18] elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_NT: kc.nt_hash = self.creds.password + kcred.enctypes = [23] elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_AES: if len(self.creds.password) == 32: kc.kerberos_key_aes_128 = self.creds.password + kcred.enctypes = [17] elif len(self.creds.password) == 64: kc.kerberos_key_aes_256 = self.creds.password + kcred.enctypes = [18] elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_RC4: kc.kerberos_key_rc4 = self.creds.password + kcred.enctypes = [23] elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_CCACHE: kc.ccache = self.creds.password + kcred.enctypes = [23,17,18] # TODO: fix this elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KEYTAB: kc.keytab = self.creds.password + kcred.enctypes = [23,17,18] # TODO: fix this + elif self.creds.auth_method == LDAPAuthProtocol.KERBEROS_KIRBI: + kcred.enctypes = [23,17,18] # TODO: fix this else: raise Exception('No suitable secret type found to set up kerberos!') - - - kcred = MSLDAPKerberosCredential() + + if self.creds.etypes is not None: + kcred.enctypes = list(set(self.creds.etypes).intersection(set(kcred.enctypes))) + kcred.ccred = kc kcred.spn = KerberosSPN.from_target_string(self.target.to_target_string()) kcred.target = KerberosTarget(self.target.dc_ip) + kcred.encrypt = self.creds.encrypt + if self.target.proxy is not None: - kcred.target.proxy = KerberosProxy() - kcred.target.proxy.target = copy.deepcopy(self.target.proxy.target) - kcred.target.proxy.target.endpoint_ip = self.target.dc_ip - kcred.target.proxy.target.endpoint_port = 88 - kcred.target.proxy.creds = copy.deepcopy(self.target.proxy.auth) + kcred.target.proxy = KerberosProxy() + kcred.target.proxy.type = self.target.proxy.type + kcred.target.proxy.target = copy.deepcopy(self.target.proxy.target) + kcred.target.proxy.target[-1].endpoint_ip = self.target.dc_ip + kcred.target.proxy.target[-1].endpoint_port = 88 handler = MSLDAPKerberos(kcred) @@ -196,9 +306,11 @@ raise Exception('Target must be specified with Kerberos SSPI!') kerbcred = MSLDAPKerberosSSPICredential() - kerbcred.client = None #creds.username #here we could submit the domain as well for impersonation? TODO! - kerbcred.password = self.creds.password - kerbcred.target = self.target.to_target_string() + kerbcred.username = self.creds.domain if self.creds.domain is not None else '' + kerbcred.username = self.creds.username if self.creds.username is not None else '' + kerbcred.password = self.creds.password if self.creds.password is not None else '' + kerbcred.spn = self.target.to_target_string() + kerbcred.encrypt = self.creds.encrypt handler = MSLDAPKerberosSSPI(kerbcred) #setting up SPNEGO @@ -208,58 +320,153 @@ elif self.creds.auth_method == LDAPAuthProtocol.SSPI_NTLM: ntlmcred = MSLDAPNTLMSSPICredential() - ntlmcred.client = self.creds.username #here we could submit the domain as well for impersonation? TODO! - ntlmcred.password = self.creds.password - + ntlmcred.username = self.creds.domain if self.creds.domain is not None else '' + ntlmcred.username = self.creds.username if self.creds.username is not None else '' + ntlmcred.password = self.creds.password if self.creds.password is not None else '' + ntlmcred.encrypt = self.creds.encrypt + handler = MSLDAPNTLMSSPI(ntlmcred) #setting up SPNEGO spneg = SPNEGO() spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) return spneg - -""" -elif creds.authentication_type.value.startswith('MULTIPLEXOR'): - if creds.authentication_type in [SMBAuthProtocol.MULTIPLEXOR_SSL_NTLM, SMBAuthProtocol.MULTIPLEXOR_NTLM]: - from aiosmb.authentication.ntlm.multiplexor import SMBNTLMMultiplexor - - ntlmcred = SMBMultiplexorCredential() + elif self.creds.auth_method.value.startswith('MULTIPLEXOR'): + if self.creds.auth_method in [LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM, LDAPAuthProtocol.MULTIPLEXOR_NTLM]: + from msldap.authentication.ntlm.multiplexor import MSLDAPNTLMMultiplexor + ntlmcred = MSLDAPMultiplexorCredential() ntlmcred.type = 'NTLM' - if creds.username is not None: - ntlmcred.username = '' - if creds.domain is not None: - ntlmcred.domain = '' - if creds.secret is not None: - ntlmcred.password = '' - ntlmcred.is_guest = False - ntlmcred.is_ssl = True if creds.authentication_type == SMBAuthProtocol.MULTIPLEXOR_SSL_NTLM else False - ntlmcred.parse_settings(creds.settings) - - handler = SMBNTLMMultiplexor(ntlmcred) + if self.creds.username is not None: + ntlmcred.username = '' + if self.creds.domain is not None: + ntlmcred.domain = '' + if self.creds.password is not None: + ntlmcred.password = '' + ntlmcred.is_guest = False + ntlmcred.is_ssl = True if self.creds.auth_method == LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM else False + ntlmcred.parse_settings(self.creds.settings) + ntlmcred.encrypt = self.creds.encrypt + + handler = MSLDAPNTLMMultiplexor(ntlmcred) #setting up SPNEGO spneg = SPNEGO() spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) return spneg - elif creds.authentication_type in [SMBAuthProtocol.MULTIPLEXOR_SSL_KERBEROS, SMBAuthProtocol.MULTIPLEXOR_KERBEROS]: - from aiosmb.authentication.kerberos.multiplexor import SMBKerberosMultiplexor - - ntlmcred = SMBMultiplexorCredential() + elif self.creds.auth_method in [LDAPAuthProtocol.MULTIPLEXOR_SSL_KERBEROS, LDAPAuthProtocol.MULTIPLEXOR_KERBEROS]: + from msldap.authentication.kerberos.multiplexor import MSLDAPKerberosMultiplexor + + ntlmcred = MSLDAPMultiplexorCredential() ntlmcred.type = 'KERBEROS' - ntlmcred.target = creds.target - if creds.username is not None: - ntlmcred.username = '' - if creds.domain is not None: - ntlmcred.domain = '' - if creds.secret is not None: - ntlmcred.password = '' - ntlmcred.is_guest = False - ntlmcred.is_ssl = True if creds.authentication_type == SMBAuthProtocol.MULTIPLEXOR_SSL_NTLM else False - ntlmcred.parse_settings(creds.settings) - - handler = SMBKerberosMultiplexor(ntlmcred) + ntlmcred.target = self.target + if self.creds.username is not None: + ntlmcred.username = '' + if self.creds.domain is not None: + ntlmcred.domain = '' + if self.creds.password is not None: + ntlmcred.password = '' + ntlmcred.is_guest = False + ntlmcred.is_ssl = True if self.creds.auth_method == LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM else False + ntlmcred.parse_settings(self.creds.settings) + ntlmcred.encrypt = self.creds.encrypt + + handler = MSLDAPKerberosMultiplexor(ntlmcred) #setting up SPNEGO spneg = SPNEGO() spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler) return spneg -""" + + elif self.creds.auth_method.value.startswith('SSPIPROXY'): + if self.creds.auth_method == LDAPAuthProtocol.SSPIPROXY_NTLM: + from msldap.authentication.ntlm.sspiproxy import MSLDAPSSPIProxyNTLMAuth + ntlmcred = MSLDAPSSPIProxyCredential() + ntlmcred.type = 'NTLM' + if self.creds.username is not None: + ntlmcred.username = '' + if self.creds.domain is not None: + ntlmcred.domain = '' + if self.creds.password is not None: + ntlmcred.password = '' + ntlmcred.is_guest = False + ntlmcred.encrypt = self.creds.encrypt + ntlmcred.host = self.creds.settings['host'][0] + ntlmcred.port = int(self.creds.settings['port'][0]) + ntlmcred.proto = 'ws' + if 'proto' in self.creds.settings: + ntlmcred.proto = self.creds.settings['proto'][0] + if 'agentid' in self.creds.settings: + ntlmcred.agent_id = bytes.fromhex(self.creds.settings['agentid'][0]) + + handler = MSLDAPSSPIProxyNTLMAuth(ntlmcred) + #setting up SPNEGO + spneg = SPNEGO() + spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) + return spneg + + elif self.creds.auth_method == LDAPAuthProtocol.SSPIPROXY_KERBEROS: + from msldap.authentication.kerberos.sspiproxyws import MSLDAPSSPIProxyKerberosAuth + + ntlmcred = MSLDAPSSPIProxyCredential() + ntlmcred.type = 'KERBEROS' + ntlmcred.target = self.target + if self.creds.username is not None: + ntlmcred.username = '' + if self.creds.domain is not None: + ntlmcred.domain = '' + if self.creds.password is not None: + ntlmcred.password = '' + ntlmcred.is_guest = False + ntlmcred.encrypt = self.creds.encrypt + ntlmcred.host = self.creds.settings['host'][0] + ntlmcred.port = self.creds.settings['port'][0] + ntlmcred.proto = 'ws' + if 'proto' in self.creds.settings: + ntlmcred.proto = self.creds.settings['proto'][0] + if 'agentid' in self.creds.settings: + ntlmcred.agent_id = bytes.fromhex(self.creds.settings['agentid'][0]) + + handler = MSLDAPSSPIProxyKerberosAuth(ntlmcred) + #setting up SPNEGO + spneg = SPNEGO() + spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler) + return spneg + + elif self.creds.auth_method.value.startswith('WSNET'): + if self.creds.auth_method in [LDAPAuthProtocol.WSNET_NTLM]: + from msldap.authentication.ntlm.wsnet import MSLDAPWSNetNTLMAuth + + ntlmcred = MSLDAPWSNETCredential() + ntlmcred.type = 'NTLM' + if self.creds.username is not None: + ntlmcred.username = '' + if self.creds.domain is not None: + ntlmcred.domain = '' + if self.creds.password is not None: + ntlmcred.password = '' + ntlmcred.is_guest = False + + handler = MSLDAPWSNetNTLMAuth(ntlmcred) + spneg = SPNEGO() + spneg.add_auth_context('NTLMSSP - Microsoft NTLM Security Support Provider', handler) + return spneg + + + elif self.creds.auth_method in [LDAPAuthProtocol.WSNET_KERBEROS]: + from msldap.authentication.kerberos.wsnet import MSLDAPWSNetKerberosAuth + + ntlmcred = MSLDAPWSNETCredential() + ntlmcred.type = 'KERBEROS' + ntlmcred.target = self.target + if self.creds.username is not None: + ntlmcred.username = '' + if self.creds.domain is not None: + ntlmcred.domain = '' + if self.creds.password is not None: + ntlmcred.password = '' + ntlmcred.is_guest = False + + handler = MSLDAPWSNetKerberosAuth(ntlmcred) + #setting up SPNEGO + spneg = SPNEGO() + spneg.add_auth_context('MS KRB5 - Microsoft Kerberos 5', handler) + return spneg \ No newline at end of file diff --git a/msldap/commons/common.py b/msldap/commons/common.py new file mode 100644 index 0000000..0f6267b --- /dev/null +++ b/msldap/commons/common.py @@ -0,0 +1,6 @@ +import enum + +class MSLDAPClientStatus(enum.Enum): + RUNNING = 'RUNNING' + STOPPED = 'STOPPED' + ERROR = 'ERROR' diff --git a/msldap/commons/credential.py b/msldap/commons/credential.py index 17a9d5d..73e4b92 100644 --- a/msldap/commons/credential.py +++ b/msldap/commons/credential.py @@ -29,16 +29,23 @@ SICILY = 'SICILY' #NTLM (old proprietary from MS) NTLM_PASSWORD = 'NTLM_PASSWORD' #actually SASL-GSSAPI-SPNEGO-NTLM NTLM_NT = 'NTLM_NT' #actually SASL-GSSAPI-SPNEGO-NTLM - KERBEROS_RC4 = 'KERBEROS_RC4' #actually SASL-GSSAPI-SPNEGO-KERBEROS - KERBEROS_NT = 'KERBEROS_NT' #actually SASL-GSSAPI-SPNEGO-KERBEROS - KERBEROS_AES = 'KERBEROS_AES' #actually SASL-GSSAPI-SPNEGO-KERBEROS - KERBEROS_PASSWORD = 'KERBEROS_PASSWORD' #actually SASL-GSSAPI-SPNEGO-KERBEROS - KERBEROS_CCACHE = 'KERBEROS_CCACHE' #actually SASL-GSSAPI-SPNEGO-KERBEROS - KERBEROS_KEYTAB = 'KERBEROS_KEYTAB' #actually SASL-GSSAPI-SPNEGO-KERBEROS - MULTIPLEXOR = 'MULTIPLEXOR' - MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL' + KERBEROS_RC4 = 'KERBEROS_RC4' + KERBEROS_NT = 'KERBEROS_NT' + KERBEROS_AES = 'KERBEROS_AES' + KERBEROS_PASSWORD = 'KERBEROS_PASSWORD' + KERBEROS_CCACHE = 'KERBEROS_CCACHE' + KERBEROS_KEYTAB = 'KERBEROS_KEYTAB' + KERBEROS_KIRBI = 'KERBEROS_KIRBI' + MULTIPLEXOR_KERBEROS = 'MULTIPLEXOR_KERBEROS' + MULTIPLEXOR_NTLM = 'MULTIPLEXOR_NTLM' + MULTIPLEXOR_SSL_KERBEROS = 'MULTIPLEXOR_SSL_KERBEROS' + MULTIPLEXOR_SSL_NTLM = 'MULTIPLEXOR_SSL_NTLM' SSPI_NTLM = 'SSPI_NTLM' #actually SASL-GSSAPI-SPNEGO-NTLM but with integrated SSPI SSPI_KERBEROS = 'SSPI_KERBEROS' #actually SASL-GSSAPI-SPNEGO-KERBEROS but with integrated SSPI + WSNET_NTLM = 'WSNET_NTLM' + WSNET_KERBEROS = 'WSNET_KERBEROS' + SSPIPROXY_NTLM = 'SSPIPROXY_NTLM' + SSPIPROXY_KERBEROS = 'SSPIPROXY_KERBEROS' MSLDAP_GSS_METHODS = [ LDAPAuthProtocol.NTLM_PASSWORD , @@ -49,18 +56,58 @@ LDAPAuthProtocol.KERBEROS_PASSWORD , LDAPAuthProtocol.KERBEROS_CCACHE , LDAPAuthProtocol.KERBEROS_KEYTAB , + LDAPAuthProtocol.KERBEROS_KIRBI , LDAPAuthProtocol.SSPI_NTLM , LDAPAuthProtocol.SSPI_KERBEROS, - + LDAPAuthProtocol.MULTIPLEXOR_KERBEROS, + LDAPAuthProtocol.MULTIPLEXOR_NTLM, + LDAPAuthProtocol.MULTIPLEXOR_SSL_KERBEROS, + LDAPAuthProtocol.MULTIPLEXOR_SSL_NTLM, + LDAPAuthProtocol.WSNET_NTLM, + LDAPAuthProtocol.WSNET_KERBEROS, + LDAPAuthProtocol.SSPIPROXY_NTLM, + LDAPAuthProtocol.SSPIPROXY_KERBEROS, +] + +MSLDAP_KERBEROS_PROTOCOLS = [ + LDAPAuthProtocol.KERBEROS_RC4 , + LDAPAuthProtocol.KERBEROS_NT , + LDAPAuthProtocol.KERBEROS_AES , + LDAPAuthProtocol.KERBEROS_PASSWORD , + LDAPAuthProtocol.KERBEROS_CCACHE , + LDAPAuthProtocol.KERBEROS_KEYTAB , + LDAPAuthProtocol.KERBEROS_KIRBI , ] class MSLDAPCredential: - def __init__(self, domain=None, username= None, password = None, auth_method = None, settings = None): + """ + Describes the user's credentials to be used for authentication during the bind operation. + + :param domain: Domain of the user + :type domain: str + :param username: Username of the user + :type username: str + :param password: The authentication secret. The actual contents depend on the `auth_method` + :type password: str + :param auth_method: The ahtentication method to be performed during bind operation + :type auth_method: :class:`LDAPAuthProtocol` + :param settings: Additional settings + :type settings: dict + :param etypes: Supported encryption types for Kerberos authentication. + :type etypes: List[:class:`int`] + :param encrypt: Use protocol-level encryption. Doesnt work on LDAPS + :type encrypt: bool + """ + def __init__(self, domain=None, username= None, password = None, auth_method = None, settings = None, etypes = None, encrypt = False): self.auth_method = auth_method self.domain = domain self.username = username self.password = password + self.signing_preferred = False + self.encryption_preferred = False self.settings = settings + self.etypes = etypes + self.encrypt = encrypt def get_msuser(self): if not self.domain: diff --git a/msldap/commons/exceptions.py b/msldap/commons/exceptions.py new file mode 100644 index 0000000..9aa4240 --- /dev/null +++ b/msldap/commons/exceptions.py @@ -0,0 +1,79 @@ + +from msldap.protocol.messages import resultCode + + +LDAPResultCodeLookup ={ + 0 : 'success', + 1 : 'operationsError', + 2 : 'protocolError', + 3 : 'timeLimitExceeded', + 4 : 'sizeLimitExceeded', + 5 : 'compareFalse', + 6 : 'compareTrue', + 7 : 'authMethodNotSupported', + 8 : 'strongerAuthRequired', + 10 : 'referral', + 11 : 'adminLimitExceeded', + 12 : 'unavailableCriticalExtension', + 13 : 'confidentialityRequired', + 14 : 'saslBindInProgress', + 16 : 'noSuchAttribute', + 17 : 'undefinedAttributeType', + 18 : 'inappropriateMatching', + 19 : 'constraintViolation', + 20 : 'attributeOrValueExists', + 21 : 'invalidAttributeSyntax', + 32 : 'noSuchObject', + 33 : 'aliasProblem', + 34 : 'invalidDNSyntax', + 36 : 'aliasDereferencingProblem', + 48 : 'inappropriateAuthentication', + 49 : 'invalidCredentials', + 50 : 'insufficientAccessRights', + 51 : 'busy', + 52 : 'unavailable', + 53 : 'unwillingToPerform', + 54 : 'loopDetect', + 64 : 'namingViolation', + 65 : 'objectClassViolation', + 66 : 'notAllowedOnNonLeaf', + 67 : 'notAllowedOnRDN', + 68 : 'entryAlreadyExists', + 69 : 'objectClassModsProhibited', + 71 : 'affectsMultipleDSAs', + 80 : 'other', +} +LDAPResultCodeLookup_inv = {v: k for k, v in LDAPResultCodeLookup.items()} + +class LDAPServerException(Exception): + def __init__(self, resultname, diagnostic_message, message = None): + self.resultcode = LDAPResultCodeLookup_inv[resultname] + self.resultname = resultname + self.diagnostic_message = diagnostic_message + self.message = message + if self.message is None: + self.message = 'LDAP server sent error! Result code: "%s" Reason: "%s"' % (self.resultcode, self.diagnostic_message) + super().__init__(self.message) + +class LDAPBindException(LDAPServerException): + def __init__(self, resultcode, diagnostic_message): + message = 'LDAP Bind failed! Result code: "%s" Reason: "%s"' % (resultcode, diagnostic_message) + super().__init__(resultcode, diagnostic_message, message) + +class LDAPAddException(LDAPServerException): + def __init__(self, dn, resultcode, diagnostic_message): + self.dn = dn + message = 'LDAP Add operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) + super().__init__(resultcode, diagnostic_message, message) + +class LDAPModifyException(LDAPServerException): + def __init__(self, dn, resultcode, diagnostic_message): + self.dn = dn + message = 'LDAP Modify operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) + super().__init__(resultcode, diagnostic_message, message) + +class LDAPDeleteException(LDAPServerException): + def __init__(self, dn, resultcode, diagnostic_message): + self.dn = dn + message = 'LDAP Delete operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) + super().__init__(resultcode, diagnostic_message, message) diff --git a/msldap/commons/proxy.py b/msldap/commons/proxy.py index 3a94d31..273659c 100644 --- a/msldap/commons/proxy.py +++ b/msldap/commons/proxy.py @@ -17,16 +17,37 @@ SOCKS5_SSL = 'SOCKS5_SSL' MULTIPLEXOR = 'MULTIPLEXOR' MULTIPLEXOR_SSL = 'MULTIPLEXOR_SSL' + WSNET = 'WSNET' + WSNETWS = 'WSNETWS' + WSNETWSS = 'WSNETWSS' class MSLDAPProxy: - def __init__(self): - self.type = None - self.target = None - self.auth = None + """ + Describes the proxy to be used when connecting to the server. Used as a parameter to the `MSLDAPTarget` object + + :param type: Specifies the proxy type + :type type: :class:`MSLDAPProxyType` + :param target: + :type target: + :param auth: Specifies the proxy authentication if any + :type auth: + """ + def __init__(self, type = None, target = None, auth = None): + self.type = type + self.target = target + self.auth = auth @staticmethod def from_params(url_str): + """ + Creates a proxy object from the parameters found in an LDAP URL string + + :param type: url_str + :type type: str + :return: The proxy object + :rtype: :class:`MSLDAPProxy` + """ proxy = MSLDAPProxy() url = urlparse(url_str) if url.query is None: @@ -37,14 +58,11 @@ return None proxy.type = MSLDAPProxyType(query['proxytype'][0].upper()) - if proxy.type in [MSLDAPProxyType.SOCKS4, MSLDAPProxyType.SOCKS4_SSL, MSLDAPProxyType.SOCKS5, MSLDAPProxyType.SOCKS5_SSL]: - cu = SocksClientURL.from_params(url_str) + if proxy.type in [MSLDAPProxyType.WSNET, MSLDAPProxyType.WSNETWS, MSLDAPProxyType.WSNETWSS,MSLDAPProxyType.SOCKS4, MSLDAPProxyType.SOCKS4_SSL, MSLDAPProxyType.SOCKS5, MSLDAPProxyType.SOCKS5_SSL]: + proxy.target = SocksClientURL.from_params(url_str) else: - raise Exception('Multiplexor not yet implemented as a proxy!') - #cu = SocksClientURL.from_params(url_str) + proxy.target = MSLDAPMultiplexorProxy.from_params(url_str) - proxy.target = cu.get_target() - proxy.auth = cu.get_creds() return proxy def __str__(self): @@ -53,7 +71,75 @@ t += '%s: %s\r\n' % (k, self.__dict__[k]) return t - - +class MSLDAPMultiplexorProxy: + def __init__(self): + self.ip = None + self.port = None + self.timeout = 10 + self.type = MSLDAPProxyType.MULTIPLEXOR + self.username = None + self.password = None + self.domain = None + self.agent_id = None + self.virtual_socks_port = None + self.virtual_socks_ip = None + + def sanity_check(self): + if self.ip is None: + raise Exception('MULTIPLEXOR server IP is missing!') + if self.port is None: + raise Exception('MULTIPLEXOR server port is missing!') + if self.agent_id is None: + raise Exception('MULTIPLEXOR proxy requires agentid to be set!') + + def get_server_url(self): + con_str = 'ws://%s:%s' % (self.ip, self.port) + if self.type == MSLDAPProxyType.MULTIPLEXOR_SSL: + con_str = 'wss://%s:%s' % (self.ip, self.port) + return con_str + + @staticmethod + def from_params(url_str): + res = MSLDAPMultiplexorProxy() + url = urlparse(url_str) + res.endpoint_ip = url.hostname + if url.port: + res.endpoint_port = int(url.port) + if url.query is not None: + query = parse_qs(url.query) + + for k in query: + if k.startswith('proxy'): + if k[5:] in multiplexorproxyurl_param2var: + + data = query[k][0] + for c in multiplexorproxyurl_param2var[k[5:]][1]: + data = c(data) + + setattr( + res, + multiplexorproxyurl_param2var[k[5:]][0], + data + ) + res.sanity_check() + + return res + +def stru(x): + return str(x).upper() + +multiplexorproxyurl_param2var = { + 'type' : ('version', [stru, MSLDAPProxyType]), + 'host' : ('ip', [str]), + 'port' : ('port', [int]), + 'timeout': ('timeout', [int]), + 'user' : ('username', [str]), + 'pass' : ('password', [str]), + #'authtype' : ('authtype', [SOCKS5Method]), + 'agentid' : ('agent_id', [str]), + 'domain' : ('domain', [str]) + +} + diff --git a/msldap/commons/target.py b/msldap/commons/target.py index e03b199..d027a86 100644 --- a/msldap/commons/target.py +++ b/msldap/commons/target.py @@ -6,7 +6,13 @@ # import enum -import ssl + +import platform +try: + import ssl +except: + if platform.system() == 'Emscripten': + pass class LDAPProtocol(enum.Enum): TCP = 'TCP' @@ -15,7 +21,27 @@ class MSLDAPTarget: - def __init__(self, host, port = 389, proto = LDAPProtocol.TCP, tree = None, proxy = None, timeout = 10): + """ + Describes the connection to the server. + + :param host: IP address or hostname of the server + :type host: str + :param port: port of the LDAP service running on the server + :type port: int + :param proto: Connection protocol to be used + :type proto: :class:`LDAPProtocol` + :param tree: The tree to connect to + :type tree: str + :param proxy: specifies what kind of proxy to be used + :type proxy: :class:`MSLDAPProxy` + :param timeout: connection timeout in seconds + :type timeout: int + :param ldap_query_page_size: Maximum number of elements to fetch in each paged_query call. + :type ldap_query_page_size: int + :param ldap_query_ratelimit: rate limit of paged queries. This will cause a sleep (in seconds) between fetching of each page of the query + :type ldap_query_ratelimit: float + """ + def __init__(self, host, port = 389, proto = LDAPProtocol.TCP, tree = None, proxy = None, timeout = 10, ldap_query_page_size = 1000, ldap_query_ratelimit = 0): self.proto = proto self.host = host self.tree = tree @@ -23,13 +49,18 @@ self.proxy = proxy self.timeout = timeout self.dc_ip = None + self.serverip = None self.domain = None self.sslctx = None + self.ldap_query_page_size = ldap_query_page_size + self.ldap_query_ratelimit = ldap_query_ratelimit def get_ssl_context(self): if self.proto == LDAPProtocol.SSL: if self.sslctx is None: - self.sslctx = ssl.create_default_context() + # TODO ssl verification :) + self.sslctx = ssl._create_unverified_context() + #self.sslctx.verify = False return self.sslctx return None diff --git a/msldap/commons/url.py b/msldap/commons/url.py index 6e511ec..01f06ab 100644 --- a/msldap/commons/url.py +++ b/msldap/commons/url.py @@ -7,37 +7,80 @@ import platform import hashlib +import getpass +import base64 +import enum from urllib.parse import urlparse, parse_qs -from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol +from msldap.commons.credential import MSLDAPCredential, LDAPAuthProtocol, MSLDAP_KERBEROS_PROTOCOLS from msldap.commons.target import MSLDAPTarget, LDAPProtocol from msldap.commons.proxy import MSLDAPProxy, MSLDAPProxyType from msldap.client import MSLDAPClient - +from msldap.connection import MSLDAPClientConnection + +class PLAINTEXTSCHEME(enum.Enum): + """ + Additional conveinence functions. + """ + SIMPLE_PROMPT = 'SIMPLE_PROMPT' + SIMPLE_HEX = 'SIMPLE_HEX' + SIMPLE_B64 = 'SIMPLE_B64' + PLAIN_PROMPT = 'PLAIN_PROMPT' + PLAIN_HEX = 'PLAIN_HEX' + PLAIN_B64 = 'PLAIN_B64' + SICILY_PROMPT = 'SICILY_PROMPT' + SICILY_HEX = 'SICILY_HEX' + SICILY_B64 = 'SICILY_B64' + NTLM_PROMPT = 'NTLM_PROMPT' + NTLM_HEX = 'NTLM_HEX' + NTLM_B64 = 'NTLM_B64' class MSLDAPURLDecoder: + """ + The URL describes both the connection target and the credentials. This class creates all necessary objects to set up the client. + + :param url: + :type url: str + """ help_epilog = """ MSLDAP URL Format: +://:@://?= sets the ldap protocol following values supported: - ldap - ldaps - can be omitted if plaintext authentication is to be performed, otherwise: - - ntlm - - sspi (windows only!) + can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: + - ntlm-password + - ntlm-nt + - kerberos-password (dc option param must be used) + - kerberos-rc4 / kerberos-nt (dc option param must be used) + - kerberos-aes (dc option param must be used) + - kerberos-keytab (dc option param must be used) + - kerberos-ccache (dc option param must be used) + - sspi-ntlm (windows only!) + - sspi-kerberos (windows only!) - anonymous - plain + - simple + - sicily (same format as ntlm-nt but using the SICILY authentication) + : + OPTIONAL. Specifies the root tree of all queries can be: - timeout : connction timeout in seconds - proxytype: currently only socks5 proxy is supported - proxyhost: Ip or hostname of the proxy server - proxyport: port of the proxy server - - proxytimeout: timeout ins ecodns for the proxy connection + - proxytimeout: timeout in secodns for the proxy connection + - dc: the IP address of the domain controller, MUST be used for kerberos authentication + - encrypt: enable encryption. Only for NTLM. DOESNT WORK WITH LDAPS + - etype: Supported encryption types for Kerberos authentication. Multiple can be specified. + - rate: LDAP paged search query rate limit. Will sleep for seconds between each new page. Default: 0 (no limit) + - pagesize: LDAP paged search query size per page. Max: 1000. Default: 1000 Examples: ldap://10.10.10.2 (anonymous bind) ldaps://test.corp (anonymous bind) - ldap+sspi:///test.corp + ldap+sspi-ntlm://test.corp + ldap+sspi-kerberos://test.corp ldap://TEST\\victim:@10.10.10.2 (defaults to SASL GSSAPI NTLM) ldap+simple://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) ldap+plain://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) @@ -58,7 +101,9 @@ self.domain = None self.username = None self.password = None + self.encrypt = False self.auth_settings = {} + self.etypes = None self.ldap_proto = None self.ldap_host = None @@ -66,39 +111,78 @@ self.ldap_tree = None self.target_timeout = 5 self.target_pagesize = 1000 + self.target_ratelimit = 0 self.dc_ip = None self.serverip = None self.proxy = None + self.__pwpreprocess = None + self.parse() def get_credential(self): - return MSLDAPCredential( + """ + Creates a credential object + + :return: Credential object + :rtype: :class:`MSLDAPCredential` + """ + t = MSLDAPCredential( domain=self.domain, username=self.username, password = self.password, auth_method=self.auth_scheme, settings = self.auth_settings ) + t.encrypt = self.encrypt + t.etypes = self.etypes + + return t def get_target(self): + """ + Creates a target object + + :return: Target object + :rtype: :class:`MSLDAPTarget` + """ target = MSLDAPTarget( self.ldap_host, port = self.ldap_port, proto = self.ldap_scheme, tree=self.ldap_tree, - timeout = self.target_timeout + timeout = self.target_timeout, + ldap_query_page_size = self.target_pagesize, + ldap_query_ratelimit = self.target_ratelimit ) target.domain = self.domain target.dc_ip = self.dc_ip target.proxy = self.proxy + target.serverip = self.serverip return target def get_client(self): + """ + Creates a client that can be used to interface with the server + + :return: LDAP client + :rtype: :class:`MSLDAPClient` + """ cred = self.get_credential() target = self.get_target() - return MSLDAPClient(target, cred, ldap_query_page_size = self.target_pagesize) + return MSLDAPClient(target, cred) + + def get_connection(self): + """ + Creates a connection that can be used to interface with the server + + :return: LDAP connection + :rtype: :class:`MSLDAPClientConnection` + """ + cred = self.get_credential() + target = self.get_target() + return MSLDAPClientConnection(target, cred) def scheme_decoder(self, scheme): schemes = [] @@ -127,9 +211,59 @@ return try: - self.auth_scheme = LDAPAuthProtocol(schemes[1]) + x = PLAINTEXTSCHEME(schemes[1]) + if x == PLAINTEXTSCHEME.SIMPLE_PROMPT: + self.auth_scheme = LDAPAuthProtocol.SIMPLE + self.__pwpreprocess = 'PROMPT' + + if x == PLAINTEXTSCHEME.SIMPLE_HEX: + self.auth_scheme = LDAPAuthProtocol.SIMPLE + self.__pwpreprocess = 'HEX' + + if x == PLAINTEXTSCHEME.SIMPLE_B64: + self.auth_scheme = LDAPAuthProtocol.SIMPLE + self.__pwpreprocess = 'B64' + + if x == PLAINTEXTSCHEME.PLAIN_PROMPT: + self.auth_scheme = LDAPAuthProtocol.PLAIN + self.__pwpreprocess = 'PROMPT' + + if x == PLAINTEXTSCHEME.PLAIN_HEX: + self.auth_scheme = LDAPAuthProtocol.PLAIN + self.__pwpreprocess = 'HEX' + + if x == PLAINTEXTSCHEME.PLAIN_B64: + self.auth_scheme = LDAPAuthProtocol.PLAIN + self.__pwpreprocess = 'B64' + + if x == PLAINTEXTSCHEME.SICILY_PROMPT: + self.auth_scheme = LDAPAuthProtocol.SICILY + self.__pwpreprocess = 'PROMPT' + + if x == PLAINTEXTSCHEME.SICILY_HEX: + self.auth_scheme = LDAPAuthProtocol.SICILY + self.__pwpreprocess = 'HEX' + + if x == PLAINTEXTSCHEME.SICILY_B64: + self.auth_scheme = LDAPAuthProtocol.SICILY + self.__pwpreprocess = 'B64' + + if x == PLAINTEXTSCHEME.NTLM_PROMPT: + self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD + self.__pwpreprocess = 'PROMPT' + + if x == PLAINTEXTSCHEME.NTLM_HEX: + self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD + self.__pwpreprocess = 'HEX' + + if x == PLAINTEXTSCHEME.NTLM_B64: + self.auth_scheme = LDAPAuthProtocol.NTLM_PASSWORD + self.__pwpreprocess = 'B64' except: - raise Exception('Uknown scheme!') + try: + self.auth_scheme = LDAPAuthProtocol(schemes[1]) + except: + raise Exception('Uknown scheme!') return @@ -138,6 +272,19 @@ self.scheme_decoder(url_e.scheme) self.password = url_e.password + if self.__pwpreprocess is not None: + if self.__pwpreprocess == 'PROMPT': + self.password = getpass.getpass() + + elif self.__pwpreprocess == 'HEX': + self.password = bytes.fromhex(self.password).decode() + + elif self.__pwpreprocess == 'B64': + self.password = base64.b64decode(self.password).decode() + + else: + raise Exception('Unknown password preprocess directive %s' % self.__pwpreprocess) + if url_e.username is not None: if url_e.username.find('\\') != -1: @@ -173,6 +320,8 @@ proxy_present = False if url_e.query is not None: query = parse_qs(url_e.query) + if 'etype' in query: + self.etypes = [] for k in query: if k.startswith('proxy') is True: proxy_present = True @@ -181,11 +330,19 @@ elif k == 'timeout': self.timeout = int(query[k][0]) elif k == 'serverip': - self.server_ip = query[k][0] + self.serverip = query[k][0] elif k == 'dns': self.dns = query[k] #multiple dns can be set, so not trimming here + elif k == 'encrypt': + self.encrypt = bool(int(query[k][0])) + elif k == 'etype': + self.etypes = [int(x) for x in query[k]] elif k.startswith('auth'): self.auth_settings[k[len('auth'):]] = query[k] + elif k == 'rate': + self.target_ratelimit = float(query[k][0]) + elif k == 'pagesize': + self.target_pagesize = int(query[k][0]) #elif k.startswith('same'): # self.auth_settings[k[len('same'):]] = query[k] @@ -202,119 +359,19 @@ if self.domain is None: self.domain = '' - - -# -# if self.auth_scheme == LDAPAuthProtocol.SSPI: -# if self.username is None: -# self.username = '' -# if self.password is None: -# self.password = '' -# if self.domain is None: -# self.domain = '' -# -# if self.auth_scheme == LDAPAuthProtocol.NTLM: -# if len(self.password) == 32: -# try: -# bytes.fromhex(self.password) -# except: -# a = hashlib.new('md4') -# a.update(self.password.encode('utf-16-le')) -# hs = a.hexdigest() -# self.password = '%s:%s' % (hs, hs) -# else: -# self.password = '%s:%s' % (self.password, self.password) -# else: -# a = hashlib.new('md4') -# a.update(self.password.encode('utf-16-le')) -# hs = a.hexdigest() -# self.password = '%s:%s' % (hs, hs) - -# -# #now for the url parameters -# """ -# ldaps://user:pass@10.10.10.2/?proxyhost=127.0.0.1&proxyport=8888&proxyuser=dddd&proxypass=ssss&dns=127.0.0.1 -# """ -# if url_e.query is not None: -# query = parse_qs(url_e.query) -# for k in query: -# if k == 'dns': -# self.dns = query[k] #multiple dns can be set, so not trimming here -# elif k.startswith('auth'): -# 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! -# elif k == 'timeout': -# self.target_timeout = int(query[k][0]) -# elif k == 'pagesize': -# self.target_pagesize = int(query[k][0]) -# elif k.startswith('proxy'): -# if k == 'proxytype': -# self.proxy_scheme = LDAPProxyType(query[k][0].upper()) -# elif k == 'proxyhost': -# self.proxy_ip = query[k][0] -# elif k == 'proxyuser': -# if query[k][0].find('\\') != -1: -# self.proxy_domain, self.proxy_username = query[k][0].split('\\') -# else: -# self.proxy_username = query[k][0] -# elif k == 'proxypass': -# self.proxy_password = query[k][0] -# elif k == 'proxytimeout': -# self.proxy_timeout = int(query[k][0]) -# elif k == 'proxyport': -# self.proxy_port = int(query[k][0]) -# else: -# 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! -# -# #####TODOOOO FIX THIS!!!! -# elif k.startswith('same'): -# self.auth_settings[k[len('same'):]] = query[k] -# if k == 'sametype': -# self.proxy_scheme = LDAPProxyType(query[k][0].upper()) -# elif k == 'samehost': -# self.proxy_ip = query[k][0] -# elif k == 'sametimeout': -# self.proxy_timeout = int(query[k][0]) -# elif k == 'sameuser': -# if query[k][0].find('\\') != -1: -# self.proxy_domain, self.proxy_username = query[k][0].split('\\') -# else: -# self.proxy_username = query[k][0] -# elif k == 'samepass': -# self.proxy_password = query[k][0] -# elif k == 'sameport': -# self.proxy_port = int(query[k][0]) -# else: -# 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! -# -# #setting default proxy ports -# if self.proxy_scheme in [LDAPProxyType.SOCKS5, LDAPProxyType.SOCKS5_SSL]: -# if self.proxy_port is None: -# self.proxy_port = 1080 -# + if self.auth_scheme in MSLDAP_KERBEROS_PROTOCOLS and self.dc_ip is None: + raise Exception('The "dc" parameter MUST be used for kerberos authentication types!') + + # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]: # if self.proxy_port is None: # self.proxy_port = 9999 -# -# #sanity checks... -# if self.proxy_scheme is not None: -# if self.proxy_ip is None: -# raise Exception('proxyserver MUST be provided if using proxy') # # if self.proxy_scheme in [LDAPProxyType.MULTIPLEXOR, LDAPProxyType.MULTIPLEXOR_SSL]: # if 'agentid' not in self.proxy_settings: # raise Exception('multiplexor proxy reuires agentid to be set! Set it via proxyagentid parameter!') # -# if self.auth_scheme in [LDAPAuthProtocol.PLAIN, LDAPAuthProtocol.NTLM, LDAPAuthProtocol.SSPI]: -# if self.username is None: -# raise Exception('For authentication protocol %s the username MUST be specified!' % self.auth_scheme.value) -# if self.password is None: -# raise Exception('For authentication protocol %s the password MUST be specified!' % self.auth_scheme.value) -# -# if self.auth_scheme is None: -# if self.username is None and self.password is None: -# self.auth_scheme = LDAPAuthProtocol.ANONYMOUS -# else: -# raise Exception('Could not parse authentication protocol!') + diff --git a/msldap/connection.py b/msldap/connection.py index f8f857f..69bdcad 100644 --- a/msldap/connection.py +++ b/msldap/connection.py @@ -1,20 +1,31 @@ import asyncio + from msldap import logger +from msldap.commons.common import MSLDAPClientStatus from msldap.protocol.messages import LDAPMessage, BindRequest, \ protocolOp, AuthenticationChoice, SaslCredentials, \ SearchRequest, AttributeDescription, Filter, Filters, \ - Controls, Control, SearchControlValue + Controls, Control, SearchControlValue, AddRequest, \ + ModifyRequest, DelRequest from msldap.protocol.utils import calcualte_length -from msldap.protocol.typeconversion import convert_result, convert_attributes +from msldap.protocol.typeconversion import convert_result, convert_attributes, encode_attributes, encode_changes +from msldap.protocol.query import escape_filter_chars, query_syntax_converter from msldap.commons.authbuilder import AuthenticatorBuilder from msldap.commons.credential import MSLDAP_GSS_METHODS from msldap.network.selector import MSLDAPNetworkSelector from msldap.commons.credential import LDAPAuthProtocol +from msldap.commons.target import LDAPProtocol +from msldap.commons.exceptions import LDAPServerException, LDAPBindException, LDAPAddException, LDAPModifyException, LDAPDeleteException +from asn1crypto.x509 import Certificate +from hashlib import sha256 +from minikerberos.gssapi.channelbindings import ChannelBindingsStruct class MSLDAPClientConnection: def __init__(self, target, creds): + if target is None: + raise Exception('Target cant be none!') self.target = target self.creds = creds self.auth = AuthenticatorBuilder(self.creds, self.target).build() @@ -25,36 +36,45 @@ self.network = None self.handle_incoming_task = None + self.status = MSLDAPClientStatus.RUNNING + self.lasterror = None self.message_id = 0 self.message_table = {} self.message_table_notify = {} - self.encryption_sequence_counter = 0 #for whatever reason it's only used during encryption, but decryption always uses 0 + self.encryption_sequence_counter = 0 # this will be set by the inderlying auth algo + self.cb_data = None #for channel binding + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, traceback): + await asyncio.wait_for(self.disconnect(), timeout = 1) async def __handle_incoming(self): try: while True: message_data, err = await self.network.in_queue.get() if err is not None: - logger.debug('Client terminating bc __handle_incoming!') + logger.debug('Client terminating bc __handle_incoming got an error!') raise err - ################################ - # # - # ADD CHANNEL BINDING HERE! # - ################################ - + #print('Incoming message data: %s' % message_data) if self.bind_ok is True: if self.__encrypt_messages is True: - #print('Encrypted %s' % message_data) #removing size message_data = message_data[4:] try: - message_data = await self.auth.decrypt(message_data, 0) + # seq number doesnt matter here, a it's in the header + message_data, err = await self.auth.decrypt(message_data, 0 ) + if err is not None: + raise err #print('Decrypted %s' % message_data.hex()) + #print('Decrypted %s' % message_data) except: import traceback traceback.print_exc() + raise elif self.__sign_messages is True: #print('Signed %s' % message_data) @@ -65,6 +85,8 @@ except: import traceback traceback.print_exc() + raise + msg_len = calcualte_length(message_data) msg_total_len = len(message_data) @@ -91,15 +113,17 @@ self.message_table_notify[message_id].set() except asyncio.CancelledError: - #not notifying clients, at this point the client is terminating + self.status = MSLDAPClientStatus.STOPPED return except Exception as e: - import traceback - traceback.print_exc() + self.status = MSLDAPClientStatus.ERROR + self.lasterror = e for msgid in self.message_table_notify: self.message_table[msgid] = [e] self.message_table_notify[msgid].set() + + self.status = MSLDAPClientStatus.STOPPED async def send_message(self, message): @@ -143,20 +167,50 @@ return messages async def connect(self): - logger.debug('Connecting!') - self.network = MSLDAPNetworkSelector.select(self.target) - res, err = await self.network.run() - if res is False: - raise err - - self.handle_incoming_task = asyncio.create_task(self.__handle_incoming()) - logger.debug('Connection succsessful!') + """ + Connects to the remote server. Establishes the session, but doesn't perform binding. + This function MUST be called first before the `bind` operation. + + :return: A tuple of (True, None) on success or (False, Exception) on error. + :rtype: (:class:`bool`, :class:`Exception`) + """ + try: + logger.debug('Connecting!') + self.network = await MSLDAPNetworkSelector.select(self.target) + res, err = await self.network.run() + if res is False: + return False, err + + # now processing channel binding options + if self.target.proto == LDAPProtocol.SSL: + certdata = self.network.get_peer_certificate() + #cert = Certificate.load(certdata).native + #print(cert) + cb_struct = ChannelBindingsStruct() + cb_struct.application_data = b'tls-server-end-point:' + sha256(certdata).digest() + + self.cb_data = cb_struct.to_bytes() + + self.handle_incoming_task = asyncio.create_task(self.__handle_incoming()) + logger.debug('Connection succsessful!') + return True, None + except Exception as e: + return False, e async def disconnect(self): + """ + Tears down the connection. + + :return: Nothing + :rtype: None + """ + logger.debug('Disconnecting!') self.bind_ok = False - self.handle_incoming_task.cancel() - await self.network.terminate() + if self.handle_incoming_task is not None: + self.handle_incoming_task.cancel() + if self.network is not None: + await self.network.terminate() def __bind_success(self): @@ -173,11 +227,21 @@ self.network.is_plain_msg = False async def bind(self): + """ + Performs the bind operation. + This is where the authentication happens. Remember to call `connect` before this function! + + :return: A tuple of (True, None) on success or (False, Exception) on error. + :rtype: (:class:`bool`, :class:`Exception`) + """ logger.debug('BIND in progress...') try: if self.creds.auth_method == LDAPAuthProtocol.SICILY: - data, _ = await self.auth.authenticate(None) - + + data, to_continue, err = await self.auth.authenticate(None) + if err is not None: + return None, err + auth = { 'sicily_disco' : b'' } @@ -198,11 +262,10 @@ return False, res res = res.native if res['protocolOp']['resultCode'] != 'success': - return False, Exception( - 'BIND failed! Result code: "%s" Reason: "%s"' % ( + return False, LDAPBindException( res['protocolOp']['resultCode'], res['protocolOp']['diagnosticMessage'] - )) + ) auth = { 'sicily_nego' : data @@ -224,13 +287,14 @@ return False, res res = res.native if res['protocolOp']['resultCode'] != 'success': - return False, Exception( - 'BIND failed! Result code: "%s" Reason: "%s"' % ( + return False, LDAPBindException( res['protocolOp']['resultCode'], res['protocolOp']['diagnosticMessage'] - )) - - data, _ = await self.auth.authenticate(res['protocolOp']['matchedDN']) + ) + + data, to_continue, err = await self.auth.authenticate(res['protocolOp']['matchedDN']) + if err is not None: + return None, err auth = { 'sicily_resp' : data @@ -252,11 +316,10 @@ return False, res res = res.native if res['protocolOp']['resultCode'] != 'success': - return False, Exception( - 'BIND failed! Result code: "%s" Reason: "%s"' % ( + return False, LDAPBindException( res['protocolOp']['resultCode'], res['protocolOp']['diagnosticMessage'] - )) + ) self.__bind_success() @@ -295,16 +358,20 @@ return True, None else: - return False, Exception( - 'BIND failed! Result code: "%s" Reason: "%s"' % ( + return False, LDAPBindException( res['protocolOp']['resultCode'], res['protocolOp']['diagnosticMessage'] - )) + ) elif self.creds.auth_method in MSLDAP_GSS_METHODS: challenge = None while True: - data, _ = await self.auth.authenticate(challenge) + try: + data, to_continue, err = await self.auth.authenticate(challenge, cb_data = self.cb_data) + if err is not None: + raise err + except Exception as e: + return False, e sasl = { 'mechanism' : 'GSS-SPNEGO'.encode(), @@ -316,7 +383,7 @@ bindreq = { 'version' : 3, - 'name': ''.encode(), + 'name': b'', 'authentication': AuthenticationChoice(auth), } @@ -330,7 +397,14 @@ return False, res res = res.native if res['protocolOp']['resultCode'] == 'success': + if 'serverSaslCreds' in res['protocolOp']: + data, _, err = await self.auth.authenticate(res['protocolOp']['serverSaslCreds'], cb_data = self.cb_data) + if err is not None: + return False, err + + self.encryption_sequence_counter = self.auth.get_seq_number() self.__bind_success() + return True, None elif res['protocolOp']['resultCode'] == 'saslBindInProgress': @@ -338,33 +412,176 @@ continue else: - return False, Exception( - 'BIND failed! Result code: "%s" Reason: "%s"' % ( + return False, LDAPBindException( res['protocolOp']['resultCode'], res['protocolOp']['diagnosticMessage'] - )) + ) - #print(res) + else: + raise Exception('Not implemented authentication method: %s' % self.creds.auth_method.name) except Exception as e: - print(str(e)) + return False, e + + async def add(self, entry, attributes): + """ + Performs the add operation. + + :param entry: The DN of the object to be added + :type entry: str + :param attributes: Attributes to be used in the operation + :type attributes: dict + :return: A tuple of (True, None) on success or (False, Exception) on error. + :rtype: (:class:`bool`, :class:`Exception`) + """ + try: + req = { + 'entry' : entry.encode(), + 'attributes' : encode_attributes(attributes) + } + br = { 'addRequest' : AddRequest(req)} + msg = { 'protocolOp' : protocolOp(br)} + + msg_id = await self.send_message(msg) + results = await self.recv_message(msg_id) + if isinstance(results[0], Exception): + return False, results[0] + + for message in results: + msg_type = message['protocolOp'].name + message = message.native + if msg_type == 'addResponse': + if message['protocolOp']['resultCode'] != 'success': + return False, LDAPAddException( + entry, + message['protocolOp']['resultCode'], + message['protocolOp']['diagnosticMessage'] + ) + + return True, None + except Exception as e: + return False, e + + async def modify(self, entry, changes, controls = None): + """ + Performs the modify operation. + + :param entry: The DN of the object whose attributes are to be modified + :type entry: str + :param changes: Describes the changes to be made on the object. Must be a dictionary of the following format: {'attribute': [('change_type', [value])]} + :type changes: dict + :param controls: additional controls to be passed in the query + :type controls: List[class:`Control`] + :return: A tuple of (True, None) on success or (False, Exception) on error. + :rtype: (:class:`bool`, :class:`Exception`) + """ + try: + req = { + 'object' : entry.encode(), + 'changes' : encode_changes(changes) + } + br = { 'modifyRequest' : ModifyRequest(req)} + msg = { 'protocolOp' : protocolOp(br)} + if controls is not None: + msg['controls'] = controls + + msg_id = await self.send_message(msg) + results = await self.recv_message(msg_id) + if isinstance(results[0], Exception): + return False, results[0] + + for message in results: + msg_type = message['protocolOp'].name + message = message.native + if msg_type == 'modifyResponse': + if message['protocolOp']['resultCode'] != 'success': + return False, LDAPModifyException( + entry, + message['protocolOp']['resultCode'], + message['protocolOp']['diagnosticMessage'] + ) + + return True, None + except Exception as e: + return False, e + + async def delete(self, entry): + """ + Performs the delete operation. + + :param entry: The DN of the object to be deleted + :type entry: str + :return: A tuple of (True, None) on success or (False, Exception) on error. + :rtype: (:class:`bool`, :class:`Exception`) + """ + try: + br = { 'delRequest' : DelRequest(entry.encode())} + msg = { 'protocolOp' : protocolOp(br)} + + msg_id = await self.send_message(msg) + results = await self.recv_message(msg_id) + if isinstance(results[0], Exception): + return False, results[0] + + for message in results: + msg_type = message['protocolOp'].name + message = message.native + if msg_type == 'delResponse': + if message['protocolOp']['resultCode'] != 'success': + return False, LDAPDeleteException( + entry, + message['protocolOp']['resultCode'], + message['protocolOp']['diagnosticMessage'] + ) + + return True, None + except Exception as e: return False, e - async def search(self, base, filter, attributes, search_scope = 2, paged_size = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None, return_done = False): - """ - This function is a generator!!!!! Dont just call it but use it with "async for" - """ + 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): + """ + Performs the search operation. + + :param base: base tree on which the search should be performed + :type base: str + :param query: filter query that defines what should be searched for + :type query: str + :param attributes: a list of attributes to be included in the response + :type attributes: List[str] + :param search_scope: Specifies the search operation's scope. Default: 2 (Subtree) + :type search_scope: int + :param types_only: indicates whether the entries returned should include attribute types only or both types and values. Default: False (both) + :type types_only: bool + :param size_limit: Size limit of result elements per query. Default: 1000 + :type size_limit: int + :param derefAliases: Specifies the behavior on how aliases are dereferenced. Default: 0 (never) + :type derefAliases: int + :param timeLimit: Maximum time the search should take. If time limit reached the server SHOULD return an error + :type timeLimit: int + :param controls: additional controls to be passed in the query + :type controls: List[class:`Control`] + :param return_done: Controls wether the final 'done' LDAP message should be returned, or just the actual results + :type return_done: bool + + :return: Async generator which yields (`LDAPMessage`, None) tuple on success or (None, `Exception`) on error + :rtype: Iterator[(:class:`LDAPMessage`, :class:`Exception`)] + """ + if self.status != MSLDAPClientStatus.RUNNING: + yield None, Exception('Connection not running! Probably encountered an error') + return try: if timeLimit is None: timeLimit = 600 #not sure + + flt = query_syntax_converter(query) searchreq = { - 'baseObject' : base, + 'baseObject' : base.encode(), 'scope': search_scope, 'derefAliases': derefAliases, - 'sizeLimit': paged_size, + 'sizeLimit': size_limit, 'timeLimit': timeLimit, - 'typesOnly': typesOnly, - 'filter': filter, + 'typesOnly': types_only, + 'filter': flt, 'attributes': attributes, } @@ -381,8 +598,6 @@ msg_type = message['protocolOp'].name message = message.native if msg_type == 'searchResDone': - #print(message) - #print('BREAKING!') if return_done is True: yield (message, None) break @@ -403,16 +618,46 @@ except Exception as e: yield (None, e) - async def pagedsearch(self, base, filter, attributes, search_scope = 2, paged_size = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None): + async def pagedsearch(self, base, query, attributes, search_scope = 2, size_limit = 1000, typesOnly = False, derefAliases = 0, timeLimit = None, controls = None, rate_limit = 0): + """ + 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. + + :param base: base tree on which the search should be performed + :type base: str + :param query: filter query that defines what should be searched for + :type query: str + :param attributes: a list of attributes to be included in the response + :type attributes: List[str] + :param search_scope: Specifies the search operation's scope. Default: 2 (Subtree) + :type search_scope: int + :param types_only: indicates whether the entries returned should include attribute types only or both types and values. Default: False (both) + :type types_only: bool + :param size_limit: Size limit of result elements per query. Default: 1000 + :type size_limit: int + :param derefAliases: Specifies the behavior on how aliases are dereferenced. Default: 0 (never) + :type derefAliases: int + :param timeLimit: Maximum time the search should take. If time limit reached the server SHOULD return an error + :type timeLimit: int + :param controls: additional controls to be passed in the query + :type controls: dict + :param rate_limit: time to sleep bwetween each query + :type rate_limit: float + :return: Async generator which yields (`dict`, None) tuple on success or (None, `Exception`) on error + :rtype: Iterator[(:class:`dict`, :class:`Exception`)] + """ + + if self.status != MSLDAPClientStatus.RUNNING: + yield None, Exception('Connection not running! Probably encountered an error') + return try: cookie = b'' while True: - + await asyncio.sleep(rate_limit) ctrl_list_temp = [ Control({ 'controlType' : b'1.2.840.113556.1.4.319', 'controlValue': SearchControlValue({ - 'size' : paged_size, + 'size' : size_limit, 'cookie': cookie }).dump() }) @@ -427,11 +672,11 @@ async for res, err in self.search( base, - filter, + query, attributes, search_scope = search_scope, - paged_size=paged_size, - typesOnly=typesOnly, + size_limit=size_limit, + types_only=typesOnly, derefAliases=derefAliases, timeLimit=timeLimit, controls = ctrs, @@ -440,7 +685,7 @@ if err is not None: yield (None, err) return - + if 'resultCode' in res['protocolOp']: for control in res['controls']: if control['controlType'] == b'1.2.840.113556.1.4.319': @@ -462,6 +707,9 @@ async def get_serverinfo(self): + if self.status != MSLDAPClientStatus.RUNNING: + return None, Exception('Connection not running! Probably encountered an error') + attributes = [ b'subschemaSubentry', b'dsServiceName', @@ -497,14 +745,13 @@ msg_id = await self.send_message(msg) res = await self.recv_message(msg_id) - res = res[0].native - + res = res[0] if isinstance(res, Exception): return None, res #print('res') #print(res) - return convert_attributes(res['protocolOp']['attributes']), None + return convert_attributes(res.native['protocolOp']['attributes']), None async def amain(): @@ -525,7 +772,7 @@ #target.dc_ip = '10.10.10.2' #target.domain = 'TEST' - url = 'ldap+kerberos-password://test\\victim:Passw0rd!1@WIN2019AD/?dc=10.10.10.2' + url = 'ldaps+ntlm-password://test\\Administrator:QLFbT8zkiFGlJuf0B3Qq@WIN2019AD/?dc=10.10.10.2' dec = MSLDAPURLDecoder(url) cred = dec.get_credential() @@ -541,31 +788,28 @@ res, err = await client.bind() if err is not None: raise err - - #res = await client.search_test_2() - #pprint.pprint(res) - #search = bytes.fromhex('30840000007702012663840000006e043c434e3d3430392c434e3d446973706c6179537065636966696572732c434e3d436f6e66696775726174696f6e2c44433d746573742c44433d636f72700a01000a010002010002020258010100870b6f626a656374436c61737330840000000d040b6f626a656374436c617373') - #msg = LDAPMessage.load(search) - + user = "CN=ldaptest_2,CN=Users,DC=test,DC=corp" + #attributes = {'objectClass': ['inetOrgPerson', 'posixGroup', 'top'], 'sn': 'user_sn', 'gidNumber': 0} + #res, err = await client.add(user, attributes) + #if err is not None: + # print(err) + + #changes = { + # 'unicodePwd': [('replace', ['"TESTPassw0rd!1"'])], + # #'lockoutTime': [('replace', [0])] + #} + + #res, err = await client.modify(user, changes) + #if err is not None: + # print('ERR! %s' % err) + #else: + # print('OK!') - qry = r'(sAMAccountName=*)' #'(userAccountControl:1.2.840.113556.1.4.803:=4194304)' #'(sAMAccountName=*)' - #qry = r'(sAMAccountType=805306368)' - #a = query_syntax_converter(qry) - #print(a.native) - #input('press bacon!') + res, err = await client.delete(user) + if err is not None: + print('ERR! %s' % err) - flt = query_syntax_converter(qry) - i = 0 - async for res, err in client.pagedsearch(base.encode(), flt, ['*'.encode()], derefAliases=3, typesOnly=False): - if err is not None: - print('Error!') - raise err - i += 1 - if i % 1000 == 0: - print(i) - #pprint.pprint(res) - await client.disconnect() @@ -578,38 +822,10 @@ logger.setLevel(2) - #from asn1crypto.core import ObjectIdentifier - - #o = ObjectIdentifier('1.2.840.113556.1.4.803') - #print(o.dump()) - - #from pprint import pprint - #a = bytes.fromhex('3082026202010b63820235040f44433d746573742c44433d636f72700a01020a0103020100020100010100a050a9358116312e322e3834302e3131333535362e312e342e3830338212757365724163636f756e74436f6e74726f6c830734313934333034a217a415040e73414d4163636f756e744e616d653003820124308201bf040e6163636f756e7445787069726573040f62616450617373776f726454696d65040b626164507764436f756e740402636e0408636f646550616765040b636f756e747279436f6465040b646973706c61794e616d65041164697374696e677569736865644e616d650409676976656e4e616d650408696e697469616c73040a6c6173744c6f676f666604096c6173744c6f676f6e04126c6173744c6f676f6e54696d657374616d70040a6c6f676f6e436f756e7404046e616d65040b6465736372697074696f6e040e6f626a65637443617465676f7279040b6f626a656374436c617373040a6f626a6563744755494404096f626a656374536964040e7072696d61727947726f75704944040a7077644c617374536574040e73414d4163636f756e744e616d65040e73414d4163636f756e74547970650402736e0412757365724163636f756e74436f6e74726f6c0411757365725072696e636970616c4e616d65040b7768656e4368616e676564040b7768656e4372656174656404086d656d6265724f6604066d656d6265720414736572766963655072696e636970616c4e616d6504186d7344532d416c6c6f776564546f44656c6567617465546fa02430220416312e322e3834302e3131333535362e312e342e33313904083006020203e80400') - #msg = LDAPMessage.load(a) - #pprint.pprint(msg.native) + + asyncio.run(amain()) + - #input() - - asyncio.run(amain()) - - - #qry = '(&(sAMAccountType=805306369)(sAMAccountName=test))' - #qry = '(sAMAccountName=*)' - #flt = LF.parse(qry) - #print(flt) - #print(flt.__dict__) - #for f in flt.filters: - # print(f.__dict__) - - #x = convert(flt) - #print(x) - #print(x.native) - - #qry = '(sAMAccountType=0x100)' - #flt = Filter.parse(qry) - #print(flt) - #print(flt.__dict__) - #print(flt.filters) diff --git a/msldap/crypto/MD4.py b/msldap/crypto/MD4.py new file mode 100644 index 0000000..3b64dda --- /dev/null +++ b/msldap/crypto/MD4.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright © 2019 James Seo (github.com/kangtastic). +# +# This file is released under the WTFPL, version 2 (wtfpl.net). +# +# md4.py: An implementation of the MD4 hash algorithm in pure Python 3. +# +# Description: Zounds! Yet another rendition of pseudocode from RFC1320! +# Bonus points for the algorithm literally being from 1992. +# +# Usage: Why would anybody use this? This is self-rolled crypto, and +# self-rolled *obsolete* crypto at that. DO NOT USE if you need +# something "performant" or "secure". :P +# +# Anyway, from the command line: +# +# $ ./md4.py [messages] +# +# where [messages] are some strings to be hashed. +# +# In Python, use similarly to hashlib (not that it even has MD4): +# +# from .md4 import MD4 +# +# digest = MD4("BEES").hexdigest() +# +# print(digest) # "501af1ef4b68495b5b7e37b15b4cda68" +# +# +# Sample console output: +# +# Testing the MD4 class. +# +# Message: b'' +# Expected: 31d6cfe0d16ae931b73c59d7e0c089c0 +# Actual: 31d6cfe0d16ae931b73c59d7e0c089c0 +# +# Message: b'The quick brown fox jumps over the lazy dog' +# Expected: 1bee69a46ba811185c194762abaeae90 +# Actual: 1bee69a46ba811185c194762abaeae90 +# +# Message: b'BEES' +# Expected: 501af1ef4b68495b5b7e37b15b4cda68 +# Actual: 501af1ef4b68495b5b7e37b15b4cda68 +# +import struct + + +class MD4: + """An implementation of the MD4 hash algorithm.""" + + width = 32 + mask = 0xFFFFFFFF + + # Unlike, say, SHA-1, MD4 uses little-endian. Fascinating! + h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476] + + def __init__(self, msg=None): + """:param ByteString msg: The message to be hashed.""" + if msg is None: + msg = b"" + + self.msg = msg + + # Pre-processing: Total length is a multiple of 512 bits. + ml = len(msg) * 8 + msg += b"\x80" + msg += b"\x00" * (-(len(msg) + 8) % 64) + msg += struct.pack("> (MD4.width - n) + return lbits | rbits + + +def main(): + # Import is intentionally delayed. + import sys + + if len(sys.argv) > 1: + messages = [msg.encode() for msg in sys.argv[1:]] + for message in messages: + print(MD4(message).hexdigest()) + else: + messages = [b"", b"The quick brown fox jumps over the lazy dog", b"BEES"] + known_hashes = [ + "31d6cfe0d16ae931b73c59d7e0c089c0", + "1bee69a46ba811185c194762abaeae90", + "501af1ef4b68495b5b7e37b15b4cda68", + ] + + print("Testing the MD4 class.") + print() + + for message, expected in zip(messages, known_hashes): + print("Message: ", message) + print("Expected:", expected) + print("Actual: ", MD4(message).hexdigest()) + print() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + pass diff --git a/msldap/crypto/RC4.py b/msldap/crypto/RC4.py index 3a53475..54fb057 100644 --- a/msldap/crypto/RC4.py +++ b/msldap/crypto/RC4.py @@ -5,8 +5,8 @@ currently it's not the perfect wrapper, needs to be extended """ -from aiosmb.crypto.BASE import symmetricBASE, cipherMODE -from aiosmb.crypto.pure.RC4.RC4 import RC4 as _pureRC4 +from msldap.crypto.BASE import symmetricBASE, cipherMODE +from msldap.crypto.pure.RC4.RC4 import RC4 as _pureRC4 try: from Crypto.Cipher import ARC4 as _pyCryptoRC4 except Exception as e: diff --git a/msldap/crypto/hashing.py b/msldap/crypto/hashing.py index 0b61bf5..ecbc5ae 100644 --- a/msldap/crypto/hashing.py +++ b/msldap/crypto/hashing.py @@ -1,7 +1,8 @@ import hashlib import hmac -from aiosmb.crypto.BASE import hashBASE, hmacBASE +from msldap.crypto.BASE import hashBASE, hmacBASE +from msldap.crypto.MD4 import MD4 class md5(hashBASE): def __init__(self, data = None): @@ -15,17 +16,19 @@ def hexdigest(self): return self._hash.hexdigest() -class md4(hashBASE): - def __init__(self, data = None): - hashBASE.__init__(self, data) - def setup_hash(self): - self._hash = hashlib.new('md4') - def update(self, data): - return self._hash.update(data) - def digest(self): - return self._hash.digest() - def hexdigest(self): - return self._hash.hexdigest() +#class md4(hashBASE): +# def __init__(self, data = None): +# hashBASE.__init__(self, data) +# def setup_hash(self): +# self._hash = hashlib.new('md4') +# def update(self, data): +# return self._hash.update(data) +# def digest(self): +# return self._hash.digest() +# def hexdigest(self): +# return self._hash.hexdigest() + +md4 = MD4 class hmac_md5(hmacBASE): def __init__(self, key): diff --git a/msldap/examples/msldapclient.py b/msldap/examples/msldapclient.py index 305676b..bed62c4 100644 --- a/msldap/examples/msldapclient.py +++ b/msldap/examples/msldapclient.py @@ -10,9 +10,10 @@ import csv import shlex import datetime - -from aiocmd import aiocmd -from asciitree import LeftAligned +import copy + +from msldap.external.aiocmd.aiocmd import aiocmd +from msldap.external.asciitree.asciitree import LeftAligned from tqdm import tqdm from msldap import logger @@ -22,6 +23,9 @@ from msldap.ldap_objects import MSADUser, MSADMachine, MSADUser_TSV_ATTRS from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR +from winacl.dtyp.ace import ACCESS_ALLOWED_OBJECT_ACE, ADS_ACCESS_MASK +from winacl.dtyp.sid import SID +from winacl.dtyp.guid import GUID class MSLDAPClientConsole(aiocmd.PromptToolkitCmd): @@ -33,26 +37,29 @@ self.connection = None self.adinfo = None self.ldapinfo = None + self.domain_name = None async def do_login(self, url = None): """Performs connection and login""" - try: - print('url %s' % repr(url)) - + try: if self.conn_url is None and url is None: print('Not url was set, cant do logon') if url is not None: self.conn_url = MSLDAPURLDecoder(url) - print(self.conn_url.get_credential()) - print(self.conn_url.get_target()) + logger.debug(self.conn_url.get_credential()) + logger.debug(self.conn_url.get_target()) self.connection = self.conn_url.get_client() - await self.connection.connect() - - except: - traceback.print_exc() + _, err = await self.connection.connect() + if err is not None: + raise err + + return True + except: + traceback.print_exc() + return False async def do_ldapinfo(self, show = True): """Prints detailed LDAP connection info (DSA)""" @@ -60,38 +67,72 @@ if self.ldapinfo is None: self.ldapinfo = self.connection.get_server_info() if show is True: - print(self.ldapinfo) - except: - traceback.print_exc() + for k in self.ldapinfo: + print('%s : %s' % (k, self.ldapinfo[k])) + return True + except: + traceback.print_exc() + return False async def do_adinfo(self, show = True): """Prints detailed Active Driectory info""" try: if self.adinfo is None: self.adinfo = self.connection._ldapinfo + self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') if show is True: print(self.adinfo) - except: - traceback.print_exc() + return True + except: + traceback.print_exc() + return False async def do_spns(self): """Fetches kerberoastable user accounts""" try: await self.do_ldapinfo(False) - async for user in self.connection.get_all_service_user_objects(): + async for user, err in self.connection.get_all_service_users(): + if err is not None: + raise err print(user.sAMAccountName) - except: - traceback.print_exc() - + + return True + except: + traceback.print_exc() + return False + async def do_asrep(self): """Fetches ASREP-roastable user accounts""" try: await self.do_ldapinfo(False) - async for user in self.connection.get_all_knoreq_user_objects(): + async for user, err in self.connection.get_all_knoreq_users(): + if err is not None: + raise err print(user.sAMAccountName) - except: - traceback.print_exc() - + return True + except: + traceback.print_exc() + return False + + async def do_computeraddr(self): + """Fetches all computer accounts""" + try: + await self.do_adinfo(False) + #machine_filename = '%s_computers_%s.txt' % (self.domain_name, datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) + + async for machine, err in self.connection.get_all_machines(attrs=['sAMAccountName', 'dNSHostName']): + if err is not None: + raise err + + dns = machine.dNSHostName + if dns is None: + dns = '%s.%s' % (machine.sAMAccountName[:-1], self.domain_name) + + print(str(dns)) + return True + except: + traceback.print_exc() + return False async def do_dump(self): """Fetches ALL user and machine accounts from the domain with a LOT of attributes""" @@ -102,7 +143,9 @@ users_filename = 'users_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") pbar = tqdm(desc = 'Writing users to file %s' % users_filename) with open(users_filename, 'w', newline='', encoding = 'utf8') as f: - async for user in self.connection.get_all_user_objects(): + async for user, err in self.connection.get_all_users(): + if err is not None: + raise err pbar.update() f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS))) print('Users dump was written to %s' % users_filename) @@ -110,13 +153,17 @@ users_filename = 'computers_%s.tsv' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") pbar = tqdm(desc = 'Writing computers to file %s' % users_filename) with open(users_filename, 'w', newline='', encoding = 'utf8') as f: - async for user in self.connection.get_all_machine_objects(): + async for user, err in self.connection.get_all_machines(): + if err is not None: + raise err pbar.update() f.write('\t'.join(user.get_row(MSADUser_TSV_ATTRS))) print('Computer dump was written to %s' % users_filename) - except: - traceback.print_exc() - + return True + except: + traceback.print_exc() + return False + async def do_query(self, query, attributes = None): """Performs a raw LDAP query against the server. Secondary parameter is the requested attributes SEPARATED WITH COMMA (,)""" try: @@ -127,10 +174,14 @@ attributes = attributes.split(',') logging.debug('Query: %s' % (query)) logging.debug('Attributes: %s' % (attributes)) - async for entry in self.connection.pagedsearch(query, attributes): + async for entry, err in self.connection.pagedsearch(query, attributes): + if err is not None: + raise err print(entry) - except: - traceback.print_exc() + return True + except: + traceback.print_exc() + return False async def do_tree(self, dn = None, level = 1): """Prints a tree from the given DN (if not set, the top) and with a given depth (default: 1)""" @@ -156,50 +207,241 @@ tr = LeftAligned() print(tr(tree_data)) - - except: - traceback.print_exc() + return True + except: + traceback.print_exc() + return False async def do_user(self, samaccountname): """Feteches a user object based on the sAMAccountName of the user""" try: await self.do_ldapinfo(False) await self.do_adinfo(False) - async for user in self.connection.get_user(samaccountname): + user, err = await self.connection.get_user(samaccountname) + if err is not None: + raise err + if user is None: + print('User not found!') + else: print(user) - except: - traceback.print_exc() - - async def do_acl(self, dn): + + return True + except: + traceback.print_exc() + return False + + async def do_machine(self, samaccountname): + """Feteches a machine object based on the sAMAccountName of the machine""" + try: + await self.do_ldapinfo(False) + await self.do_adinfo(False) + machine, err = await self.connection.get_machine(samaccountname) + if err is not None: + raise err + if machine is None: + print('machine not found!') + else: + print(machine) + ####TEST + x = SECURITY_DESCRIPTOR.from_bytes(machine.allowedtoactonbehalfofotheridentity) + print(x) + + return True + except: + traceback.print_exc() + return False + + async def do_schemaentry(self, cn): + """Feteches a schema object entry object based on the DN of the object (must start with CN=)""" + try: + await self.do_ldapinfo(False) + await self.do_adinfo(False) + schemaentry, err = await self.connection.get_schemaentry(cn) + if err is not None: + raise err + + print(str(schemaentry)) + return True + except: + traceback.print_exc() + return False + + async def do_allschemaentry(self): + """Feteches all schema object entry objects""" + try: + await self.do_ldapinfo(False) + await self.do_adinfo(False) + async for schemaentry, err in self.connection.get_all_schemaentry(): + if err is not None: + raise err + + print(str(schemaentry)) + return True + except: + traceback.print_exc() + return False + + #async def do_addallowedtoactonbehalfofotheridentity(self, target_name, add_computer_name): + # """Adds a SID to the msDS-AllowedToActOnBehalfOfOtherIdentity protperty of target_dn""" + # try: + # await self.do_ldapinfo(False) + # await self.do_adinfo(False) + # + # try: + # new_owner_sid = SID.from_string(sid) + # except: + # print('Incorrect SID!') + # return False, Exception('Incorrect SID') + # + # + # target_sd = None + # if target_attribute is None or target_attribute == '': + # target_attribute = 'nTSecurityDescriptor' + # res, err = await self.connection.get_objectacl_by_dn(target_dn) + # if err is not None: + # raise err + # target_sd = SECURITY_DESCRIPTOR.from_bytes(res) + # else: + # + # query = '(distinguishedName=%s)' % target_dn + # async for entry, err in self.connection.pagedsearch(query, [target_attribute]): + # if err is not None: + # raise err + # print(entry['attributes'][target_attribute]) + # target_sd = SECURITY_DESCRIPTOR.from_bytes(entry['attributes'][target_attribute]) + # break + # else: + # print('Target DN not found!') + # return False, Exception('Target DN not found!') + # + # print(target_sd) + # new_sd = copy.deepcopy(target_sd) + # new_sd.Owner = new_owner_sid + # print(new_sd) + # + # changes = { + # target_attribute : [('replace', [new_sd.to_bytes()])] + # } + # _, err = await self.connection.modify(target_dn, changes) + # if err is not None: + # raise err + # + # print('Change OK!') + # except: + # traceback.print_exc() + + async def do_changeowner(self, new_owner_sid, target_dn, target_attribute = None): + """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""" + try: + await self.do_ldapinfo(False) + await self.do_adinfo(False) + + _, err = await self.connection.change_priv_owner(new_owner_sid, target_dn, target_attribute = target_attribute) + if err is not None: + raise err + except: + traceback.print_exc() + return False + + async def do_addprivdcsync(self, user_dn, forest = None): + """Adds DCSync rights to the given user by modifying the forest's Security Descriptor to add GetChanges and GetChangesAll ACE""" + try: + await self.do_ldapinfo(False) + await self.do_adinfo(False) + + _, err = await self.connection.add_priv_dcsync(user_dn, self.adinfo.distinguishedName) + if err is not None: + raise err + + print('Change OK!') + return True + except: + traceback.print_exc() + return False + + async def do_addprivaddmember(self, user_dn, group_dn): + """Adds AddMember rights to the user on the group specified by group_dn""" + try: + await self.do_ldapinfo(False) + await self.do_adinfo(False) + + _, err = await self.connection.add_priv_addmember(user_dn, group_dn) + if err is not None: + raise err + + print('Change OK!') + return True + except: + traceback.print_exc() + return False + + async def do_setsd(self, target_dn, sddl): + """Updates the security descriptor of an object""" + try: + await self.do_ldapinfo(False) + await self.do_adinfo(False) + + try: + new_sd = SECURITY_DESCRIPTOR.from_sddl(sddl) + except: + print('Incorrect SDDL input!') + return False, Exception('Incorrect SDDL input!') + + _, err = await self.connection.set_objectacl_by_dn(target_dn, new_sd.to_bytes()) + if err is not None: + raise err + print('Change OK!') + return True + except: + print('Erro while updating security descriptor!') + traceback.print_exc() + return False + + async def do_getsd(self, dn): """Feteches security info for a given DN""" try: await self.do_ldapinfo(False) await self.do_adinfo(False) - async for sec_info in self.connection.get_objectacl_by_dn(dn): - print(str(SECURITY_DESCRIPTOR.from_bytes(sec_info.nTSecurityDescriptor))) - except: - traceback.print_exc() + sec_info, err = await self.connection.get_objectacl_by_dn(dn) + if err is not None: + raise err + sd = SECURITY_DESCRIPTOR.from_bytes(sec_info) + print(sd.to_sddl()) + return True + except: + traceback.print_exc() + return False async def do_gpos(self): """Feteches security info for a given DN""" try: await self.do_ldapinfo(False) await self.do_adinfo(False) - async for gpo in self.connection.get_all_gpos(): + async for gpo, err in self.connection.get_all_gpos(): + if err is not None: + raise err print(gpo) - except: - traceback.print_exc() + + return True + except: + traceback.print_exc() + return False async def do_laps(self): """Feteches all laps passwords""" try: - async for entry in self.connection.get_all_laps(): + async for entry, err in self.connection.get_all_laps(): + if err is not None: + raise err pwd = '' - if 'ms-mcs-AdmPwd' in entry['attributes']: - pwd = entry['attributes']['ms-mcs-AdmPwd'] + if 'ms-Mcs-AdmPwd' in entry['attributes']: + pwd = entry['attributes']['ms-Mcs-AdmPwd'] print('%s : %s' % (entry['attributes']['cn'], pwd)) - except: - traceback.print_exc() + + return True + except: + traceback.print_exc() + return False async def do_groupmembership(self, dn): """Feteches names all groupnames the user is a member of for a given DN""" @@ -207,15 +449,22 @@ await self.do_ldapinfo(False) await self.do_adinfo(False) group_sids = [] - async for group_sid in self.connection.get_tokengroups(dn): + async for group_sid, err in self.connection.get_tokengroups(dn): + if err is not None: + raise err group_sids.append(group_sids) - group_dn = await self.connection.get_dn_for_objectsid(group_sid) + group_dn, err = await self.connection.get_dn_for_objectsid(group_sid) + if err is not None: + raise err print('%s - %s' % (group_dn, group_sid)) if len(group_sids) == 0: print('No memberships found') - except: - traceback.print_exc() + + return True + except Exception as e: + traceback.print_exc() + return False async def do_bindtree(self, newtree): """Changes the LDAP TREE for future queries. @@ -226,19 +475,151 @@ async def do_trusts(self): """Feteches gives back domain trusts""" try: - async for entry in self.connection.get_all_trusts(): + async for entry, err in self.connection.get_all_trusts(): + if err is not None: + raise err print(entry.get_line()) - except: - traceback.print_exc() - + + return True + except: + traceback.print_exc() + return False + + async def do_adduser(self, user_dn, password): + """Creates a new domain user with password""" + try: + _, err = await self.connection.create_user_dn(user_dn, password) + if err is not None: + raise err + print('User added') + return True + except: + traceback.print_exc() + return False + + + async def do_deluser(self, user_dn): + """Deletes the user! This action is irrecoverable (actually domain admins can do that but probably will shout with you)""" + try: + _, err = await self.connection.delete_user(user_dn) + if err is not None: + raise err + print('Goodbye, Caroline.') + return True + except: + traceback.print_exc() + return False + + async def do_changeuserpw(self, user_dn, newpass, oldpass = None): + """Changes user password, if you are admin then old pw doesnt need to be supplied""" + try: + _, err = await self.connection.change_password(user_dn, newpass, oldpass) + if err is not None: + raise err + print('User password changed') + return True + except: + traceback.print_exc() + return False + + async def do_unlockuser(self, user_dn): + """Unlock user by setting lockoutTime to 0""" + try: + _, err = await self.connection.unlock_user(user_dn) + if err is not None: + raise err + print('User unlocked') + return True + except: + traceback.print_exc() + return False + + async def do_enableuser(self, user_dn): + """Unlock user by flipping useraccountcontrol bits""" + try: + _, err = await self.connection.enable_user(user_dn) + if err is not None: + raise err + print('User enabled') + return True + except: + traceback.print_exc() + return False + + async def do_disableuser(self, user_dn): + """Unlock user by flipping useraccountcontrol bits""" + try: + _, err = await self.connection.disable_user(user_dn) + if err is not None: + raise err + print('User disabled') + return True + except: + traceback.print_exc() + return False + + async def do_addspn(self, user_dn, spn): + """Adds an SPN entry to the users account""" + try: + _, err = await self.connection.add_user_spn(user_dn, spn) + if err is not None: + raise err + print('SPN added!') + return True + except: + traceback.print_exc() + return False + + async def do_addhostname(self, user_dn, hostname): + """Adds additional hostname to computer account""" + try: + _, err = await self.connection.add_additional_hostname(user_dn, hostname) + if err is not None: + raise err + print('Hostname added!') + return True + except: + traceback.print_exc() + return False + + async def do_addusertogroup(self, user_dn, group_dn): + """Adds user to specified group. Both user and group must be in DN format!""" + try: + _, err = await self.connection.add_user_to_group(user_dn, group_dn) + if err is not None: + raise err + print('User added to group!') + return True + except: + traceback.print_exc() + return False + + async def do_deluserfromgroup(self, user_dn, group_dn): + """Removes user from specified group. Both user and group must be in DN format!""" + try: + _, err = await self.connection.del_user_from_group(user_dn, group_dn) + if err is not None: + raise err + print('User added to group!') + return True + except: + traceback.print_exc() + return False + async def do_test(self): """testing, dontuse""" try: - async for entry in self.connection.get_all_objectacl(): + async for entry, err in self.connection.get_all_objectacl(): + if err is not None: + raise err + if entry.objectClass[-1] != 'user': print(entry.objectClass) - except: - traceback.print_exc() + + return True + except: + traceback.print_exc() + return False """ async def do_info(self): @@ -259,8 +640,13 @@ await client.run() else: for command in args.commands: + if command == 'i': + await client.run() + return cmd = shlex.split(command) - await client._run_single_command(cmd[0], cmd[1:]) + res = await client._run_single_command(cmd[0], cmd[1:]) + if res is False: + return def main(): import argparse @@ -268,7 +654,7 @@ parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') parser.add_argument('-n', '--no-interactive', action='store_true') parser.add_argument('url', help='Connection string in URL format.') - parser.add_argument('commands', nargs='*') + 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.") args = parser.parse_args() diff --git a/msldap/examples/msldapcompdnslist.py b/msldap/examples/msldapcompdnslist.py new file mode 100644 index 0000000..6ef2e8e --- /dev/null +++ b/msldap/examples/msldapcompdnslist.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# +# Author: +# Tamas Jos (@skelsec) +# + +import asyncio +import traceback +import logging +import csv +import shlex +import datetime + +from msldap.external.aiocmd.aiocmd import aiocmd +from msldap.external.asciitree.asciitree import LeftAligned +from tqdm import tqdm + +from msldap import logger +from asysocks import logger as sockslogger +from msldap.client import MSLDAPClient +from msldap.commons.url import MSLDAPURLDecoder +from msldap.ldap_objects import MSADUser, MSADMachine, MSADUser_TSV_ATTRS + +from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR + + +class MSLDAPCompDomainList: + def __init__(self, ldap_url): + self.conn_url = ldap_url + self.connection = None + self.adinfo = None + self.ldapinfo = None + self.domain_name = None + + async def login(self): + """Performs connection and login""" + try: + logger.debug(self.conn_url.get_credential()) + logger.debug(self.conn_url.get_target()) + + + self.connection = self.conn_url.get_client() + _, err = await self.connection.connect() + if err is not None: + raise err + + return True, None + except Exception as e: + return False, e + + async def do_adinfo(self, show = True): + """Prints detailed Active Driectory info""" + try: + if self.adinfo is None: + self.adinfo = self.connection._ldapinfo + self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') + if show is True: + print(self.adinfo) + + return True, None + except Exception as e: + return False, e + + async def run(self): + try: + _, err = await self.login() + if err is not None: + raise err + _, err = await self.do_adinfo(False) + if err is not None: + raise err + #machine_filename = '%s_computers_%s.txt' % (self.domain_name, datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) + + async for machine, err in self.connection.get_all_machines(attrs=['sAMAccountName', 'dNSHostName']): + if err is not None: + raise err + + dns = machine.dNSHostName + if dns is None: + dns = '%s.%s' % (machine.sAMAccountName[:-1], self.domain_name) + + print(str(dns)) + except: + traceback.print_exc() + + +def main(): + import argparse + parser = argparse.ArgumentParser(description='MS LDAP library') + parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') + parser.add_argument('-n', '--no-interactive', action='store_true') + parser.add_argument('url', help='Connection string in URL format.') + + args = parser.parse_args() + + + ###### VERBOSITY + if args.verbose == 0: + logging.basicConfig(level=logging.INFO) + else: + sockslogger.setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) + logging.basicConfig(level=logging.DEBUG) + + ldap_url = MSLDAPURLDecoder(args.url) + compdomlist = MSLDAPCompDomainList(ldap_url) + + + asyncio.run(compdomlist.run()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/msldap/external/__init__.py b/msldap/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msldap/external/aiocmd/__init__.py b/msldap/external/aiocmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msldap/external/aiocmd/aiocmd/__init__.py b/msldap/external/aiocmd/aiocmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msldap/external/aiocmd/aiocmd/aiocmd.py b/msldap/external/aiocmd/aiocmd/aiocmd.py new file mode 100644 index 0000000..6c759f7 --- /dev/null +++ b/msldap/external/aiocmd/aiocmd/aiocmd.py @@ -0,0 +1,159 @@ +import asyncio +import inspect +import shlex +import signal +import sys + +from prompt_toolkit import PromptSession +from prompt_toolkit.completion import WordCompleter +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.patch_stdout import patch_stdout + +try: + from prompt_toolkit.completion.nested import NestedCompleter +except ImportError: + from aiocmd.nested_completer import NestedCompleter + + +class ExitPromptException(Exception): + pass + + +class PromptToolkitCmd: + """Baseclass for custom CLIs + + Works similarly to the built-in Cmd class. You can inherit from this class and implement: + - do_ - This will add the "" command to the cli. + The method may receive arguments (required) and keyword arguments (optional). + - __completions - Returns a custom Completer class to use as a completer for this action. + Additionally, the user cant change the "prompt" variable to change how the prompt looks, and add + command aliases to the 'aliases' dict. + """ + ATTR_START = "do_" + prompt = "$ " + doc_header = "Documented commands:" + aliases = {"?": "help", "exit": "quit"} + + def __init__(self, ignore_sigint=True): + self.completer = self._make_completer() + self.session = None + self._ignore_sigint = ignore_sigint + self._currently_running_task = None + + async def run(self): + if self._ignore_sigint and sys.platform != "win32": + asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self._sigint_handler) + self.session = PromptSession(enable_history_search=True, key_bindings=self._get_bindings()) + try: + with patch_stdout(): + await self._run_prompt_forever() + finally: + if self._ignore_sigint and sys.platform != "win32": + asyncio.get_event_loop().remove_signal_handler(signal.SIGINT) + self._on_close() + + async def _run_prompt_forever(self): + while True: + try: + result = await self.session.prompt_async(self.prompt, completer=self.completer) + except EOFError: + return + + if not result: + continue + args = shlex.split(result) + if args[0] in self.command_list: + try: + self._currently_running_task = asyncio.ensure_future( + self._run_single_command(args[0], args[1:])) + await self._currently_running_task + except asyncio.CancelledError: + print() + continue + except ExitPromptException: + return + else: + print("Command %s not found!" % args[0]) + + def _sigint_handler(self): + if self._currently_running_task: + self._currently_running_task.cancel() + + def _get_bindings(self): + bindings = KeyBindings() + bindings.add("c-c")(lambda event: self._interrupt_handler(event)) + return bindings + + async def _run_single_command(self, command, args): + command_real_args, command_real_kwargs = self._get_command_args(command) + if len(args) < len(command_real_args) or len(args) > (len(command_real_args) + + len(command_real_kwargs)): + print("Bad command args. Usage: %s" % self._get_command_usage(command, command_real_args, + command_real_kwargs)) + return + + try: + com_func = self._get_command(command) + if asyncio.iscoroutinefunction(com_func): + res = await com_func(*args) + else: + res = com_func(*args) + return res + except (ExitPromptException, asyncio.CancelledError): + raise + except Exception as ex: + print("Command failed: ", ex) + + def _interrupt_handler(self, event): + event.cli.current_buffer.text = "" + + def _make_completer(self): + return NestedCompleter({com: self._completer_for_command(com) for com in self.command_list}) + + def _completer_for_command(self, command): + if not hasattr(self, "_%s_completions" % command): + return WordCompleter([]) + return getattr(self, "_%s_completions" % command)() + + def _get_command(self, command): + if command in self.aliases: + command = self.aliases[command] + return getattr(self, self.ATTR_START + command) + + def _get_command_args(self, command): + args = [param for param in inspect.signature(self._get_command(command)).parameters.values() + if param.default == param.empty] + kwargs = [param for param in inspect.signature(self._get_command(command)).parameters.values() + if param.default != param.empty] + return args, kwargs + + def _get_command_usage(self, command, args, kwargs): + return ("%s %s %s" % (command, + " ".join("<%s>" % arg for arg in args), + " ".join("[%s]" % kwarg for kwarg in kwargs), + )).strip() + + @property + def command_list(self): + return [attr[len(self.ATTR_START):] + for attr in dir(self) if attr.startswith(self.ATTR_START)] + list(self.aliases.keys()) + + def do_help(self): + print() + print(self.doc_header) + print("=" * len(self.doc_header)) + print() + + get_usage = lambda command: self._get_command_usage(command, *self._get_command_args(command)) + max_usage_len = max([len(get_usage(command)) for command in self.command_list]) + for command in sorted(self.command_list): + command_doc = self._get_command(command).__doc__ + print(("%-" + str(max_usage_len + 2) + "s%s") % (get_usage(command), command_doc or "")) + + def do_quit(self): + """Exit the prompt""" + raise ExitPromptException() + + def _on_close(self): + """Optional hook to call on closing the cmd""" + pass diff --git a/msldap/external/aiocmd/aiocmd/nested_completer.py b/msldap/external/aiocmd/aiocmd/nested_completer.py new file mode 100644 index 0000000..95cca10 --- /dev/null +++ b/msldap/external/aiocmd/aiocmd/nested_completer.py @@ -0,0 +1,97 @@ +""" +Nestedcompleter for completion of hierarchical data structures. +""" +from typing import Dict, Iterable, Mapping, Optional, Set, Union + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.completion.word_completer import WordCompleter +from prompt_toolkit.document import Document + +__all__ = [ + 'NestedCompleter' +] + +NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] + + +class NestedCompleter(Completer): + """ + Completer which wraps around several other completers, and calls any the + one that corresponds with the first word of the input. + By combining multiple `NestedCompleter` instances, we can achieve multiple + hierarchical levels of autocompletion. This is useful when `WordCompleter` + is not sufficient. + If you need multiple levels, check out the `from_nested_dict` classmethod. + """ + def __init__(self, options: Dict[str, Optional[Completer]], + ignore_case: bool = True) -> None: + + self.options = options + self.ignore_case = ignore_case + + def __repr__(self) -> str: + return 'NestedCompleter(%r, ignore_case=%r)' % (self.options, self.ignore_case) + + @classmethod + def from_nested_dict(cls, data: NestedDict) -> 'NestedCompleter': + """ + Create a `NestedCompleter`, starting from a nested dictionary data + structure, like this: + .. code:: + data = { + 'show': { + 'version': None, + 'interfaces': None, + 'clock': None, + 'ip': {'interface': {'brief'}} + }, + 'exit': None + 'enable': None + } + The value should be `None` if there is no further completion at some + point. If all values in the dictionary are None, it is also possible to + use a set instead. + Values in this data structure can be a completers as well. + """ + options = {} + for key, value in data.items(): + if isinstance(value, Completer): + options[key] = value + elif isinstance(value, dict): + options[key] = cls.from_nested_dict(value) + elif isinstance(value, set): + options[key] = cls.from_nested_dict({item: None for item in value}) + else: + assert value is None + options[key] = None + + return cls(options) + + def get_completions(self, document: Document, + complete_event: CompleteEvent) -> Iterable[Completion]: + # Split document. + text = document.text_before_cursor.lstrip() + + # If there is a space, check for the first term, and use a + # subcompleter. + if ' ' in text: + first_term = text.split()[0] + completer = self.options.get(first_term) + + # If we have a sub completer, use this for the completions. + if completer is not None: + remaining_text = document.text[len(first_term):].lstrip() + move_cursor = len(document.text) - len(remaining_text) + + new_document = Document( + remaining_text, + cursor_position=document.cursor_position - move_cursor) + + for c in completer.get_completions(new_document, complete_event): + yield c + + # No space in the input: behave exactly like `WordCompleter`. + else: + completer = WordCompleter(list(self.options.keys()), ignore_case=self.ignore_case) + for c in completer.get_completions(document, complete_event): + yield c diff --git a/msldap/external/aiocmd/setup.py b/msldap/external/aiocmd/setup.py new file mode 100644 index 0000000..faee602 --- /dev/null +++ b/msldap/external/aiocmd/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages + +setup(name='aiocmd', + packages=find_packages("."), + version='0.1.4', + author='Dor Green', + author_email='dorgreen1@gmail.com', + description='Coroutine-based CLI generator using prompt_toolkit', + url='http://github.com/KimiNewt/aiocmd', + keywords=['asyncio', 'cmd'], + license='MIT', + install_requires=[ + 'prompt_toolkit>=2.0.9' + ], + classifiers=[ + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7' + ]) diff --git a/msldap/external/asciitree/__init__.py b/msldap/external/asciitree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msldap/external/asciitree/asciitree/__init__.py b/msldap/external/asciitree/asciitree/__init__.py new file mode 100644 index 0000000..0803948 --- /dev/null +++ b/msldap/external/asciitree/asciitree/__init__.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from .drawing import BoxStyle +from .traversal import DictTraversal +from .util import KeyArgsConstructor + + +class LeftAligned(KeyArgsConstructor): + """Creates a renderer for a left-aligned tree. + + Any attributes of the resulting class instances can be set using + constructor arguments.""" + + draw = BoxStyle() + "The draw style used. See :class:`~asciitree.drawing.Style`." + traverse = DictTraversal() + "Traversal method. See :class:`~asciitree.traversal.Traversal`." + + def render(self, node): + """Renders a node. This function is used internally, as it returns + a list of lines. Use :func:`~asciitree.LeftAligned.__call__` instead. + """ + lines = [] + + children = self.traverse.get_children(node) + lines.append(self.draw.node_label(self.traverse.get_text(node))) + + for n, child in enumerate(children): + child_tree = self.render(child) + + if n == len(children) - 1: + # last child does not get the line drawn + lines.append(self.draw.last_child_head(child_tree.pop(0))) + lines.extend(self.draw.last_child_tail(l) + for l in child_tree) + else: + lines.append(self.draw.child_head(child_tree.pop(0))) + lines.extend(self.draw.child_tail(l) + for l in child_tree) + + return lines + + def __call__(self, tree): + """Render the tree into string suitable for console output. + + :param tree: A tree.""" + return '\n'.join(self.render(self.traverse.get_root(tree))) + + +# legacy support below + +from .drawing import Style +from .traversal import Traversal + + +class LegacyStyle(Style): + def node_label(self, text): + return text + + def child_head(self, label): + return ' +--' + label + + def child_tail(self, line): + return ' |' + line + + def last_child_head(self, label): + return ' +--' + label + + def last_child_tail(self, line): + return ' ' + line + + +def draw_tree(node, + child_iter=lambda n: n.children, + text_str=str): + """Support asciitree 0.2 API. + + This function solely exist to not break old code (using asciitree 0.2). + Its use is deprecated.""" + return LeftAligned(traverse=Traversal(get_text=text_str, + get_children=child_iter), + draw=LegacyStyle())(node) diff --git a/msldap/external/asciitree/asciitree/drawing.py b/msldap/external/asciitree/asciitree/drawing.py new file mode 100644 index 0000000..9f91863 --- /dev/null +++ b/msldap/external/asciitree/asciitree/drawing.py @@ -0,0 +1,101 @@ +from .util import KeyArgsConstructor + +BOX_LIGHT = { + 'UP_AND_RIGHT': u'\u2514', + 'HORIZONTAL': u'\u2500', + 'VERTICAL': u'\u2502', + 'VERTICAL_AND_RIGHT': u'\u251C', +} #: Unicode box-drawing glyphs, light style + + +BOX_HEAVY = { + 'UP_AND_RIGHT': u'\u2517', + 'HORIZONTAL': u'\u2501', + 'VERTICAL': u'\u2503', + 'VERTICAL_AND_RIGHT': u'\u2523', +} #: Unicode box-drawing glyphs, heavy style + + +BOX_DOUBLE = { + 'UP_AND_RIGHT': u'\u255A', + 'HORIZONTAL': u'\u2550', + 'VERTICAL': u'\u2551', + 'VERTICAL_AND_RIGHT': u'\u2560', +} #: Unicode box-drawing glyphs, double-line style + + +BOX_ASCII = { + 'UP_AND_RIGHT': u'+', + 'HORIZONTAL': u'-', + 'VERTICAL': u'|', + 'VERTICAL_AND_RIGHT': u'+', +} #: Unicode box-drawing glyphs, using only ascii ``|+-`` characters. + + +BOX_BLANK = { + 'UP_AND_RIGHT': u' ', + 'HORIZONTAL': u' ', + 'VERTICAL': u' ', + 'VERTICAL_AND_RIGHT': u' ', +} #: Unicode box-drawing glyphs, using only spaces. + + +class Style(KeyArgsConstructor): + """Rendering style for trees.""" + label_format = u'{}' #: Format for labels. + + def node_label(self, text): + """Render a node text into a label.""" + return self.label_format.format(text) + + def child_head(self, label): + """Render a node label into final output.""" + return label + + def child_tail(self, line): + """Render a node line that is not a label into final output.""" + return line + + def last_child_head(self, label): + """Like :func:`~asciitree.drawing.Style.child_head` but only called + for the last child.""" + return label + + def last_child_tail(self, line): + """Like :func:`~asciitree.drawing.Style.child_tail` but only called + for the last child.""" + return line + + +class BoxStyle(Style): + """A rendering style that uses box draw characters and a common layout.""" + gfx = BOX_ASCII #: Glyhps to use. + label_space = 1 #: Space between glyphs and label. + horiz_len = 2 #: Length of horizontal lines + indent = 1 #: Indent for subtrees + + def child_head(self, label): + return (' ' * self.indent + + self.gfx['VERTICAL_AND_RIGHT'] + + self.gfx['HORIZONTAL'] * self.horiz_len + + ' ' * self.label_space + + label) + + def child_tail(self, line): + return (' ' * self.indent + + self.gfx['VERTICAL'] + + ' ' * self.horiz_len + + line) + + def last_child_head(self, label): + return (' ' * self.indent + + self.gfx['UP_AND_RIGHT'] + + self.gfx['HORIZONTAL'] * self.horiz_len + + ' ' * self.label_space + + label) + + def last_child_tail(self, line): + return (' ' * self.indent + + ' ' * len(self.gfx['VERTICAL']) + + ' ' * self.horiz_len + + line) diff --git a/msldap/external/asciitree/asciitree/traversal.py b/msldap/external/asciitree/asciitree/traversal.py new file mode 100644 index 0000000..325135f --- /dev/null +++ b/msldap/external/asciitree/asciitree/traversal.py @@ -0,0 +1,43 @@ +from .util import KeyArgsConstructor + + +class Traversal(KeyArgsConstructor): + """Traversal method. + + Used by the tree rendering functions like :class:`~asciitree.LeftAligned`. + """ + def get_children(self, node): + """Return a list of children of a node.""" + raise NotImplementedError + + def get_root(self, tree): + """Return a node representing the tree root from the tree.""" + return tree + + def get_text(self, node): + """Return the text associated with a node.""" + return str(node) + + +class DictTraversal(Traversal): + """Traversal suitable for a dictionary. Keys are tree labels, all values + must be dictionaries as well.""" + def get_children(self, node): + return list(node[1].items()) + + def get_root(self, tree): + return list(tree.items())[0] + + def get_text(self, node): + return node[0] + + +class AttributeTraversal(Traversal): + """Attribute traversal. + + Uses an attribute of a node as its list of children. + """ + attribute = 'children' #: Attribute to use. + + def get_children(self, node): + return getattr(node, self.attribute) diff --git a/msldap/external/asciitree/asciitree/util.py b/msldap/external/asciitree/asciitree/util.py new file mode 100644 index 0000000..0b27f17 --- /dev/null +++ b/msldap/external/asciitree/asciitree/util.py @@ -0,0 +1,4 @@ +class KeyArgsConstructor(object): + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) diff --git a/msldap/external/asciitree/setup.py b/msldap/external/asciitree/setup.py new file mode 100644 index 0000000..2ab4227 --- /dev/null +++ b/msldap/external/asciitree/setup.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os + +from setuptools import setup, find_packages + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +setup( + name='asciitree', + version='0.3.3', + description='Draws ASCII trees.', + long_description=read('README.rst'), + author='Marc Brinkmann', + author_email='git@marcbrinkmann.de', + url='http://github.com/mbr/asciitree', + license='MIT', + packages=find_packages(exclude=['tests']), + install_requires=[], +) diff --git a/msldap/ldap_objects/__init__.py b/msldap/ldap_objects/__init__.py index d354cfd..f7c026b 100644 --- a/msldap/ldap_objects/__init__.py +++ b/msldap/ldap_objects/__init__.py @@ -9,10 +9,11 @@ from msldap.ldap_objects.adcomp import MSADMachine, MSADMachine_ATTRS, MSADMachine_TSV_ATTRS from msldap.ldap_objects.adsec import MSADSecurityInfo, MSADTokenGroup from msldap.ldap_objects.common import MSLDAP_UAC -from msldap.ldap_objects.adgroup import MSADGroup -from msldap.ldap_objects.adou import MSADOU +from msldap.ldap_objects.adgroup import MSADGroup, MSADGroup_ATTRS +from msldap.ldap_objects.adou import MSADOU, MSADOU_ATTRS from msldap.ldap_objects.adgpo import MSADGPO, MSADGPO_ATTRS from msldap.ldap_objects.adtrust import MSADDomainTrust, MSADDomainTrust_ATTRS +from msldap.ldap_objects.adschemaentry import MSADSCHEMAENTRY_ATTRS, MSADSchemaEntry __all__ = [ 'MSADUser', @@ -32,4 +33,8 @@ 'MSADGPO_ATTRS', 'MSADDomainTrust', 'MSADDomainTrust_ATTRS', + 'MSADGroup_ATTRS', + 'MSADOU_ATTRS', + 'MSADSCHEMAENTRY_ATTRS', + 'MSADSchemaEntry', ]diff --git a/msldap/ldap_objects/adcomp.py b/msldap/ldap_objects/adcomp.py index 99f3e9e..649b7a4 100644 --- a/msldap/ldap_objects/adcomp.py +++ b/msldap/ldap_objects/adcomp.py @@ -4,6 +4,7 @@ # Tamas Jos (@skelsec) # +import datetime from msldap.ldap_objects.common import MSLDAP_UAC, vn MSADMachine_ATTRS = [ @@ -15,6 +16,7 @@ 'operatingSystem', 'operatingSystemVersion','primaryGroupID', 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl', 'whenChanged', 'whenCreated', 'servicePrincipalName','msDS-AllowedToDelegateTo', + 'msDS-AllowedToActOnBehalfOfOtherIdentity' ] MSADMachine_TSV_ATTRS = [ @@ -63,6 +65,57 @@ self.whenCreated = None self.servicePrincipalName = None self.allowedtodelegateto = None + self.allowedtoactonbehalfofotheridentity = None + + ## calculated properties + self.when_pw_change = None #datetime + self.when_pw_expires = None #datetime + self.must_change_pw = None #datetime + self.canLogon = None #bool + + # https://msdn.microsoft.com/en-us/library/cc245739.aspx + def calc_PasswordMustChange(self, adinfo): + # Crtieria 1 + 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] + for flag in flags: + if flag & self.userAccountControl: + return datetime.datetime.max #never + + #criteria 2 + if self.pwdLastSet == datetime.timedelta(): + return datetime.datetime.min + + if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta + return datetime.datetime.max #never + + if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum + return datetime.datetime.max #never + + return (self.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) + + + # https://msdn.microsoft.com/en-us/library/cc223991.aspx + def calc_CanLogon(self): + 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] + for flag in flags: + if flag & self.userAccountControl: + return False + + if (not (MSLDAP_UAC.DONT_EXPIRE_PASSWD & self.userAccountControl)) and (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: + return False + + # + # TODO: logonHours check! + # + + if self.must_change_pw == datetime.datetime.min: + #can logon, but must change the password! + return True + + if (self.must_change_pw - datetime.datetime.now()).total_seconds() < 0: + return False + + return True @staticmethod def from_ldap(entry, adinfo = None): @@ -102,10 +155,21 @@ adi.servicePrincipalName = entry['attributes'].get('servicePrincipalName') adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo') + adi.allowedtoactonbehalfofotheridentity = entry['attributes'].get('msDS-AllowedToActOnBehalfOfOtherIdentity') temp = entry['attributes'].get('userAccountControl') if temp: adi.userAccountControl = MSLDAP_UAC(temp) + + if adinfo: + adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge).replace(tzinfo=None) + if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum + adi.when_pw_expires = datetime.datetime.max + else: + adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) if adinfo.maxPwdAge != 0 else adi.pwdLastSet + adi.must_change_pw = adi.calc_PasswordMustChange(adinfo) #datetime + adi.canLogon = adi.calc_CanLogon() #bool + return adi def to_dict(self): @@ -156,4 +220,11 @@ def get_row(self, attrs): t = self.to_dict() - return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] \ No newline at end of file + return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] + + + def __str__(self): + t = '' + for k in self.__dict__: + t += '%s : %s\r\n' % (k, self.__dict__[k]) + return t \ No newline at end of file diff --git a/msldap/ldap_objects/adgpo.py b/msldap/ldap_objects/adgpo.py index 7c884ca..e775f48 100644 --- a/msldap/ldap_objects/adgpo.py +++ b/msldap/ldap_objects/adgpo.py @@ -8,8 +8,9 @@ MSADGPO_ATTRS = [ 'cn', 'displayName', 'distinguishedName', 'flags', 'gPCFileSysPath', - 'gPCFunctionalityVersion', 'gPCMachineExtensionNames', 'objectClass', - 'objectGUID', 'systemFlags', 'versionNumber', 'whenChanged', 'whenCreated', + 'gPCFunctionalityVersion', 'gPCMachineExtensionNames', 'gPCUserExtensionNames', + 'objectClass', 'objectGUID', 'systemFlags', 'versionNumber', 'whenChanged', + 'whenCreated', ] class MSADGPO: @@ -21,11 +22,13 @@ self.gPCFileSysPath = None #str self.gPCFunctionalityVersion = None #str self.gPCMachineExtensionNames = None + self.gPCUserExtensionNames = None self.objectClass = None #str self.objectGUID = None #uid self.systemFlags = None #str self.whenChanged = None #uid self.whenCreated = None #str + self.versionNumber = None @staticmethod @@ -38,11 +41,13 @@ adi.gPCFileSysPath = entry['attributes'].get('gPCFileSysPath') adi.gPCFunctionalityVersion = entry['attributes'].get('gPCFunctionalityVersion') adi.gPCMachineExtensionNames = entry['attributes'].get('gPCMachineExtensionNames') + adi.gPCUserExtensionNames = entry['attributes'].get('gPCUserExtensionNames') adi.objectClass = entry['attributes'].get('objectClass') adi.objectGUID = entry['attributes'].get('objectGUID') adi.systemFlags = entry['attributes'].get('systemFlags') adi.whenChanged = entry['attributes'].get('whenChanged') adi.whenCreated = entry['attributes'].get('whenCreated') + adi.versionNumber = entry['attributes'].get('versionNumber') return adi @@ -55,11 +60,13 @@ t['gPCFileSysPath'] = vn(self.gPCFileSysPath) t['gPCFunctionalityVersion'] = vn(self.gPCFunctionalityVersion) t['gPCMachineExtensionNames'] = vn(self.gPCMachineExtensionNames) + t['gPCUserExtensionNames'] = vn(self.gPCUserExtensionNames) t['systemFlags'] = vn(self.systemFlags) t['objectClass'] = vn(self.objectClass) t['objectGUID'] = vn(self.objectGUID) t['whenChanged'] = vn(self.whenChanged) t['whenCreated'] = vn(self.whenCreated) + t['versionNumber'] = vn(self.versionNumber) return t def __str__(self): diff --git a/msldap/ldap_objects/adgroup.py b/msldap/ldap_objects/adgroup.py index 31e0357..0cfacce 100644 --- a/msldap/ldap_objects/adgroup.py +++ b/msldap/ldap_objects/adgroup.py @@ -6,6 +6,15 @@ from msldap.wintypes import * from msldap.ldap_objects.common import MSLDAP_UAC, vn +from winacl.dtyp.sid import SID + +MSADGroup_ATTRS = [ + 'cn', 'distinguishedName', 'objectGUID', 'objectSid', 'groupType', + 'instanceType', 'name', 'member', 'sAMAccountName', 'systemFlags', + 'whenChanged', 'whenCreated', 'description', 'nTSecurityDescriptor', + 'sAMAccountType', +] + class MSADGroup: def __init__(self): @@ -69,9 +78,9 @@ t.description = ', '.join(t.description) - temp = entry['attributes'].get('nTSecurityDescriptor') - if temp: - t.nTSecurityDescriptor = SID.from_bytes(temp) + #temp = entry['attributes'].get('nTSecurityDescriptor') + #if temp: + # t.nTSecurityDescriptor = SID.from_bytes(temp) return t diff --git a/msldap/ldap_objects/adinfo.py b/msldap/ldap_objects/adinfo.py index aa3a576..e4c3ba1 100644 --- a/msldap/ldap_objects/adinfo.py +++ b/msldap/ldap_objects/adinfo.py @@ -11,7 +11,8 @@ 'name', 'nextRid', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', 'objectGUID', 'objectSid', 'pwdHistoryLength', 'pwdProperties', 'serverState', 'systemFlags', 'uASCompat', 'uSNChanged', - 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference','msDS-Behavior-Version' + 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference', + 'msDS-Behavior-Version' ] class MSADInfo: def __init__(self): @@ -152,4 +153,5 @@ t += 'uSNCreated: %s\n' % self.uSNCreated t += 'whenChanged: %s\n' % self.whenChanged t += 'whenCreated: %s\n' % self.whenCreated + t += 'domainmodelevel: %s\n' % self.domainmodelevel return t diff --git a/msldap/ldap_objects/adou.py b/msldap/ldap_objects/adou.py index b61293d..21e8646 100644 --- a/msldap/ldap_objects/adou.py +++ b/msldap/ldap_objects/adou.py @@ -3,6 +3,14 @@ # Author: # Tamas Jos (@skelsec) # + + +MSADOU_ATTRS = [ + 'description', 'distinguishedName', 'dSCorePropagationData', 'gPLink', 'instanceType', + 'isCriticalSystemObject', 'name', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', + 'objectGUID', 'ou', 'showInAdvancedViewOnly', 'systemFlags', 'uSNChanged', 'uSNCreated', + 'whenChanged', 'whenCreated', +] class MSADOU: def __init__(self): diff --git a/msldap/ldap_objects/adschemaentry.py b/msldap/ldap_objects/adschemaentry.py new file mode 100644 index 0000000..1714fe8 --- /dev/null +++ b/msldap/ldap_objects/adschemaentry.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# +# Author: +# Tamas Jos (@skelsec) +# + + +MSADSCHEMAENTRY_ATTRS = [ + 'cn', 'distinguishedName', 'adminDescription', + 'adminDisplayName', 'objectGUID', 'schemaIDGUID', + 'lDAPDisplayName', 'name', +] + +class MSADSchemaEntry: + def __init__(self): + self.cn = None #str + self.distinguishedName = None #dn + self.adminDescription = None #dunno + self.adminDisplayName = None #datetime + self.objectGUID = None #int + self.schemaIDGUID = None + self.lDAPDisplayName = None + self.name = None #int + + + @staticmethod + def from_ldap(entry): + adi = MSADSchemaEntry() + adi.cn = entry['attributes'].get('cn') + adi.distinguishedName = entry['attributes'].get('distinguishedName') + adi.adminDescription = entry['attributes'].get('adminDescription') + adi.adminDisplayName = entry['attributes'].get('adminDisplayName') + adi.objectGUID = entry['attributes'].get('objectGUID') #str + adi.schemaIDGUID = entry['attributes'].get('schemaIDGUID') #list + adi.lDAPDisplayName = entry['attributes'].get('lDAPDisplayName') #int + adi.name = entry['attributes'].get('name') #int + return adi + + def to_dict(self): + d = {} + d['cn'] = self.cn + d['distinguishedName'] = self.distinguishedName + d['adminDescription'] = self.adminDescription + d['adminDisplayName'] = self.adminDisplayName + d['objectGUID'] = self.objectGUID + d['schemaIDGUID'] = self.schemaIDGUID + d['lDAPDisplayName'] = self.lDAPDisplayName + d['name'] = self.name + return d + + + def __str__(self): + t = 'MSADSchemaEntry\r\n' + d = self.to_dict() + for k in d: + t += '%s: %s\r\n' % (k, d[k]) + return t \ No newline at end of file diff --git a/msldap/ldap_objects/aduser.py b/msldap/ldap_objects/aduser.py index b628e5c..a353b27 100644 --- a/msldap/ldap_objects/aduser.py +++ b/msldap/ldap_objects/aduser.py @@ -14,7 +14,7 @@ 'objectCategory', 'objectClass', 'objectGUID', 'objectSid', 'primaryGroupID', 'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl', 'userPrincipalName', 'whenChanged', 'whenCreated','memberOf','member', 'servicePrincipalName', - 'msDS-AllowedToDelegateTo', + 'msDS-AllowedToDelegateTo', 'adminCount' ] MSADUser_TSV_ATTRS = [ 'sAMAccountName', 'userPrincipalName' ,'canLogon', 'badPasswordTime', 'description', @@ -22,7 +22,7 @@ 'whenCreated', 'whenChanged', 'member', 'memberOf', 'servicePrincipalName', 'objectSid', 'cn', 'UAC_SCRIPT', 'UAC_ACCOUNTDISABLE', 'UAC_LOCKOUT', 'UAC_PASSWD_NOTREQD', 'UAC_PASSWD_CANT_CHANGE', 'UAC_ENCRYPTED_TEXT_PASSWORD_ALLOWED', 'UAC_DONT_EXPIRE_PASSWD', - 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED' + 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED', 'adminCount' ] class MSADUser: @@ -67,6 +67,7 @@ self.sAMAccountType = None #int self.userAccountControl = None #UserAccountControl intflag self.allowedtodelegateto = None + self.admincount = None ## other @@ -80,7 +81,7 @@ self.canLogon = None #bool # https://msdn.microsoft.com/en-us/library/cc245739.aspx - def calc_PasswordMustChange(self): + def calc_PasswordMustChange(self, adinfo): # Crtieria 1 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] for flag in flags: @@ -88,13 +89,16 @@ return datetime.datetime.max #never #criteria 2 - if self.pwdLastSet == 0: + if self.pwdLastSet == datetime.timedelta(): return datetime.datetime.min - if (self.when_pw_expires - datetime.datetime.now()).total_seconds() > 0: + if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta return datetime.datetime.max #never - return self.pwdLastSet.replace(tzinfo=None) + if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum + return datetime.datetime.max #never + + return (self.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) # https://msdn.microsoft.com/en-us/library/cc223991.aspx @@ -103,8 +107,8 @@ for flag in flags: if flag & self.userAccountControl: return False - - if (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: + + if (not (MSLDAP_UAC.DONT_EXPIRE_PASSWD & self.userAccountControl)) and (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0: return False # @@ -156,17 +160,20 @@ adi.countryCode = entry['attributes'].get('countryCode') adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo') + adi.admincount = entry['attributes'].get('adminCount') temp = entry['attributes'].get('userAccountControl') if temp: adi.userAccountControl = MSLDAP_UAC(temp) if adinfo: - adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge/10000000).replace(tzinfo=None) - adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge/10000000).replace(tzinfo=None) - adi.must_change_pw = adi.calc_PasswordMustChange() #datetime - if adi.sAMAccountName[-1] != '$': - adi.canLogon = adi.calc_CanLogon() #bool + adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge).replace(tzinfo=None) + if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum + adi.when_pw_expires = datetime.datetime.max + else: + adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) if adinfo.maxPwdAge != 0 else adi.pwdLastSet + adi.must_change_pw = adi.calc_PasswordMustChange(adinfo) #datetime + adi.canLogon = adi.calc_CanLogon() #bool return adi @@ -208,6 +215,7 @@ t['when_pw_change'] = vn(self.when_pw_change) t['when_pw_expires'] = vn(self.when_pw_expires) t['must_change_pw'] = vn(self.must_change_pw) + t['admincount'] = self.admincount t['canLogon'] = vn(self.canLogon) return t @@ -256,6 +264,7 @@ t += 'when_pw_change: %s\n' % self.when_pw_change t += 'when_pw_expires: %s\n' % self.when_pw_expires t += 'must_change_pw: %s\n' % self.must_change_pw + t += 'admincount: %s\n' % self.admincount t += 'canLogon: %s\n' % self.canLogon return t diff --git a/msldap/network/multiplexor.py b/msldap/network/multiplexor.py new file mode 100644 index 0000000..b3730e8 --- /dev/null +++ b/msldap/network/multiplexor.py @@ -0,0 +1,81 @@ +import enum +import asyncio +import ipaddress +import copy + +from asysocks.common.clienturl import SocksClientURL +from asysocks.common.constants import SocksServerVersion, SocksProtocol, SOCKS5Method +from asysocks.common.target import SocksTarget + +from msldap import logger +from msldap.network.socks import SocksProxyConnection +from msldap.commons.proxy import MSLDAPProxy, MSLDAPProxyType +from minikerberos.common.target import KerberosTarget +from minikerberos.common.proxy import KerberosProxy + + + +class MultiplexorProxyConnection: + """ + """ + def __init__(self, target): + self.target = target + + async def connect(self, is_kerberos = False): + """ + + """ + #hiding the import, so you'll only need to install multiplexor only when actually using it + from multiplexor.operator import MultiplexorOperator + + con_str = self.target.proxy.target.get_server_url() + #creating operator and connecting to multiplexor server + self.operator = MultiplexorOperator(con_str, logging_sink = logger) + await self.operator.connect() + #creating socks5 proxy + server_info = await self.operator.start_socks5(self.target.proxy.target.agent_id) + await self.operator.terminate() + #print(server_info) + if is_kerberos is False: + + #copying the original target, then feeding it to socks5proxy object. it will hold the actual socks5 proxy server address we created before + tp = MSLDAPProxy() + tp.target = SocksTarget() + tp.target.version = SocksServerVersion.SOCKS5 + tp.target.server_ip = server_info['listen_ip'] + tp.target.server_port = server_info['listen_port'] + tp.target.is_bind = False + tp.target.proto = SocksProtocol.TCP + tp.target.timeout = self.target.timeout + tp.target.buffer_size = 4096 + + tp.target.endpoint_ip = self.target.host + tp.target.endpoint_port = self.target.port + tp.target.endpoint_timeout = None # TODO: maybe implement endpoint timeout in the msldap target? + tp.type = MSLDAPProxyType.SOCKS5 + + newtarget = copy.deepcopy(self.target) + newtarget.proxy = tp + + + + return SocksProxyConnection(target = newtarget) + + else: + kt = copy.deepcopy(self.target) + kt.proxy = KerberosProxy() + kt.proxy.target = SocksTarget() + kt.proxy.target.version = SocksServerVersion.SOCKS5 + kt.proxy.target.server_ip = server_info['listen_ip'] + kt.proxy.target.server_port = server_info['listen_port'] + kt.proxy.target.is_bind = False + kt.proxy.target.proto = SocksProtocol.TCP + kt.proxy.target.timeout = 10 + kt.proxy.target.buffer_size = 4096 + + kt.proxy.target.endpoint_ip = self.target.ip + kt.proxy.target.endpoint_port = self.target.port + #kt.proxy.creds = copy.deepcopy(self.target.proxy.auth) + + return kt + diff --git a/msldap/network/selector.py b/msldap/network/selector.py index 8d40f48..9863937 100644 --- a/msldap/network/selector.py +++ b/msldap/network/selector.py @@ -1,24 +1,32 @@ +from msldap import logger from msldap.network.tcp import MSLDAPTCPNetwork from msldap.network.socks import SocksProxyConnection +from msldap.network.multiplexor import MultiplexorProxyConnection from msldap.commons.proxy import MSLDAPProxyType MSLDAP_SOCKS_PROXY_TYPES = [ - MSLDAPProxyType.SOCKS4 , - MSLDAPProxyType.SOCKS4_SSL , - MSLDAPProxyType.SOCKS5 , - MSLDAPProxyType.SOCKS5_SSL] + MSLDAPProxyType.SOCKS4, + MSLDAPProxyType.SOCKS4_SSL, + MSLDAPProxyType.SOCKS5, + MSLDAPProxyType.SOCKS5_SSL, + MSLDAPProxyType.WSNET, + MSLDAPProxyType.WSNETWS, + MSLDAPProxyType.WSNETWSS, +] class MSLDAPNetworkSelector: def __init__(self): pass @staticmethod - def select(target): + async def select(target): if target.proxy is not None: if target.proxy.type in MSLDAP_SOCKS_PROXY_TYPES: return SocksProxyConnection(target) else: - raise Exception('Multiplexor coming soon!') + mpc = MultiplexorProxyConnection(target) + socks_proxy = await mpc.connect() + return socks_proxy return MSLDAPTCPNetwork(target)diff --git a/msldap/network/socks.py b/msldap/network/socks.py index 0d70126..7c0f3bd 100644 --- a/msldap/network/socks.py +++ b/msldap/network/socks.py @@ -1,11 +1,3 @@ - -# -# -# -# -# -# - import enum import asyncio @@ -41,8 +33,19 @@ Disconnects from the socket. Stops the reader and writer streams. """ - self.proxy_task.cancel() - self.handle_in_task.cancel() + if self.client is not None: + await self.client.terminate() + if self.proxy_task is not None: + self.proxy_task.cancel() + if self.handle_in_q is not None: + self.handle_in_task.cancel() + + async def terminate(self): + await self.disconnect() + + def get_peer_certificate(self): + raise Exception('Not yet implemented! SSL implementation on socks is missing!') + return self.writer.get_extra_info('socket').getpeercert(True) def get_one_message(self,data): if len(data) < 6: @@ -83,8 +86,8 @@ data += temp continue - #except asyncio.CancelledError: - # return + except asyncio.CancelledError: + return except Exception as e: logger.exception('handle_in_q') await self.in_queue.put((None, e)) @@ -105,10 +108,11 @@ self.proxy_in_queue = asyncio.Queue() comms = SocksQueueComms(self.out_queue, self.proxy_in_queue) - self.target.proxy.target.endpoint_ip = self.target.host - self.target.proxy.target.endpoint_port = int(self.target.port) - - self.client = SOCKSClient(comms, self.target.proxy.target, self.target.proxy.auth) + self.target.proxy.target[-1].endpoint_ip = self.target.host if self.target.serverip is None else self.target.serverip + self.target.proxy.target[-1].endpoint_port = int(self.target.port) + self.target.proxy.target[-1].endpoint_timeout = None #TODO: maybe implement endpoint timeout? + self.target.proxy.target[-1].timeout = self.target.timeout + self.client = SOCKSClient(comms, self.target.proxy.target) self.proxy_task = asyncio.create_task(self.client.run()) self.handle_in_task = asyncio.create_task(self.handle_in_q()) return True, None diff --git a/msldap/network/tcp.py b/msldap/network/tcp.py index c933f69..b01fe8f 100644 --- a/msldap/network/tcp.py +++ b/msldap/network/tcp.py @@ -21,7 +21,9 @@ async def terminate(self): self.handle_in_task.cancel() self.handle_out_task.cancel() - + + def get_peer_certificate(self): + return self.writer.get_extra_info('ssl_object').getpeercert(True) async def handle_in_q(self): try: @@ -82,7 +84,11 @@ self.in_queue = asyncio.Queue() self.out_queue = asyncio.Queue() self.reader, self.writer = await asyncio.wait_for( - asyncio.open_connection(self.target.host, self.target.port, ssl=self.target.get_ssl_context()), + asyncio.open_connection( + self.target.serverip if self.target.serverip is not None else self.target.host, + self.target.port, + ssl=self.target.get_ssl_context() + ), timeout = self.target.timeout ) diff --git a/msldap/network/wsnet.py b/msldap/network/wsnet.py new file mode 100644 index 0000000..65095e4 --- /dev/null +++ b/msldap/network/wsnet.py @@ -0,0 +1,125 @@ + +# +# +# +# +# +# + + +import enum +import asyncio +import ipaddress + +from msldap import logger +from msldap.protocol.utils import calcualte_length + +from pyodidewsnet.client import WSNetworkTCP + + + +class WSNetProxyConnection: + """ + Generic asynchronous TCP socket class, nothing SMB related. + Creates the connection and channels incoming/outgoing bytes via asynchonous queues. + """ + def __init__(self, target): + self.target = target + + self.client = None + self.handle_in_task = None + + self.out_queue = None#asyncio.Queue() + self.in_queue = None#asyncio.Queue() + + self.proxy_in_queue = None#asyncio.Queue() + self.is_plain_msg = True + + async def disconnect(self): + """ + Disconnects from the socket. + Stops the reader and writer streams. + """ + if self.client is not None: + await self.client.terminate() + if self.handle_in_q is not None: + self.handle_in_task.cancel() + + async def terminate(self): + await self.disconnect() + + def get_peer_certificate(self): + raise Exception('Not yet implemented! SSL implementation on socks is missing!') + return self.writer.get_extra_info('socket').getpeercert(True) + + def get_one_message(self,data): + if len(data) < 6: + return None + + if self.is_plain_msg is True: + dl = calcualte_length(data[:6]) + else: + dl = int.from_bytes(data[:4], byteorder = 'big', signed = False) + dl = dl + 4 + + + #print(dl) + if len(data) >= dl: + return data[:dl] + + async def handle_in_q(self): + try: + data = b'' + while True: + while True: + msg_data = self.get_one_message(data) + if msg_data is None: + break + + await self.in_queue.put((msg_data, None)) + data = data[len(msg_data):] + + temp, err = await self.proxy_in_queue.get() + #print(temp) + if err is not None: + raise err + + if temp == b'' or temp is None: + logger.debug('Server finished!') + return + + data += temp + continue + + except asyncio.CancelledError: + return + except Exception as e: + logger.exception('handle_in_q') + await self.in_queue.put((None, e)) + + finally: + await self.client.terminate() + + + + async def run(self): + """ + + """ + try: + self.out_queue = asyncio.Queue() + self.in_queue = asyncio.Queue() + self.proxy_in_queue = asyncio.Queue() + + self.client = WSNetworkTCP(self.target.host, int(self.target.port), self.proxy_in_queue, self.out_queue) + _, err = await self.client.run() + if err is not None: + raise err + + self.handle_in_task = asyncio.create_task(self.handle_in_q()) + + return True, None + + except Exception as e: + return False, e + diff --git a/msldap/protocol/ldap_filter/filter.py b/msldap/protocol/ldap_filter/filter.py index 199322f..1f1ee32 100644 --- a/msldap/protocol/ldap_filter/filter.py +++ b/msldap/protocol/ldap_filter/filter.py @@ -1,7 +1,7 @@ import re import platform -import msldap.protocol.ldap_filter.parser as parser +from msldap.protocol.ldap_filter import parser from msldap.protocol.ldap_filter.soundex import soundex_compare diff --git a/msldap/protocol/ldap_filter/parser.py b/msldap/protocol/ldap_filter/parser.py index 12b38f7..af715af 100644 --- a/msldap/protocol/ldap_filter/parser.py +++ b/msldap/protocol/ldap_filter/parser.py @@ -142,7 +142,7 @@ _input_size = None _actions = None - REGEX_1 = re.compile('^[^!*\\x29]') + REGEX_1 = re.compile('^[^*\\x29]') #re.compile('^[^!*\\x29]') REGEX_2 = re.compile('^[a-fA-F0-9]') REGEX_3 = re.compile('^[\\x20]') REGEX_4 = re.compile('^[\\x09]') diff --git a/msldap/protocol/messages.py b/msldap/protocol/messages.py index e898649..b8ec97c 100644 --- a/msldap/protocol/messages.py +++ b/msldap/protocol/messages.py @@ -50,7 +50,7 @@ class resultCode(core.Enumerated): _map = { - 0 : 'success', + 0 : 'success', 1 : 'operationsError', 2 : 'protocolError', 3 : 'timeLimitExceeded', @@ -91,7 +91,7 @@ 80 : 'other', } -class changeoperation(core.Enumerated): +class ChangeOperation(core.Enumerated): _map = { 0 : 'add', 1 : 'delete', @@ -288,7 +288,7 @@ class Change(core.Sequence): _fields = [ - ('operation', changeoperation), + ('operation', ChangeOperation), ('modification', PartialAttribute), ] diff --git a/msldap/protocol/query.py b/msldap/protocol/query.py index f2342ac..dac50a1 100644 --- a/msldap/protocol/query.py +++ b/msldap/protocol/query.py @@ -7,12 +7,8 @@ def equality(attr, value): - #print(attr) - #print(value) if attr[-1] == ':': - #possible OID name, oid_raw = attr[:-1].split(':') - #print(oid_raw) return Filter({ 'extensibleMatch' : MatchingRuleAssertion({ 'matchingRule' : oid_raw.encode(), @@ -61,9 +57,6 @@ def query_syntax_converter_inner(ftr): - #print(ftr.__dict__) - #print(ftr.comp) - #print(ftr.type) if ftr.type == 'filter': if ftr.comp == '=': return equality(ftr.attr, ftr.val) diff --git a/msldap/protocol/typeconversion.py b/msldap/protocol/typeconversion.py index d260f27..4a55338 100644 --- a/msldap/protocol/typeconversion.py +++ b/msldap/protocol/typeconversion.py @@ -5,6 +5,7 @@ from winacl.dtyp.guid import GUID from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR from msldap import logger +from msldap.protocol.messages import Attribute, Change, PartialAttribute MSLDAP_DT_WIN_EPOCH = datetime.datetime(1601, 1, 1) @@ -62,17 +63,35 @@ def list_str(x): return [e.decode() for e in x ] +def list_str_enc(x): + return [e.encode() for e in x ] + def list_int(x): return [int(e) for e in x ] +def list_int_enc(x): + return [str(e).encode() for e in x ] + def list_int_one(x): return int(x[0]) +def list_int_one_enc(x): + return [str(x[0]).encode()] + def list_str_one(x): return x[0].decode() +def list_str_one_enc(x): + return [x[0].encode()] + +def list_str_one_utf16le_enc(x): + return [x[0].encode('utf-16-le')] + def list_bytes_one(x): return x[0] + +def list_bytes_one_enc(x): + return x def int2timedelta(x): x = int(x[0]) @@ -147,6 +166,7 @@ t.append(ts2dt((a, None))) return t + LDAP_ATTRIBUTE_TYPES = { 'supportedCapabilities' : list_str, 'serverName' : list_str_one, @@ -158,8 +178,12 @@ 'supportedControl' : list_str, 'rootDomainNamingContext' : list_str_one, 'configurationNamingContext' : list_str_one, + 'schemaIDGUID' : x2guid, + 'lDAPDisplayName' : list_str_one, 'schemaNamingContext' : list_str_one, 'defaultNamingContext' : list_str_one, + 'adminDescription' : list_str_one, + 'adminDisplayName' : list_str_one, 'namingContexts' : list_str, 'dsServiceName' : list_str_one, 'subschemaSubentry' : list_str_one, @@ -204,7 +228,7 @@ 'lockoutThreshold' : list_int_one, 'lockOutObservationWindow' : list_int_one, 'lockoutDuration' : list_int_one, - 'forceLogoff' : list_str_one, + 'forceLogoff' : int2timedelta, 'creationTime' : int2dt, 'maxPwdAge' : int2timedelta, 'pwdHistoryLength' : list_int_one, @@ -234,6 +258,7 @@ 'versionNumber' : list_int_one, 'gPCFunctionalityVersion' : list_int_one, 'gPCMachineExtensionNames' : list_str, + 'gPCUserExtensionNames' : list_str, 'groupType' : list_int_one, 'member' : list_str, 'adminCount' : list_int_one, @@ -247,8 +272,44 @@ 'trustPartner' : list_str_one, 'securityIdentifier' : list_bytes_one, 'versionNumber' : list_int_one, - + 'unicodePwd' : list_str_one, + 'ms-Mcs-AdmPwd' : list_str_one, + 'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one, } + +LDAP_ATTRIBUTE_TYPES_ENC = { + 'objectClass' : list_str_enc, + 'sn' : list_str_one_enc, + 'gidNumber' : list_int_one_enc, + 'unicodePwd' : list_str_one_utf16le_enc, + 'lockoutTime' : list_int_one_enc, + 'sAMAccountName' : list_str_one_enc, + 'userAccountControl' : list_int_one_enc, + 'displayName' : list_str_one_enc, + 'userPrincipalName' : list_str_one_enc, + 'servicePrincipalName' : list_str_enc, + 'msds-additionaldnshostname' : list_str_enc, + 'gPCMachineExtensionNames' : list_str_enc, + 'gPCUserExtensionNames' : list_str_enc, + 'versionNumber' : list_int_one_enc, + 'member' : list_str_enc, + 'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one_enc, + 'nTSecurityDescriptor' : list_bytes_one_enc, +} + +def encode_attributes(x): + """converts a dict to attributelist""" + res = [] + for k in x: + if k not in LDAP_ATTRIBUTE_TYPES_ENC: + raise Exception('Unknown conversion type for key "%s"' % k) + + res.append(Attribute({ + 'type' : k.encode(), + 'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](x[k]) + })) + + return res def convert_attributes(x): t = {} @@ -270,4 +331,21 @@ return { 'objectName' : x['objectName'].decode(), 'attributes' : convert_attributes(x['attributes']) - } \ No newline at end of file + } + + +def encode_changes(x): + res = [] + for k in x: + if k not in LDAP_ATTRIBUTE_TYPES_ENC: + raise Exception('Unknown conversion type for key "%s"' % k) + + for mod, value in x[k]: + res.append(Change({ + 'operation' : mod, + 'modification' : PartialAttribute({ + 'type' : k.encode(), + 'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](value) + }) + })) + return res \ No newline at end of file diff --git a/msldap.egg-info/PKG-INFO b/msldap.egg-info/PKG-INFO index 5cce983..49648ad 100644 --- a/msldap.egg-info/PKG-INFO +++ b/msldap.egg-info/PKG-INFO @@ -1,14 +1,15 @@ Metadata-Version: 1.2 Name: msldap -Version: 0.2.10 +Version: 0.3.30 Summary: Python library to play with MS LDAP Home-page: https://github.com/skelsec/msldap Author: Tamas Jos -Author-email: info@skelsec.com +Author-email: info@skelsecprojects.com License: UNKNOWN Description: Python library to play with MS LDAP Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent -Requires-Python: >=3.6 +Requires-Python: >=3.7 diff --git a/msldap.egg-info/SOURCES.txt b/msldap.egg-info/SOURCES.txt index decca53..888af08 100644 --- a/msldap.egg-info/SOURCES.txt +++ b/msldap.egg-info/SOURCES.txt @@ -11,19 +11,24 @@ msldap.egg-info/SOURCES.txt msldap.egg-info/dependency_links.txt msldap.egg-info/entry_points.txt +msldap.egg-info/not-zip-safe msldap.egg-info/requires.txt msldap.egg-info/top_level.txt -msldap.egg-info/zip-safe msldap/authentication/__init__.py msldap/authentication/kerberos/__init__.py +msldap/authentication/kerberos/gssapi.py msldap/authentication/kerberos/multiplexor.py msldap/authentication/kerberos/native.py msldap/authentication/kerberos/sspi.py +msldap/authentication/kerberos/sspiproxyws.py +msldap/authentication/kerberos/wsnet.py msldap/authentication/ntlm/__init__.py msldap/authentication/ntlm/creds_calc.py msldap/authentication/ntlm/multiplexor.py msldap/authentication/ntlm/native.py msldap/authentication/ntlm/sspi.py +msldap/authentication/ntlm/sspiproxy.py +msldap/authentication/ntlm/wsnet.py msldap/authentication/ntlm/messages/__init__.py msldap/authentication/ntlm/messages/authenticate.py msldap/authentication/ntlm/messages/challenge.py @@ -45,7 +50,9 @@ msldap/authentication/spnego/sspi.py msldap/commons/__init__.py msldap/commons/authbuilder.py +msldap/commons/common.py msldap/commons/credential.py +msldap/commons/exceptions.py msldap/commons/proxy.py msldap/commons/target.py msldap/commons/url.py @@ -53,6 +60,7 @@ msldap/crypto/AES.py msldap/crypto/BASE.py msldap/crypto/DES.py +msldap/crypto/MD4.py msldap/crypto/RC4.py msldap/crypto/TDES.py msldap/crypto/__init__.py @@ -69,20 +77,36 @@ msldap/crypto/pure/RC4/__init__.py msldap/examples/__init__.py msldap/examples/msldapclient.py +msldap/examples/msldapcompdnslist.py +msldap/external/__init__.py +msldap/external/aiocmd/__init__.py +msldap/external/aiocmd/setup.py +msldap/external/aiocmd/aiocmd/__init__.py +msldap/external/aiocmd/aiocmd/aiocmd.py +msldap/external/aiocmd/aiocmd/nested_completer.py +msldap/external/asciitree/__init__.py +msldap/external/asciitree/setup.py +msldap/external/asciitree/asciitree/__init__.py +msldap/external/asciitree/asciitree/drawing.py +msldap/external/asciitree/asciitree/traversal.py +msldap/external/asciitree/asciitree/util.py msldap/ldap_objects/__init__.py msldap/ldap_objects/adcomp.py msldap/ldap_objects/adgpo.py msldap/ldap_objects/adgroup.py msldap/ldap_objects/adinfo.py msldap/ldap_objects/adou.py +msldap/ldap_objects/adschemaentry.py msldap/ldap_objects/adsec.py msldap/ldap_objects/adtrust.py msldap/ldap_objects/aduser.py msldap/ldap_objects/common.py msldap/network/__init__.py +msldap/network/multiplexor.py msldap/network/selector.py msldap/network/socks.py msldap/network/tcp.py +msldap/network/wsnet.py msldap/network/proxy/__init__.py msldap/network/proxy/handler.py msldap/protocol/__init__.py diff --git a/msldap.egg-info/entry_points.txt b/msldap.egg-info/entry_points.txt index dd0bea5..6ac49b7 100644 --- a/msldap.egg-info/entry_points.txt +++ b/msldap.egg-info/entry_points.txt @@ -1,3 +1,4 @@ [console_scripts] msldap = msldap.examples.msldapclient:main +msldapcompdns = msldap.examples.msldapcompdnslist:main diff --git a/msldap.egg-info/not-zip-safe b/msldap.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/msldap.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/msldap.egg-info/requires.txt b/msldap.egg-info/requires.txt index d815664..e0ff19c 100644 --- a/msldap.egg-info/requires.txt +++ b/msldap.egg-info/requires.txt @@ -1,8 +1,9 @@ asn1crypto -aiocmd -asciitree -asysocks -winacl>=0.0.2 +asysocks>=0.1.1 +minikerberos>=0.2.14 +prompt-toolkit>=3.0.2 +tqdm +winacl>=0.1.1 [:platform_system == "Windows"] -winsspi +winsspi>=0.0.9 diff --git a/msldap.egg-info/zip-safe b/msldap.egg-info/zip-safe deleted file mode 100644 index 8b13789..0000000 --- a/msldap.egg-info/zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/setup.py b/setup.py index ba53799..022c48b 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ # Application author details: author="Tamas Jos", - author_email="info@skelsec.com", + author_email="info@skelsecprojects.com", # Packages packages=find_packages(), @@ -32,31 +32,33 @@ # Details url="https://github.com/skelsec/msldap", - zip_safe = True, + zip_safe = False, # # license="LICENSE.txt", description="Python library to play with MS LDAP", long_description="Python library to play with MS LDAP", # long_description=open("README.txt").read(), - python_requires='>=3.6', + python_requires='>=3.7', classifiers=( - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ), install_requires=[ 'asn1crypto', - 'winsspi;platform_system=="Windows"', - 'aiocmd', - 'asciitree', - #'ldap_filter', - 'asysocks', - 'winacl>=0.0.2' + 'winsspi>=0.0.9;platform_system=="Windows"', + 'minikerberos>=0.2.14', + 'asysocks>=0.1.1', + 'winacl>=0.1.1', + 'prompt-toolkit>=3.0.2', + 'tqdm', ], entry_points={ 'console_scripts': [ 'msldap = msldap.examples.msldapclient:main', + 'msldapcompdns = msldap.examples.msldapcompdnslist:main', ], } )