mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
332 lines
16 KiB
Python
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"]))
|