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

332 lines
16 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 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"]))