sqlmap/extra/kerberos/crypto.py
2026-07-24 22:26:43 +02:00

256 lines
9.9 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Dependency-free Kerberos "simplified profile" crypto (RFC 3961) for the AES-CTS-HMAC-SHA1-96 etypes
# (RFC 3962: aes128-cts-hmac-sha1-96 = etype 17, aes256-cts-hmac-sha1-96 = etype 18), built on the
# pure-Python AES core. Provides n-fold, DK/DR key derivation, string-to-key (PBKDF2-HMAC-SHA1) and
# authenticated encrypt/decrypt. Validated against the RFC 3961 A / RFC 3962 B test vectors.
# Python 2.7 / 3.x.
import binascii
import hashlib
import hmac
import os
import struct
from extra.kerberos.aes import AES, _xor # _xor reused (no second copy) from the AES core
from lib.request.ntlm import _md4 # proven RFC 1320 MD4 (reused, not re-derived)
# RFC 3962 string-to-key default work factor when the KDC advertises no explicit count
DEFAULT_PBKDF2_ITERATIONS = 4096
def _to_bytes(value):
return value if isinstance(value, bytes) else value.encode("utf-8")
def _b2i(data):
data = bytearray(data)
return int(binascii.hexlify(bytes(data)), 16) if data else 0
def _i2b(n, length):
if length <= 0:
return bytearray()
return bytearray(binascii.unhexlify(("%0*x" % (length * 2, n))[-length * 2:]))
def _eq(a, b):
return hmac.compare_digest(bytes(a), bytes(b)) if hasattr(hmac, "compare_digest") else (bytes(a) == bytes(b))
def _pbkdf2(password, salt, iterations, dklen):
"""PBKDF2-HMAC-SHA1. Uses the stdlib primitive when present, with an RFC 2898 fallback for the
pre-2.7.8 interpreters that lack hashlib.pbkdf2_hmac."""
if hasattr(hashlib, "pbkdf2_hmac"):
return hashlib.pbkdf2_hmac("sha1", password, salt, iterations, dklen)
out = bytearray()
block = 1
while len(out) < dklen:
u = hmac.new(password, salt + struct.pack(">I", block), hashlib.sha1).digest()
acc = bytearray(u)
for _ in range(iterations - 1):
u = hmac.new(password, u, hashlib.sha1).digest()
acc = bytearray(x ^ y for x, y in zip(acc, bytearray(u)))
out += acc
block += 1
return bytes(out[:dklen])
def _rotate_right(data, nbits):
"""Rotate a byte string right by 'nbits' bits, preserving its length."""
data = bytearray(data)
if not data:
return data
total = len(data) * 8
nbits %= total
value = ((_b2i(data) >> nbits) | (_b2i(data) << (total - nbits))) & ((1 << total) - 1)
return _i2b(value, len(data))
def nfold(data, nbytes):
"""RFC 3961 n-fold: spread 'data' over 'nbytes' bytes via 13-bit rotated copies summed with an
end-around carry (ones-complement addition)."""
data = bytearray(data)
def gcd(a, b):
while b:
a, b = b, a % b
return a
lcm = len(data) * nbytes // gcd(len(data), nbytes)
buf = bytearray()
rotation = 0
while len(buf) < lcm:
buf += _rotate_right(data, rotation)
rotation += 13
bits = 8 * nbytes
mask = (1 << bits) - 1
acc = sum(_b2i(buf[off:off + nbytes]) for off in range(0, lcm, nbytes))
while acc > mask:
acc = (acc & mask) + (acc >> bits)
return bytes(_i2b(acc, nbytes))
class AESEnctype(object):
"""AES-CTS-HMAC-SHA1-96 simplified-profile enctype (RFC 3962). keysize 16 => etype 17, 32 => 18."""
blocksize = 16
macsize = 12
def __init__(self, keysize):
self.keysize = keysize
self.cksumtype = 16 if keysize == 32 else 15 # hmac-sha1-96-aes256 / -aes128
def checksum(self, key, usage, data):
"""Keyed checksum (RFC 3961 get_mic): HMAC-SHA1-96 under the checksum key DK(key, usage|0x99)."""
kc = self.dk(key, struct.pack(">IB", usage, 0x99))
return hmac.new(kc, data, hashlib.sha1).digest()[:self.macsize]
# --- key schedule -------------------------------------------------------------------------------
def _dr(self, key, constant):
"""RFC 3961 DR: iterate the single-block cipher over the (n-folded) constant to seedsize."""
aes = AES(key)
block = nfold(constant, self.blocksize)
out = bytearray()
while len(out) < self.keysize:
block = aes.encryptBlock(block) # single 16-byte block => CBC(iv=0) == ECB
out += bytearray(block)
return bytes(out[:self.keysize])
def dk(self, key, constant):
"""RFC 3961 DK = random-to-key(DR(...)); random-to-key is the identity for AES."""
return self._dr(key, constant)
def string2key(self, password, salt, iterations=None):
"""RFC 3962 string-to-key: DK(PBKDF2-HMAC-SHA1(password, salt), "kerberos")."""
iterations = iterations or DEFAULT_PBKDF2_ITERATIONS
tkey = _pbkdf2(_to_bytes(password), _to_bytes(salt), iterations, self.keysize)
return self.dk(tkey, b"kerberos")
# --- CBC ciphertext stealing (RFC 3962, CS3: always swap the final two blocks) ------------------
def _basicEncrypt(self, key, data):
aes = AES(key)
padded = data + b"\x00" * ((-len(data)) % self.blocksize)
ct = aes.cbcEncrypt(b"\x00" * self.blocksize, padded)
if len(data) > self.blocksize:
lastlen = len(data) % self.blocksize or self.blocksize
ct = ct[:-2 * self.blocksize] + ct[-self.blocksize:] + ct[-2 * self.blocksize:-self.blocksize][:lastlen]
return ct
def _basicDecrypt(self, key, data):
aes = AES(key)
if len(data) == self.blocksize:
return aes.decryptBlock(data)
blocks = [bytearray(data[p:p + self.blocksize]) for p in range(0, len(data), self.blocksize)]
lastlen = len(blocks[-1])
prev = bytearray(self.blocksize)
out = bytearray()
for block in blocks[:-2]:
out += bytearray(_xor(aes.decryptBlock(bytes(block)), prev))
prev = block
decrypted = bytearray(aes.decryptBlock(bytes(blocks[-2])))
lastPlain = _xor(decrypted[:lastlen], blocks[-1])
omitted = decrypted[lastlen:]
secondLast = _xor(aes.decryptBlock(bytes(blocks[-1] + omitted)), prev)
return bytes(out) + secondLast + lastPlain
# --- authenticated encryption (RFC 3961 section 5.3) --------------------------------------------
def _keys(self, key, usage):
ke = self.dk(key, struct.pack(">IB", usage, 0xAA))
ki = self.dk(key, struct.pack(">IB", usage, 0x55))
return ke, ki
def encrypt(self, key, usage, plaintext, confounder=None):
ke, ki = self._keys(key, usage)
if confounder is None:
confounder = os.urandom(self.blocksize)
basic = confounder + plaintext
return self._basicEncrypt(ke, basic) + hmac.new(ki, basic, hashlib.sha1).digest()[:self.macsize]
def decrypt(self, key, usage, ciphertext):
if len(ciphertext) < self.blocksize + self.macsize: # confounder block + HMAC; guards a hostile short reply
raise ValueError("Kerberos ciphertext too short")
ke, ki = self._keys(key, usage)
ct, mac = ciphertext[:-self.macsize], ciphertext[-self.macsize:]
basic = self._basicDecrypt(ke, ct)
if not _eq(mac, hmac.new(ki, basic, hashlib.sha1).digest()[:self.macsize]):
raise ValueError("Kerberos integrity check failed (wrong key or corrupted ciphertext)")
return basic[self.blocksize:]
def _rc4(key, data):
"""RC4 (ARCFOUR) stream cipher."""
key, data = bytearray(key), bytearray(data)
if not key:
raise ValueError("RC4 requires a non-empty key")
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) & 0xff
s[i], s[j] = s[j], s[i]
out = bytearray(len(data))
i = j = 0
for n in range(len(data)):
i = (i + 1) & 0xff
j = (j + s[i]) & 0xff
s[i], s[j] = s[j], s[i]
out[n] = data[n] ^ s[(s[i] + s[j]) & 0xff]
return bytes(out)
class RC4Enctype(object):
"""rc4-hmac (etype 23, RFC 4757). The long-term key is the NT hash MD4(UTF-16LE(password)); the
salt and iteration count are unused. Legacy, but still enabled in many AD environments."""
keysize = 16
cksumtype = -138 # hmac-md5
def string2key(self, password, salt=None, iterations=None):
# the password is text; encode it UTF-16LE (in py2 a str is bytes, so decode to text first)
if isinstance(password, bytes):
password = password.decode("utf-8")
return _md4(password.encode("utf-16-le"))
@staticmethod
def _usage(usage):
# RFC 4757 section 3: a couple of Kerberos usages map to Microsoft-specific values
return struct.pack("<I", {3: 8, 9: 8}.get(usage, usage))
def encrypt(self, key, usage, plaintext, confounder=None):
if confounder is None:
confounder = os.urandom(8)
ki = hmac.new(key, self._usage(usage), hashlib.md5).digest()
cksum = hmac.new(ki, confounder + plaintext, hashlib.md5).digest()
ke = hmac.new(ki, cksum, hashlib.md5).digest()
return cksum + _rc4(ke, confounder + plaintext)
def decrypt(self, key, usage, ciphertext):
if len(ciphertext) < 24:
raise ValueError("rc4-hmac ciphertext too short")
cksum, data = ciphertext[:16], ciphertext[16:]
ki = hmac.new(key, self._usage(usage), hashlib.md5).digest()
ke = hmac.new(ki, cksum, hashlib.md5).digest()
plaintext = _rc4(ke, data)
if not _eq(cksum, hmac.new(ki, plaintext, hashlib.md5).digest()):
raise ValueError("Kerberos integrity check failed (wrong key or corrupted ciphertext)")
return plaintext[8:] # strip the 8-byte confounder
def checksum(self, key, usage, data):
ksign = hmac.new(key, b"signaturekey\x00", hashlib.md5).digest()
return hmac.new(ksign, hashlib.md5(self._usage(usage) + bytes(data)).digest(), hashlib.md5).digest()
# etype number -> enctype implementation
ENCTYPES = {
17: AESEnctype(16),
18: AESEnctype(32),
23: RC4Enctype(),
}