mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-20 18:54:02 +00:00
Adding Negotiate/Kerberos authentication (#3404)
This commit is contained in:
parent
2d9e9ed959
commit
1bd1b457d7
12 changed files with 1479 additions and 9 deletions
6
extra/kerberos/__init__.py
Normal file
6
extra/kerberos/__init__.py
Normal 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
174
extra/kerberos/aes.py
Normal 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
332
extra/kerberos/client.py
Normal 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
256
extra/kerberos/crypto.py
Normal 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
142
extra/kerberos/der.py
Normal 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
191
extra/kerberos/discovery.py
Normal 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
33
extra/kerberos/spnego.py
Normal 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))
|
||||||
|
|
@ -439,6 +439,7 @@ class AUTH_TYPE(object):
|
||||||
DIGEST = "digest"
|
DIGEST = "digest"
|
||||||
BEARER = "bearer"
|
BEARER = "bearer"
|
||||||
NTLM = "ntlm"
|
NTLM = "ntlm"
|
||||||
|
NEGOTIATE = "negotiate"
|
||||||
PKI = "pki"
|
PKI = "pki"
|
||||||
|
|
||||||
class AUTOCOMPLETE_TYPE(object):
|
class AUTOCOMPLETE_TYPE(object):
|
||||||
|
|
|
||||||
|
|
@ -1332,8 +1332,11 @@ def _setHTTPHandlers():
|
||||||
# proxy - the keep-alive handler pools the proxy socket for plain HTTP and the CONNECT-tunnelled
|
# proxy - the keep-alive handler pools the proxy socket for plain HTTP and the CONNECT-tunnelled
|
||||||
# socket per origin for HTTPS); '--no-keep-alive' opts out, and they are automatically disabled
|
# socket per origin for HTTPS); '--no-keep-alive' opts out, and they are automatically disabled
|
||||||
# when incompatible (authentication methods, or chunked transfer-encoding of the request body -
|
# when incompatible (authentication methods, or chunked transfer-encoding of the request body -
|
||||||
# handled by a dedicated, non-pooling handler)
|
# handled by a dedicated, non-pooling handler). Negotiate is the one auth exception: its token is
|
||||||
conf.keepAlive = not conf.noKeepAlive and not conf.authType and not conf.chunked
|
# a per-request, end-to-end header (minted fresh each request, no connection-bound handshake), so
|
||||||
|
# persistent connections remain safe and worthwhile.
|
||||||
|
negotiateAuth = (conf.authType or "").lower() == AUTH_TYPE.NEGOTIATE
|
||||||
|
conf.keepAlive = not conf.noKeepAlive and not conf.chunked and (not conf.authType or negotiateAuth)
|
||||||
|
|
||||||
if conf.keepAlive:
|
if conf.keepAlive:
|
||||||
# persistent connections for both HTTP and HTTPS; the keep-alive HTTPS
|
# persistent connections for both HTTP and HTTPS; the keep-alive HTTPS
|
||||||
|
|
@ -1449,7 +1452,7 @@ def _setAuthCred():
|
||||||
|
|
||||||
def _setHTTPAuthentication():
|
def _setHTTPAuthentication():
|
||||||
"""
|
"""
|
||||||
Check and set the HTTP(s) authentication method (Basic, Digest, Bearer, NTLM or PKI),
|
Check and set the HTTP(s) authentication method (Basic, Digest, Bearer, NTLM, Negotiate or PKI),
|
||||||
username and password for first three methods, or PEM private key file for
|
username and password for first three methods, or PEM private key file for
|
||||||
PKI authentication
|
PKI authentication
|
||||||
"""
|
"""
|
||||||
|
|
@ -1472,9 +1475,9 @@ def _setHTTPAuthentication():
|
||||||
errMsg += "but did not provide the type (e.g. --auth-type=\"basic\")"
|
errMsg += "but did not provide the type (e.g. --auth-type=\"basic\")"
|
||||||
raise SqlmapSyntaxException(errMsg)
|
raise SqlmapSyntaxException(errMsg)
|
||||||
|
|
||||||
elif (conf.authType or "").lower() not in (AUTH_TYPE.BASIC, AUTH_TYPE.DIGEST, AUTH_TYPE.BEARER, AUTH_TYPE.NTLM, AUTH_TYPE.PKI):
|
elif (conf.authType or "").lower() not in (AUTH_TYPE.BASIC, AUTH_TYPE.DIGEST, AUTH_TYPE.BEARER, AUTH_TYPE.NTLM, AUTH_TYPE.NEGOTIATE, AUTH_TYPE.PKI):
|
||||||
errMsg = "HTTP authentication type value must be "
|
errMsg = "HTTP authentication type value must be "
|
||||||
errMsg += "Basic, Digest, Bearer, NTLM or PKI"
|
errMsg += "Basic, Digest, Bearer, NTLM, Negotiate or PKI"
|
||||||
raise SqlmapSyntaxException(errMsg)
|
raise SqlmapSyntaxException(errMsg)
|
||||||
|
|
||||||
if not conf.authFile:
|
if not conf.authFile:
|
||||||
|
|
@ -1490,11 +1493,12 @@ def _setHTTPAuthentication():
|
||||||
elif authType == AUTH_TYPE.BEARER:
|
elif authType == AUTH_TYPE.BEARER:
|
||||||
conf.httpHeaders.append((HTTP_HEADER.AUTHORIZATION, "Bearer %s" % conf.authCred.strip()))
|
conf.httpHeaders.append((HTTP_HEADER.AUTHORIZATION, "Bearer %s" % conf.authCred.strip()))
|
||||||
return
|
return
|
||||||
elif authType == AUTH_TYPE.NTLM:
|
elif authType in (AUTH_TYPE.NTLM, AUTH_TYPE.NEGOTIATE):
|
||||||
# Note: the DOMAIN\username part is colon-free, so the password group takes the full
|
# Note: the DOMAIN\username part is colon-free, so the password group takes the full
|
||||||
# remainder (a greedy first group would otherwise swallow colons inside the password)
|
# remainder (a greedy first group would otherwise swallow colons inside the password).
|
||||||
|
# For Negotiate, DOMAIN is the Kerberos realm.
|
||||||
regExp = "^([^:]*\\\\[^:]*):(.*)$"
|
regExp = "^([^:]*\\\\[^:]*):(.*)$"
|
||||||
errMsg = "HTTP NTLM authentication credentials value must "
|
errMsg = "HTTP %s authentication credentials value must " % authType
|
||||||
errMsg += "be in format 'DOMAIN\\username:password'"
|
errMsg += "be in format 'DOMAIN\\username:password'"
|
||||||
elif authType == AUTH_TYPE.PKI:
|
elif authType == AUTH_TYPE.PKI:
|
||||||
errMsg = "HTTP PKI authentication require "
|
errMsg = "HTTP PKI authentication require "
|
||||||
|
|
@ -1522,6 +1526,12 @@ def _setHTTPAuthentication():
|
||||||
elif authType == AUTH_TYPE.NTLM:
|
elif authType == AUTH_TYPE.NTLM:
|
||||||
from lib.request.ntlm import HTTPNtlmAuthHandler
|
from lib.request.ntlm import HTTPNtlmAuthHandler
|
||||||
authHandler = HTTPNtlmAuthHandler(kb.passwordMgr)
|
authHandler = HTTPNtlmAuthHandler(kb.passwordMgr)
|
||||||
|
|
||||||
|
elif authType == AUTH_TYPE.NEGOTIATE:
|
||||||
|
from lib.request.kerberos import HTTPNegotiateAuthHandler
|
||||||
|
# DOMAIN is the Kerberos realm; the KDC is auto-discovered (env / krb5.conf / DNS SRV / realm)
|
||||||
|
realm, _, user = conf.authUsername.partition('\\')
|
||||||
|
authHandler = HTTPNegotiateAuthHandler(realm, user, conf.authPassword)
|
||||||
else:
|
else:
|
||||||
debugMsg = "setting the HTTP(s) authentication PEM private key"
|
debugMsg = "setting the HTTP(s) authentication PEM private key"
|
||||||
logger.debug(debugMsg)
|
logger.debug(debugMsg)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
||||||
from thirdparty import six
|
from thirdparty import six
|
||||||
|
|
||||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||||
VERSION = "1.10.7.182"
|
VERSION = "1.10.7.183"
|
||||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||||
|
|
|
||||||
84
lib/request/kerberos.py
Normal file
84
lib/request/kerberos.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||||
|
See the file 'LICENSE' for copying permission
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Native, dependency-free HTTP "Negotiate" (SPNEGO/Kerberos) authentication, built on the in-tree
|
||||||
|
# pure-Python Kerberos client (extra/kerberos). No 'pykerberos'/'gssapi'/'requests-kerberos' needed.
|
||||||
|
# The TGT and per-service tickets are obtained once and cached; a fresh AP-REQ is minted for every
|
||||||
|
# request from the cached ticket (as replay caches require), so an entire scan costs a single AS+TGS
|
||||||
|
# exchange and the token is sent pre-emptively (no extra 401 round-trip per request). Python 2.7 / 3.x.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from lib.core.common import getSafeExString
|
||||||
|
from lib.core.common import singleTimeLogMessage
|
||||||
|
from lib.core.convert import getText
|
||||||
|
from lib.core.enums import HTTP_HEADER
|
||||||
|
from thirdparty.six.moves import urllib as _urllib
|
||||||
|
|
||||||
|
from extra.kerberos.client import getServiceTicket
|
||||||
|
from extra.kerberos.client import getTGT
|
||||||
|
from extra.kerberos.client import KerberosError
|
||||||
|
from extra.kerberos.client import spnegoFromTicket
|
||||||
|
from extra.kerberos.discovery import discoverKdc
|
||||||
|
|
||||||
|
class HTTPNegotiateAuthHandler(_urllib.request.BaseHandler):
|
||||||
|
handler_order = 480
|
||||||
|
|
||||||
|
def __init__(self, realm, username, password, kdcHost=None, kdcPort=None):
|
||||||
|
self.realm = realm.upper()
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.kdcHost = kdcHost # None -> discovered from the realm on first use
|
||||||
|
self.kdcPort = kdcPort
|
||||||
|
self._tgt = None
|
||||||
|
self._tickets = {} # target host -> service-ticket dict
|
||||||
|
self._tgtFailure = None # realm-wide failure (bad creds / KDC down)
|
||||||
|
self._hostFailures = {} # per-host failure (e.g. no HTTP/<host> SPN)
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def _serviceTicket(self, host):
|
||||||
|
with self._lock:
|
||||||
|
if self._tgtFailure is not None: # TGT unobtainable -> nothing in the realm works
|
||||||
|
raise self._tgtFailure
|
||||||
|
if host in self._hostFailures: # this host already failed -> don't retry it
|
||||||
|
raise self._hostFailures[host]
|
||||||
|
if host not in self._tickets:
|
||||||
|
if self._tgt is None:
|
||||||
|
if self.kdcHost is None: # krb5.conf / DNS SRV / realm-name discovery
|
||||||
|
self.kdcHost, self.kdcPort = discoverKdc(self.realm)
|
||||||
|
try:
|
||||||
|
self._tgt = getTGT(self.realm, self.username, self.password, self.kdcHost, self.kdcPort)
|
||||||
|
except Exception as ex: # cache so the AS exchange runs at most once
|
||||||
|
self._tgtFailure = ex
|
||||||
|
raise
|
||||||
|
try:
|
||||||
|
self._tickets[host] = getServiceTicket(self._tgt, self.realm, self.username, ["HTTP", host], self.kdcHost, self.kdcPort)
|
||||||
|
except Exception as ex: # host-specific -> other hosts remain usable
|
||||||
|
self._hostFailures[host] = ex
|
||||||
|
raise
|
||||||
|
return self._tickets[host]
|
||||||
|
|
||||||
|
def _requestHandler(self, req):
|
||||||
|
host = _urllib.parse.urlsplit(req.get_full_url()).hostname
|
||||||
|
if host:
|
||||||
|
try:
|
||||||
|
token = spnegoFromTicket(self._serviceTicket(host), self.realm, self.username)
|
||||||
|
req.add_unredirected_header(HTTP_HEADER.AUTHORIZATION, "Negotiate %s" % getText(base64.b64encode(token)))
|
||||||
|
except KerberosError as ex:
|
||||||
|
# bad credentials / KDC-refused: log once, fall through unauthenticated (server 401s)
|
||||||
|
singleTimeLogMessage("Negotiate (Kerberos) authentication failed: %s" % getSafeExString(ex), logging.ERROR)
|
||||||
|
except Exception as ex:
|
||||||
|
# unreachable KDC, malformed reply, etc. - never let it crash the run
|
||||||
|
singleTimeLogMessage("could not obtain a Kerberos ticket (is the KDC '%s:%s' reachable?): %s" % (self.kdcHost, self.kdcPort, getSafeExString(ex)), logging.ERROR)
|
||||||
|
return req
|
||||||
|
|
||||||
|
def http_request(self, req):
|
||||||
|
return self._requestHandler(req)
|
||||||
|
|
||||||
|
https_request = http_request
|
||||||
241
tests/test_kerberos.py
Normal file
241
tests/test_kerberos.py
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||||
|
See the file 'LICENSE' for copying permission
|
||||||
|
|
||||||
|
Tests for the dependency-free Kerberos stack under extra/kerberos: the AES core (FIPS-197), the
|
||||||
|
RFC 3961/3962 etype crypto (n-fold, string-to-key, authenticated encryption) and the ASN.1 DER codec.
|
||||||
|
All assertions use published FIPS/RFC test vectors, so they validate the crypto and encoding offline
|
||||||
|
(the AS/TGS protocol and the HTTP Negotiate handler are exercised against a live KDC, not here).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from _testutils import bootstrap
|
||||||
|
bootstrap()
|
||||||
|
|
||||||
|
from extra.kerberos import client
|
||||||
|
from extra.kerberos import der
|
||||||
|
from extra.kerberos import discovery
|
||||||
|
from extra.kerberos.aes import AES
|
||||||
|
from extra.kerberos.crypto import ENCTYPES, nfold
|
||||||
|
|
||||||
|
|
||||||
|
def _dnsName(name):
|
||||||
|
out = bytearray()
|
||||||
|
for label in name.split("."):
|
||||||
|
out.append(len(label))
|
||||||
|
out += label.encode("ascii")
|
||||||
|
out.append(0)
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _h(value):
|
||||||
|
return binascii.unhexlify(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TestKerberosAES(unittest.TestCase):
|
||||||
|
def test_fips197_known_answer(self):
|
||||||
|
# FIPS-197 Appendix C.1 (AES-128) and C.3 (AES-256)
|
||||||
|
for key, pt, ct in (
|
||||||
|
("000102030405060708090a0b0c0d0e0f",
|
||||||
|
"00112233445566778899aabbccddeeff", "69c4e0d86a7b0430d8cdb78070b4c55a"),
|
||||||
|
("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
||||||
|
"00112233445566778899aabbccddeeff", "8ea2b7ca516745bfeafc49904b496089"),
|
||||||
|
):
|
||||||
|
aes = AES(_h(key))
|
||||||
|
self.assertEqual(aes.encryptBlock(_h(pt)), _h(ct))
|
||||||
|
self.assertEqual(aes.decryptBlock(_h(ct)), _h(pt))
|
||||||
|
|
||||||
|
def test_cbc_round_trip(self):
|
||||||
|
aes = AES(_h("00" * 32))
|
||||||
|
iv, data = _h("0f" * 16), os.urandom(64)
|
||||||
|
self.assertEqual(aes.cbcDecrypt(iv, aes.cbcEncrypt(iv, data)), data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestKerberosCrypto(unittest.TestCase):
|
||||||
|
def test_nfold_rfc3961(self):
|
||||||
|
# RFC 3961 Appendix A.1
|
||||||
|
for text, size, expected in (
|
||||||
|
("012345", 8, "be072631276b1955"),
|
||||||
|
("password", 7, "78a07b6caf85fa"),
|
||||||
|
("Rough Consensus, and Running Code", 8, "bb6ed30870b7f0e0"),
|
||||||
|
("password", 21, "59e4a8ca7c0385c3c37b3f6d2000247cb6e6bd5b3e"),
|
||||||
|
("MASSACHVSETTS INSTITVTE OF TECHNOLOGY", 24,
|
||||||
|
"db3b0d8f0b061e603282b308a50841229ad798fab9540c1b"),
|
||||||
|
):
|
||||||
|
self.assertEqual(binascii.hexlify(nfold(text.encode(), size)).decode(), expected)
|
||||||
|
|
||||||
|
def test_string2key_rfc3962(self):
|
||||||
|
# RFC 3962 Appendix B (pass 'password', salt 'ATHENA.MIT.EDUraeburn')
|
||||||
|
for iterations, keysize, expected in (
|
||||||
|
(1, 16, "42263c6e89f4fc28b8df68ee09799f15"),
|
||||||
|
(1, 32, "fe697b52bc0d3ce14432ba036a92e65bbb52280990a2fa27883998d72af30161"),
|
||||||
|
(1200, 16, "4c01cd46d632d01e6dbe230a01ed642a"),
|
||||||
|
(1200, 32, "55a6ac740ad17b4846941051e1e8b0a7548d93b0ab30a8bc3ff16280382b8c2a"),
|
||||||
|
):
|
||||||
|
key = ENCTYPES[17 if keysize == 16 else 18].string2key("password", "ATHENA.MIT.EDUraeburn", iterations)
|
||||||
|
self.assertEqual(binascii.hexlify(key).decode(), expected)
|
||||||
|
|
||||||
|
def test_encrypt_decrypt_round_trip(self):
|
||||||
|
for etype in (17, 18):
|
||||||
|
enc = ENCTYPES[etype]
|
||||||
|
key = os.urandom(enc.keysize)
|
||||||
|
for length in (0, 1, 15, 16, 17, 31, 32, 100):
|
||||||
|
plaintext = os.urandom(length)
|
||||||
|
self.assertEqual(enc.decrypt(key, 1024, enc.encrypt(key, 1024, plaintext)), plaintext)
|
||||||
|
|
||||||
|
def test_integrity_check(self):
|
||||||
|
enc = ENCTYPES[18]
|
||||||
|
key = os.urandom(32)
|
||||||
|
ciphertext = bytearray(enc.encrypt(key, 3, b"secret"))
|
||||||
|
ciphertext[-1] ^= 1
|
||||||
|
self.assertRaises(ValueError, enc.decrypt, key, 3, bytes(ciphertext))
|
||||||
|
|
||||||
|
def test_decrypt_short_ciphertext(self):
|
||||||
|
# a hostile/truncated enc-part (< blocksize + macsize) must raise ValueError, not IndexError
|
||||||
|
enc = ENCTYPES[18]
|
||||||
|
key = os.urandom(32)
|
||||||
|
for length in (0, 1, 12, 27):
|
||||||
|
self.assertRaises(ValueError, enc.decrypt, key, 3, os.urandom(length))
|
||||||
|
|
||||||
|
def test_string2key_bytes_salt(self):
|
||||||
|
# the salt is opaque octets (RFC 3961): a bytes salt must derive the same key as the str form
|
||||||
|
enc = ENCTYPES[18]
|
||||||
|
self.assertEqual(enc.string2key("password", b"ATHENA.MIT.EDUraeburn", 1200),
|
||||||
|
enc.string2key("password", "ATHENA.MIT.EDUraeburn", 1200))
|
||||||
|
|
||||||
|
|
||||||
|
class TestKerberosRC4(unittest.TestCase):
|
||||||
|
def test_nt_hash_string2key(self):
|
||||||
|
# rc4-hmac long-term key is the NT hash: MD4(UTF-16LE(password))
|
||||||
|
self.assertEqual(binascii.hexlify(ENCTYPES[23].string2key("password")).decode(),
|
||||||
|
"8846f7eaee8fb117ad06bdd830b7586c")
|
||||||
|
|
||||||
|
def test_encrypt_decrypt_round_trip(self):
|
||||||
|
enc = ENCTYPES[23]
|
||||||
|
key = enc.string2key("Secret123")
|
||||||
|
for length in (0, 1, 16, 100):
|
||||||
|
plaintext = os.urandom(length)
|
||||||
|
self.assertEqual(enc.decrypt(key, 1024, enc.encrypt(key, 1024, plaintext)), plaintext)
|
||||||
|
|
||||||
|
def test_integrity_check(self):
|
||||||
|
enc = ENCTYPES[23]
|
||||||
|
key = enc.string2key("x")
|
||||||
|
ciphertext = bytearray(enc.encrypt(key, 3, b"secret"))
|
||||||
|
ciphertext[-1] ^= 1
|
||||||
|
self.assertRaises(ValueError, enc.decrypt, key, 3, bytes(ciphertext))
|
||||||
|
|
||||||
|
|
||||||
|
class TestKerberosDER(unittest.TestCase):
|
||||||
|
def test_integer_canonical(self):
|
||||||
|
for value, expected in ((0, "020100"), (127, "02017f"), (128, "02020080"),
|
||||||
|
(256, "02020100"), (-1, "0201ff"), (-129, "0202ff7f")):
|
||||||
|
self.assertEqual(binascii.hexlify(der.integer(value)).decode(), expected)
|
||||||
|
self.assertEqual(der.decodeInteger(der.peel(der.integer(value))[1]), value)
|
||||||
|
|
||||||
|
def test_application_tags(self):
|
||||||
|
self.assertEqual(bytearray(der.application(10, der.sequence()))[0], 0x6a) # AS-REQ
|
||||||
|
self.assertEqual(bytearray(der.application(14, der.sequence()))[0], 0x6e) # AP-REQ
|
||||||
|
self.assertEqual(bytearray(der.tagged(0, der.integer(1)))[0], 0xa0) # [0] EXPLICIT
|
||||||
|
|
||||||
|
def test_nested_round_trip(self):
|
||||||
|
pname = der.sequence(
|
||||||
|
der.tagged(0, der.integer(1)),
|
||||||
|
der.tagged(1, der.sequenceOf([der.generalString("HTTP"), der.generalString("web.example.com")])),
|
||||||
|
)
|
||||||
|
_, content, _ = der.peel(pname)
|
||||||
|
fields = dict(der.children(content))
|
||||||
|
components = [der.decodeGeneralString(c) for _, c in der.children(der.peel(fields[0xa1])[1])]
|
||||||
|
self.assertEqual(der.decodeInteger(der.peel(fields[0xa0])[1]), 1)
|
||||||
|
self.assertEqual(components, ["HTTP", "web.example.com"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestKerberosClient(unittest.TestCase):
|
||||||
|
def test_malformed_reply_raises_kerberoserror(self):
|
||||||
|
# a hostile/truncated KDC reply must surface as KerberosError, never a raw parse exception
|
||||||
|
key, nonce = os.urandom(32), 0x11223344
|
||||||
|
for blob in (b"", b"\x7e\x01", b"\x6b\x02\x30\x00", os.urandom(40)):
|
||||||
|
self.assertRaises(client.KerberosError, client._parseRep, blob, key, 3, nonce, client.AS_REP)
|
||||||
|
self.assertRaises(client.KerberosError, client._replyEtype, blob)
|
||||||
|
|
||||||
|
def test_etype_info2_best_effort(self):
|
||||||
|
# a malformed PA-ETYPE-INFO2 must yield no advertised info (fall back to defaults), not crash
|
||||||
|
self.assertEqual(client._parseEtypeInfo2({12: der.octetString(b"\xff\xff\xff")}), [])
|
||||||
|
self.assertEqual(client._parseEtypeInfo2({}), [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestKerberosDiscovery(unittest.TestCase):
|
||||||
|
def test_krb5conf(self):
|
||||||
|
content = ("[realms]\n"
|
||||||
|
" EXAMPLE.COM = {\n kdc = dc1.example.com:88\n admin_server = dc1.example.com\n }\n"
|
||||||
|
" OTHER.COM = { kdc = other-dc }\n"
|
||||||
|
# a nested '{ }' block ahead of 'kdc =' must not truncate the realm section
|
||||||
|
" NESTED.COM = {\n auth_to_local_names = {\n joe = joe\n }\n kdc = dc.nested.com\n }\n")
|
||||||
|
handle, path = tempfile.mkstemp()
|
||||||
|
os.write(handle, content.encode("utf-8"))
|
||||||
|
os.close(handle)
|
||||||
|
saved = os.environ.get("KRB5_CONFIG")
|
||||||
|
os.environ["KRB5_CONFIG"] = path
|
||||||
|
try:
|
||||||
|
self.assertEqual(discovery._fromKrb5Conf("EXAMPLE.COM"), "dc1.example.com:88")
|
||||||
|
self.assertEqual(discovery._fromKrb5Conf("OTHER.COM"), "other-dc")
|
||||||
|
self.assertEqual(discovery._fromKrb5Conf("NESTED.COM"), "dc.nested.com")
|
||||||
|
self.assertIsNone(discovery._fromKrb5Conf("MISSING.COM"))
|
||||||
|
finally:
|
||||||
|
os.remove(path)
|
||||||
|
os.environ.pop("KRB5_CONFIG", None) if saved is None else os.environ.__setitem__("KRB5_CONFIG", saved)
|
||||||
|
|
||||||
|
def test_split_host_port(self):
|
||||||
|
self.assertEqual(discovery._splitHostPort("dc.example.com"), ("dc.example.com", 88))
|
||||||
|
self.assertEqual(discovery._splitHostPort("dc.example.com:1088"), ("dc.example.com", 1088))
|
||||||
|
self.assertEqual(discovery._splitHostPort("[2001:db8::1]:1088"), ("2001:db8::1", 1088))
|
||||||
|
self.assertEqual(discovery._splitHostPort("[2001:db8::1]"), ("2001:db8::1", 88))
|
||||||
|
self.assertEqual(discovery._splitHostPort("2001:db8::1"), ("2001:db8::1", 88))
|
||||||
|
|
||||||
|
def test_srv_parse(self):
|
||||||
|
header = struct.pack(">HHHHHH", 0x2a2a, 0x8180, 1, 1, 0, 0)
|
||||||
|
question = _dnsName("_kerberos._tcp.EXAMPLE.COM") + struct.pack(">HH", 33, 1)
|
||||||
|
rdata = struct.pack(">HHH", 0, 100, 88) + _dnsName("dc.example.com")
|
||||||
|
answer = b"\xc0\x0c" + struct.pack(">HHIH", 33, 1, 300, len(rdata)) + rdata # name = ptr to question
|
||||||
|
self.assertEqual(discovery.parseSrv(header + question + answer), [(0, 100, 88, "dc.example.com")])
|
||||||
|
|
||||||
|
def test_srv_parse_hostile_input(self):
|
||||||
|
# a compression-pointer cycle (name at offset 12 points to itself) must not hang or crash
|
||||||
|
cycle = struct.pack(">HHHHHH", 1, 0x8180, 0, 1, 0, 0) + b"\xc0\x0c"
|
||||||
|
self.assertEqual(discovery.parseSrv(cycle), [])
|
||||||
|
self.assertEqual(discovery.parseSrv(b""), [])
|
||||||
|
self.assertEqual(discovery.parseSrv(b"\x00\x00\x81\x80\x00\x00\x00\x05\xff\xff"), [])
|
||||||
|
|
||||||
|
def test_precedence_env_overrides(self):
|
||||||
|
saved = os.environ.get("SQLMAP_KERBEROS_KDC")
|
||||||
|
os.environ["SQLMAP_KERBEROS_KDC"] = "10.0.0.1:8888"
|
||||||
|
try:
|
||||||
|
self.assertEqual(discovery.discoverKdc("EXAMPLE.COM"), ("10.0.0.1", 8888))
|
||||||
|
finally:
|
||||||
|
os.environ.pop("SQLMAP_KERBEROS_KDC", None) if saved is None else os.environ.__setitem__("SQLMAP_KERBEROS_KDC", saved)
|
||||||
|
|
||||||
|
def test_fallback_to_realm(self):
|
||||||
|
savedEnv = os.environ.pop("SQLMAP_KERBEROS_KDC", None)
|
||||||
|
savedCfg = os.environ.get("KRB5_CONFIG")
|
||||||
|
os.environ["KRB5_CONFIG"] = "/nonexistent/sqlmap-krb5.conf"
|
||||||
|
savedDns = discovery._fromDnsSrv
|
||||||
|
discovery._fromDnsSrv = lambda realm: None # avoid real DNS I/O in the test
|
||||||
|
try:
|
||||||
|
self.assertEqual(discovery.discoverKdc("CORP.EXAMPLE"), ("corp.example", 88))
|
||||||
|
finally:
|
||||||
|
discovery._fromDnsSrv = savedDns
|
||||||
|
if savedEnv is not None:
|
||||||
|
os.environ["SQLMAP_KERBEROS_KDC"] = savedEnv
|
||||||
|
os.environ.pop("KRB5_CONFIG", None) if savedCfg is None else os.environ.__setitem__("KRB5_CONFIG", savedCfg)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue