Adding Negotiate/Kerberos authentication (#3404)

This commit is contained in:
Miroslav Štampar 2026-07-24 22:26:43 +02:00
parent 2d9e9ed959
commit 1bd1b457d7
12 changed files with 1479 additions and 9 deletions

View file

@ -0,0 +1,6 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""

174
extra/kerberos/aes.py Normal file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Dependency-free AES (FIPS-197) block cipher with CBC mode, supporting 128- and 256-bit keys. It is
# the primitive underneath Kerberos' AES-CTS-HMAC-SHA1-96 (RFC 3962) etypes, kept pure-Python so
# '--auth-type=Negotiate' needs no third-party crypto library. Validated against the FIPS-197
# known-answer vectors. Python 2.7 / 3.x.
#
# The state is a flat list of 16 ints in AES column-major order: byte i holds row (i % 4), column
# (i // 4), i.e. column c occupies positions [4*c : 4*c + 4].
def _gmul(a, b):
"""Multiplication in GF(2**8) with the AES reduction polynomial 0x11b."""
p = 0
for _ in range(8):
if b & 1:
p ^= a
high = a & 0x80
a = (a << 1) & 0xff
if high:
a ^= 0x1b
b >>= 1
return p
# GF(2**8) log/exp tables (generator 0x03) -> multiplicative inverse -> S-box (affine transform),
# computed rather than transcribed so there is no 256-entry table to get wrong
_EXP = [0] * 256
_LOG = [0] * 256
_x = 1
for _i in range(255):
_EXP[_i] = _x
_LOG[_x] = _i
_x = _gmul(_x, 0x03)
def _inv(b):
return 0 if b == 0 else _EXP[(255 - _LOG[b]) % 255]
def _rotl8(b, n):
return ((b << n) | (b >> (8 - n))) & 0xff
SBOX = []
for _b in range(256):
_v = _inv(_b)
SBOX.append(_v ^ _rotl8(_v, 1) ^ _rotl8(_v, 2) ^ _rotl8(_v, 3) ^ _rotl8(_v, 4) ^ 0x63)
INV_SBOX = [0] * 256
for _b in range(256):
INV_SBOX[SBOX[_b]] = _b
RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d]
def _xor(a, b):
if len(a) != len(b): # equal-length by construction; fail loud (not via assert, which -O strips)
raise ValueError("XOR operands differ in length")
return bytes(bytearray(x ^ y for x, y in zip(bytearray(a), bytearray(b))))
class AES(object):
"""AES-128/256 block cipher (16-byte block) with a minimal CBC mode."""
def __init__(self, key):
key = bytearray(key)
if len(key) not in (16, 32):
raise ValueError("AES key must be 16 or 32 bytes")
self.rounds = 10 if len(key) == 16 else 14
self._roundKeys = self._expand(key)
def _expand(self, key):
nk = len(key) // 4
words = [list(key[4 * i:4 * i + 4]) for i in range(nk)]
for i in range(nk, 4 * (self.rounds + 1)):
temp = list(words[i - 1])
if i % nk == 0:
temp = temp[1:] + temp[:1] # RotWord
temp = [SBOX[b] for b in temp] # SubWord
temp[0] ^= RCON[i // nk - 1]
elif nk > 6 and i % nk == 4:
temp = [SBOX[b] for b in temp]
words.append([words[i - nk][j] ^ temp[j] for j in range(4)])
roundKeys = []
for r in range(self.rounds + 1):
rk = []
for c in range(4):
rk.extend(words[4 * r + c])
roundKeys.append(rk)
return roundKeys
@staticmethod
def _addRoundKey(state, rk):
for i in range(16):
state[i] ^= rk[i]
@staticmethod
def _shiftRows(s):
out = [0] * 16
for r in range(4):
for c in range(4):
out[r + 4 * c] = s[r + 4 * ((c + r) % 4)]
return out
@staticmethod
def _invShiftRows(s):
out = [0] * 16
for r in range(4):
for c in range(4):
out[r + 4 * c] = s[r + 4 * ((c - r) % 4)]
return out
@staticmethod
def _mixColumns(s):
out = [0] * 16
for c in range(4):
col = s[4 * c:4 * c + 4]
out[4 * c + 0] = _gmul(col[0], 2) ^ _gmul(col[1], 3) ^ col[2] ^ col[3]
out[4 * c + 1] = col[0] ^ _gmul(col[1], 2) ^ _gmul(col[2], 3) ^ col[3]
out[4 * c + 2] = col[0] ^ col[1] ^ _gmul(col[2], 2) ^ _gmul(col[3], 3)
out[4 * c + 3] = _gmul(col[0], 3) ^ col[1] ^ col[2] ^ _gmul(col[3], 2)
return out
@staticmethod
def _invMixColumns(s):
out = [0] * 16
for c in range(4):
col = s[4 * c:4 * c + 4]
out[4 * c + 0] = _gmul(col[0], 14) ^ _gmul(col[1], 11) ^ _gmul(col[2], 13) ^ _gmul(col[3], 9)
out[4 * c + 1] = _gmul(col[0], 9) ^ _gmul(col[1], 14) ^ _gmul(col[2], 11) ^ _gmul(col[3], 13)
out[4 * c + 2] = _gmul(col[0], 13) ^ _gmul(col[1], 9) ^ _gmul(col[2], 14) ^ _gmul(col[3], 11)
out[4 * c + 3] = _gmul(col[0], 11) ^ _gmul(col[1], 13) ^ _gmul(col[2], 9) ^ _gmul(col[3], 14)
return out
def encryptBlock(self, block):
state = list(bytearray(block))
self._addRoundKey(state, self._roundKeys[0])
for r in range(1, self.rounds):
state = self._mixColumns(self._shiftRows([SBOX[b] for b in state]))
self._addRoundKey(state, self._roundKeys[r])
state = self._shiftRows([SBOX[b] for b in state])
self._addRoundKey(state, self._roundKeys[self.rounds])
return bytes(bytearray(state))
def decryptBlock(self, block):
state = list(bytearray(block))
self._addRoundKey(state, self._roundKeys[self.rounds])
for r in range(self.rounds - 1, 0, -1):
state = [INV_SBOX[b] for b in self._invShiftRows(state)]
self._addRoundKey(state, self._roundKeys[r])
state = self._invMixColumns(state)
state = [INV_SBOX[b] for b in self._invShiftRows(state)]
self._addRoundKey(state, self._roundKeys[0])
return bytes(bytearray(state))
def cbcEncrypt(self, iv, data):
if len(data) % 16 != 0:
raise ValueError("CBC input is not block-aligned")
prev, out = iv, []
for i in range(0, len(data), 16):
prev = self.encryptBlock(_xor(data[i:i + 16], prev))
out.append(prev)
return b"".join(out)
def cbcDecrypt(self, iv, data):
if len(data) % 16 != 0:
raise ValueError("CBC input is not block-aligned")
prev, out = iv, []
for i in range(0, len(data), 16):
block = data[i:i + 16]
out.append(_xor(self.decryptBlock(block), prev))
prev = block
return b"".join(out)

332
extra/kerberos/client.py Normal file
View file

@ -0,0 +1,332 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Dependency-free Kerberos 5 client (RFC 4120) built on the in-tree DER codec and RFC 3961/3962
# crypto. Implements the AS exchange (password -> TGT) with PA-ENC-TIMESTAMP pre-authentication;
# the TGS exchange and AP-REQ follow. Talks to the KDC over TCP (4-byte length framing).
# Python 2.7 / 3.x.
import os
import socket
import struct
import time
from extra.kerberos import der
from extra.kerberos import spnego
from extra.kerberos.crypto import ENCTYPES
GSS_CHECKSUM_TYPE = 0x8003 # RFC 4121 section 4.1.1 authenticator checksum
GSS_CHECKSUM_FLAGS = 0 # no GSS context flags requested (no mutual/deleg)
TICKET_LIFETIME_SECONDS = 10 * 3600 # requested 'till' offset (KDC clamps to its max)
# message types
AS_REQ, AS_REP, TGS_REQ, TGS_REP, AP_REQ, KRB_ERROR = 10, 11, 12, 13, 14, 30
# principal name types
NT_PRINCIPAL, NT_SRV_INST = 1, 2
# PA-DATA types
PA_TGS_REQ, PA_ENC_TIMESTAMP, PA_ETYPE_INFO2 = 1, 2, 19
# KDC error code that carries the PA-ETYPE-INFO2 hint (etype/salt/iteration count) for pre-auth
KDC_ERR_PREAUTH_REQUIRED = 25
# key usages (RFC 4120 section 7.5.1)
USAGE_AS_REQ_PA_ENC_TIMESTAMP = 1
USAGE_AS_REP_ENCPART = 3
USAGE_TGS_REQ_AUTH_CKSUM = 6
USAGE_TGS_REQ_AUTH = 7
USAGE_TGS_REP_ENCPART = 8
USAGE_AP_REQ_AUTH = 11
PVNO = 5
DEFAULT_ETYPES = (18, 17, 23) # aes256-cts, aes128-cts, rc4-hmac (best first)
KDC_TIMEOUT = 10 # seconds for the KDC TCP exchange
MAX_KDC_RESPONSE = 8 * 1024 * 1024 # cap on a KDC reply (guards a hostile length prefix)
def _enctype(etype):
if etype not in ENCTYPES:
raise KerberosError(-1, "unsupported encryption type %d (only AES-CTS-HMAC-SHA1 is implemented)" % etype)
return ENCTYPES[etype]
class KerberosError(Exception):
def __init__(self, code, text=None):
Exception.__init__(self, "KDC error %d%s" % (code, ": %s" % text if text else ""))
self.code = code
# ---- EXPLICIT-tag unwrap helpers ------------------------------------------------------------------
# Kerberos uses EXPLICIT tagging: an [n] field's content is a complete inner TLV, so it must be
# peeled before the value can be read. _fields() maps a SEQUENCE's [n] children to that inner TLV.
def _fields(sequenceContent):
out = {}
for tag, inner in der.children(sequenceContent):
if 0xA0 <= tag <= 0xBE: # context-specific, constructed [0]..[30]
out[tag - 0xA0] = inner
return out
def _expInteger(field):
return der.decodeInteger(der.peel(field)[1])
def _expString(field):
return der.decodeGeneralString(der.peel(field)[1])
def _expOctet(field):
return bytes(der.peel(field)[1])
def _expFields(field):
"""For an [n] field whose inner TLV is a SEQUENCE, return that SEQUENCE's field map."""
return _fields(der.peel(field)[1])
# ---- message building -----------------------------------------------------------------------------
def _nonce():
return struct.unpack(">I", os.urandom(4))[0] & 0x7fffffff
def _kerberosTime(offsetSeconds=0):
return time.strftime("%Y%m%d%H%M%SZ", time.gmtime(time.time() + offsetSeconds))
def _principalName(nameType, components):
return der.sequence(
der.tagged(0, der.integer(nameType)),
der.tagged(1, der.sequenceOf([der.generalString(_) for _ in components])),
)
def _encryptedData(etype, cipher, kvno=None):
parts = [der.tagged(0, der.integer(etype))]
if kvno is not None:
parts.append(der.tagged(1, der.integer(kvno)))
parts.append(der.tagged(2, der.octetString(cipher)))
return der.sequence(*parts)
# ---- KDC transport (RFC 4120 section 7.2.2: 4-byte length-prefixed over TCP) ----------------------
def _recvExactly(sock, count):
buf = b""
while len(buf) < count:
chunk = sock.recv(count - len(buf))
if not chunk:
raise KerberosError(-1, "connection closed by KDC")
buf += chunk
return buf
def _sendReceive(host, port, request):
sock = socket.create_connection((host, port), timeout=KDC_TIMEOUT)
try:
sock.sendall(struct.pack(">I", len(request)) + request)
length = struct.unpack(">I", _recvExactly(sock, 4))[0]
if length > MAX_KDC_RESPONSE:
raise KerberosError(-1, "KDC reply length %d exceeds the sane maximum" % length)
return _recvExactly(sock, length)
finally:
sock.close()
def _raiseIfError(message):
tag, content, _ = der.peel(message)
if tag == der.applicationTag(KRB_ERROR):
fields = _fields(der.peel(content)[1])
raise KerberosError(_expInteger(fields[6]) if 6 in fields else -1,
_expString(fields[11]) if 11 in fields else None)
return tag, content
def _parseEtypeInfo2(errorFields):
"""Extract [(etype, salt, iterations), ...] from a KDC_ERR_PREAUTH_REQUIRED error's PA-ETYPE-INFO2,
telling us which etype/salt/s2kparams the KDC expects for the long-term key."""
out = []
if 12 not in errorFields: # no e-data
return out
try:
methodData = der.peel(errorFields[12])[1] # e-data OCTET STRING -> METHOD-DATA (SEQ OF PA-DATA)
for _, paData in der.children(der.peel(methodData)[1]):
pa = _fields(paData)
if 1 in pa and 2 in pa and _expInteger(pa[1]) == PA_ETYPE_INFO2:
info = der.peel(pa[2])[1] # padata-value OCTET STRING -> ETYPE-INFO2 (SEQ OF entry)
for _, entry in der.children(der.peel(info)[1]):
fields = _fields(entry)
etype = _expInteger(fields[0])
salt = _expOctet(fields[1]) if 1 in fields else None # opaque octets for string2key (RFC 3961), not UTF-8
iterations = None
if 2 in fields:
raw = bytes(der.peel(fields[2])[1]) # s2kparams: 4-byte BE iteration count for AES
iterations = struct.unpack(">I", raw)[0] if len(raw) == 4 else None
out.append((etype, salt, iterations))
except (KeyError, IndexError, ValueError, struct.error):
del out[:] # malformed hint -> fall back to the default etype/salt
return out
def _replyEtype(response):
"""Return the etype of a KDC-REP's enc-part (which etype the KDC used for the client's key)."""
try:
rep = _fields(der.peel(der.peel(response)[1])[1])
return _expInteger(_expFields(rep[6])[0])
except (KeyError, IndexError, ValueError, struct.error):
raise KerberosError(-1, "malformed KDC reply")
def _parseRep(response, key, usage, expectedNonce, expectedType):
"""Parse an AS-REP / TGS-REP: decrypt its enc-part with 'key' under 'usage', returning the
opaque ticket and the freshly issued session key. The two replies are structurally identical.
The reply's application tag MUST match the expected message type, and the nonce carried in the
(integrity-protected) enc-part MUST equal the request nonce (RFC 4120)."""
try: # any structural defect in a hostile/truncated reply -> KerberosError
tag, repContent = _raiseIfError(response)
if tag != der.applicationTag(expectedType):
raise KerberosError(-1, "unexpected reply message type (tag 0x%02x)" % tag)
rep = _fields(der.peel(repContent)[1])
encData = _expFields(rep[6]) # enc-part (EncryptedData)
repEtype = _expInteger(encData[0])
try:
encRepPart = _enctype(repEtype).decrypt(key, usage, _expOctet(encData[2]))
except ValueError: # HMAC mismatch -> we hold the wrong long-term key
raise KerberosError(-1, "reply decryption failed (wrong password or salt)")
# Enc*RepPart = [APPLICATION 25/26] EncKDCRepPart ; key is field [0], nonce is field [2]
encKdcRep = _fields(der.peel(der.peel(encRepPart)[1])[1])
if _expInteger(encKdcRep[2]) != expectedNonce:
raise KerberosError(-1, "reply nonce does not match the request (possible replay)")
keyFields = _expFields(encKdcRep[0])
return {
"ticket": bytes(rep[5]),
"sessionKey": _expOctet(keyFields[1]),
"sessionKeyType": _expInteger(keyFields[0]),
"etype": repEtype,
"crealm": _expString(rep[3]),
}
except (KeyError, IndexError, ValueError, struct.error):
raise KerberosError(-1, "malformed KDC reply")
def _reqBody(realm, snameType, snameComponents, etypes, nonce, cnameComponents=None):
parts = [der.tagged(0, der.bitString(b"\x00\x00\x00\x00"))] # kdc-options
if cnameComponents is not None:
parts.append(der.tagged(1, _principalName(NT_PRINCIPAL, cnameComponents))) # cname (AS only)
parts.append(der.tagged(2, der.generalString(realm))) # realm
parts.append(der.tagged(3, _principalName(snameType, snameComponents))) # sname
parts.append(der.tagged(5, der.generalizedTime(_kerberosTime(offsetSeconds=TICKET_LIFETIME_SECONDS)))) # till
parts.append(der.tagged(7, der.integer(nonce))) # nonce
parts.append(der.tagged(8, der.sequenceOf([der.integer(_) for _ in etypes]))) # etype
return der.sequence(*parts)
def _authenticator(crealm, cnameComponents, cksum=None):
parts = [
der.tagged(0, der.integer(PVNO)),
der.tagged(1, der.generalString(crealm)),
der.tagged(2, _principalName(NT_PRINCIPAL, cnameComponents)),
]
if cksum is not None:
parts.append(der.tagged(3, der.sequence(der.tagged(0, der.integer(cksum[0])),
der.tagged(1, der.octetString(cksum[1])))))
parts.append(der.tagged(4, der.integer(struct.unpack(">I", os.urandom(4))[0] % 1000000))) # cusec
parts.append(der.tagged(5, der.generalizedTime(_kerberosTime()))) # ctime
return der.application(2, der.sequence(*parts))
def _apReq(ticket, encAuthenticator, etype, apOptions=b"\x00\x00\x00\x00"):
return der.application(AP_REQ, der.sequence(
der.tagged(0, der.integer(PVNO)),
der.tagged(1, der.integer(AP_REQ)),
der.tagged(2, der.bitString(apOptions)),
der.tagged(3, ticket), # raw Ticket TLV (already [APPLICATION 1])
der.tagged(4, _encryptedData(etype, encAuthenticator)),
))
# ---- AS exchange (password -> TGT) ----------------------------------------------------------------
def _asReq(realm, username, etypes, nonce, padata=None):
reqBody = _reqBody(realm, NT_SRV_INST, ["krbtgt", realm], etypes, nonce, cnameComponents=[username])
parts = [der.tagged(1, der.integer(PVNO)), der.tagged(2, der.integer(AS_REQ))]
if padata is not None:
parts.append(der.tagged(3, der.sequenceOf([padata])))
parts.append(der.tagged(4, reqBody))
return der.application(AS_REQ, der.sequence(*parts))
def getTGT(realm, username, password, kdcHost, kdcPort=88, etypes=DEFAULT_ETYPES, salt=None):
"""Run the AS exchange and return the TGT and its session key.
Follows the standard two-step flow: an initial request without pre-auth learns the KDC's expected
etype/salt/iteration-count from PA-ETYPE-INFO2 (so non-default salts and AES-128-only principals
work), then a PA-ENC-TIMESTAMP-authenticated request obtains the ticket. Returns
{'ticket': <raw Ticket TLV>, 'sessionKey': bytes, 'sessionKeyType': int, 'crealm': str}.
"""
realm = realm.upper()
chosenSalt = salt if salt is not None else realm + username
# 1) probe without pre-auth to discover the etype/salt/iterations (or get the TGT outright)
nonce = _nonce()
response = _sendReceive(kdcHost, kdcPort, _asReq(realm, username, etypes, nonce))
tag = der.peel(response)[0]
if tag == der.applicationTag(AS_REP): # KDC issued the ticket without pre-auth
etype = _replyEtype(response) # derive the key for the etype the KDC actually used
clientKey = _enctype(etype).string2key(password, chosenSalt)
return _parseRep(response, clientKey, USAGE_AS_REP_ENCPART, nonce, AS_REP)
etype, iterations = etypes[0], None
if tag == der.applicationTag(KRB_ERROR):
errorFields = _fields(der.peel(der.peel(response)[1])[1])
code = _expInteger(errorFields[6]) if 6 in errorFields else -1
if code != KDC_ERR_PREAUTH_REQUIRED:
raise KerberosError(code, _expString(errorFields[11]) if 11 in errorFields else None)
for advertisedEtype, advertisedSalt, advertisedIters in _parseEtypeInfo2(errorFields):
if advertisedEtype in ENCTYPES:
etype = advertisedEtype
iterations = advertisedIters
if salt is None and advertisedSalt is not None:
chosenSalt = advertisedSalt
break
enc = _enctype(etype)
clientKey = enc.string2key(password, chosenSalt, iterations)
# 2) authenticated request with PA-ENC-TIMESTAMP under the discovered etype/salt
paTsEnc = der.sequence(der.tagged(0, der.generalizedTime(_kerberosTime())), der.tagged(1, der.integer(0)))
cipher = enc.encrypt(clientKey, USAGE_AS_REQ_PA_ENC_TIMESTAMP, paTsEnc)
paData = der.sequence(
der.tagged(1, der.integer(PA_ENC_TIMESTAMP)),
der.tagged(2, der.octetString(_encryptedData(etype, cipher))),
)
nonce = _nonce()
response = _sendReceive(kdcHost, kdcPort, _asReq(realm, username, etypes, nonce, padata=paData))
return _parseRep(response, clientKey, USAGE_AS_REP_ENCPART, nonce, AS_REP)
# ---- TGS exchange (TGT -> service ticket) ---------------------------------------------------------
def getServiceTicket(tgt, realm, username, serviceComponents, kdcHost, kdcPort=88, etypes=DEFAULT_ETYPES):
"""Present the TGT in a PA-TGS-REQ AP-REQ to obtain a ticket for the named service.
Returns the same shape as getTGT (the 'ticket' is now the service ticket).
"""
realm = realm.upper()
enc = _enctype(tgt["sessionKeyType"])
nonce = _nonce()
reqBody = _reqBody(realm, NT_SRV_INST, serviceComponents, etypes, nonce)
cksum = (enc.cksumtype, enc.checksum(tgt["sessionKey"], USAGE_TGS_REQ_AUTH_CKSUM, reqBody))
authenticator = _authenticator(realm, [username], cksum=cksum)
encAuth = enc.encrypt(tgt["sessionKey"], USAGE_TGS_REQ_AUTH, authenticator)
apReq = _apReq(tgt["ticket"], encAuth, tgt["sessionKeyType"])
paTgs = der.sequence(der.tagged(1, der.integer(PA_TGS_REQ)), der.tagged(2, der.octetString(apReq)))
tgsReq = der.application(TGS_REQ, der.sequence(
der.tagged(1, der.integer(PVNO)),
der.tagged(2, der.integer(TGS_REQ)),
der.tagged(3, der.sequenceOf([paTgs])),
der.tagged(4, reqBody),
))
return _parseRep(_sendReceive(kdcHost, kdcPort, tgsReq), tgt["sessionKey"], USAGE_TGS_REP_ENCPART, nonce, TGS_REP)
# ---- SPNEGO "Negotiate" token (cached service ticket -> ready-to-send HTTP token) -----------------
def spnegoFromTicket(service, realm, username):
"""Build a fresh SPNEGO token from an already-obtained service ticket (no KDC round-trip). Each
call produces a new AP-REQ authenticator, as replay caches require, so a cached ticket can back
every request of a scan cheaply."""
enc = _enctype(service["sessionKeyType"])
gssChecksum = (GSS_CHECKSUM_TYPE, struct.pack("<I", 16) + b"\x00" * 16 + struct.pack("<I", GSS_CHECKSUM_FLAGS))
authenticator = _authenticator(realm.upper(), [username], cksum=gssChecksum)
encAuth = enc.encrypt(service["sessionKey"], USAGE_AP_REQ_AUTH, authenticator)
return spnego.negTokenInit(_apReq(service["ticket"], encAuth, service["sessionKeyType"]))

256
extra/kerberos/crypto.py Normal file
View file

@ -0,0 +1,256 @@
#!/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(),
}

142
extra/kerberos/der.py Normal file
View file

@ -0,0 +1,142 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Minimal, dependency-free ASN.1 DER codec covering exactly the constructs Kerberos (RFC 4120) uses:
# INTEGER, OCTET STRING, GeneralString, GeneralizedTime, BIT STRING, SEQUENCE / SEQUENCE OF, EXPLICIT
# context tags [n] and [APPLICATION n]. All Kerberos tag numbers are <= 30, so only the low-tag-number
# form is needed. Encoders return bytes; decoders accept bytes/bytearray. Python 2.7 / 3.x.
# universal tag bytes
INTEGER = 0x02
BIT_STRING = 0x03
OCTET_STRING = 0x04
GENERAL_STRING = 0x1b
GENERALIZED_TIME = 0x18
SEQUENCE = 0x30 # 0x10 | constructed(0x20)
def _encodeLength(length):
if length < 0x80:
return bytearray([length])
out = bytearray()
while length:
out.insert(0, length & 0xff)
length >>= 8
return bytearray([0x80 | len(out)]) + out
def _tlv(tag, value):
value = bytearray(value)
return bytes(bytearray([tag]) + _encodeLength(len(value)) + value)
# ---- context / application tags (EXPLICIT) --------------------------------------------------------
def contextTag(number):
return 0x80 | 0x20 | number # context-specific, constructed
def applicationTag(number):
return 0x40 | 0x20 | number # application, constructed
def tagged(number, innerTLV):
"""EXPLICIT [n] wrapper around an already-encoded inner TLV."""
return _tlv(contextTag(number), innerTLV)
def application(number, innerTLV):
"""[APPLICATION n] wrapper around an already-encoded inner TLV."""
return _tlv(applicationTag(number), innerTLV)
# ---- primitive encoders ---------------------------------------------------------------------------
def integer(value):
content = bytearray()
if value == 0:
content = bytearray([0])
elif value > 0:
n = value
while n:
content.insert(0, n & 0xff)
n >>= 8
if content[0] & 0x80: # keep the sign bit clear for a positive value
content.insert(0, 0x00)
else:
n = value
while True:
content.insert(0, n & 0xff)
n >>= 8
if n == -1 and (content[0] & 0x80):
break
return _tlv(INTEGER, content)
def octetString(value):
return _tlv(OCTET_STRING, value)
def generalString(value):
return _tlv(GENERAL_STRING, value if isinstance(value, bytes) else value.encode("utf-8"))
def generalizedTime(value):
"""'value' is a 'YYYYMMDDHHMMSSZ' UTC string."""
return _tlv(GENERALIZED_TIME, value if isinstance(value, bytes) else value.encode("ascii"))
def bitString(value, unusedBits=0):
return _tlv(BIT_STRING, bytearray([unusedBits]) + bytearray(value))
def sequence(*elements):
return _tlv(SEQUENCE, b"".join(bytes(_) for _ in elements))
def sequenceOf(elements):
return _tlv(SEQUENCE, b"".join(bytes(_) for _ in elements))
# ---- decoding -------------------------------------------------------------------------------------
def peel(data, offset=0):
"""Parse one TLV at 'offset'; return (tag, content_bytearray, next_offset). Raises ValueError on
truncated or indefinite-length input (the data may come from the network, so fail predictably)."""
data = bytearray(data)
if offset + 2 > len(data):
raise ValueError("truncated DER header")
tag = data[offset]
first = data[offset + 1]
offset += 2
if first < 0x80:
length = first
elif first == 0x80:
raise ValueError("indefinite-length DER is not permitted")
else:
count = first & 0x7f
if offset + count > len(data):
raise ValueError("truncated DER length")
length = 0
for _ in range(count):
length = (length << 8) | data[offset]
offset += 1
if offset + length > len(data):
raise ValueError("truncated DER content")
return tag, data[offset:offset + length], offset + length
def children(content):
"""Iterate the TLVs contained in a constructed value; yields (tag, content_bytearray)."""
content = bytearray(content)
offset = 0
out = []
while offset < len(content):
tag, inner, offset = peel(content, offset)
out.append((tag, inner))
return out
def decodeInteger(content):
content = bytearray(content)
if not content:
return 0
value = 0
for b in content:
value = (value << 8) | b
if content[0] & 0x80: # negative (two's complement)
value -= 1 << (8 * len(content))
return value
def decodeGeneralString(content):
return bytes(bytearray(content)).decode("utf-8", "replace")

191
extra/kerberos/discovery.py Normal file
View file

@ -0,0 +1,191 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Dependency-free KDC discovery for a realm, so '--auth-type=Negotiate' works without an explicit
# KDC address. Resolution order: the 'SQLMAP_KERBEROS_KDC' environment variable, then the local
# krb5.conf [realms] section, then a DNS SRV lookup (_kerberos._tcp.<realm>), then the realm name
# itself as a host. Returns (host, port). Python 2.7 / 3.x.
import os
import re
import socket
import struct
DEFAULT_KDC_PORT = 88
_DNS_TIMEOUT = 3
_SRV_TYPE = 33
_IN_CLASS = 1
def _splitHostPort(value, defaultPort=DEFAULT_KDC_PORT):
value = value.strip()
if value.startswith("["): # [IPv6] or [IPv6]:port
host, _, rest = value[1:].partition("]")
port = rest[1:] if rest.startswith(":") else ""
elif value.count(":") == 1: # host:port (a single colon rules out bare IPv6)
host, _, port = value.partition(":")
else: # bare host or bare IPv6 literal
host, port = value, ""
return host, (int(port) if port.isdigit() else defaultPort)
# ---- krb5.conf --------------------------------------------------------------------------------
def _fromKrb5Conf(realm):
path = os.environ.get("KRB5_CONFIG") or "/etc/krb5.conf"
try:
with open(path) as f:
content = f.read()
except (IOError, OSError):
return None
header = re.search(r"(?im)^\s*%s\s*=\s*\{" % re.escape(realm), content)
if not header:
return None
start = header.end() # brace-match so a nested '{ }' block cannot truncate us
depth, i = 1, start
while i < len(content) and depth > 0:
if content[i] == "{":
depth += 1
elif content[i] == "}":
depth -= 1
i += 1
block = content[start:i - 1]
kdc = re.search(r"(?im)^\s*kdc\s*=\s*(\S+)", block)
return kdc.group(1) if kdc else None
# ---- DNS SRV (_kerberos._tcp.<realm>) ---------------------------------------------------------
def _nameservers():
servers = []
try:
with open("/etc/resolv.conf") as f:
for line in f:
parts = line.split()
if len(parts) >= 2 and parts[0] == "nameserver":
servers.append(parts[1])
except (IOError, OSError):
pass
return servers
def _encodeName(name):
out = bytearray()
for label in name.split("."):
out.append(len(label))
out += label.encode("ascii")
out.append(0)
return bytes(out)
_MAX_NAME_JUMPS = 64 # guards against compression-pointer cycles
def _skipName(data, offset):
while True:
if offset >= len(data):
raise ValueError("truncated DNS name")
length = data[offset]
if length == 0:
return offset + 1
if length & 0xc0 == 0xc0: # compression pointer ends the name
if offset + 2 > len(data):
raise ValueError("truncated DNS pointer")
return offset + 2
offset += 1 + length
def _readName(data, offset):
labels = []
end = None
jumps = 0
while True:
if offset >= len(data):
raise ValueError("truncated DNS name")
length = data[offset]
if length == 0:
offset += 1
break
if length & 0xc0 == 0xc0: # follow compression pointer (bounded, cycle-safe)
if offset + 2 > len(data):
raise ValueError("truncated DNS pointer")
jumps += 1
if jumps > _MAX_NAME_JUMPS:
raise ValueError("too many DNS compression jumps")
if end is None:
end = offset + 2
offset = ((length & 0x3f) << 8) | data[offset + 1]
continue
if offset + 1 + length > len(data):
raise ValueError("truncated DNS label")
labels.append(bytes(data[offset + 1:offset + 1 + length]).decode("ascii", "replace"))
offset += 1 + length
return ".".join(labels), (end if end is not None else offset)
def parseSrv(response):
"""Parse SRV records from a (possibly hostile) DNS response into [(priority, weight, port,
target), ...]. Malformed input yields an empty list rather than raising."""
data = bytearray(response)
if len(data) < 12:
return []
try:
qdcount, ancount = struct.unpack(">HH", bytes(data[4:8]))
offset = 12
for _ in range(qdcount):
offset = _skipName(data, offset) + 4 # + qtype/qclass
records = []
for _ in range(ancount):
offset = _skipName(data, offset)
if offset + 10 > len(data):
break
rtype, _cls, _ttl, rdlength = struct.unpack(">HHIH", bytes(data[offset:offset + 10]))
offset += 10
if offset + rdlength > len(data):
break
if rtype == _SRV_TYPE and rdlength >= 6:
priority, weight, port = struct.unpack(">HHH", bytes(data[offset:offset + 6]))
target = _readName(data, offset + 6)[0].rstrip(".")
if target:
records.append((priority, weight, port, target))
offset += rdlength
return records
except (ValueError, struct.error, IndexError):
return []
def _fromDnsSrv(realm):
queryId = os.urandom(2)
query = (queryId + struct.pack(">HHHHH", 0x0100, 1, 0, 0, 0) +
_encodeName("_kerberos._tcp.%s" % realm) + struct.pack(">HH", _SRV_TYPE, _IN_CLASS))
for server in _nameservers():
family = socket.AF_INET6 if ":" in server else socket.AF_INET
sock = socket.socket(family, socket.SOCK_DGRAM)
sock.settimeout(_DNS_TIMEOUT)
try:
sock.connect((server, 53)) # connect() so the kernel drops replies from any other source
sock.send(query)
response = sock.recv(4096)
except socket.error:
continue
finally:
sock.close()
if len(response) < 2 or response[:2] != queryId: # ignore stray / spoofed replies
continue
records = parseSrv(response)
if records:
best = min(records, key=lambda r: (r[0], -r[1])) # lowest priority, then highest weight
return best[3], best[2]
return None
def discoverKdc(realm):
"""Resolve (host, port) of a KDC for the realm; falls back to the realm name itself as a host."""
override = os.environ.get("SQLMAP_KERBEROS_KDC")
if override:
return _splitHostPort(override)
configured = _fromKrb5Conf(realm)
if configured:
return _splitHostPort(configured)
fromDns = _fromDnsSrv(realm)
if fromDns:
return fromDns
return realm.lower(), DEFAULT_KDC_PORT

33
extra/kerberos/spnego.py Normal file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Minimal GSS-API / SPNEGO (RFC 2743, RFC 4178) wrapping of a Kerberos AP-REQ into the token carried
# by the HTTP "Authorization: Negotiate <base64>" header. Only the initiator's NegTokenInit is built
# (the one-shot token an HTTP client sends); the mechanism-specific OIDs are fixed constants.
# Python 2.7 / 3.x.
from extra.kerberos import der
# fully-encoded OBJECT IDENTIFIER TLVs
KRB5_OID = bytes(bytearray([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02])) # 1.2.840.113554.1.2.2
SPNEGO_OID = bytes(bytearray([0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02])) # 1.3.6.1.5.5.2
TOK_ID_AP_REQ = b"\x01\x00" # GSS Kerberos token id for KRB_AP_REQ
def gssApReq(apReq):
"""GSS InitialContextToken: [APPLICATION 0] { Kerberos OID, tok-id, AP-REQ }."""
return der.application(0, KRB5_OID + TOK_ID_AP_REQ + apReq)
def negTokenInit(apReq):
"""SPNEGO NegTokenInit wrapping the Kerberos GSS token (Kerberos advertised as the sole mech)."""
inner = der.sequence(
der.tagged(0, der.sequenceOf([KRB5_OID])), # mechTypes
der.tagged(2, der.octetString(gssApReq(apReq))), # mechToken
)
return der.application(0, SPNEGO_OID + der.tagged(0, inner))