sqlmap/lib/request/kerberos.py
2026-07-25 00:24:52 +02:00

99 lines
5.1 KiB
Python

#!/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
import time
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
TICKET_REFRESH_SKEW = 300 # re-fetch a ticket this long before it expires
def _expiring(ticket):
"""True for a cached ticket close enough to its expiry to be worth replacing (a scan can easily
run longer than the ticket lifetime, and an expired AP-REQ is rejected by every acceptor)."""
return ticket is not None and ticket.get("endtime") is not None and time.time() + TICKET_REFRESH_SKEW >= ticket["endtime"]
class HTTPNegotiateAuthHandler(_urllib.request.BaseHandler):
handler_order = 480
def __init__(self, realm, username, password, kdcHost=None, kdcPort=None):
# Kerberos realms are case-sensitive, but the credentials arrive in the Windows 'DOMAIN\\user'
# form where the domain is not, so normalize here rather than inside the protocol client
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 _expiring(self._tickets.get(host)): # drop a ticket that a long scan has outlived
del self._tickets[host]
if _expiring(self._tgt):
self._tgt = None
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