Embedding the NTLM auth support (without the deprecated 3rd party lib)

This commit is contained in:
Miroslav Štampar 2026-07-09 10:38:19 +02:00
parent 8837dd2435
commit 767feba3a7
6 changed files with 275 additions and 39 deletions

View file

@ -1515,15 +1515,8 @@ def _setHTTPAuthentication():
authHandler = _urllib.request.HTTPDigestAuthHandler(kb.passwordMgr)
elif authType == AUTH_TYPE.NTLM:
try:
from ntlm import HTTPNtlmAuthHandler
except ImportError:
errMsg = "sqlmap requires Python NTLM third-party library "
errMsg += "in order to authenticate via NTLM. Download from "
errMsg += "'https://github.com/mullender/python-ntlm'"
raise SqlmapMissingDependence(errMsg)
authHandler = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(kb.passwordMgr)
from lib.request.ntlm import HTTPNtlmAuthHandler
authHandler = HTTPNtlmAuthHandler(kb.passwordMgr)
else:
debugMsg = "setting the HTTP(s) authentication PEM private key"
logger.debug(debugMsg)

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.54"
VERSION = "1.10.7.55"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
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)

269
lib/request/ntlm.py Normal file
View file

@ -0,0 +1,269 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Native, dependency-free NTLM-over-HTTP authentication (MS-NLMP / MS-NTHT), replacing the ancient
# and unmaintained 'python-ntlm' third-party library. Pure standard library + the in-tree pyDes for
# the DES core (hashlib's MD4 is unavailable under OpenSSL 3's unloaded legacy provider, so MD4 is
# implemented here per RFC 1320). Python 2.7 / 3.x. Crypto validated against the [MS-NLMP] test vectors.
import base64
import binascii
import os
import re
import socket
import ssl
import struct
from lib.core.convert import getBytes
from lib.core.convert import getText
from thirdparty.pydes.pyDes import des
from thirdparty.pydes.pyDes import ECB
from thirdparty.six.moves import http_client as _http_client
from thirdparty.six.moves import urllib as _urllib
# ---------- MD4 (RFC 1320) ----------
def _md4(data):
data = bytearray(data)
n = len(data)
data.append(0x80)
while len(data) % 64 != 56:
data.append(0)
data += struct.pack("<Q", (n * 8) & 0xffffffffffffffff)
a, b, c, d = 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
M = 0xffffffff
rot = lambda x, s: ((x << s) | (x >> (32 - s))) & M
for off in range(0, len(data), 64):
X = struct.unpack("<16I", bytes(data[off:off + 64]))
A, B, C, D = a, b, c, d
F = lambda x, y, z: (x & y) | (~x & z)
G = lambda x, y, z: (x & y) | (x & z) | (y & z)
H = lambda x, y, z: x ^ y ^ z
for k in (0, 4, 8, 12):
A = rot((A + F(B, C, D) + X[k]) & M, 3)
D = rot((D + F(A, B, C) + X[k + 1]) & M, 7)
C = rot((C + F(D, A, B) + X[k + 2]) & M, 11)
B = rot((B + F(C, D, A) + X[k + 3]) & M, 19)
for k in (0, 1, 2, 3):
A = rot((A + G(B, C, D) + X[k] + 0x5a827999) & M, 3)
D = rot((D + G(A, B, C) + X[k + 4] + 0x5a827999) & M, 5)
C = rot((C + G(D, A, B) + X[k + 8] + 0x5a827999) & M, 9)
B = rot((B + G(C, D, A) + X[k + 12] + 0x5a827999) & M, 13)
for k in (0, 2, 1, 3):
A = rot((A + H(B, C, D) + X[k] + 0x6ed9eba1) & M, 3)
D = rot((D + H(A, B, C) + X[k + 8] + 0x6ed9eba1) & M, 9)
C = rot((C + H(D, A, B) + X[k + 4] + 0x6ed9eba1) & M, 11)
B = rot((B + H(C, D, A) + X[k + 12] + 0x6ed9eba1) & M, 15)
a, b, c, d = (a + A) & M, (b + B) & M, (c + C) & M, (d + D) & M
return struct.pack("<4I", a, b, c, d)
# ---------- DES core (in-tree pyDes) with the NTLM 7->8 byte parity key expansion ----------
def _str_to_key(chunk):
s = bytearray(chunk)
k = [s[0] >> 1,
((s[0] & 0x01) << 6) | (s[1] >> 2),
((s[1] & 0x03) << 5) | (s[2] >> 3),
((s[2] & 0x07) << 4) | (s[3] >> 4),
((s[3] & 0x0f) << 3) | (s[4] >> 5),
((s[4] & 0x1f) << 2) | (s[5] >> 6),
((s[5] & 0x3f) << 1) | (s[6] >> 7),
s[6] & 0x7f]
return bytes(bytearray((_ << 1) & 0xff for _ in k))
def _des(key7, block8):
return des(_str_to_key(key7), ECB).encrypt(block8)
# ---------- negotiate flags ----------
NTLM_NegotiateUnicode = 0x00000001
NTLM_NegotiateOEM = 0x00000002
NTLM_RequestTarget = 0x00000004
NTLM_NegotiateNTLM = 0x00000200
NTLM_NegotiateOemDomainSupplied = 0x00001000
NTLM_NegotiateOemWorkstationSupplied = 0x00002000
NTLM_NegotiateAlwaysSign = 0x00008000
NTLM_NegotiateExtendedSecurity = 0x00080000
NTLM_NegotiateTargetInfo = 0x00800000
NTLM_NegotiateVersion = 0x02000000
NTLM_Negotiate128 = 0x20000000
NTLM_Negotiate56 = 0x80000000
NTLM_MsvAvTimestamp = 7
NTLM_TYPE1_FLAGS = (NTLM_NegotiateUnicode | NTLM_NegotiateOEM | NTLM_RequestTarget | NTLM_NegotiateNTLM |
NTLM_NegotiateOemDomainSupplied | NTLM_NegotiateOemWorkstationSupplied |
NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateVersion |
NTLM_Negotiate128 | NTLM_Negotiate56)
NTLM_TYPE2_FLAGS = (NTLM_NegotiateUnicode | NTLM_RequestTarget | NTLM_NegotiateNTLM |
NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateTargetInfo |
NTLM_NegotiateVersion | NTLM_Negotiate128 | NTLM_Negotiate56)
_HASH_RE = re.compile(r"\A[0-9a-fA-F]{32}:[0-9a-fA-F]{32}\Z")
# ---------- password hashes / challenge responses ----------
def create_LM_hashed_password_v1(password):
if _HASH_RE.match(password):
return binascii.unhexlify(password.split(':')[0])
pw = (getBytes(password.upper()) + b"\0" * 14)[:14]
return _des(pw[0:7], b"KGS!@#$%") + _des(pw[7:14], b"KGS!@#$%")
def create_NT_hashed_password_v1(password, user=None, domain=None):
if _HASH_RE.match(password):
return binascii.unhexlify(password.split(':')[1])
return _md4(password.encode("utf-16-le"))
def calc_resp(password_hash, server_challenge):
ph = password_hash + b"\0" * (21 - len(password_hash))
return _des(ph[0:7], server_challenge) + _des(ph[7:14], server_challenge) + _des(ph[14:21], server_challenge)
def ntlm2sr_calc_resp(nt_hash, server_challenge, client_challenge):
import hashlib
lm_response = client_challenge + b"\0" * 16
session = hashlib.md5(server_challenge + client_challenge).digest()
nt_response = calc_resp(nt_hash, session[0:8])
return (nt_response, lm_response)
# ---------- messages ----------
def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS):
workstation = getBytes(socket.gethostname().upper())
domain = getBytes(user.split('\\', 1)[0].upper())
body = 40
msg = b"NTLMSSP\0" + struct.pack("<I", 1) + struct.pack("<I", type1_flags)
msg += struct.pack("<HHI", len(domain), len(domain), body)
msg += struct.pack("<HHI", len(workstation), len(workstation), body + len(domain))
msg += struct.pack("<BBHBBBB", 5, 1, 2600, 0, 0, 0, 15) # advertised OS version
msg += workstation + domain
return getText(base64.b64encode(msg))
def parse_NTLM_CHALLENGE_MESSAGE(msg2):
msg = base64.b64decode(msg2)
negotiate_flags = struct.unpack("<I", msg[20:24])[0]
server_challenge = msg[24:32]
return (server_challenge, negotiate_flags)
def create_NTLM_AUTHENTICATE_MESSAGE(nonce, user, domain, password, negotiate_flags):
is_unicode = negotiate_flags & NTLM_NegotiateUnicode
is_extended = negotiate_flags & NTLM_NegotiateExtendedSecurity
workstation = socket.gethostname().upper()
domain = domain.upper()
if is_unicode:
workstation = workstation.encode("utf-16-le")
domain = domain.encode("utf-16-le")
username = user.encode("utf-16-le")
else:
workstation = getBytes(workstation)
domain = getBytes(domain)
username = getBytes(user)
lm_response = calc_resp(create_LM_hashed_password_v1(password), nonce)
nt_response = calc_resp(create_NT_hashed_password_v1(password), nonce)
if is_extended:
nt_hash = create_NT_hashed_password_v1(password)
client_challenge = os.urandom(8)
nt_response, lm_response = ntlm2sr_calc_resp(nt_hash, nonce, client_challenge)
body = 72
payload = domain + username + workstation + lm_response + nt_response
fields = b"NTLMSSP\0" + struct.pack("<I", 3)
fields += struct.pack("<HHI", len(lm_response), len(lm_response), body + len(domain) + len(username) + len(workstation))
fields += struct.pack("<HHI", len(nt_response), len(nt_response), body + len(domain) + len(username) + len(workstation) + len(lm_response))
fields += struct.pack("<HHI", len(domain), len(domain), body)
fields += struct.pack("<HHI", len(username), len(username), body + len(domain))
fields += struct.pack("<HHI", len(workstation), len(workstation), body + len(domain) + len(username))
fields += struct.pack("<HHI", 0, 0, body + len(payload)) # empty EncryptedRandomSessionKey
fields += struct.pack("<I", NTLM_TYPE2_FLAGS)
fields += struct.pack("<BBHBBBB", 5, 1, 2600, 0, 0, 0, 15)
return getText(base64.b64encode(fields + payload))
# ---------- urllib handler ----------
class _AbstractNtlmAuthHandler(object):
def __init__(self, password_mgr=None):
self.passwd = password_mgr or _urllib.request.HTTPPasswordMgr()
self.add_password = self.passwd.add_password
def _authenticate(self, auth_header_field, req, headers):
auth_header_value = headers.get(auth_header_field, None)
if auth_header_value is not None and 'ntlm' in auth_header_value.lower():
return self._retry_ntlm(req, auth_header_field)
def _retry_ntlm(self, req, auth_header_field):
user, pw = self.passwd.find_user_password(None, req.get_full_url())
if pw is None:
return None
parts = user.split('\\', 1)
if len(parts) == 1:
username, domain = parts[0], ''
type1_flags = NTLM_TYPE1_FLAGS & ~NTLM_NegotiateOemDomainSupplied
else:
domain, username = parts[0].upper(), parts[1]
type1_flags = NTLM_TYPE1_FLAGS
# NTLM authenticates the CONNECTION, not the request, so the whole handshake rides one socket
headers = dict(req.headers)
headers.update(req.unredirected_hdrs)
negotiate = 'NTLM %s' % create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags)
if req.headers.get(self.auth_header, None) == negotiate:
return None
headers[self.auth_header] = negotiate
host = req.host if hasattr(req, "host") else req.get_host()
if not host:
raise _urllib.error.URLError("no host given")
if req.get_full_url().startswith("https://"):
conn = _http_client.HTTPSConnection(host, context=ssl._create_unverified_context())
else:
conn = _http_client.HTTPConnection(host)
selector = req.selector if hasattr(req, "selector") else req.get_selector()
method = req.get_method()
headers["Connection"] = "Keep-Alive"
headers = dict((name.title(), value) for name, value in headers.items())
conn.request(method, selector, req.data, headers)
resp = conn.getresponse()
resp.read() # drain the challenge body, keep the socket
if resp.getheader("set-cookie"): # some apps track auth state in a cookie
headers["Cookie"] = resp.getheader("set-cookie")
auth_header_value = resp.getheader(auth_header_field, None)
match = re.match(r"(NTLM [A-Za-z0-9+\-/=]+)", auth_header_value or "")
if not match:
return None
server_challenge, negotiate_flags = parse_NTLM_CHALLENGE_MESSAGE(match.group(1)[5:])
headers[self.auth_header] = 'NTLM %s' % create_NTLM_AUTHENTICATE_MESSAGE(server_challenge, username, domain, pw, negotiate_flags)
headers["Connection"] = "Close"
headers = dict((name.title(), value) for name, value in headers.items())
try:
conn.request(method, selector, req.data, headers)
resp = conn.getresponse()
if not hasattr(resp, "readline"): # Python 2 httplib responses lack it; addinfourl copies it
resp.readline = lambda *args, **kwargs: b""
infourl = _urllib.response.addinfourl(resp, resp.msg, req.get_full_url())
infourl.code = resp.status
infourl.msg = resp.reason
return infourl
except socket.error as ex:
raise _urllib.error.URLError(ex)
class HTTPNtlmAuthHandler(_AbstractNtlmAuthHandler, _urllib.request.BaseHandler):
auth_header = "Authorization"
def http_error_401(self, req, fp, code, msg, headers):
fp.close()
return self._authenticate("www-authenticate", req, headers)
class ProxyNtlmAuthHandler(_AbstractNtlmAuthHandler, _urllib.request.BaseHandler):
auth_header = "Proxy-authorization"
def http_error_407(self, req, fp, code, msg, headers):
fp.close()
return self._authenticate("proxy-authenticate", req, headers)

View file

@ -83,17 +83,6 @@ def checkDependencies():
logger.warning(warnMsg)
missing_libraries.add('python-impacket')
try:
__import__("ntlm")
debugMsg = "'python-ntlm' third-party library is found"
logger.debug(debugMsg)
except ImportError:
warnMsg = "sqlmap requires 'python-ntlm' third-party library "
warnMsg += "if you plan to attack a web application behind NTLM "
warnMsg += "authentication. Download from 'https://github.com/mullender/python-ntlm'"
logger.warning(warnMsg)
missing_libraries.add('python-ntlm')
try:
__import__("websocket._abnf")
debugMsg = "'websocket-client' library is found"

View file

@ -432,11 +432,6 @@ def main():
logger.critical(errMsg)
raise SystemExit
elif all(_ in excMsg for _ in ("ntlm", "socket.error, err", "SyntaxError")):
errMsg = "wrong initialization of 'python-ntlm' detected (using Python2 syntax)"
logger.critical(errMsg)
raise SystemExit
elif all(_ in excMsg for _ in ("drda", "to_bytes")):
errMsg = "wrong initialization of 'drda' detected (using Python3 syntax)"
logger.critical(errMsg)
@ -518,12 +513,6 @@ def main():
logger.critical(errMsg)
raise SystemExit
elif all(_ in excMsg for _ in ("HTTPNtlmAuthHandler", "'str' object has no attribute 'decode'")):
errMsg = "package 'python-ntlm' has a known compatibility issue with the "
errMsg += "Python 3 (Reference: 'https://github.com/mullender/python-ntlm/pull/61')"
logger.critical(errMsg)
raise SystemExit
elif "'DictObject' object has no attribute '" in excMsg and all(_ in errMsg for _ in ("(fingerprinted)", "(identified)")):
errMsg = "there has been a problem in enumeration. "
errMsg += "Because of a considerable chance of false-positive case "

View file

@ -37,7 +37,6 @@ from lib.core.settings import IGNORE_CODE_WILDCARD
from lib.core.settings import MAX_CONNECT_RETRIES
from lib.core.exception import SqlmapFilePathException
from lib.core.exception import SqlmapGenericException
from lib.core.exception import SqlmapMissingDependence
from lib.core.exception import SqlmapMissingMandatoryOptionException
from lib.core.exception import SqlmapSyntaxException
from lib.core.exception import SqlmapSystemException
@ -1491,12 +1490,9 @@ class TestOptionSetHTTPAuthentication(unittest.TestCase):
conf.authFile = None
conf.authUsername = None
conf.authPassword = None
# The python-ntlm handler module is optional; credential parsing happens
# before the handler import, so the parsed creds are set regardless.
try:
option._setHTTPAuthentication()
except SqlmapMissingDependence:
pass
# The NTLM handler is now native (lib/request/ntlm.py), so no optional
# dependency is involved and credential parsing must succeed outright.
option._setHTTPAuthentication()
self.assertEqual(conf.authUsername, "DOMAIN\\user")
self.assertEqual(conf.authPassword, "pa:ss")