From b6ef4ba8fa761dcf52262e37b63c2f913455ae33 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Wed, 18 Mar 2026 08:39:05 +0100 Subject: [PATCH 01/23] work on aes --- aiowmi/connection.py | 33 +++++---- aiowmi/kerberos/ap_req.py | 144 ++++++++++++++++++++++-------------- aiowmi/kerberos/as_req.py | 16 ++-- aiowmi/kerberos/const.py | 2 + aiowmi/kerberos/gss.py | 60 +++++++++++++++ aiowmi/kerberos/krb5_pdu.py | 36 ++++++--- aiowmi/kerberos/tgs.py | 7 +- aiowmi/kerberos/tgt.py | 9 ++- aiowmi/kerberos/tools.py | 126 ++++++++++++++----------------- aiowmi/kerberos/wrappers.py | 83 +++++++++++++++++++++ 10 files changed, 352 insertions(+), 164 deletions(-) create mode 100644 aiowmi/kerberos/wrappers.py diff --git a/aiowmi/connection.py b/aiowmi/connection.py index e057080..6fdcd6f 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -7,8 +7,9 @@ from .kerberos.krb5_pdu import get_neg_token, build_alter_context from .kerberos.tgs import get_tgs from .kerberos.tgt import get_tgt -from .kerberos.tools import gss_unwrap_kerberos -from .kerberos.tools import sign_func_kerberos, seal_func_kerberos +from .kerberos.tools import get_etype_from_ticket +from .kerberos.wrappers import sign_func_kerberos, seal_func_kerberos +from .kerberos.wrappers import gss_unwrap_kerberos from .ntlm.auth_authenticate import NTLMAuthAuthenticate from .ntlm.auth_challange import NTLMAuthChallenge from .ntlm.auth_negotiate import NTLMAuthNegotiate @@ -206,7 +207,7 @@ async def _negotiate_kerberos(self): self._kerberos_cache.write(self._tgt, self._tgs, logger) - async def negotiate_kerberos(self, max_retry: int = 3) -> Protocol: + async def negotiate_kerberos(self, max_retry: int = 1) -> Protocol: if not self._domain: raise Exception('domain is required for Kerberos authentication') attempt = 1 @@ -220,7 +221,7 @@ async def negotiate_kerberos(self, max_retry: int = 3) -> Protocol: proto._context_id = 0x0001357f if not has_keys: - logger.info('Start Kerberos negotion for TGT/TGS') + logger.info('Start Kerberos negotiation for TGT/TGS') await self._negotiate_kerberos() has_keys = True else: @@ -248,10 +249,13 @@ async def negotiate_kerberos(self, max_retry: int = 3) -> Protocol: async def _bind_kerberos(self, iid: bytes, proto: Protocol): ticket, service_session_key = self._tgs + etype = get_etype_from_ticket(ticket) ap_req = build_ap_req(self._username, self._domain, - ticket, service_session_key) - auth_value = wrap_gss_kerberos(ap_req) + ticket, + service_session_key, + etype) + auth_value = wrap_gss_kerberos(ap_req, etype) bind_pkg = proto._dcom.get_bind_kerberos_pkg( iid, auth_value, @@ -260,13 +264,14 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): ) rpc_bind_ack = await proto.get_dcom_response(bind_pkg) active_key, seq_number = get_active_key(rpc_bind_ack.auth.auth_value, - service_session_key) + service_session_key, + etype) if active_key is None: raise NoNewActiveKey() proto._auth_type = rpc_bind_ack.auth.auth_type proto._auth_level = rpc_bind_ack.auth.auth_level - neg_token = get_neg_token(service_session_key, seq_number) + neg_token = get_neg_token(service_session_key, seq_number, etype) alter_context_pkg = build_alter_context( iid, proto._dcom.get_new_call_id(), @@ -275,11 +280,10 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): neg_token, ) _ = await proto.get_dcom_response(alter_context_pkg) - if proto._auth_level >= RPC_C_AUTHN_LEVEL_PKT_INTEGRITY: - proto._client_sign = sign_func_kerberos(active_key) - proto._client_seal = seal_func_kerberos(active_key) - proto._server_seal = gss_unwrap_kerberos(active_key) + proto._client_sign = sign_func_kerberos(active_key, etype) + proto._client_seal = seal_func_kerberos(active_key, etype) + proto._server_seal = gss_unwrap_kerberos(active_key, etype) async def negotiate_ntlm(self) -> Protocol: proto = self._protocol @@ -304,7 +308,7 @@ async def login_ntlm( request.set_pdu_data(ntlm_login_pkg) request_pkg = request.sign_data(proto) - + print(request_pkg) rpc_response: RpcResponse = \ await proto.get_dcom_response( request_pkg, @@ -329,6 +333,7 @@ async def _if_binding(self, IID_IWbemLevel1Login) rci_pkg = remote_create_instance.get_data() + context_id = proto._context_id request = RpcRequest(op_num=4) request.set_pdu_data(rci_pkg) @@ -376,6 +381,6 @@ async def _if_binding(self, proto._auth_level = max( interface.scm_reply_info_data.authn_hint, m_auth_level) - + proto._context_id = context_id await bind_func(IID_IWbemLevel1Login, proto) return proto diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index 0a4bfa2..9245035 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -2,34 +2,43 @@ from datetime import datetime, timezone from .asn1 import asn1_len, asn1_tag, asn1_gt, asn1_int, asn1_ostr from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4 -from .const import OID_KERBEROS_V5, OID_MS_LEGACY_KRB, OID_SPNEGO +from .tools import decrypt_kerberos_aes_cts, encrypt_kerberos_aes_cts +from .const import OID_SPNEGO, OID_IETF_KRB5, OID_MS_KRB5 -def wrap_gss_kerberos(ap_req_bytes): - # MechTypes [0] - mech_types = asn1_tag(0, b'\x30\x0b' + OID_MS_LEGACY_KRB) +def wrap_gss_kerberos(ap_req_bytes: bytes, etype: int): + if etype == 18: + outer_oid = OID_IETF_KRB5 + inner_oid = OID_MS_KRB5 + elif etype == 17: + outer_oid = OID_IETF_KRB5 + inner_oid = OID_MS_KRB5 + elif etype == 23: + outer_oid = OID_MS_KRB5 + inner_oid = OID_MS_KRB5 + else: + raise ValueError(f"Invalid E-type: {etype}") - # MechToken [2] -> GSS-API Token wrapper (0x60) - inner_token = OID_KERBEROS_V5 + b'\x01\x00' + ap_req_bytes - gss_token = b'\x60' + asn1_len(inner_token) + inner_token + mech_types_inner = b'\x30\x0b' + outer_oid + mech_types = asn1_tag(0, mech_types_inner) - # OctetString wrapper (0x04) MechToken [2] - mech_token = asn1_tag(2, asn1_ostr(gss_token)) + inner_gss_content = inner_oid + b'\x01\x00' + ap_req_bytes + gss_wrapper = b'\x60' + asn1_len(inner_gss_content) + inner_gss_content - # NegTokenInit Sequence [30] -> Wrapper [0] - neg_token_body = mech_types + mech_token - neg_token_seq = b'\x30' + asn1_len(neg_token_body) + neg_token_body - neg_token_init = asn1_tag(0, neg_token_seq) + mech_token = asn1_tag(2, asn1_ostr(gss_wrapper)) - # Application 0 (0x60) wrapper - final_body = OID_SPNEGO + neg_token_init - return b'\x60' + asn1_len(final_body) + final_body + neg_body = mech_types + mech_token + neg_init = asn1_tag(0, b'\x30' + asn1_len(neg_body) + neg_body) + + final_payload = OID_SPNEGO + neg_init + return b'\x60' + asn1_len(final_payload) + final_payload def build_ap_req(username: str, domain: str, ticket: bytes, - service_session_key: bytes) -> bytes: + service_session_key: bytes, + etype: int) -> bytes: now = datetime.now(timezone.utc) timestamp = now.strftime('%Y%m%d%H%M%SZ').encode('ascii') @@ -68,10 +77,22 @@ def build_ap_req(username: str, # Encrypt authenticator auth_inner = b'\x30' + asn1_len(auth_body) + auth_body auth_asn1 = b'\x62' + asn1_len(auth_inner) + auth_inner - enc_auth = encrypt_kerberos_rc4(service_session_key, 11, auth_asn1) - etype_asn1 = asn1_tag(0, b'\x02\x01\x17') # RC4-HMAC (23) + if etype == 18: # AES-256 + enc_auth = encrypt_kerberos_aes_cts(service_session_key, 11, auth_asn1) + etype_byte = b'\x12' + elif etype == 17: # AES-128 + enc_auth = encrypt_kerberos_aes_cts(service_session_key, 11, auth_asn1) + etype_byte = b'\x11' + elif etype == 23: # RC4 + enc_auth = encrypt_kerberos_rc4(service_session_key, 11, auth_asn1) + etype_byte = b'\x17' + else: + raise ValueError(f"Invalid E-type: {etype}") + + etype_asn1 = asn1_tag(0, b'\x02\x01' + etype_byte) cipher_asn1 = asn1_tag(2, asn1_ostr(enc_auth)) + enc_body = etype_asn1 + cipher_asn1 enc_part = asn1_tag(4, b'\x30' + asn1_len(enc_body) + enc_body) @@ -88,44 +109,59 @@ def build_ap_req(username: str, def get_active_key(auth_bytes: bytes, - service_session_key: bytes) -> tuple[Optional[bytes], int]: + service_session_key: bytes, + etype: int) -> tuple[Optional[bytes], int]: active_key, seq_number = None, 0 - - etype_idx = auth_bytes.find(b'\x02\x01\x17') - if etype_idx >= 0: - # Read new session key... - idx_a2 = auth_bytes.find(b'\xa2', etype_idx) - idx_04 = auth_bytes.find(b'\x04', idx_a2) - - pos = idx_04 + 1 - length_byte = auth_bytes[pos] - pos += 1 - if length_byte & 0x80: - n = length_byte & 0x7f - length = int.from_bytes(auth_bytes[pos: pos + n], 'big') - pos += n - else: - length = length_byte - - cipher_blob = auth_bytes[pos: pos + length] - + etype_idx = auth_bytes.find(bytes([0x02, 0x01, etype])) + if etype_idx == -1: + return None, 0 + + # Read new session key... + idx_a2 = auth_bytes.find(b'\xa2', etype_idx) + idx_04 = auth_bytes.find(b'\x04', idx_a2) + if idx_04 == -1: + return None, 0 + + pos = idx_04 + 1 + length_byte = auth_bytes[pos] + pos += 1 + if length_byte & 0x80: + n = length_byte & 0x7f + length = int.from_bytes(auth_bytes[pos: pos + n], 'big') + pos += n + else: + length = length_byte + + cipher_blob = auth_bytes[pos: pos + length] + + if etype == 0x17: decrypted_raw = \ decrypt_kerberos_rc4(service_session_key, 12, cipher_blob) asn1_data = decrypted_raw[8:] # Skip 8 bytes confounder - - KEY_MARKER = b'\xa1\x12\x04\x10' - key_idx = asn1_data.find(KEY_MARKER) - if key_idx != -1: - active_key = asn1_data[key_idx + 4: key_idx + 4 + 16] - - SEQ_MARKER = b'\xa3' - seq_idx = asn1_data.find(SEQ_MARKER) - if seq_idx != -1: - int_tag_idx = asn1_data.find(b'\x02', seq_idx) - if int_tag_idx != -1 and int_tag_idx < seq_idx + 4: - int_len = asn1_data[int_tag_idx + 1] - offset = int_tag_idx + 2 - seq_bytes = asn1_data[offset: offset + int_len] - seq_number = int.from_bytes(seq_bytes, 'big') + elif etype in [0x11, 0x12]: + decrypted_raw = \ + decrypt_kerberos_aes_cts(service_session_key, 12, cipher_blob) + asn1_data = decrypted_raw[16:] # Skip 16 bytes confounder + + if etype == 0x12: # AES256 + KEY_MARKER = b'\x04\x20' + k_len = 32 + else: # AES128 / RC4 + KEY_MARKER = b'\x04\x10' + k_len = 16 + + key_idx = asn1_data.find(KEY_MARKER) + if key_idx != -1: + active_key = asn1_data[key_idx + 2: key_idx + 2 + k_len] + + SEQ_MARKER = b'\xa3' + seq_idx = asn1_data.find(SEQ_MARKER) + if seq_idx != -1: + int_tag_idx = asn1_data.find(b'\x02', seq_idx) + if int_tag_idx != -1 and int_tag_idx < seq_idx + 4: + int_len = asn1_data[int_tag_idx + 1] + offset = int_tag_idx + 2 + seq_bytes = asn1_data[offset: offset + int_len] + seq_number = int.from_bytes(seq_bytes, 'big') return active_key, seq_number diff --git a/aiowmi/kerberos/as_req.py b/aiowmi/kerberos/as_req.py index b27e0ca..d8b2743 100644 --- a/aiowmi/kerberos/as_req.py +++ b/aiowmi/kerberos/as_req.py @@ -61,6 +61,9 @@ def build_as_req(username: str, domain: str, pa_enc: bytes = b'') -> bytes: # Renewable time: 1 day (must be >= till) rtime_time = (now + timedelta(days=1)).strftime("%Y%m%d%H%M%SZ").encode() + # Etype list: RC4 (23), AES128 (16), AES256 (18) + etypes = asn1_int(18) + # kdc-options: Forwardable, Proxiable, Renewable, Canonicalize kdc_options = b'\x03\x05\x00\x50\x80\x00\x00' req_body_fields = ( @@ -72,8 +75,7 @@ def build_as_req(username: str, domain: str, pa_enc: bytes = b'') -> bytes: asn1_tag(5, asn1_gt(till_time)) + asn1_tag(6, asn1_gt(rtime_time)) + asn1_tag(7, asn1_int(nonce)) + - # etype: aes256-cts-hmac-sha1-96 (18) - asn1_tag(8, asn1_seq(asn1_int(18))) + asn1_tag(8, asn1_seq(etypes)) ) req_body_sequence = asn1_seq(req_body_fields) @@ -91,7 +93,7 @@ def build_as_req(username: str, domain: str, pa_enc: bytes = b'') -> bytes: return final_as_req -def build_full_as_req(username: str, domain: str, base_key: bytes): +def build_full_as_req(username: str, domain: str, base_key: bytes, etype: int): now = datetime.now(timezone.utc) ts_str = now.strftime("%Y%m%d%H%M%SZ").encode() original_plain_body = ( @@ -105,13 +107,14 @@ def build_full_as_req(username: str, domain: str, base_key: bytes): ke = derive_key(base_key, 1, 0xAA) ki = derive_key(base_key, 1, 0x55) + signature = hmac.new(ki, full_plain, hashlib.sha1).digest()[:12] cipher_only = aes_cts_encrypt(ke, full_plain) final_payload = cipher_only + signature enc_data_content = ( - asn1_tag(0, asn1_int(18)) + # etype 18 (AES-256) + asn1_tag(0, asn1_int(etype)) + # etype 17 (AES128) or 18 (AES256) asn1_tag(2, asn1_ostr(final_payload)) ) @@ -144,6 +147,9 @@ def build_full_as_req(username: str, domain: str, base_key: bytes): asn1_tag(1, asn1_seq(asn1_gs(b"krbtgt") + asn1_gs(domcaps.encode()))) ) + # Etype list: RC4 (23), AES128 (16), AES256 (18) + etypes = asn1_int(18) + req_body_content = ( asn1_tag(0, b'\x03\x05\x00\x50\x80\x00\x00') + # kdc-options asn1_tag(1, asn1_seq(cname_content)) + @@ -152,7 +158,7 @@ def build_full_as_req(username: str, domain: str, base_key: bytes): asn1_tag(5, asn1_gt(till)) + asn1_tag(6, asn1_gt(till)) + asn1_tag(7, asn1_int(nonce)) + - asn1_tag(8, asn1_seq(asn1_int(18))) # etype list + asn1_tag(8, asn1_seq(etypes)) ) as_req_content = ( diff --git a/aiowmi/kerberos/const.py b/aiowmi/kerberos/const.py index 6702e1d..75f0064 100644 --- a/aiowmi/kerberos/const.py +++ b/aiowmi/kerberos/const.py @@ -1,3 +1,5 @@ OID_KERBEROS_V5 = b'\x06\t\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' OID_MS_LEGACY_KRB = b'\x06\t\x2a\x86\x48\x82\xf7\x12\x01\x02\x02' OID_SPNEGO = b'\x06\x06\x2b\x06\x01\x05\x05\x02' +OID_IETF_KRB5 = b'\x06\x09\x2a\x86\x48\x82\xf7\x12\x01\x02\x02' +OID_MS_KRB5 = b'\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' diff --git a/aiowmi/kerberos/gss.py b/aiowmi/kerberos/gss.py index 17521ac..1b1eaa8 100644 --- a/aiowmi/kerberos/gss.py +++ b/aiowmi/kerberos/gss.py @@ -2,6 +2,7 @@ from Crypto.Hash import HMAC, MD5 from Crypto.Cipher import ARC4 from ..tools import get_random_bytes +from .tools import encrypt_kerberos_aes_cts, decrypt_kerberos_aes_cts GSS_WRAP_HEADER = b'\x60\x2b\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' @@ -93,3 +94,62 @@ def gss_unwrap_rc4(session_key: bytes, raise Exception(f"Integrity Check Failed! Server Seq was: {svr_seq}") return data + + +def gss_wrap_aes(session_key: bytes, data: bytes, seq_num: int): + pad = (16 - (len(data) % 16)) & 15 + pad_str = b'\xFF' * pad + + header_for_hash = ( + b'\x05\x04\x06\xff\x00\x00\x00\x00' + + struct.pack('>Q', seq_num) + ) + + plaintext = data + pad_str + header_for_hash + raw_cipher = encrypt_kerberos_aes_cts(session_key, 24, plaintext) + + def rotate(data, numBytes): + numBytes %= len(data) + left = len(data) - numBytes + return data[left:] + data[:left] + + rrc = 28 + total_rotate = rrc + pad + cipher_rotated = rotate(raw_cipher, total_rotate) + + wire_header = ( + b'\x05\x04\x06\xff' + + struct.pack('>H', pad) + + struct.pack('>H', rrc) + + struct.pack('>Q', seq_num) + ) + split_offset = 16 + rrc + pad + + ret2 = wire_header + cipher_rotated[:split_offset] + ret1 = cipher_rotated[split_offset:] + + return ret1, ret2 + + +def gss_unwrap_aes(session_key: bytes, cipher_text: bytes, auth_data: bytes): + header = auth_data[:16] + pad = struct.unpack('>H', header[4:6])[0] # EC + rrc = struct.unpack('>H', header[6:8])[0] # RRC + + cipher_from_trailer = auth_data[16:] + + rotated_blob = cipher_from_trailer + cipher_text + + def unrotate(data, n): + if not data: + return data + n %= len(data) + return data[n:] + data[:n] + + full_cipher_blob = unrotate(rotated_blob, rrc + pad) + + decrypted_payload = \ + decrypt_kerberos_aes_cts(session_key, 22, full_cipher_blob) + actual_data = decrypted_payload[16: -(pad + 16)] + + return actual_data diff --git a/aiowmi/kerberos/krb5_pdu.py b/aiowmi/kerberos/krb5_pdu.py index 6c42258..a364de1 100644 --- a/aiowmi/kerberos/krb5_pdu.py +++ b/aiowmi/kerberos/krb5_pdu.py @@ -1,12 +1,12 @@ import datetime import struct from .asn1 import asn1_len, asn1_seq, asn1_tag, asn1_int, asn1_gt, asn1_ostr -from .tools import encrypt_kerberos_rc4 +from .tools import encrypt_kerberos_rc4, encrypt_kerberos_aes_cts from ..dcom_const import NDR_TransferSyntaxIdentifier from ..rpc.const import RPC_C_AUTHN_GSS_NEGOTIATE -def get_neg_token(service_session_key: bytes, seq_number: int): +def get_neg_token(service_session_key: bytes, seq_number: int, etype: int): now = datetime.datetime.now(datetime.timezone.utc) timestamp = now.strftime('%Y%m%d%H%M%SZ').encode('ascii') cusec = now.microsecond @@ -14,26 +14,38 @@ def get_neg_token(service_session_key: bytes, seq_number: int): enc_ap_rep_body = ( asn1_tag(0, asn1_gt(timestamp)) + # [0] ctime asn1_tag(1, asn1_int(cusec)) + # [1] cusec - asn1_tag(3, asn1_int(seq_number)) + asn1_tag(3, asn1_int(seq_number)) # [3] seq-number ) - inner_seq = asn1_seq(enc_ap_rep_body) - enc_ap_rep = b'\x7b' + asn1_len(inner_seq) + inner_seq - encrypted_data = encrypt_kerberos_rc4(service_session_key, 12, enc_ap_rep) + inner_seq = b'\x30' + asn1_len(enc_ap_rep_body) + enc_ap_rep_body + plaintext = b'\x7b' + asn1_len(inner_seq) + inner_seq + + if etype in [17, 18]: # AES-128 / AES-256 + enc_data = encrypt_kerberos_aes_cts(service_session_key, 12, plaintext) + current_etype = etype + else: # RC4 (23) + enc_data = encrypt_kerberos_rc4(service_session_key, 12, plaintext) + current_etype = 23 + enc_part_content = ( - asn1_tag(0, asn1_int(23)) + # etype 23 (RC4) - asn1_tag(2, asn1_ostr(encrypted_data)) # cipher + asn1_tag(0, asn1_int(current_etype)) + + asn1_tag(2, asn1_ostr(enc_data)) ) ap_rep_body = ( asn1_tag(0, asn1_int(5)) + # pvno asn1_tag(1, asn1_int(15)) + # msg-type 15 (AP-REP) - asn1_tag(2, asn1_seq(enc_part_content)) # enc-part + asn1_tag(2, b'\x30' + asn1_len(enc_part_content) + enc_part_content) ) - ap_rep = b'\x6f' + asn1_len(asn1_seq(ap_rep_body)) + asn1_seq(ap_rep_body) - response_token = asn1_tag(2, asn1_ostr(ap_rep)) - auth_neg_token = asn1_tag(1, asn1_seq(response_token)) + ap_rep_wrap = b'\x30' + asn1_len(ap_rep_body) + ap_rep_body + ap_rep_full = b'\x6f' + asn1_len(ap_rep_wrap) + ap_rep_wrap + + # MechToken [2] -> Octet String -> AP-REP + resp_token = asn1_tag(2, asn1_ostr(ap_rep_full)) + + # NegTokenResp [1] -> Sequence -> MechToken + auth_neg_token = asn1_tag(1, b'\x30' + asn1_len(resp_token) + resp_token) return auth_neg_token diff --git a/aiowmi/kerberos/tgs.py b/aiowmi/kerberos/tgs.py index 66bbcbd..cfb5043 100644 --- a/aiowmi/kerberos/tgs.py +++ b/aiowmi/kerberos/tgs.py @@ -160,8 +160,9 @@ def build_tgs_req(username: str, )) ) - # Etype list: RC4, AES128, DES, AES256 - etype_list = asn1_int(23) + asn1_int(16) + asn1_int(3) + asn1_int(18) + # Etype list: RC4 (23), AES128 (16), AES256 (18) + # etypes = asn1_int(23) + asn1_int(16) + asn1_int(18) + etypes = asn1_int(23) nonce = random.getrandbits(31) req_body_content = ( @@ -170,7 +171,7 @@ def build_tgs_req(username: str, asn1_tag(3, asn1_seq(sname_inner)) + # sname asn1_tag(5, asn1_gt(till_ts)) + # Till asn1_tag(7, asn1_int(nonce)) + # Nonce - asn1_tag(8, asn1_seq(etype_list)) # Etypes + asn1_tag(8, asn1_seq(etypes)) # Etypes ) req_body_field = asn1_tag(4, asn1_seq(req_body_content)) diff --git a/aiowmi/kerberos/tgt.py b/aiowmi/kerberos/tgt.py index d11afc1..5e1e7cd 100644 --- a/aiowmi/kerberos/tgt.py +++ b/aiowmi/kerberos/tgt.py @@ -46,7 +46,7 @@ def nfold_for_kerberos(): return b'\x6b\x65\x72\x62\x65\x72\x6f\x73\x7b\x9b\x5b\x2b\x93\x13\x2b\x93' -def aes_string_to_key(password, salt, key_len=32): +def aes_string_to_key(password: str, salt: str, key_len: int = 32): """ Kerberos String-to-Key for AES (RFC 3962) key_len should be 16 for AES-128 or 32 for AES-256 @@ -83,9 +83,10 @@ async def get_tgt(username: str, password: str, domain: str, resp = await send_kerberos_packet(as_req, kdc_host, kdc_port) parse_krb_error(resp) salt, etype = extract_salt_and_etype(resp) - key_len = 16 if etype == 17 else 32 - base_key = aes_string_to_key(password, salt, key_len) - full_as_req = build_full_as_req(username, domain, base_key) + if etype != 18: + raise ValueError('Only AES256 encryption supported for negotiation') + base_key = aes_string_to_key(password, salt) + full_as_req = build_full_as_req(username, domain, base_key, etype) as_res_bytes = await send_kerberos_packet(full_as_req, kdc_host, kdc_port) parse_krb_error(as_res_bytes) return as_res_bytes, base_key diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index eec04d7..175f7d4 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -4,12 +4,12 @@ import hmac import math import struct +from typing import Optional from functools import reduce from pyasn1.codec.der import decoder, encoder from Crypto.Cipher import AES from Crypto.Hash import HMAC, SHA1, MD5 from .rc4 import RC4 -from .gss import gss_wrap_rc4, gss_unwrap_rc4 from ..exceptions import KerberosErr from ..tools import get_random_bytes @@ -66,19 +66,22 @@ def lcm(a, b): return bytes(reduce(add_ones_complement, parts)) -def derive_key(base_key, usage_int, payload_byte=None): - constant = struct.pack('>I', usage_int) - if payload_byte is not None: - constant += bytes([payload_byte]) +def derive_key(base_key: bytes, usage: int, payload: int = None) -> bytes: + key_len = len(base_key) # 16 or 32 + + constant = struct.pack('>I', usage) + if payload is not None: + constant += bytes([payload]) nfolded = _nfold(constant, 16) - cipher1 = AES.new(base_key, AES.MODE_ECB) - b1 = cipher1.encrypt(nfolded) + cipher = AES.new(base_key, AES.MODE_ECB) + b1 = cipher.encrypt(nfolded) - cipher2 = AES.new(base_key, AES.MODE_ECB) - b2 = cipher2.encrypt(b1) + if key_len == 16: + return b1 + b2 = cipher.encrypt(b1) return b1 + b2 @@ -129,40 +132,35 @@ def decrypt_kerberos_aes_cts(key: bytes, usage: int, cipher: bytes) -> bytes: if n == block_size: # Simple one-block case plaintext_with_confounder = aes_ecb.decrypt(ciphertext) - elif n % block_size == 0: - # Standard CBC - cipher_cbc = AES.new(ke, AES.MODE_CBC, b'\x00' * block_size) - plaintext_with_confounder = cipher_cbc.decrypt(ciphertext) else: - # Manual CTS Decryption (NIST CS3) - m = (n // block_size) * block_size - last_len = n % block_size + # NIST CTS Decryption + block_size = 16 + n = len(ciphertext) + m = ((n - 1) // block_size) * block_size + + cipher_cbc = AES.new(ke, AES.MODE_CBC, b'\x00' * block_size) + p_start = b"" + if m > block_size: + p_start = cipher_cbc.decrypt(ciphertext[:m-block_size]) + + iv = b'\x00' * block_size \ + if m == block_size \ + else ciphertext[m-block_size*2:m-block_size] - # All blocks except last two - prev_blocks = ciphertext[:m-block_size] cn_minus_1 = ciphertext[m-block_size:m] cn = ciphertext[m:] - # Decrypt cn_minus_1 to get the 'stolen' padding + aes_ecb = AES.new(ke, AES.MODE_ECB) tmp = aes_ecb.decrypt(cn_minus_1) - pn = bytes(a ^ b for a, b in zip(tmp[:last_len], cn)) - # Reconstruct the full last block - last_block_full = cn + tmp[last_len:] - pn_minus_1_dec = aes_ecb.decrypt(last_block_full) + pn = bytes(a ^ b for a, b in zip(tmp[:len(cn)], cn)) - # IV for the second-to-last block - iv = b'\x00' * block_size \ - if m == block_size \ - else ciphertext[m-block_size*2:m-block_size] - pn_minus_1 = bytes(a ^ b for a, b in zip(pn_minus_1_dec, iv)) + last_block_full = cn + tmp[len(cn):] - if m > block_size: - cipher_cbc = AES.new(ke, AES.MODE_CBC, b'\x00' * block_size) - p_start = cipher_cbc.decrypt(prev_blocks) - plaintext_with_confounder = p_start + pn_minus_1 + pn - else: - plaintext_with_confounder = pn_minus_1 + pn + pn_minus_1 = bytes(a ^ b + for a, b + in zip(aes_ecb.decrypt(last_block_full), iv)) + plaintext_with_confounder = p_start + pn_minus_1 + pn h = HMAC.new(ki, plaintext_with_confounder, SHA1) if h.digest()[:12] != expected_hmac: @@ -173,11 +171,13 @@ def decrypt_kerberos_aes_cts(key: bytes, usage: int, cipher: bytes) -> bytes: def encrypt_kerberos_aes_cts(session_key: bytes, usage: int, - plain_text: bytes): + plain_text: bytes, + confounder: Optional[bytes] = None): ki = derive_key(session_key, usage, 0x55) ke = derive_key(session_key, usage, 0xAA) - confounder = get_random_bytes(16) + if confounder is None: + confounder = get_random_bytes(16) basic_plaintext = confounder + plain_text h = HMAC.new(ki, basic_plaintext, SHA1) @@ -188,6 +188,9 @@ def encrypt_kerberos_aes_cts(session_key: bytes, if n % 16 == 0: final_ctext = aes.encrypt(basic_plaintext) + x = final_ctext[-32:-16] + y = final_ctext[-16:] + final_ctext = final_ctext[:-32] + y + x else: pad_len = 16 - (n % 16) padded_data = basic_plaintext + b'\x00' * pad_len @@ -252,42 +255,6 @@ def read_session_key(data: bytes) -> bytes: return session_key -def seal_func_kerberos(session_key: bytes): - def _kerberos_sealer( - flags: int, - seq_num: int, - message_to_sign: bytes, - message_to_encrypt: bytes, - session_key: bytes) -> tuple[bytes, bytes]: - return gss_wrap_rc4(session_key, - message_to_encrypt, - seq_num) - return functools.partial(_kerberos_sealer, session_key=session_key) - - -def gss_unwrap_kerberos(session_key: bytes): - def _kerberos_unwrap( - cipher_text: bytes, - auth_data: bytes, - session_key: bytes) -> bytes: - return gss_unwrap_rc4(session_key, - cipher_text, - auth_data) - return functools.partial(_kerberos_unwrap, session_key=session_key) - - -def sign_func_kerberos(session_key: bytes): - def _kerberos_signer( - flags: int, - seq_num: int, - message_to_sign: bytes, - session_key: bytes) -> tuple[bytes, bytes]: - return gss_wrap_rc4(session_key, - message_to_sign, - seq_num) - return functools.partial(_kerberos_signer, session_key=session_key) - - KDC_ERR_PREAUTH_REQUIRED = 25 KRB_ERRORS = { @@ -402,3 +369,18 @@ def parse_krb_error(data: bytes): raise KerberosErr( KRB_ERRORS.get(code, f"Unknown Kerberos Error: {code}") ) + + +def get_etype_from_ticket(ticket_bytes: bytes) -> int: + if not ticket_bytes.startswith(b'\x61'): + raise ValueError('Invalid ticket') + + header_area = ticket_bytes[:256] + idx_a3 = header_area.find(b'\xa3') + if idx_a3 != -1: + etype_idx = header_area.find(b'\x02\x01', idx_a3) + if etype_idx != -1: + etype = header_area[etype_idx + 2] + return etype + + raise ValueError('E-Type not found in ticket') diff --git a/aiowmi/kerberos/wrappers.py b/aiowmi/kerberos/wrappers.py new file mode 100644 index 0000000..d919b70 --- /dev/null +++ b/aiowmi/kerberos/wrappers.py @@ -0,0 +1,83 @@ +import functools +from .gss import gss_wrap_rc4, gss_wrap_aes, gss_unwrap_rc4, gss_unwrap_aes + + +def seal_func_kerberos(session_key: bytes, etype: int): + def _sealer_rc4( + flags: int, + seq_num: int, + message_to_sign: bytes, + message_to_encrypt: bytes, + session_key: bytes) -> tuple[bytes, bytes]: + return gss_wrap_rc4(session_key, + message_to_encrypt, + seq_num) + + def _sealer_aes( + flags: int, + seq_num: int, + message_to_sign: bytes, + message_to_encrypt: bytes, + session_key: bytes) -> tuple[bytes, bytes]: + return gss_wrap_aes(session_key, + message_to_encrypt, + seq_num) + if etype in (17, 18): + sealer = _sealer_aes + elif etype == 23: + sealer = _sealer_rc4 + else: + ValueError(f"Invalid E-type: {etype}") + return functools.partial(sealer, session_key=session_key) + + +def gss_unwrap_kerberos(session_key: bytes, etype: int): + def _unwrap_rc4( + cipher_text: bytes, + auth_data: bytes, + session_key: bytes) -> bytes: + return gss_unwrap_rc4(session_key, + cipher_text, + auth_data) + + def _unwrap_aes( + cipher_text: bytes, + auth_data: bytes, + session_key: bytes) -> bytes: + return gss_unwrap_aes(session_key, + cipher_text, + auth_data) + if etype in (17, 18): + unwrapper = _unwrap_aes + elif etype == 23: + unwrapper = _unwrap_rc4 + else: + ValueError(f"Invalid E-type: {etype}") + return functools.partial(unwrapper, session_key=session_key) + + +def sign_func_kerberos(session_key: bytes, etype: int): + def _signer_rc4( + flags: int, + seq_num: int, + message_to_sign: bytes, + session_key: bytes) -> tuple[bytes, bytes]: + return gss_wrap_rc4(session_key, + message_to_sign, + seq_num) + + def _signer_aes( + flags: int, + seq_num: int, + message_to_sign: bytes, + session_key: bytes) -> tuple[bytes, bytes]: + return gss_wrap_aes(session_key, + message_to_sign, + seq_num) + if etype in (17, 18): + signer = _signer_aes + elif etype == 23: + signer = _signer_rc4 + else: + ValueError(f"Invalid E-type: {etype}") + return functools.partial(signer, session_key=session_key) From 93c7107b8e080a98a473490260b585e576527e08 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Wed, 18 Mar 2026 10:37:46 +0100 Subject: [PATCH 02/23] AES --- aiowmi/connection.py | 10 ++++------ aiowmi/kerberos/ap_req.py | 2 ++ aiowmi/kerberos/cache.py | 9 +++++---- aiowmi/kerberos/tgs.py | 14 ++++++++------ aiowmi/kerberos/tgt.py | 2 +- aiowmi/kerberos/tools.py | 20 ++------------------ aiowmi/kerberos/wrappers.py | 6 +++--- 7 files changed, 25 insertions(+), 38 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index 6fdcd6f..6a326c5 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -7,7 +7,6 @@ from .kerberos.krb5_pdu import get_neg_token, build_alter_context from .kerberos.tgs import get_tgs from .kerberos.tgt import get_tgt -from .kerberos.tools import get_etype_from_ticket from .kerberos.wrappers import sign_func_kerberos, seal_func_kerberos from .kerberos.wrappers import gss_unwrap_kerberos from .ntlm.auth_authenticate import NTLMAuthAuthenticate @@ -248,8 +247,7 @@ async def negotiate_kerberos(self, max_retry: int = 1) -> Protocol: return iface async def _bind_kerberos(self, iid: bytes, proto: Protocol): - ticket, service_session_key = self._tgs - etype = get_etype_from_ticket(ticket) + ticket, service_session_key, etype = self._tgs ap_req = build_ap_req(self._username, self._domain, ticket, @@ -266,7 +264,7 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): active_key, seq_number = get_active_key(rpc_bind_ack.auth.auth_value, service_session_key, etype) - if active_key is None: + if active_key is None and proto._client_sign is None: raise NoNewActiveKey() proto._auth_type = rpc_bind_ack.auth.auth_type proto._auth_level = rpc_bind_ack.auth.auth_level @@ -280,7 +278,7 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): neg_token, ) _ = await proto.get_dcom_response(alter_context_pkg) - if proto._auth_level >= RPC_C_AUTHN_LEVEL_PKT_INTEGRITY: + if active_key and proto._auth_level >= RPC_C_AUTHN_LEVEL_PKT_INTEGRITY: proto._client_sign = sign_func_kerberos(active_key, etype) proto._client_seal = seal_func_kerberos(active_key, etype) proto._server_seal = gss_unwrap_kerberos(active_key, etype) @@ -308,7 +306,7 @@ async def login_ntlm( request.set_pdu_data(ntlm_login_pkg) request_pkg = request.sign_data(proto) - print(request_pkg) + # print(request_pkg) rpc_response: RpcResponse = \ await proto.get_dcom_response( request_pkg, diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index 9245035..3a7d391 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -142,6 +142,8 @@ def get_active_key(auth_bytes: bytes, decrypted_raw = \ decrypt_kerberos_aes_cts(service_session_key, 12, cipher_blob) asn1_data = decrypted_raw[16:] # Skip 16 bytes confounder + else: + raise ValueError(f'Invalid E-type: {etype}') if etype == 0x12: # AES256 KEY_MARKER = b'\x04\x20' diff --git a/aiowmi/kerberos/cache.py b/aiowmi/kerberos/cache.py index a350be8..6115d4f 100644 --- a/aiowmi/kerberos/cache.py +++ b/aiowmi/kerberos/cache.py @@ -17,26 +17,27 @@ def __init__(self, file_path: Optional[str] = None): """ self._file_path = file_path self._tgt: Optional[tuple[bytes, bytes]] = None - self._tgs: Optional[tuple[bytes, bytes]] = None + self._tgs: Optional[tuple[bytes, bytes, int]] = None def open(self, logger: Logger) -> tuple[ Optional[tuple[bytes, bytes]], - Optional[tuple[bytes, bytes]]]: + Optional[tuple[bytes, bytes, int]]]: if self._file_path is not None and \ (self._tgs is None or self._tgt is None): try: with open(self._file_path, 'rb') as fp: dump = pickle.load(fp) self._tgt = dump[0], dump[1] - self._tgs = dump[2], dump[3] + self._tgs = dump[2], dump[3], dump[4] except Exception: logger.warning(f'Failed to load from: {self._file_path}') + self._tgt, self._tgs = None, None return self._tgt, self._tgs def write(self, tgt: tuple[bytes, bytes], - tgs: tuple[bytes, bytes], + tgs: tuple[bytes, bytes, int], logger: Logger): self._tgt = tgt self._tgs = tgs diff --git a/aiowmi/kerberos/tgs.py b/aiowmi/kerberos/tgs.py index cfb5043..bf0b038 100644 --- a/aiowmi/kerberos/tgs.py +++ b/aiowmi/kerberos/tgs.py @@ -162,7 +162,7 @@ def build_tgs_req(username: str, # Etype list: RC4 (23), AES128 (16), AES256 (18) # etypes = asn1_int(23) + asn1_int(16) + asn1_int(18) - etypes = asn1_int(23) + etypes = asn1_int(18) nonce = random.getrandbits(31) req_body_content = ( @@ -218,7 +218,7 @@ def extract_ticket(data): def get_service_key(resp_bytes: bytes, - session_key: bytes) -> tuple[bytes, bytes]: + session_key: bytes) -> tuple[bytes, bytes, int]: raw_obj, _ = decoder.decode(resp_bytes) enc_part = raw_obj.getComponentByPosition(5) @@ -237,6 +237,7 @@ def get_service_key(resp_bytes: bytes, inner_obj, _ = decoder.decode(payload) key_sequence = inner_obj.getComponentByPosition(0) + service_etype = int(key_sequence.getComponentByPosition(0)) service_session_key = bytes(key_sequence.getComponentByPosition(1)) ticket = raw_obj.getComponentByPosition(4) @@ -251,12 +252,13 @@ def get_service_key(resp_bytes: bytes, except ValueError: pass - return ticket_bytes, service_session_key + return ticket_bytes, service_session_key, service_etype async def get_tgs(username: str, domain: str, host: str, as_rep_bytes: bytes, base_key: bytes, - kdc_host: str, kdc_port: int = 88) -> tuple[bytes, bytes]: + kdc_host: str, + kdc_port: int = 88) -> tuple[bytes, bytes, int]: tgs_session_key = get_session_key(as_rep_bytes, base_key) tgs_ticket = extract_ticket(as_rep_bytes) tgs_req = build_tgs_req(username, @@ -266,5 +268,5 @@ async def get_tgs(username: str, domain: str, host: str, ("host", host)) as_res_bytes = await send_kerberos_packet(tgs_req, kdc_host, kdc_port) parse_krb_error(as_res_bytes) - ticket, service_key = get_service_key(as_res_bytes, tgs_session_key) - return ticket, service_key + ticket, service_key, etype = get_service_key(as_res_bytes, tgs_session_key) + return ticket, service_key, etype diff --git a/aiowmi/kerberos/tgt.py b/aiowmi/kerberos/tgt.py index 5e1e7cd..d066eec 100644 --- a/aiowmi/kerberos/tgt.py +++ b/aiowmi/kerberos/tgt.py @@ -52,7 +52,7 @@ def aes_string_to_key(password: str, salt: str, key_len: int = 32): key_len should be 16 for AES-128 or 32 for AES-256 """ tkey = PBKDF2( - password.encode(), + password, salt.encode(), dkLen=key_len, count=4096, diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index 175f7d4..cac307f 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -66,12 +66,11 @@ def lcm(a, b): return bytes(reduce(add_ones_complement, parts)) -def derive_key(base_key: bytes, usage: int, payload: int = None) -> bytes: +def derive_key(base_key: bytes, usage: int, payload: int) -> bytes: key_len = len(base_key) # 16 or 32 constant = struct.pack('>I', usage) - if payload is not None: - constant += bytes([payload]) + constant += bytes([payload]) nfolded = _nfold(constant, 16) @@ -369,18 +368,3 @@ def parse_krb_error(data: bytes): raise KerberosErr( KRB_ERRORS.get(code, f"Unknown Kerberos Error: {code}") ) - - -def get_etype_from_ticket(ticket_bytes: bytes) -> int: - if not ticket_bytes.startswith(b'\x61'): - raise ValueError('Invalid ticket') - - header_area = ticket_bytes[:256] - idx_a3 = header_area.find(b'\xa3') - if idx_a3 != -1: - etype_idx = header_area.find(b'\x02\x01', idx_a3) - if etype_idx != -1: - etype = header_area[etype_idx + 2] - return etype - - raise ValueError('E-Type not found in ticket') diff --git a/aiowmi/kerberos/wrappers.py b/aiowmi/kerberos/wrappers.py index d919b70..eeb8bac 100644 --- a/aiowmi/kerberos/wrappers.py +++ b/aiowmi/kerberos/wrappers.py @@ -27,7 +27,7 @@ def _sealer_aes( elif etype == 23: sealer = _sealer_rc4 else: - ValueError(f"Invalid E-type: {etype}") + raise ValueError(f"Invalid E-type: {etype}") return functools.partial(sealer, session_key=session_key) @@ -52,7 +52,7 @@ def _unwrap_aes( elif etype == 23: unwrapper = _unwrap_rc4 else: - ValueError(f"Invalid E-type: {etype}") + raise ValueError(f"Invalid E-type: {etype}") return functools.partial(unwrapper, session_key=session_key) @@ -79,5 +79,5 @@ def _signer_aes( elif etype == 23: signer = _signer_rc4 else: - ValueError(f"Invalid E-type: {etype}") + raise ValueError(f"Invalid E-type: {etype}") return functools.partial(signer, session_key=session_key) From 4e8f74aa4e71918fc0e602052be9a8977497b637 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Wed, 18 Mar 2026 11:00:02 +0100 Subject: [PATCH 03/23] rewrite --- aiowmi/kerberos/tools.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index cac307f..523c7bb 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -186,23 +186,22 @@ def encrypt_kerberos_aes_cts(session_key: bytes, n = len(basic_plaintext) if n % 16 == 0: - final_ctext = aes.encrypt(basic_plaintext) - x = final_ctext[-32:-16] - y = final_ctext[-16:] - final_ctext = final_ctext[:-32] + y + x + pad_len = 0 + padded_data = basic_plaintext else: pad_len = 16 - (n % 16) padded_data = basic_plaintext + b'\x00' * pad_len - ctext = aes.encrypt(padded_data) - last_full_block = ctext[-32:-16] - truncated_block = ctext[-16:] + ctext = aes.encrypt(padded_data) - final_ctext = ( - ctext[:-32] + - truncated_block + - last_full_block[:16-pad_len] - ) + last_full_block = ctext[-32:-16] + truncated_block = ctext[-16:] + + final_ctext = ( + ctext[:-32] + + truncated_block + + last_full_block[:16-pad_len] + ) return final_ctext + checksum From b92bf23ef2d989cdf6ce606c03b7e8849b44f18a Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Wed, 18 Mar 2026 11:01:42 +0100 Subject: [PATCH 04/23] rewrite --- aiowmi/kerberos/tools.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index 523c7bb..4b3ca7d 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -183,14 +183,9 @@ def encrypt_kerberos_aes_cts(session_key: bytes, checksum = h.digest()[:12] aes = AES.new(ke, AES.MODE_CBC, b'\x00' * 16) - n = len(basic_plaintext) - if n % 16 == 0: - pad_len = 0 - padded_data = basic_plaintext - else: - pad_len = 16 - (n % 16) - padded_data = basic_plaintext + b'\x00' * pad_len + pad_len = (16 - (len(basic_plaintext) % 16)) % 16 + padded_data = basic_plaintext + (b'\x00' * pad_len) ctext = aes.encrypt(padded_data) From f7be948f4033bec7bc27cf3e3741420c32e9b5fe Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Wed, 18 Mar 2026 11:02:54 +0100 Subject: [PATCH 05/23] plain --- aiowmi/kerberos/tools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index 4b3ca7d..4c8ddbf 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -177,15 +177,15 @@ def encrypt_kerberos_aes_cts(session_key: bytes, if confounder is None: confounder = get_random_bytes(16) - basic_plaintext = confounder + plain_text + plaintext = confounder + plain_text - h = HMAC.new(ki, basic_plaintext, SHA1) + h = HMAC.new(ki, plaintext, SHA1) checksum = h.digest()[:12] aes = AES.new(ke, AES.MODE_CBC, b'\x00' * 16) - pad_len = (16 - (len(basic_plaintext) % 16)) % 16 - padded_data = basic_plaintext + (b'\x00' * pad_len) + pad_len = (16 - (len(plaintext) % 16)) % 16 + padded_data = plaintext + (b'\x00' * pad_len) ctext = aes.encrypt(padded_data) From 7d316ca74e6f8de567eed22ca36e1d64b049b1ec Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Wed, 18 Mar 2026 14:59:37 +0100 Subject: [PATCH 06/23] typing --- aiowmi/kerberos/gss.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/aiowmi/kerberos/gss.py b/aiowmi/kerberos/gss.py index 1b1eaa8..917b0d0 100644 --- a/aiowmi/kerberos/gss.py +++ b/aiowmi/kerberos/gss.py @@ -96,7 +96,9 @@ def gss_unwrap_rc4(session_key: bytes, return data -def gss_wrap_aes(session_key: bytes, data: bytes, seq_num: int): +def gss_wrap_aes(session_key: bytes, + data: bytes, + seq_num: int) -> tuple[bytes, bytes]: pad = (16 - (len(data) % 16)) & 15 pad_str = b'\xFF' * pad @@ -108,10 +110,10 @@ def gss_wrap_aes(session_key: bytes, data: bytes, seq_num: int): plaintext = data + pad_str + header_for_hash raw_cipher = encrypt_kerberos_aes_cts(session_key, 24, plaintext) - def rotate(data, numBytes): - numBytes %= len(data) - left = len(data) - numBytes - return data[left:] + data[:left] + def rotate(bytes_data: bytes, n: int) -> bytes: + n %= len(bytes_data) + left = len(bytes_data) - n + return bytes_data[left:] + bytes_data[:left] rrc = 28 total_rotate = rrc + pad @@ -131,7 +133,8 @@ def rotate(data, numBytes): return ret1, ret2 -def gss_unwrap_aes(session_key: bytes, cipher_text: bytes, auth_data: bytes): +def gss_unwrap_aes(session_key: bytes, cipher_text: bytes, + auth_data: bytes) -> bytes: header = auth_data[:16] pad = struct.unpack('>H', header[4:6])[0] # EC rrc = struct.unpack('>H', header[6:8])[0] # RRC From 751366a66d4dcadedb6701de35547b5c052bb2f6 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 09:38:43 +0100 Subject: [PATCH 07/23] small changes --- aiowmi/connection.py | 2 ++ aiowmi/kerberos/tgs.py | 14 +++++--------- aiowmi/kerberos/tools.py | 42 ++++++++++++++++++---------------------- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index 6a326c5..47f323c 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -231,6 +231,7 @@ async def negotiate_kerberos(self, max_retry: int = 1) -> Protocol: bind_func=self._bind_kerberos, m_auth_level=proto._auth_level) except (AccessDenied, NoNewActiveKey, BindNak) as e: + raise msg = str(e) or type(e).__name__ if attempt <= max_retry: logger.info(f'{msg} (attempt {attempt}/{max_retry})') @@ -268,6 +269,7 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): raise NoNewActiveKey() proto._auth_type = rpc_bind_ack.auth.auth_type proto._auth_level = rpc_bind_ack.auth.auth_level + proto._dcom._seq_num = 0 neg_token = get_neg_token(service_session_key, seq_number, etype) alter_context_pkg = build_alter_context( diff --git a/aiowmi/kerberos/tgs.py b/aiowmi/kerberos/tgs.py index bf0b038..abb5771 100644 --- a/aiowmi/kerberos/tgs.py +++ b/aiowmi/kerberos/tgs.py @@ -171,7 +171,7 @@ def build_tgs_req(username: str, asn1_tag(3, asn1_seq(sname_inner)) + # sname asn1_tag(5, asn1_gt(till_ts)) + # Till asn1_tag(7, asn1_int(nonce)) + # Nonce - asn1_tag(8, asn1_seq(etypes)) # Etypes + asn1_tag(8, asn1_seq(etypes)) # Etypes ) req_body_field = asn1_tag(4, asn1_seq(req_body_content)) @@ -243,14 +243,10 @@ def get_service_key(resp_bytes: bytes, ticket = raw_obj.getComponentByPosition(4) ticket_bytes = encoder.encode(ticket) - WRAPPERS = [0xa3, 0xa4, 0xa5] - - if ticket_bytes[0] in WRAPPERS: - try: - ticket_start = ticket_bytes.index(b'\x61', 0, 10) - ticket_bytes = ticket_bytes[ticket_start:] - except ValueError: - pass + if ticket_bytes[0] != 0x61: + first_len_byte = ticket_bytes[1] + num_len_bytes = first_len_byte & 0x7f if first_len_byte & 0x80 else 0 + ticket_bytes = ticket_bytes[2 + num_len_bytes:] return ticket_bytes, service_session_key, service_etype diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index 4c8ddbf..4b0c1e0 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -16,39 +16,35 @@ def _nfold(ba, nbytes): def rotate_right(ba, nbits): - ba = bytearray(ba) n = len(ba) nbits %= (n * 8) - # Rotate bytes - for _ in range(nbits // 8): - ba = bytearray([ba[-1]]) + ba[:-1] - # Rotate remaining bits - nbits %= 8 - if nbits > 0: - last_bit = 0 - for i in range(len(ba)): - new_last_bit = ba[i] & ((1 << nbits) - 1) - ba[i] = (ba[i] >> nbits) | (last_bit << (8 - nbits)) - last_bit = new_last_bit - ba[0] |= (last_bit << (8 - nbits)) - return ba + if nbits == 0: + return bytearray(ba) + + val = int.from_bytes(ba, 'big') + + mask = (1 << (n * 8)) - 1 + rotated = ((val >> nbits) | (val << (n * 8 - nbits))) & mask + + return bytearray(rotated.to_bytes(n, 'big')) def add_ones_complement(ba1, ba2): n = len(ba1) - res = [0] * n + res = bytearray(n) carry = 0 for i in range(n-1, -1, -1): s = ba1[i] + ba2[i] + carry res[i] = s & 0xff carry = s >> 8 - pos = n - 1 - while carry and pos >= 0: - s = res[pos] + carry - res[pos] = s & 0xff - carry = s >> 8 - pos -= 1 - return bytearray(res) + while carry: + for i in range(n-1, -1, -1): + s = res[i] + carry + res[i] = s & 0xff + carry = s >> 8 + if not carry: + break + return res slen = len(ba) @@ -58,7 +54,7 @@ def lcm(a, b): lcm_val = lcm(slen, nbytes) big_str = bytearray() curr = bytearray(ba) - for i in range(lcm_val // slen): + for _ in range(lcm_val // slen): big_str += curr curr = rotate_right(curr, 13) From 32042ebabde14f12c491b8106ad51ed27d1a9ad5 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 12:30:27 +0100 Subject: [PATCH 08/23] . --- aiowmi/connection.py | 4 ++-- aiowmi/kerberos/tools.py | 22 +++++++++------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index 47f323c..d88d5d3 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -217,7 +217,7 @@ async def negotiate_kerberos(self, max_retry: int = 1) -> Protocol: assert self._protocol proto = self._protocol proto._auth_level = RPC_C_AUTHN_LEVEL_PKT_PRIVACY - proto._context_id = 0x0001357f + proto._context_id = 79231 if not has_keys: logger.info('Start Kerberos negotiation for TGT/TGS') @@ -381,6 +381,6 @@ async def _if_binding(self, proto._auth_level = max( interface.scm_reply_info_data.authn_hint, m_auth_level) - proto._context_id = context_id + proto._context_id = context_id + 1 await bind_func(IID_IWbemLevel1Login, proto) return proto diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index 4b0c1e0..1a24824 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -129,23 +129,18 @@ def decrypt_kerberos_aes_cts(key: bytes, usage: int, cipher: bytes) -> bytes: plaintext_with_confounder = aes_ecb.decrypt(ciphertext) else: # NIST CTS Decryption - block_size = 16 - n = len(ciphertext) - m = ((n - 1) // block_size) * block_size + m = ((n - block_size - 1) // block_size) * block_size - cipher_cbc = AES.new(ke, AES.MODE_CBC, b'\x00' * block_size) p_start = b"" - if m > block_size: - p_start = cipher_cbc.decrypt(ciphertext[:m-block_size]) + iv = b'\x00' * block_size + if m > 0: + cipher_cbc = AES.new(ke, AES.MODE_CBC, iv) + p_start = cipher_cbc.decrypt(ciphertext[:m]) + iv = ciphertext[m-block_size:m] - iv = b'\x00' * block_size \ - if m == block_size \ - else ciphertext[m-block_size*2:m-block_size] + cn_minus_1 = ciphertext[m : m + block_size] + cn = ciphertext[m + block_size :] - cn_minus_1 = ciphertext[m-block_size:m] - cn = ciphertext[m:] - - aes_ecb = AES.new(ke, AES.MODE_ECB) tmp = aes_ecb.decrypt(cn_minus_1) pn = bytes(a ^ b for a, b in zip(tmp[:len(cn)], cn)) @@ -155,6 +150,7 @@ def decrypt_kerberos_aes_cts(key: bytes, usage: int, cipher: bytes) -> bytes: pn_minus_1 = bytes(a ^ b for a, b in zip(aes_ecb.decrypt(last_block_full), iv)) + plaintext_with_confounder = p_start + pn_minus_1 + pn h = HMAC.new(ki, plaintext_with_confounder, SHA1) From e2cd3099dfed6e65c871b4ad63802c8bf1910ff3 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 13:42:09 +0100 Subject: [PATCH 09/23] more stable --- aiowmi/connection.py | 10 +- aiowmi/kerberos/ap_req.py | 189 ++++++++++++++++++++++++++++++-------- aiowmi/kerberos/as_req.py | 4 +- 3 files changed, 162 insertions(+), 41 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index d88d5d3..98a01b3 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -265,11 +265,15 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): active_key, seq_number = get_active_key(rpc_bind_ack.auth.auth_value, service_session_key, etype) - if active_key is None and proto._client_sign is None: - raise NoNewActiveKey() + if active_key is None: + active_key = service_session_key + + if seq_number is None: + seq_number = 1 + proto._dcom._seq_num += 1 + proto._auth_type = rpc_bind_ack.auth.auth_type proto._auth_level = rpc_bind_ack.auth.auth_level - proto._dcom._seq_num = 0 neg_token = get_neg_token(service_session_key, seq_number, etype) alter_context_pkg = build_alter_context( diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index 3a7d391..3f2e8cd 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -4,6 +4,7 @@ from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4 from .tools import decrypt_kerberos_aes_cts, encrypt_kerberos_aes_cts from .const import OID_SPNEGO, OID_IETF_KRB5, OID_MS_KRB5 +from ..exceptions import NoNewActiveKey def wrap_gss_kerberos(ap_req_bytes: bytes, etype: int): @@ -112,58 +113,172 @@ def get_active_key(auth_bytes: bytes, service_session_key: bytes, etype: int) -> tuple[Optional[bytes], int]: active_key, seq_number = None, 0 + etype_idx = auth_bytes.find(bytes([0x02, 0x01, etype])) if etype_idx == -1: return None, 0 - - # Read new session key... - idx_a2 = auth_bytes.find(b'\xa2', etype_idx) - idx_04 = auth_bytes.find(b'\x04', idx_a2) + idx_04 = auth_bytes.find(b'\x04', etype_idx) if idx_04 == -1: return None, 0 pos = idx_04 + 1 - length_byte = auth_bytes[pos] + lb = auth_bytes[pos] pos += 1 - if length_byte & 0x80: - n = length_byte & 0x7f + if lb & 0x80: + n = lb & 0x7f length = int.from_bytes(auth_bytes[pos: pos + n], 'big') pos += n - else: - length = length_byte - + else: length = lb cipher_blob = auth_bytes[pos: pos + length] - if etype == 0x17: - decrypted_raw = \ + if etype == 23: + decrypted = \ decrypt_kerberos_rc4(service_session_key, 12, cipher_blob) - asn1_data = decrypted_raw[8:] # Skip 8 bytes confounder - elif etype in [0x11, 0x12]: - decrypted_raw = \ + asn1_data = decrypted[8:] + elif etype in [17, 18]: + decrypted = \ decrypt_kerberos_aes_cts(service_session_key, 12, cipher_blob) - asn1_data = decrypted_raw[16:] # Skip 16 bytes confounder + asn1_data = decrypted[16:] else: - raise ValueError(f'Invalid E-type: {etype}') - - if etype == 0x12: # AES256 - KEY_MARKER = b'\x04\x20' - k_len = 32 - else: # AES128 / RC4 - KEY_MARKER = b'\x04\x10' - k_len = 16 - - key_idx = asn1_data.find(KEY_MARKER) - if key_idx != -1: - active_key = asn1_data[key_idx + 2: key_idx + 2 + k_len] - - SEQ_MARKER = b'\xa3' - seq_idx = asn1_data.find(SEQ_MARKER) - if seq_idx != -1: - int_tag_idx = asn1_data.find(b'\x02', seq_idx) - if int_tag_idx != -1 and int_tag_idx < seq_idx + 4: - int_len = asn1_data[int_tag_idx + 1] - offset = int_tag_idx + 2 - seq_bytes = asn1_data[offset: offset + int_len] + raise ValueError(f"Invalid E-type: {etype}") + + def get_tag_data(data: bytes, target_tag: int) -> Optional[bytes]: + p = 0 + while p < len(data): + tag = data[p] + if tag == target_tag: + try: + p += 1 + lb = data[p] + p += 1 + if lb & 0x80: + n_lb = lb & 0x7f + c_len = int.from_bytes(data[p: p+n_lb], 'big') + p += n_lb + else: + c_len = lb + return data[p : p + c_len] + except: + return None + p += 1 + return None + + subkey_cont = get_tag_data(asn1_data, 0xa2) + if subkey_cont: + key_seq = get_tag_data(subkey_cont, 0x30) + if key_seq: + key_val_wrapper = get_tag_data(key_seq, 0xa1) + if key_val_wrapper: + active_key = get_tag_data(key_val_wrapper, 0x04) + + seq_cont = get_tag_data(asn1_data, 0xa3) + if seq_cont: + seq_bytes = get_tag_data(seq_cont, 0x02) + if seq_bytes: seq_number = int.from_bytes(seq_bytes, 'big') return active_key, seq_number + +import struct +import binascii +from typing import Optional, Tuple + +def get_active_key(auth_bytes: bytes, + service_session_key: bytes, + etype: int) -> Tuple[Optional[bytes], Optional[int]]: + active_key, seq_number = None, None + kerberos_data = auth_bytes + + if auth_bytes.startswith(b'\xa1'): + idx_a2 = auth_bytes.find(b'\xa2') + if idx_a2 == -1: + raise NoNewActiveKey() + + idx_04 = auth_bytes.find(b'\x04', idx_a2) + if idx_04 != -1: + pos = idx_04 + 1 + lb = auth_bytes[pos] + pos += 1 + if lb & 0x80: + n = lb & 0x7f + length = int.from_bytes(auth_bytes[pos:pos+n], 'big') + pos += n + else: + length = lb + kerberos_data = auth_bytes[pos: pos + length] + else: + raise NoNewActiveKey() + + etype_idx = kerberos_data.find(bytes([0x02, 0x01, etype])) + if etype_idx == -1: + return None, None + + idx_04_cipher = kerberos_data.find(b'\x04', etype_idx) + if idx_04_cipher == -1: + return None, None + + pos = idx_04_cipher + 1 + lb = kerberos_data[pos] + pos += 1 + if lb & 0x80: + n = lb & 0x7f + c_length = int.from_bytes(kerberos_data[pos : pos + n], 'big') + pos += n + else: + c_length = lb + cipher_blob = kerberos_data[pos : pos + c_length] + + if etype == 23: # RC4 + decrypted = \ + decrypt_kerberos_rc4(service_session_key, 12, cipher_blob) + asn1_data = decrypted[8:] # Skip 8 bytes confounder + elif etype in [17, 18]: # AES + decrypted = \ + decrypt_kerberos_aes_cts(service_session_key, 12, cipher_blob) + asn1_data = decrypted[16:] # Skip 16 bytes confounder + else: + raise ValueError(f"Invalid E-type: {etype}") + + def peel_tag(data: bytes, target_tag: int): + if not data: + return None + p = 0 + while p < len(data): + tag = data[p] + p += 1 + if p >= len(data): + break + lb = data[p] + p += 1 + if lb & 0x80: + n_lb = lb & 0x7f + c_len = int.from_bytes(data[p: p+n_lb], 'big') + p += n_lb + else: + c_len = lb + if tag == target_tag: + return data[p : p + c_len] + p += c_len + return None + + current = asn1_data + if asn1_data.startswith(b'\x7b'): + current = peel_tag(asn1_data, 0x7b) + if current and current.startswith(b'\x30'): + current = peel_tag(current, 0x30) + + subkey_cont = peel_tag(current, 0xa2) + if subkey_cont: + k_seq = peel_tag(subkey_cont, 0x30) + if k_seq: + k_val_wrap = peel_tag(k_seq, 0xa1) + if k_val_wrap: + active_key = peel_tag(k_val_wrap, 0x04) + + seq_cont = peel_tag(current, 0xa3) + if seq_cont: + inner_seq = peel_tag(seq_cont, 0x02) + if inner_seq: + seq_number = int.from_bytes(inner_seq, 'big') + + return active_key, seq_number \ No newline at end of file diff --git a/aiowmi/kerberos/as_req.py b/aiowmi/kerberos/as_req.py index d8b2743..1326a74 100644 --- a/aiowmi/kerberos/as_req.py +++ b/aiowmi/kerberos/as_req.py @@ -95,10 +95,12 @@ def build_as_req(username: str, domain: str, pa_enc: bytes = b'') -> bytes: def build_full_as_req(username: str, domain: str, base_key: bytes, etype: int): now = datetime.now(timezone.utc) + micro = now.microsecond + ts_str = now.strftime("%Y%m%d%H%M%SZ").encode() original_plain_body = ( asn1_tag(0, asn1_gt(ts_str)) + # [0] pausec (GeneralizedTime) - asn1_tag(1, asn1_int(0)) # [1] pusec (Microseconds, 0) + asn1_tag(1, asn1_int(micro)) # [1] pusec (Microseconds) ) original_plain = asn1_seq(original_plain_body) From 9ab1a793375dde1705cc10441e24b6ba15a26ca4 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 13:46:55 +0100 Subject: [PATCH 10/23] Typing --- aiowmi/kerberos/ap_req.py | 22 ++++++++++------------ aiowmi/kerberos/asn1.py | 4 ++-- aiowmi/kerberos/cache.py | 16 ++++++++-------- aiowmi/kerberos/gss.py | 3 ++- aiowmi/kerberos/tgs.py | 7 ++++--- aiowmi/kerberos/tgt.py | 6 +++--- aiowmi/kerberos/tools.py | 4 ++-- aiowmi/kerberos/wrappers.py | 9 +++++---- 8 files changed, 36 insertions(+), 35 deletions(-) diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index 3f2e8cd..bd28452 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Tuple from datetime import datetime, timezone from .asn1 import asn1_len, asn1_tag, asn1_gt, asn1_int, asn1_ostr from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4 @@ -111,7 +111,7 @@ def build_ap_req(username: str, def get_active_key(auth_bytes: bytes, service_session_key: bytes, - etype: int) -> tuple[Optional[bytes], int]: + etype: int) -> Tuple[Optional[bytes], int]: active_key, seq_number = None, 0 etype_idx = auth_bytes.find(bytes([0x02, 0x01, etype])) @@ -128,7 +128,8 @@ def get_active_key(auth_bytes: bytes, n = lb & 0x7f length = int.from_bytes(auth_bytes[pos: pos + n], 'big') pos += n - else: length = lb + else: + length = lb cipher_blob = auth_bytes[pos: pos + length] if etype == 23: @@ -157,8 +158,8 @@ def get_tag_data(data: bytes, target_tag: int) -> Optional[bytes]: p += n_lb else: c_len = lb - return data[p : p + c_len] - except: + return data[p: p + c_len] + except Exception: return None p += 1 return None @@ -179,9 +180,6 @@ def get_tag_data(data: bytes, target_tag: int) -> Optional[bytes]: return active_key, seq_number -import struct -import binascii -from typing import Optional, Tuple def get_active_key(auth_bytes: bytes, service_session_key: bytes, @@ -222,11 +220,11 @@ def get_active_key(auth_bytes: bytes, pos += 1 if lb & 0x80: n = lb & 0x7f - c_length = int.from_bytes(kerberos_data[pos : pos + n], 'big') + c_length = int.from_bytes(kerberos_data[pos: pos + n], 'big') pos += n else: c_length = lb - cipher_blob = kerberos_data[pos : pos + c_length] + cipher_blob = kerberos_data[pos: pos + c_length] if etype == 23: # RC4 decrypted = \ @@ -257,7 +255,7 @@ def peel_tag(data: bytes, target_tag: int): else: c_len = lb if tag == target_tag: - return data[p : p + c_len] + return data[p: p + c_len] p += c_len return None @@ -281,4 +279,4 @@ def peel_tag(data: bytes, target_tag: int): if inner_seq: seq_number = int.from_bytes(inner_seq, 'big') - return active_key, seq_number \ No newline at end of file + return active_key, seq_number diff --git a/aiowmi/kerberos/asn1.py b/aiowmi/kerberos/asn1.py index 0ac9ab2..fdc2bf8 100644 --- a/aiowmi/kerberos/asn1.py +++ b/aiowmi/kerberos/asn1.py @@ -1,5 +1,5 @@ import struct -from typing import Union +from typing import Union, Tuple def asn1_tag(tag_num: int, content: bytes) -> bytes: @@ -46,7 +46,7 @@ def asn1_ostr(val: bytes) -> bytes: return b'\x04' + asn1_len(val) + val -def get_asn1_len(data: bytes, pos: int) -> tuple[int, int]: +def get_asn1_len(data: bytes, pos: int) -> Tuple[int, int]: """Read ASN.1 lengte from data.""" b = data[pos] if b < 128: diff --git a/aiowmi/kerberos/cache.py b/aiowmi/kerberos/cache.py index 6115d4f..63bebb4 100644 --- a/aiowmi/kerberos/cache.py +++ b/aiowmi/kerberos/cache.py @@ -1,6 +1,6 @@ import pickle from logging import Logger -from typing import Optional +from typing import Optional, Tuple class KerberosCache: @@ -16,12 +16,12 @@ def __init__(self, file_path: Optional[str] = None): If None, memory is used (must keep the KerberosCache alive) """ self._file_path = file_path - self._tgt: Optional[tuple[bytes, bytes]] = None - self._tgs: Optional[tuple[bytes, bytes, int]] = None + self._tgt: Optional[Tuple[bytes, bytes]] = None + self._tgs: Optional[Tuple[bytes, bytes, int]] = None - def open(self, logger: Logger) -> tuple[ - Optional[tuple[bytes, bytes]], - Optional[tuple[bytes, bytes, int]]]: + def open(self, logger: Logger) -> Tuple[ + Optional[Tuple[bytes, bytes]], + Optional[Tuple[bytes, bytes, int]]]: if self._file_path is not None and \ (self._tgs is None or self._tgt is None): try: @@ -36,8 +36,8 @@ def open(self, logger: Logger) -> tuple[ return self._tgt, self._tgs def write(self, - tgt: tuple[bytes, bytes], - tgs: tuple[bytes, bytes, int], + tgt: Tuple[bytes, bytes], + tgs: Tuple[bytes, bytes, int], logger: Logger): self._tgt = tgt self._tgs = tgs diff --git a/aiowmi/kerberos/gss.py b/aiowmi/kerberos/gss.py index 917b0d0..1ed2964 100644 --- a/aiowmi/kerberos/gss.py +++ b/aiowmi/kerberos/gss.py @@ -1,4 +1,5 @@ import struct +from typing import Tuple from Crypto.Hash import HMAC, MD5 from Crypto.Cipher import ARC4 from ..tools import get_random_bytes @@ -98,7 +99,7 @@ def gss_unwrap_rc4(session_key: bytes, def gss_wrap_aes(session_key: bytes, data: bytes, - seq_num: int) -> tuple[bytes, bytes]: + seq_num: int) -> Tuple[bytes, bytes]: pad = (16 - (len(data) % 16)) & 15 pad_str = b'\xFF' * pad diff --git a/aiowmi/kerberos/tgs.py b/aiowmi/kerberos/tgs.py index abb5771..73adaba 100644 --- a/aiowmi/kerberos/tgs.py +++ b/aiowmi/kerberos/tgs.py @@ -1,4 +1,5 @@ import random +from typing import Tuple from Crypto.Cipher import AES from datetime import datetime, timezone, timedelta from pyasn1.codec.der import decoder, encoder @@ -105,7 +106,7 @@ def build_tgs_req(username: str, domain: str, session_key: bytes, ticket_bytes: bytes, - target_service: tuple[str, str]): + target_service: Tuple[str, str]): now = datetime.now(timezone.utc) ts_str = now.strftime("%Y%m%d%H%M%SZ").encode() till_ts = (now + timedelta(hours=8)).strftime("%Y%m%d%H%M%SZ").encode() @@ -218,7 +219,7 @@ def extract_ticket(data): def get_service_key(resp_bytes: bytes, - session_key: bytes) -> tuple[bytes, bytes, int]: + session_key: bytes) -> Tuple[bytes, bytes, int]: raw_obj, _ = decoder.decode(resp_bytes) enc_part = raw_obj.getComponentByPosition(5) @@ -254,7 +255,7 @@ def get_service_key(resp_bytes: bytes, async def get_tgs(username: str, domain: str, host: str, as_rep_bytes: bytes, base_key: bytes, kdc_host: str, - kdc_port: int = 88) -> tuple[bytes, bytes, int]: + kdc_port: int = 88) -> Tuple[bytes, bytes, int]: tgs_session_key = get_session_key(as_rep_bytes, base_key) tgs_ticket = extract_ticket(as_rep_bytes) tgs_req = build_tgs_req(username, diff --git a/aiowmi/kerberos/tgt.py b/aiowmi/kerberos/tgt.py index d066eec..3df6da9 100644 --- a/aiowmi/kerberos/tgt.py +++ b/aiowmi/kerberos/tgt.py @@ -1,4 +1,4 @@ -import hashlib +from typing import Tuple from Crypto.Protocol.KDF import PBKDF2 from Crypto.Cipher import AES from Crypto.Hash import SHA1 @@ -8,7 +8,7 @@ from .tools import parse_krb_error -def extract_salt_and_etype(error_data: bytes) -> tuple[str, int]: +def extract_salt_and_etype(error_data: bytes) -> Tuple[str, int]: if error_data[0] != 0x7e: raise ValueError("Invalid KRB-ERROR packet") @@ -78,7 +78,7 @@ def aes_string_to_key(password: str, salt: str, key_len: int = 32): async def get_tgt(username: str, password: str, domain: str, - kdc_host: str, kdc_port: int = 88) -> tuple[bytes, bytes]: + kdc_host: str, kdc_port: int = 88) -> Tuple[bytes, bytes]: as_req = build_as_req(username, domain) resp = await send_kerberos_packet(as_req, kdc_host, kdc_port) parse_krb_error(resp) diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index 1a24824..bf10723 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -138,8 +138,8 @@ def decrypt_kerberos_aes_cts(key: bytes, usage: int, cipher: bytes) -> bytes: p_start = cipher_cbc.decrypt(ciphertext[:m]) iv = ciphertext[m-block_size:m] - cn_minus_1 = ciphertext[m : m + block_size] - cn = ciphertext[m + block_size :] + cn_minus_1 = ciphertext[m: m + block_size] + cn = ciphertext[m + block_size:] tmp = aes_ecb.decrypt(cn_minus_1) diff --git a/aiowmi/kerberos/wrappers.py b/aiowmi/kerberos/wrappers.py index eeb8bac..0ecc687 100644 --- a/aiowmi/kerberos/wrappers.py +++ b/aiowmi/kerberos/wrappers.py @@ -1,4 +1,5 @@ import functools +from typing import Tuple from .gss import gss_wrap_rc4, gss_wrap_aes, gss_unwrap_rc4, gss_unwrap_aes @@ -8,7 +9,7 @@ def _sealer_rc4( seq_num: int, message_to_sign: bytes, message_to_encrypt: bytes, - session_key: bytes) -> tuple[bytes, bytes]: + session_key: bytes) -> Tuple[bytes, bytes]: return gss_wrap_rc4(session_key, message_to_encrypt, seq_num) @@ -18,7 +19,7 @@ def _sealer_aes( seq_num: int, message_to_sign: bytes, message_to_encrypt: bytes, - session_key: bytes) -> tuple[bytes, bytes]: + session_key: bytes) -> Tuple[bytes, bytes]: return gss_wrap_aes(session_key, message_to_encrypt, seq_num) @@ -61,7 +62,7 @@ def _signer_rc4( flags: int, seq_num: int, message_to_sign: bytes, - session_key: bytes) -> tuple[bytes, bytes]: + session_key: bytes) -> Tuple[bytes, bytes]: return gss_wrap_rc4(session_key, message_to_sign, seq_num) @@ -70,7 +71,7 @@ def _signer_aes( flags: int, seq_num: int, message_to_sign: bytes, - session_key: bytes) -> tuple[bytes, bytes]: + session_key: bytes) -> Tuple[bytes, bytes]: return gss_wrap_aes(session_key, message_to_sign, seq_num) From 25a83cde3559afec7dca65d9b5448096bc1c375d Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 13:49:56 +0100 Subject: [PATCH 11/23] pyright --- aiowmi/kerberos/ap_req.py | 101 ++++++-------------------------------- 1 file changed, 15 insertions(+), 86 deletions(-) diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index bd28452..467fcc9 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -109,78 +109,6 @@ def build_ap_req(username: str, return b'\x6e' + asn1_len(inner_pdu) + inner_pdu -def get_active_key(auth_bytes: bytes, - service_session_key: bytes, - etype: int) -> Tuple[Optional[bytes], int]: - active_key, seq_number = None, 0 - - etype_idx = auth_bytes.find(bytes([0x02, 0x01, etype])) - if etype_idx == -1: - return None, 0 - idx_04 = auth_bytes.find(b'\x04', etype_idx) - if idx_04 == -1: - return None, 0 - - pos = idx_04 + 1 - lb = auth_bytes[pos] - pos += 1 - if lb & 0x80: - n = lb & 0x7f - length = int.from_bytes(auth_bytes[pos: pos + n], 'big') - pos += n - else: - length = lb - cipher_blob = auth_bytes[pos: pos + length] - - if etype == 23: - decrypted = \ - decrypt_kerberos_rc4(service_session_key, 12, cipher_blob) - asn1_data = decrypted[8:] - elif etype in [17, 18]: - decrypted = \ - decrypt_kerberos_aes_cts(service_session_key, 12, cipher_blob) - asn1_data = decrypted[16:] - else: - raise ValueError(f"Invalid E-type: {etype}") - - def get_tag_data(data: bytes, target_tag: int) -> Optional[bytes]: - p = 0 - while p < len(data): - tag = data[p] - if tag == target_tag: - try: - p += 1 - lb = data[p] - p += 1 - if lb & 0x80: - n_lb = lb & 0x7f - c_len = int.from_bytes(data[p: p+n_lb], 'big') - p += n_lb - else: - c_len = lb - return data[p: p + c_len] - except Exception: - return None - p += 1 - return None - - subkey_cont = get_tag_data(asn1_data, 0xa2) - if subkey_cont: - key_seq = get_tag_data(subkey_cont, 0x30) - if key_seq: - key_val_wrapper = get_tag_data(key_seq, 0xa1) - if key_val_wrapper: - active_key = get_tag_data(key_val_wrapper, 0x04) - - seq_cont = get_tag_data(asn1_data, 0xa3) - if seq_cont: - seq_bytes = get_tag_data(seq_cont, 0x02) - if seq_bytes: - seq_number = int.from_bytes(seq_bytes, 'big') - - return active_key, seq_number - - def get_active_key(auth_bytes: bytes, service_session_key: bytes, etype: int) -> Tuple[Optional[bytes], Optional[int]]: @@ -237,7 +165,7 @@ def get_active_key(auth_bytes: bytes, else: raise ValueError(f"Invalid E-type: {etype}") - def peel_tag(data: bytes, target_tag: int): + def peel_tag(data: bytes, target_tag: int) -> Optional[bytes]: if not data: return None p = 0 @@ -265,18 +193,19 @@ def peel_tag(data: bytes, target_tag: int): if current and current.startswith(b'\x30'): current = peel_tag(current, 0x30) - subkey_cont = peel_tag(current, 0xa2) - if subkey_cont: - k_seq = peel_tag(subkey_cont, 0x30) - if k_seq: - k_val_wrap = peel_tag(k_seq, 0xa1) - if k_val_wrap: - active_key = peel_tag(k_val_wrap, 0x04) - - seq_cont = peel_tag(current, 0xa3) - if seq_cont: - inner_seq = peel_tag(seq_cont, 0x02) - if inner_seq: - seq_number = int.from_bytes(inner_seq, 'big') + if current: + subkey_cont = peel_tag(current, 0xa2) + if subkey_cont: + k_seq = peel_tag(subkey_cont, 0x30) + if k_seq: + k_val_wrap = peel_tag(k_seq, 0xa1) + if k_val_wrap: + active_key = peel_tag(k_val_wrap, 0x04) + + seq_cont = peel_tag(current, 0xa3) + if seq_cont: + inner_seq = peel_tag(seq_cont, 0x02) + if inner_seq: + seq_number = int.from_bytes(inner_seq, 'big') return active_key, seq_number From eb1b870e830289d0bcda7e17c5fa1ac4c5daa31c Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 13:51:46 +0100 Subject: [PATCH 12/23] no access denied --- aiowmi/connection.py | 55 +++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index 98a01b3..d872299 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -1,7 +1,7 @@ import asyncio from typing import Optional, Callable from Crypto.Cipher import ARC4 -from .exceptions import AccessDenied, NoNewActiveKey, BindNak +from .exceptions import AccessDenied, BindNak from .kerberos.ap_req import build_ap_req, wrap_gss_kerberos, get_active_key from .kerberos.cache import KerberosCache from .kerberos.krb5_pdu import get_neg_token, build_alter_context @@ -206,45 +206,28 @@ async def _negotiate_kerberos(self): self._kerberos_cache.write(self._tgt, self._tgs, logger) - async def negotiate_kerberos(self, max_retry: int = 1) -> Protocol: + async def negotiate_kerberos(self) -> Protocol: if not self._domain: raise Exception('domain is required for Kerberos authentication') - attempt = 1 has_keys = self._tgt is not None and self._tgt is not None - while True: - if self._protocol is None: - await self.connect(timeout=self._timeout) - assert self._protocol - proto = self._protocol - proto._auth_level = RPC_C_AUTHN_LEVEL_PKT_PRIVACY - proto._context_id = 79231 - - if not has_keys: - logger.info('Start Kerberos negotiation for TGT/TGS') - await self._negotiate_kerberos() - has_keys = True - else: - logger.info('Using Kerberos TGT/TGS from cache') - try: - await self._bind_kerberos(IID_IRemoteSCMActivator, proto) - iface = await self._if_binding(proto, - bind_func=self._bind_kerberos, - m_auth_level=proto._auth_level) - except (AccessDenied, NoNewActiveKey, BindNak) as e: - raise - msg = str(e) or type(e).__name__ - if attempt <= max_retry: - logger.info(f'{msg} (attempt {attempt}/{max_retry})') - if attempt % 2 == 0: - has_keys = False # attemp to get new keys - self.close() - await asyncio.sleep(0.1 * attempt) - attempt += 1 - continue - raise - else: - break + if self._protocol is None: + await self.connect(timeout=self._timeout) + assert self._protocol + proto = self._protocol + proto._auth_level = RPC_C_AUTHN_LEVEL_PKT_PRIVACY + proto._context_id = 79231 + + if not has_keys: + logger.info('Start Kerberos negotiation for TGT/TGS') + await self._negotiate_kerberos() + has_keys = True + else: + logger.info('Using Kerberos TGT/TGS from cache') + await self._bind_kerberos(IID_IRemoteSCMActivator, proto) + iface = await self._if_binding(proto, + bind_func=self._bind_kerberos, + m_auth_level=proto._auth_level) return iface async def _bind_kerberos(self, iid: bytes, proto: Protocol): From e6ed8b1b871d5e87c68399ccd33b615345af9d1b Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 13:54:52 +0100 Subject: [PATCH 13/23] fix --- aiowmi/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index d872299..f46b493 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -209,7 +209,7 @@ async def _negotiate_kerberos(self): async def negotiate_kerberos(self) -> Protocol: if not self._domain: raise Exception('domain is required for Kerberos authentication') - has_keys = self._tgt is not None and self._tgt is not None + has_keys = self._tgt is not None and self._tgs is not None if self._protocol is None: await self.connect(timeout=self._timeout) assert self._protocol From f268ac7541251c778e534cc9288c2020734c72d7 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 14:48:49 +0100 Subject: [PATCH 14/23] Pre-auth failed --- aiowmi/connection.py | 8 +++-- aiowmi/kerberos/ap_req.py | 24 +-------------- aiowmi/kerberos/tgt.py | 64 +++++++++++++++++++++++++-------------- aiowmi/kerberos/tools.py | 24 +++++++++++++++ 4 files changed, 72 insertions(+), 48 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index f46b493..1913dfc 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -226,8 +226,8 @@ async def negotiate_kerberos(self) -> Protocol: await self._bind_kerberos(IID_IRemoteSCMActivator, proto) iface = await self._if_binding(proto, - bind_func=self._bind_kerberos, - m_auth_level=proto._auth_level) + bind_func=self._bind_kerberos, + m_auth_level=proto._auth_level) return iface async def _bind_kerberos(self, iid: bytes, proto: Protocol): @@ -249,11 +249,13 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): service_session_key, etype) if active_key is None: + logger.warning("No new active key, use existing service key") active_key = service_session_key if seq_number is None: + logger.warning("No new seq_number, seq_number 1") seq_number = 1 - proto._dcom._seq_num += 1 + proto._dcom._seq_num += 1 # TODO: I'm not sure about this proto._auth_type = rpc_bind_ack.auth.auth_type proto._auth_level = rpc_bind_ack.auth.auth_level diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index 467fcc9..f240a2d 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -1,7 +1,7 @@ from typing import Optional, Tuple from datetime import datetime, timezone from .asn1 import asn1_len, asn1_tag, asn1_gt, asn1_int, asn1_ostr -from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4 +from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4, peel_tag from .tools import decrypt_kerberos_aes_cts, encrypt_kerberos_aes_cts from .const import OID_SPNEGO, OID_IETF_KRB5, OID_MS_KRB5 from ..exceptions import NoNewActiveKey @@ -165,28 +165,6 @@ def get_active_key(auth_bytes: bytes, else: raise ValueError(f"Invalid E-type: {etype}") - def peel_tag(data: bytes, target_tag: int) -> Optional[bytes]: - if not data: - return None - p = 0 - while p < len(data): - tag = data[p] - p += 1 - if p >= len(data): - break - lb = data[p] - p += 1 - if lb & 0x80: - n_lb = lb & 0x7f - c_len = int.from_bytes(data[p: p+n_lb], 'big') - p += n_lb - else: - c_len = lb - if tag == target_tag: - return data[p: p + c_len] - p += c_len - return None - current = asn1_data if asn1_data.startswith(b'\x7b'): current = peel_tag(asn1_data, 0x7b) diff --git a/aiowmi/kerberos/tgt.py b/aiowmi/kerberos/tgt.py index 3df6da9..009ef9c 100644 --- a/aiowmi/kerberos/tgt.py +++ b/aiowmi/kerberos/tgt.py @@ -5,7 +5,7 @@ from .as_req import build_as_req, build_full_as_req from .kdc import send_kerberos_packet from .asn1 import get_asn1_len -from .tools import parse_krb_error +from .tools import parse_krb_error, peel_tag def extract_salt_and_etype(error_data: bytes) -> Tuple[str, int]: @@ -14,31 +14,51 @@ def extract_salt_and_etype(error_data: bytes) -> Tuple[str, int]: e_data_idx = error_data.find(b'\xac') if e_data_idx == -1: - raise ValueError("e-data (Tag 12) not found") + raise ValueError("e-data (Tag 12) not found in KRB-ERROR") - salt_tag_idx = error_data.find(b'\x1b', e_data_idx) + etype_18_marker = b'\x02\x01\x12' + marker_idx = error_data.find(etype_18_marker, e_data_idx) + + etype = 18 + if marker_idx == -1: + etype_17_marker = b'\x02\x01\x11' + marker_idx = error_data.find(etype_17_marker, e_data_idx) + etype = 17 + + if marker_idx == -1: + raise ValueError("No AES (17/18) etype found in e-data") + + salt_tag_idx = error_data.find(b'\xa1', marker_idx) if salt_tag_idx == -1: - # Default to no salt/empty string if not found - salt = "" + raise ValueError(f"Salt tag (0xa1) not found for etype {etype}") + + salt_start_idx = -1 + for tag in [b'\x1b', b'\x04', b'\x1d']: + tag_pos = error_data.find(tag, salt_tag_idx, salt_tag_idx + 15) + if tag_pos != -1: + salt_start_idx = tag_pos + break + + if salt_start_idx == -1: + raise ValueError( + "Could not find string tag (1b/04) within salt container") + + pos = salt_start_idx + 1 + length_byte = error_data[pos] + pos += 1 + + if length_byte & 0x80: + n_bytes = length_byte & 0x7f + salt_len = int.from_bytes(error_data[pos: pos + n_bytes], 'big') + pos += n_bytes else: - salt_len, salt_len_size = get_asn1_len(error_data, salt_tag_idx + 1) - salt_start = salt_tag_idx + 1 + salt_len_size - salt = error_data[salt_start: salt_start + salt_len].decode('utf-8') - - etype = 18 # Default to AES256 if we can't find it - etype_search_start = error_data.find( - b'\xa0', - e_data_idx, - salt_tag_idx if salt_tag_idx != -1 else None) - - if etype_search_start != -1: - # The etype is an integer inside context tag 0xa0 - val_idx = error_data.find(b'\x02', etype_search_start) - if val_idx != -1: - e_len = error_data[val_idx + 1] - offset = val_idx + 2 - etype = int.from_bytes(error_data[offset: offset + e_len], 'big') + salt_len = length_byte + + salt_bytes = error_data[pos: pos + salt_len] + if not salt_bytes: + raise ValueError("Extracted salt is empty") + salt = salt_bytes.decode('utf-8', errors='replace') return salt, etype diff --git a/aiowmi/kerberos/tools.py b/aiowmi/kerberos/tools.py index bf10723..77311a4 100644 --- a/aiowmi/kerberos/tools.py +++ b/aiowmi/kerberos/tools.py @@ -354,3 +354,27 @@ def parse_krb_error(data: bytes): raise KerberosErr( KRB_ERRORS.get(code, f"Unknown Kerberos Error: {code}") ) + + +def peel_tag(data: bytes, target_tag: int) -> Optional[bytes]: + if not data: + return None + p = 0 + while p < len(data): + tag = data[p] + p += 1 + if p >= len(data): + break + lb = data[p] + p += 1 + if lb & 0x80: + n_lb = lb & 0x7f + c_len = int.from_bytes(data[p: p+n_lb], 'big') + p += n_lb + else: + c_len = lb + if tag == target_tag: + return data[p: p + c_len] + p += c_len + + return None From 4e1bb772f1218dbdd009db13ff44ea8751c7a5f5 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 14:49:56 +0100 Subject: [PATCH 15/23] unused import --- aiowmi/kerberos/tgt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aiowmi/kerberos/tgt.py b/aiowmi/kerberos/tgt.py index 009ef9c..1750242 100644 --- a/aiowmi/kerberos/tgt.py +++ b/aiowmi/kerberos/tgt.py @@ -4,7 +4,6 @@ from Crypto.Hash import SHA1 from .as_req import build_as_req, build_full_as_req from .kdc import send_kerberos_packet -from .asn1 import get_asn1_len from .tools import parse_krb_error, peel_tag From df2b3967b4fce50f0ae3d5bdbf525e05dc1e9822 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 15:24:23 +0100 Subject: [PATCH 16/23] fix rc4 --- aiowmi/connection.py | 7 +-- aiowmi/kerberos/ap_req.py | 91 ++++++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index 1913dfc..7e1e47e 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -253,9 +253,10 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): active_key = service_session_key if seq_number is None: - logger.warning("No new seq_number, seq_number 1") - seq_number = 1 - proto._dcom._seq_num += 1 # TODO: I'm not sure about this + logger.warning( + "No new seq_number; " + f"proto._dcom._seq_num = {proto._dcom._seq_num}") + seq_number = proto._dcom.get_seq_num() proto._auth_type = rpc_bind_ack.auth.auth_type proto._auth_level = rpc_bind_ack.auth.auth_level diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index f240a2d..27e55f3 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -1,7 +1,7 @@ from typing import Optional, Tuple from datetime import datetime, timezone from .asn1 import asn1_len, asn1_tag, asn1_gt, asn1_int, asn1_ostr -from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4, peel_tag +from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4,peel_tag from .tools import decrypt_kerberos_aes_cts, encrypt_kerberos_aes_cts from .const import OID_SPNEGO, OID_IETF_KRB5, OID_MS_KRB5 from ..exceptions import NoNewActiveKey @@ -109,6 +109,68 @@ def build_ap_req(username: str, return b'\x6e' + asn1_len(inner_pdu) + inner_pdu +def _rc4_helper(asn1_data: bytes) -> Tuple[Optional[bytes], Optional[int]]: + active_key = None + seq_number = None + + for i in range(len(asn1_data) - 10): + if asn1_data[i] == 0xa2: + content = peel_tag(asn1_data[i:], 0xa2) + if content: + val_idx = content.find(b'\xa1') + if val_idx != -1: + val_wrap = peel_tag(content[val_idx:], 0xa1) + if val_wrap: + res_key = peel_tag(val_wrap, 0x04) + if res_key and len(res_key) == 16: + active_key = res_key + break + + for i in range(len(asn1_data) - 2): + if asn1_data[i] == 0xa3: + content = peel_tag(asn1_data[i:], 0xa3) + if content: + # Zoek de Integer tag (0x02) binnen de container + inner = peel_tag(content, 0x02) + if inner: + seq_number = int.from_bytes(inner, 'big') + break + # Fallback: als 0x02 ontbreekt, neem de rauwe content (max 8 bytes voor een Long) + elif 0 < len(content) <= 8: + seq_number = int.from_bytes(content, 'big') + break + + return active_key, seq_number + + +def _aes_helper(asn1_data: bytes) -> Tuple[Optional[bytes], Optional[int]]: + active_key = None + seq_number = None + + current = asn1_data + if asn1_data.startswith(b'\x7b'): + current = peel_tag(asn1_data, 0x7b) + if current and current.startswith(b'\x30'): + current = peel_tag(current, 0x30) + + if current: + subkey_cont = peel_tag(current, 0xa2) + if subkey_cont: + k_seq = peel_tag(subkey_cont, 0x30) + if k_seq: + k_val_wrap = peel_tag(k_seq, 0xa1) + if k_val_wrap: + active_key = peel_tag(k_val_wrap, 0x04) + + seq_cont = peel_tag(current, 0xa3) + if seq_cont: + inner_seq = peel_tag(seq_cont, 0x02) + if inner_seq: + seq_number = int.from_bytes(inner_seq, 'big') + + return active_key, seq_number + + def get_active_key(auth_bytes: bytes, service_session_key: bytes, etype: int) -> Tuple[Optional[bytes], Optional[int]]: @@ -157,33 +219,14 @@ def get_active_key(auth_bytes: bytes, if etype == 23: # RC4 decrypted = \ decrypt_kerberos_rc4(service_session_key, 12, cipher_blob) - asn1_data = decrypted[8:] # Skip 8 bytes confounder + # Skip 8 bytes confounder + active_key, seq_number = _rc4_helper(decrypted[8:]) elif etype in [17, 18]: # AES decrypted = \ decrypt_kerberos_aes_cts(service_session_key, 12, cipher_blob) - asn1_data = decrypted[16:] # Skip 16 bytes confounder + # Skip 16 bytes confounder + active_key, seq_number = _aes_helper(decrypted[16:]) else: raise ValueError(f"Invalid E-type: {etype}") - current = asn1_data - if asn1_data.startswith(b'\x7b'): - current = peel_tag(asn1_data, 0x7b) - if current and current.startswith(b'\x30'): - current = peel_tag(current, 0x30) - - if current: - subkey_cont = peel_tag(current, 0xa2) - if subkey_cont: - k_seq = peel_tag(subkey_cont, 0x30) - if k_seq: - k_val_wrap = peel_tag(k_seq, 0xa1) - if k_val_wrap: - active_key = peel_tag(k_val_wrap, 0x04) - - seq_cont = peel_tag(current, 0xa3) - if seq_cont: - inner_seq = peel_tag(seq_cont, 0x02) - if inner_seq: - seq_number = int.from_bytes(inner_seq, 'big') - return active_key, seq_number From 6172bd6f17382d4259a59f0316074a6495772cc8 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 15:24:59 +0100 Subject: [PATCH 17/23] pep --- aiowmi/kerberos/ap_req.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index 27e55f3..f73f47b 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -1,7 +1,7 @@ from typing import Optional, Tuple from datetime import datetime, timezone from .asn1 import asn1_len, asn1_tag, asn1_gt, asn1_int, asn1_ostr -from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4,peel_tag +from .tools import encrypt_kerberos_rc4, decrypt_kerberos_rc4, peel_tag from .tools import decrypt_kerberos_aes_cts, encrypt_kerberos_aes_cts from .const import OID_SPNEGO, OID_IETF_KRB5, OID_MS_KRB5 from ..exceptions import NoNewActiveKey @@ -130,12 +130,10 @@ def _rc4_helper(asn1_data: bytes) -> Tuple[Optional[bytes], Optional[int]]: if asn1_data[i] == 0xa3: content = peel_tag(asn1_data[i:], 0xa3) if content: - # Zoek de Integer tag (0x02) binnen de container inner = peel_tag(content, 0x02) if inner: seq_number = int.from_bytes(inner, 'big') break - # Fallback: als 0x02 ontbreekt, neem de rauwe content (max 8 bytes voor een Long) elif 0 < len(content) <= 8: seq_number = int.from_bytes(content, 'big') break From eff7f1031b1a5626ce342b5044d4d5bdd1c9bdd5 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 15:29:48 +0100 Subject: [PATCH 18/23] Fix comment --- aiowmi/kerberos/tgs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiowmi/kerberos/tgs.py b/aiowmi/kerberos/tgs.py index 73adaba..ef9ca95 100644 --- a/aiowmi/kerberos/tgs.py +++ b/aiowmi/kerberos/tgs.py @@ -161,8 +161,8 @@ def build_tgs_req(username: str, )) ) - # Etype list: RC4 (23), AES128 (16), AES256 (18) - # etypes = asn1_int(23) + asn1_int(16) + asn1_int(18) + # Etype list: RC4 (23), AES128 (17), AES256 (18) + # etypes = asn1_int(23) + asn1_int(17) + asn1_int(18) etypes = asn1_int(18) nonce = random.getrandbits(31) From 71945e2a43bbae04f84dabb272ce863512eb7687 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 15:30:31 +0100 Subject: [PATCH 19/23] . --- aiowmi/kerberos/as_req.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiowmi/kerberos/as_req.py b/aiowmi/kerberos/as_req.py index 1326a74..695d409 100644 --- a/aiowmi/kerberos/as_req.py +++ b/aiowmi/kerberos/as_req.py @@ -61,7 +61,7 @@ def build_as_req(username: str, domain: str, pa_enc: bytes = b'') -> bytes: # Renewable time: 1 day (must be >= till) rtime_time = (now + timedelta(days=1)).strftime("%Y%m%d%H%M%SZ").encode() - # Etype list: RC4 (23), AES128 (16), AES256 (18) + # Etype list: RC4 (23), AES128 (17), AES256 (18) etypes = asn1_int(18) # kdc-options: Forwardable, Proxiable, Renewable, Canonicalize @@ -149,7 +149,7 @@ def build_full_as_req(username: str, domain: str, base_key: bytes, etype: int): asn1_tag(1, asn1_seq(asn1_gs(b"krbtgt") + asn1_gs(domcaps.encode()))) ) - # Etype list: RC4 (23), AES128 (16), AES256 (18) + # Etype list: RC4 (23), AES128 (17), AES256 (18) etypes = asn1_int(18) req_body_content = ( From c59d9129090c52513ed030afb52a4ddaf8a056fa Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 15:32:07 +0100 Subject: [PATCH 20/23] tuple --- aiowmi/kerberos/ap_req.py | 2 +- aiowmi/kerberos/krb5_pdu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index f73f47b..0f17ac6 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -219,7 +219,7 @@ def get_active_key(auth_bytes: bytes, decrypt_kerberos_rc4(service_session_key, 12, cipher_blob) # Skip 8 bytes confounder active_key, seq_number = _rc4_helper(decrypted[8:]) - elif etype in [17, 18]: # AES + elif etype in (17, 18): # AES decrypted = \ decrypt_kerberos_aes_cts(service_session_key, 12, cipher_blob) # Skip 16 bytes confounder diff --git a/aiowmi/kerberos/krb5_pdu.py b/aiowmi/kerberos/krb5_pdu.py index a364de1..aa9de65 100644 --- a/aiowmi/kerberos/krb5_pdu.py +++ b/aiowmi/kerberos/krb5_pdu.py @@ -20,7 +20,7 @@ def get_neg_token(service_session_key: bytes, seq_number: int, etype: int): inner_seq = b'\x30' + asn1_len(enc_ap_rep_body) + enc_ap_rep_body plaintext = b'\x7b' + asn1_len(inner_seq) + inner_seq - if etype in [17, 18]: # AES-128 / AES-256 + if etype in (17, 18): # AES-128 / AES-256 enc_data = encrypt_kerberos_aes_cts(service_session_key, 12, plaintext) current_etype = etype else: # RC4 (23) From b963be6162eea6410ab4375fd8efc0bdf274c6ac Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Thu, 19 Mar 2026 17:13:45 +0100 Subject: [PATCH 21/23] cleanup --- aiowmi/connection.py | 2 +- aiowmi/kerberos/ap_req.py | 18 +++++------------- aiowmi/kerberos/const.py | 2 -- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/aiowmi/connection.py b/aiowmi/connection.py index 7e1e47e..46c7d83 100644 --- a/aiowmi/connection.py +++ b/aiowmi/connection.py @@ -237,7 +237,7 @@ async def _bind_kerberos(self, iid: bytes, proto: Protocol): ticket, service_session_key, etype) - auth_value = wrap_gss_kerberos(ap_req, etype) + auth_value = wrap_gss_kerberos(ap_req) bind_pkg = proto._dcom.get_bind_kerberos_pkg( iid, auth_value, diff --git a/aiowmi/kerberos/ap_req.py b/aiowmi/kerberos/ap_req.py index 0f17ac6..0cfaeec 100644 --- a/aiowmi/kerberos/ap_req.py +++ b/aiowmi/kerberos/ap_req.py @@ -7,20 +7,11 @@ from ..exceptions import NoNewActiveKey -def wrap_gss_kerberos(ap_req_bytes: bytes, etype: int): - if etype == 18: - outer_oid = OID_IETF_KRB5 - inner_oid = OID_MS_KRB5 - elif etype == 17: - outer_oid = OID_IETF_KRB5 - inner_oid = OID_MS_KRB5 - elif etype == 23: - outer_oid = OID_MS_KRB5 - inner_oid = OID_MS_KRB5 - else: - raise ValueError(f"Invalid E-type: {etype}") +def wrap_gss_kerberos(ap_req_bytes: bytes): + outer_oid = OID_IETF_KRB5 + inner_oid = OID_MS_KRB5 - mech_types_inner = b'\x30\x0b' + outer_oid + mech_types_inner = b'\x30' + asn1_len(outer_oid) + outer_oid mech_types = asn1_tag(0, mech_types_inner) inner_gss_content = inner_oid + b'\x01\x00' + ap_req_bytes @@ -32,6 +23,7 @@ def wrap_gss_kerberos(ap_req_bytes: bytes, etype: int): neg_init = asn1_tag(0, b'\x30' + asn1_len(neg_body) + neg_body) final_payload = OID_SPNEGO + neg_init + return b'\x60' + asn1_len(final_payload) + final_payload diff --git a/aiowmi/kerberos/const.py b/aiowmi/kerberos/const.py index 75f0064..2da134d 100644 --- a/aiowmi/kerberos/const.py +++ b/aiowmi/kerberos/const.py @@ -1,5 +1,3 @@ -OID_KERBEROS_V5 = b'\x06\t\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' -OID_MS_LEGACY_KRB = b'\x06\t\x2a\x86\x48\x82\xf7\x12\x01\x02\x02' OID_SPNEGO = b'\x06\x06\x2b\x06\x01\x05\x05\x02' OID_IETF_KRB5 = b'\x06\x09\x2a\x86\x48\x82\xf7\x12\x01\x02\x02' OID_MS_KRB5 = b'\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02' From 284d78df3257cf25fd3a3482c74d3ee84c2cf3ca Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 19 Mar 2026 22:53:32 +0100 Subject: [PATCH 22/23] fix header --- aiowmi/kerberos/gss.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aiowmi/kerberos/gss.py b/aiowmi/kerberos/gss.py index 1ed2964..6f8e9e4 100644 --- a/aiowmi/kerberos/gss.py +++ b/aiowmi/kerberos/gss.py @@ -103,8 +103,11 @@ def gss_wrap_aes(session_key: bytes, pad = (16 - (len(data) % 16)) & 15 pad_str = b'\xFF' * pad + rcc = 0 header_for_hash = ( - b'\x05\x04\x06\xff\x00\x00\x00\x00' + + b'\x05\x04\x06\xff' + + struct.pack('>H', pad) + + struct.pack('>H', rcc) + struct.pack('>Q', seq_num) ) From 12e725cddb3fbd8a286c9e7086686350fed889d8 Mon Sep 17 00:00:00 2001 From: Jeroen van der Heijden Date: Fri, 20 Mar 2026 07:43:07 +0100 Subject: [PATCH 23/23] ci 9 --- .github/workflows/ci.yml | 2 +- aiowmi/kerberos/tgs.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa51cee..f7adbae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v9 + - uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: diff --git a/aiowmi/kerberos/tgs.py b/aiowmi/kerberos/tgs.py index ef9ca95..2d1b238 100644 --- a/aiowmi/kerberos/tgs.py +++ b/aiowmi/kerberos/tgs.py @@ -162,8 +162,7 @@ def build_tgs_req(username: str, ) # Etype list: RC4 (23), AES128 (17), AES256 (18) - # etypes = asn1_int(23) + asn1_int(17) + asn1_int(18) - etypes = asn1_int(18) + etypes = asn1_int(23) + asn1_int(17) + asn1_int(18) nonce = random.getrandbits(31) req_body_content = (