From 308eec96cdb969b8157f26a43856a402aeb31bc6 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:42:12 +0200 Subject: [PATCH 1/2] Add WinSSPI SSP for Windows --- doc/scapy/layers/gssapi.rst | 23 +- scapy/arch/__init__.py | 1 + scapy/arch/windows/sspi.py | 696 ++++++++++++++++++++++++++++++++ scapy/layers/ntlm.py | 30 +- scapy/layers/smbclient.py | 19 +- scapy/layers/tls/crypto/hash.py | 26 +- test/scapy/layers/ntlm.uts | 4 +- 7 files changed, 776 insertions(+), 23 deletions(-) create mode 100644 scapy/arch/windows/sspi.py diff --git a/doc/scapy/layers/gssapi.rst b/doc/scapy/layers/gssapi.rst index d4b3d6571bd..d405d6f8ff6 100644 --- a/doc/scapy/layers/gssapi.rst +++ b/doc/scapy/layers/gssapi.rst @@ -21,6 +21,7 @@ The following SSPs are currently provided: - :class:`~scapy.layers.kerberos.KerberosSSP` - :class:`~scapy.layers.spnego.SPNEGOSSP` - :class:`~scapy.layers.msrpce.msnrpc.NetlogonSSP` + - :class:`~scapy.arch.windows.sspi.WinSSP` (Windows only) Basically those are classes that implement two functions, trying to micmic the RFCs: @@ -134,4 +135,24 @@ Let's use :class:`~scapy.layers.ntlm.NTLMSSP` as an example of server-side SSP. } ) -You'll find other examples of how to instantiate a SSP in the docstrings of each SSP. See `the list <#ssplist>`_ \ No newline at end of file +You'll find other examples of how to instantiate a SSP in the docstrings of each SSP. See `the list <#ssplist>`_ + +WinSSP +~~~~~~ + +WinSSP is a special SSP that is only available on Windows, which calls the actual Windows SSPs local to the machine it's running on. +It allows to use the implicit authentication of the logged-in user with Scapy and its various clients, and is also improves support of loopback connections. + +For instance using SPNEGO: + +.. code:: python + + from scapy.arch.windows.sspi import * + clissp = WinSSP(Package="Negotiate") + +For instance using NTLM: + +.. code:: python + + from scapy.arch.windows.sspi import * + clissp = WinSSP(Package="NTLM") diff --git a/scapy/arch/__init__.py b/scapy/arch/__init__.py index 316d398f570..bea0a570e02 100644 --- a/scapy/arch/__init__.py +++ b/scapy/arch/__init__.py @@ -143,6 +143,7 @@ def get_if_raw_addr6(iff): elif WINDOWS: from scapy.arch.windows import * # noqa F403 from scapy.arch.windows.native import * # noqa F403 + from scapy.arch.windows.sspi import * # noqa F403 SIOCGIFHWADDR = 0 # mypy compat else: log_loading.critical( diff --git a/scapy/arch/windows/sspi.py b/scapy/arch/windows/sspi.py new file mode 100644 index 00000000000..8d50a0532f9 --- /dev/null +++ b/scapy/arch/windows/sspi.py @@ -0,0 +1,696 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +SSP for implicit authentication on Windows +""" + +import ctypes +import ctypes.wintypes +import enum + +from scapy.layers.gssapi import ( + GSS_C_FLAGS, + GSS_S_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GssChannelBindings, + GSSAPI_BLOB, + SSP, + GSS_S_BAD_NAME, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_DEFECTIVE_CREDENTIAL, + GSS_S_DEFECTIVE_TOKEN, + GSS_S_FAILURE, + GSS_S_UNAUTHORIZED, + GSS_S_UNAVAILABLE, +) + +# Typing imports +from typing import ( + Optional, +) + +# Windows bindings + +SECPKG_CRED_INBOUND = 0x00000001 +SECPKG_CRED_OUTBOUND = 0x00000002 +SECPKG_CRED_BOTH = 0x00000003 + +SECPKG_ATTR_SESSION_KEY = 9 +SECPKG_ATTR_SERVER_FLAGS = 14 + + +class SecPkgContext_SessionKey(ctypes.Structure): + _fields_ = [ + ("SessionKeyLength", ctypes.wintypes.ULONG), + ("SessionKey", ctypes.wintypes.LPBYTE), + ] + + +class SecPkgContext_Flags(ctypes.Structure): + _fields_ = [ + ("Flags", ctypes.wintypes.ULONG), + ] + + +SECURITY_NETWORK_DREP = 0 + + +class SEC_CODES(enum.IntEnum): + """ + Windows sspi.h return codes + """ + + SEC_E_OK = 0x00000000 + SEC_I_CONTINUE_NEEDED = 0x00090312 + SEC_I_COMPLETE_AND_CONTINUE = 0x00090314 + SEC_E_INSUFFICIENT_MEMORY = 0x80090300 + SEC_E_INTERNAL_ERROR = 0x80090304 + SEC_E_INVALID_HANDLE = 0x80090301 + SEC_E_INVALID_TOKEN = 0x80090308 + SEC_E_LOGON_DENIED = 0x8009030C + SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311 + SEC_E_NO_CREDENTIALS = 0x8009030E + SEC_E_TARGET_UNKNOWN = 0x80090303 + SEC_E_UNSUPPORTED_FUNCTION = 0x80090302 + SEC_E_WRONG_PRINCIPAL = 0x80090322 + + @staticmethod + def to_GSS(code: int): + if code in _GSS_REG_TRANSLATION: + return _GSS_REG_TRANSLATION[code] + else: + return code + + +_GSS_REG_TRANSLATION = { + SEC_CODES.SEC_E_OK: GSS_S_COMPLETE, + SEC_CODES.SEC_I_CONTINUE_NEEDED: GSS_S_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE: GSS_S_CONTINUE_NEEDED, + SEC_CODES.SEC_E_INSUFFICIENT_MEMORY: GSS_S_FAILURE, + SEC_CODES.SEC_E_INTERNAL_ERROR: GSS_S_FAILURE, + SEC_CODES.SEC_E_INVALID_HANDLE: GSS_S_DEFECTIVE_CREDENTIAL, + SEC_CODES.SEC_E_INVALID_TOKEN: GSS_S_DEFECTIVE_TOKEN, + SEC_CODES.SEC_E_LOGON_DENIED: GSS_S_UNAUTHORIZED, + SEC_CODES.SEC_E_NO_AUTHENTICATING_AUTHORITY: GSS_S_UNAVAILABLE, + SEC_CODES.SEC_E_NO_CREDENTIALS: GSS_S_DEFECTIVE_CREDENTIAL, + SEC_CODES.SEC_E_TARGET_UNKNOWN: GSS_S_BAD_NAME, + SEC_CODES.SEC_E_UNSUPPORTED_FUNCTION: GSS_S_UNAVAILABLE, + SEC_CODES.SEC_E_WRONG_PRINCIPAL: GSS_S_BAD_NAME, +} + + +class SECURITY_INTEGER(ctypes.Structure): + _fields_ = [ + ("LowPart", ctypes.wintypes.ULONG), + ("HighPart", ctypes.wintypes.LONG), + ] + + +class SecHandle(ctypes.Structure): + _fields_ = [ + ("dwLower", ctypes.POINTER(ctypes.wintypes.ULONG)), + ("dwUpper", ctypes.POINTER(ctypes.wintypes.ULONG)), + ] + + +_winapi_AcquireCredentialsHandle = ctypes.windll.secur32.AcquireCredentialsHandleW +_winapi_AcquireCredentialsHandle.restype = ctypes.wintypes.DWORD +_winapi_AcquireCredentialsHandle.argtypes = [ + ctypes.wintypes.LPWSTR, # pszPrincipal + ctypes.wintypes.LPWSTR, # pszPackage + ctypes.wintypes.ULONG, # fCredentialUse + ctypes.c_void_p, # pvLogonID + ctypes.c_void_p, # pAuthData + ctypes.c_void_p, # pGetKeyFn + ctypes.c_void_p, # pvGetKeyArgument + ctypes.POINTER(SecHandle), # phCredential, + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + + +class SecBuffer(ctypes.Structure): + _fields_ = [ + ("cbBuffer", ctypes.wintypes.ULONG), + ("BufferType", ctypes.wintypes.ULONG), + ("pvBuffer", ctypes.c_void_p), + ] + + +SECBUFFER_VERSION = 0 +SECBUFFER_TOKEN = 2 +SECBUFFER_CHANNEL_BINDINGS = 14 + + +class SecBufferDesc(ctypes.Structure): + _fields_ = [ + ("ulVersion", ctypes.wintypes.ULONG), + ("cBuffers", ctypes.wintypes.ULONG), + ("pBuffers", ctypes.POINTER(ctypes.POINTER(SecBuffer))), + ] + + +_winapi_InitializeSecurityContext = ctypes.windll.secur32.InitializeSecurityContextW +_winapi_InitializeSecurityContext.restype = ctypes.wintypes.DWORD +_winapi_InitializeSecurityContext.argtypes = [ + ctypes.POINTER(SecHandle), # phCredential + ctypes.POINTER(SecHandle), # phContext (NULL on first call) + ctypes.wintypes.LPCWSTR, # pszTargetName + ctypes.wintypes.ULONG, # fContextReq + ctypes.wintypes.ULONG, # Reserved1 (must be 0) + ctypes.wintypes.ULONG, # TargetDataRep (e.g. SECURITY_NATIVE_DREP) + ctypes.POINTER(SecBufferDesc), # pInput (can be NULL) + ctypes.wintypes.ULONG, # Reserved2 (must be 0) + ctypes.POINTER(SecHandle), # phNewContext + ctypes.POINTER(SecBufferDesc), # pOutput + ctypes.POINTER(ctypes.wintypes.ULONG), # pfContextAttr + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + +_winapi_AcceptSecurityContext = ctypes.windll.secur32.AcceptSecurityContext +_winapi_AcceptSecurityContext.restype = ctypes.wintypes.DWORD +_winapi_AcceptSecurityContext.argtypes = [ + ctypes.POINTER(SecHandle), # phCredential + ctypes.POINTER(SecHandle), # phContext (NULL on first call) + ctypes.POINTER(SecBufferDesc), # pInput + ctypes.wintypes.ULONG, # fContextReq + ctypes.wintypes.ULONG, # TargetDataRep (e.g. SECURITY_NATIVE_DREP) + ctypes.POINTER(SecHandle), # phNewContext + ctypes.POINTER(SecBufferDesc), # pOutput + ctypes.POINTER(ctypes.wintypes.ULONG), # pfContextAttr + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + +_winapi_FreeContextBuffer = ctypes.windll.secur32.FreeContextBuffer +_winapi_FreeContextBuffer.restype = ctypes.wintypes.DWORD +_winapi_FreeContextBuffer.argtypes = [ctypes.c_void_p] + +_winapi_QueryContextAttributesW = ctypes.windll.secur32.QueryContextAttributesW +_winapi_QueryContextAttributesW.restype = ctypes.wintypes.DWORD +_winapi_QueryContextAttributesW.argtypes = [ + ctypes.POINTER(SecHandle), + ctypes.wintypes.ULONG, + ctypes.c_void_p, +] + +_winapi_SspiGetTargetHostName = ctypes.windll.secur32.SspiGetTargetHostName +_winapi_SspiGetTargetHostName.restype = ctypes.wintypes.DWORD +_winapi_SspiGetTargetHostName.argtypes = [ + ctypes.wintypes.LPCWSTR, + ctypes.POINTER(ctypes.wintypes.LPWSTR), +] + + +# Types + + +class ISC_REQ_FLAGS(enum.IntFlag): + """ + ISC_REQ Flags per sspi.h + """ + + ISC_REQ_DELEGATE = 0x00000001 + ISC_REQ_MUTUAL_AUTH = 0x00000002 + ISC_REQ_REPLAY_DETECT = 0x00000004 + ISC_REQ_SEQUENCE_DETECT = 0x00000008 + ISC_REQ_CONFIDENTIALITY = 0x00000010 + ISC_REQ_USE_SESSION_KEY = 0x00000020 + ISC_REQ_PROMPT_FOR_CREDS = 0x00000040 + ISC_REQ_USE_SUPPLIED_CREDS = 0x00000080 + ISC_REQ_ALLOCATE_MEMORY = 0x00000100 + ISC_REQ_USE_DCE_STYLE = 0x00000200 + ISC_REQ_DATAGRAM = 0x00000400 + ISC_REQ_CONNECTION = 0x00000800 + ISC_REQ_CALL_LEVEL = 0x00001000 + ISC_REQ_FRAGMENT_SUPPLIED = 0x00002000 + ISC_REQ_EXTENDED_ERROR = 0x00004000 + ISC_REQ_STREAM = 0x00008000 + ISC_REQ_INTEGRITY = 0x00010000 + ISC_REQ_IDENTIFY = 0x00020000 + ISC_REQ_NULL_SESSION = 0x00040000 + ISC_REQ_MANUAL_CRED_VALIDATION = 0x00080000 + ISC_REQ_RESERVED1 = 0x00100000 + ISC_REQ_FRAGMENT_TO_FIT = 0x00200000 + ISC_REQ_FORWARD_CREDENTIALS = 0x00400000 + ISC_REQ_NO_INTEGRITY = 0x00800000 + ISC_REQ_USE_HTTP_STYLE = 0x01000000 + ISC_REQ_UNVERIFIED_TARGET_NAME = 0x20000000 + ISC_REQ_CONFIDENTIALITY_ONLY = 0x40000000 + ISC_REQ_MESSAGES = 0x0000000100000000 + ISC_REQ_DEFERRED_CRED_VALIDATION = 0x0000000200000000 + ISC_REQ_NO_POST_HANDSHAKE_AUTH = 0x0000000400000000 + ISC_REQ_REUSE_SESSION_TICKETS = 0x0000000800000000 + ISC_REQ_EXPLICIT_SESSION = 0x0000001000000000 + + @staticmethod + def from_GSS(flags: GSS_C_FLAGS) -> "ISC_REQ_FLAGS": + """ + Convert GSS_C_FLAGS into ISC_REQ_FLAGS + """ + result = 0 + for gssf, iscf in _GSS_ISC_TRANSLATION.items(): + if flags & gssf: + result |= iscf + return ISC_REQ_FLAGS(result) + + @staticmethod + def to_GSS(flags: "ISC_REQ_FLAGS") -> GSS_C_FLAGS: + """ + Convert ISC_REQ_FLAGS into GSS_C_FLAGS + """ + result = 0 + for gssf, iscf in _GSS_ISC_TRANSLATION.items(): + if flags & iscf: + result |= gssf + return GSS_C_FLAGS(result) + + +_GSS_ISC_TRANSLATION = { + GSS_C_FLAGS.GSS_C_DELEG_FLAG: ISC_REQ_FLAGS.ISC_REQ_DELEGATE, + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG: ISC_REQ_FLAGS.ISC_REQ_MUTUAL_AUTH, + GSS_C_FLAGS.GSS_C_REPLAY_FLAG: ISC_REQ_FLAGS.ISC_REQ_REPLAY_DETECT, + GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG: ISC_REQ_FLAGS.ISC_REQ_SEQUENCE_DETECT, + GSS_C_FLAGS.GSS_C_CONF_FLAG: ISC_REQ_FLAGS.ISC_REQ_CONFIDENTIALITY, + GSS_C_FLAGS.GSS_C_INTEG_FLAG: ISC_REQ_FLAGS.ISC_REQ_INTEGRITY, + GSS_C_FLAGS.GSS_C_DCE_STYLE: ISC_REQ_FLAGS.ISC_REQ_USE_DCE_STYLE, + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG: ISC_REQ_FLAGS.ISC_REQ_IDENTIFY, + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG: ISC_REQ_FLAGS.ISC_REQ_EXTENDED_ERROR, +} + + +class ASC_REQ_FLAGS(enum.IntFlag): + ASC_REQ_DELEGATE = 0x00000001 + ASC_REQ_MUTUAL_AUTH = 0x00000002 + ASC_REQ_REPLAY_DETECT = 0x00000004 + ASC_REQ_SEQUENCE_DETECT = 0x00000008 + ASC_REQ_CONFIDENTIALITY = 0x00000010 + ASC_REQ_USE_SESSION_KEY = 0x00000020 + ASC_REQ_SESSION_TICKET = 0x00000040 + ASC_REQ_ALLOCATE_MEMORY = 0x00000100 + ASC_REQ_USE_DCE_STYLE = 0x00000200 + ASC_REQ_DATAGRAM = 0x00000400 + ASC_REQ_CONNECTION = 0x00000800 + ASC_REQ_CALL_LEVEL = 0x00001000 + ASC_REQ_FRAGMENT_SUPPLIED = 0x00002000 + ASC_REQ_EXTENDED_ERROR = 0x00008000 + ASC_REQ_STREAM = 0x00010000 + ASC_REQ_INTEGRITY = 0x00020000 + ASC_REQ_LICENSING = 0x00040000 + ASC_REQ_IDENTIFY = 0x00080000 + ASC_REQ_ALLOW_NULL_SESSION = 0x00100000 + ASC_REQ_ALLOW_NON_USER_LOGONS = 0x00200000 + ASC_REQ_ALLOW_CONTEXT_REPLAY = 0x00400000 + ASC_REQ_FRAGMENT_TO_FIT = 0x00800000 + ASC_REQ_NO_TOKEN = 0x01000000 + ASC_REQ_PROXY_BINDINGS = 0x04000000 + ASC_REQ_ALLOW_MISSING_BINDINGS = 0x10000000 + + @staticmethod + def from_GSS(flags: GSS_C_FLAGS) -> "ASC_REQ_FLAGS": + """ + Convert GSS_C_FLAGS into ASC_REQ_FLAGS + """ + result = 0 + for gssf, ascf in _GSS_ASC_TRANSLATION.items(): + if flags & gssf: + result |= ascf + return ASC_REQ_FLAGS(result) + + @staticmethod + def to_GSS(flags: "ASC_REQ_FLAGS") -> GSS_C_FLAGS: + """ + Convert ASC_REQ_FLAGS into GSS_C_FLAGS + """ + result = 0 + for gssf, ascf in _GSS_ASC_TRANSLATION.items(): + if flags & ascf: + result |= gssf + return GSS_C_FLAGS(result) + + +_GSS_ASC_TRANSLATION = { + GSS_C_FLAGS.GSS_C_DELEG_FLAG: ASC_REQ_FLAGS.ASC_REQ_DELEGATE, + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG: ASC_REQ_FLAGS.ASC_REQ_MUTUAL_AUTH, + GSS_C_FLAGS.GSS_C_REPLAY_FLAG: ASC_REQ_FLAGS.ASC_REQ_REPLAY_DETECT, + GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG: ASC_REQ_FLAGS.ASC_REQ_SEQUENCE_DETECT, + GSS_C_FLAGS.GSS_C_CONF_FLAG: ASC_REQ_FLAGS.ASC_REQ_CONFIDENTIALITY, + GSS_C_FLAGS.GSS_C_INTEG_FLAG: ASC_REQ_FLAGS.ASC_REQ_INTEGRITY, + GSS_C_FLAGS.GSS_C_DCE_STYLE: ASC_REQ_FLAGS.ASC_REQ_USE_DCE_STYLE, + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG: ASC_REQ_FLAGS.ASC_REQ_IDENTIFY, + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG: ASC_REQ_FLAGS.ASC_REQ_EXTENDED_ERROR, + GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS: ASC_REQ_FLAGS.ASC_REQ_ALLOW_MISSING_BINDINGS, # noqa: E501 +} + + +# The SSP + + +class WinSSP(SSP): + """ + Use a native Windows SSP through SSPI + + :param Package: the SSP to use + """ + + class STATE(SSP.STATE): + NEGOTIATING = 1 + COMPLETED = 2 + + class CONTEXT(SSP.CONTEXT): + __slots__ = [ + "state", + "Credential", + "Package", + "phContext", + "ptsExpiry", + "SessionKey", + "ServerHostname", + ] + + def __init__( + self, + Package: str, + CredentialUse: int, + req_flags: Optional["GSS_C_FLAGS | GSS_S_FLAGS"] = None, + ): + self.Credential = SecHandle() + self.phContext = None + self.ptsExpiry = SECURITY_INTEGER() + self.Package = Package + self.state = WinSSP.STATE.NEGOTIATING + self.ServerHostname = None + + status = _winapi_AcquireCredentialsHandle( + None, + Package, + CredentialUse, + None, + None, + None, + None, + ctypes.byref(self.Credential), + ctypes.byref(self.ptsExpiry), + ) + if status != SEC_CODES.SEC_E_OK: + raise OSError(f"AcquireCredentialsHandle failed: {hex(status)}") + + super(WinSSP.CONTEXT, self).__init__( + req_flags=req_flags, + ) + + def QuerySessionKey(self): + """ + Query the session key + """ + Buffer = SecPkgContext_SessionKey() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SESSION_KEY, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + SessionKeyBuf = ctypes.cast( + Buffer.SessionKey, + ctypes.POINTER(ctypes.wintypes.BYTE * Buffer.SessionKeyLength), + ) + self.SessionKey = bytes(SessionKeyBuf.contents) + + def QueryNegotiatedFlags(self): + """ + Query the negotiated flags. + """ + Buffer = SecPkgContext_Flags() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SERVER_FLAGS, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + self.flags = ISC_REQ_FLAGS.to_GSS(Buffer.Flags) + + def __repr__(self): + return "[Native SSP: %s]" % self.Package + + def __init__(self, Package: str = "Negotiate"): + self.Package = Package + super(WinSSP, self).__init__() + + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + # Get context + if not Context: + Context = self.CONTEXT( + self.Package, + SECPKG_CRED_OUTBOUND, + req_flags=req_flags, + ) + + if Context.state == self.STATE.COMPLETED: + # SSPI and GSSAPI count completion differently, so we might + # be called one time for nothing. Return that we completed properly. + return Context, None, GSS_S_COMPLETE + + # Create and populate the input buffers + InputBuffers = [] + if input_token: + input_token = bytes(input_token) + InputBuffers.append( + SecBuffer( + len(input_token), + SECBUFFER_TOKEN, + ctypes.cast( + ctypes.create_string_buffer(input_token), ctypes.c_void_p + ), + ) + ) + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + raise NotImplementedError("Channel bindings !") + if InputBuffers: + InputBuffers = ctypes.ARRAY(SecBuffer, len(InputBuffers))(*InputBuffers) + Input = SecBufferDesc( + SECBUFFER_VERSION, + len(InputBuffers), + ctypes.cast( + ctypes.byref(InputBuffers), + ctypes.POINTER(ctypes.POINTER(SecBuffer)), + ), + ) + else: + Input = None + + # Create the output buffers (empty for now) + OutputBuffers = ctypes.ARRAY(SecBuffer, 1)( + *[ + SecBuffer( + ctypes.wintypes.ULONG(0), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.c_void_p(), + ) + ] + ) + Output = SecBufferDesc( + SECBUFFER_VERSION, + len(OutputBuffers), + ctypes.cast( + ctypes.byref(OutputBuffers), ctypes.POINTER(ctypes.POINTER(SecBuffer)) + ), + ) + + # Prepare other arguments + phNewContext = Context.phContext or SecHandle() + pfContextAttr = ctypes.wintypes.ULONG() + if target_name: + TargetName = ctypes.cast( + ctypes.create_string_buffer( + target_name.encode("utf-16le") + b"\x00\x00" + ), + ctypes.wintypes.LPCWSTR, + ) + + HostName = ctypes.wintypes.LPWSTR() + status = _winapi_SspiGetTargetHostName(TargetName, ctypes.byref(HostName)) + if status == SEC_CODES.SEC_E_OK: + Context.ServerHostname = HostName.value + else: + TargetName = None + + # Call SSPI + status = _winapi_InitializeSecurityContext( + ctypes.byref(Context.Credential), + Context.phContext if Context.phContext else None, + TargetName, + ISC_REQ_FLAGS.from_GSS(Context.flags) + | ISC_REQ_FLAGS.ISC_REQ_ALLOCATE_MEMORY, + 0, + SECURITY_NETWORK_DREP, + Input and ctypes.byref(Input), + 0, + ctypes.byref(phNewContext), + ctypes.byref(Output), + ctypes.byref(pfContextAttr), + ctypes.byref(Context.ptsExpiry), + ) + + # Find the output token, if any + output_token = None + if status in [ + SEC_CODES.SEC_E_OK, + SEC_CODES.SEC_I_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE, + ]: + if Context.phContext is None: + Context.phContext = phNewContext + + for OutputBuffer in OutputBuffers: + if ( + OutputBuffer.BufferType == SECBUFFER_TOKEN + and OutputBuffer.cbBuffer != 0 + ): + buf = ctypes.cast( + OutputBuffer.pvBuffer, + ctypes.POINTER(ctypes.wintypes.BYTE * OutputBuffer.cbBuffer), + ) + output_token = GSSAPI_BLOB(bytes(buf.contents)) + break + + # If we succeeded, query the session key + if status in [SEC_CODES.SEC_E_OK, SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE]: + Context.QuerySessionKey() + Context.QueryNegotiatedFlags() + Context.state = self.STATE.COMPLETED + + # Free things we did not create (won't be freed by GC) + for OutputBuffer in OutputBuffers: + if OutputBuffer.pvBuffer is not None: + _winapi_FreeContextBuffer(OutputBuffer.pvBuffer) + + return Context, output_token, SEC_CODES.to_GSS(status) + + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + # Get context + if not Context: + Context = self.CONTEXT( + self.Package, + SECPKG_CRED_INBOUND, + req_flags=req_flags, + ) + + # Create and populate the input buffers + InputBuffers = [] + if input_token: + input_token = bytes(input_token) + InputBuffers.append( + SecBuffer( + len(input_token), + SECBUFFER_TOKEN, + ctypes.cast( + ctypes.create_string_buffer(input_token), ctypes.c_void_p + ), + ) + ) + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + raise NotImplementedError("Channel bindings !") + if InputBuffers: + InputBuffers = ctypes.ARRAY(SecBuffer, len(InputBuffers))(*InputBuffers) + Input = SecBufferDesc( + SECBUFFER_VERSION, + len(InputBuffers), + ctypes.cast( + ctypes.byref(InputBuffers), + ctypes.POINTER(ctypes.POINTER(SecBuffer)), + ), + ) + else: + Input = None + + # Create the output buffers (empty for now) + OutputBuffers = ctypes.ARRAY(SecBuffer, 1)( + *[ + SecBuffer( + ctypes.wintypes.ULONG(0), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.c_void_p(), + ) + ] + ) + Output = SecBufferDesc( + SECBUFFER_VERSION, + len(OutputBuffers), + ctypes.cast( + ctypes.byref(OutputBuffers), ctypes.POINTER(ctypes.POINTER(SecBuffer)) + ), + ) + + # Prepare other arguments + phNewContext = Context.phContext or SecHandle() + pfContextAttr = ctypes.wintypes.ULONG() + + # Call SSPI + status = _winapi_AcceptSecurityContext( + ctypes.byref(Context.Credential), + Context.phContext if Context.phContext else None, + Input and ctypes.byref(Input), + ASC_REQ_FLAGS.from_GSS(Context.flags) + | ASC_REQ_FLAGS.ASC_REQ_ALLOCATE_MEMORY, + SECURITY_NETWORK_DREP, + ctypes.byref(phNewContext), + ctypes.byref(Output), + ctypes.byref(pfContextAttr), + ctypes.byref(Context.ptsExpiry), + ) + + # Find the output token, if any + output_token = None + if status in [ + SEC_CODES.SEC_E_OK, + SEC_CODES.SEC_I_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE, + ]: + if Context.phContext is None: + Context.phContext = phNewContext + + for OutputBuffer in OutputBuffers: + if ( + OutputBuffer.BufferType == SECBUFFER_TOKEN + and OutputBuffer.cbBuffer != 0 + ): + buf = ctypes.cast( + OutputBuffer.pvBuffer, + ctypes.POINTER(ctypes.wintypes.BYTE * OutputBuffer.cbBuffer), + ) + output_token = GSSAPI_BLOB(bytes(buf.contents)) + break + + # If we succeeded, query the session key + if status in [SEC_CODES.SEC_E_OK, SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE]: + Context.QuerySessionKey() + Context.QueryNegotiatedFlags() + Context.state = self.STATE.COMPLETED + + # Free things we did not create (won't be freed by GC) + for OutputBuffer in OutputBuffers: + if OutputBuffer.pvBuffer is not None: + _winapi_FreeContextBuffer(OutputBuffer.pvBuffer) + + return Context, output_token, SEC_CODES.to_GSS(status) diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 77f466d0a27..fd94cc6f2ec 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -29,6 +29,7 @@ ASN1F_SEQUENCE_OF, ) from scapy.asn1packet import ASN1_Packet +from scapy.config import crypto_validator from scapy.compat import bytes_base64 from scapy.error import log_runtime from scapy.fields import ( @@ -490,7 +491,7 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): "J", "NEGOTIATE_OEM_DOMAIN_SUPPLIED", # K "NEGOTIATE_OEM_WORKSTATION_SUPPLIED", # L - "r7", + "NEGOTIATE_LOCAL_CALL", "NEGOTIATE_ALWAYS_SIGN", # M "TARGET_TYPE_DOMAIN", # N "TARGET_TYPE_SERVER", # O @@ -590,8 +591,9 @@ class NTLM_NEGOTIATE(_NTLM_VARIANT_Packet, NTLM_Header): "Payload", OFFSET, [ - _NTLMStrField("DomainName", b""), - _NTLMStrField("WorkstationName", b""), + # "MUST be encoded using the OEM character set" + StrField("DomainName", b""), + StrField("WorkstationName", b""), ], ), ] @@ -1267,7 +1269,6 @@ class NTLMSSP(SSP): Common arguments: - :param auth_level: One of DCE_C_AUTHN_LEVEL :param USE_MIC: whether to use a MIC or not (default: True) :param NTLM_VALUES: a dictionary used to override the following values @@ -1295,6 +1296,7 @@ class NTLMSSP(SSP): if without domain) :param HASHNT: the password to use for NTLM auth :param PASSWORD: the password to use for NTLM auth + :param LOCAL: use local authentication (must be running locally on Windows) Server-only arguments: @@ -1335,6 +1337,7 @@ class CONTEXT(SSP.CONTEXT): "ServerDomain", ] + @crypto_validator def __init__(self, IsAcceptor, req_flags=None): self.state = NTLMSSP.STATE.INIT self.SessionKey = None @@ -1392,7 +1395,7 @@ def __init__( self.USE_MIC = False else: self.USE_MIC = USE_MIC - self.NTLM_VALUES = NTLM_VALUES + if UPN is not None: # Populate values used only in server mode. from scapy.layers.kerberos import _parse_upn @@ -1407,7 +1410,7 @@ def __init__( pass # Compute various netbios/fqdn names - self.DOMAIN_FQDN = DOMAIN_FQDN or "domain.local" + self.DOMAIN_FQDN = DOMAIN_FQDN or "WORKGROUP" self.DOMAIN_NB_NAME = ( DOMAIN_NB_NAME or self.DOMAIN_FQDN.split(".")[0].upper()[:15] ) @@ -1415,6 +1418,7 @@ def __init__( self.COMPUTER_FQDN = COMPUTER_FQDN or ( self.COMPUTER_NB_NAME.lower() + "." + self.DOMAIN_FQDN ) + self.NTLM_VALUES = NTLM_VALUES if IDENTITIES: self.IDENTITIES = { @@ -1542,6 +1546,7 @@ def GSS_Init_sec_context( if Context.state == self.STATE.INIT: # Client: negotiate + # Create a default token tok = NTLM_NEGOTIATE( VARIANT=self.VARIANT, @@ -1594,15 +1599,18 @@ def GSS_Init_sec_context( ), ProductMajorVersion=10, ProductMinorVersion=0, - ProductBuild=19041, + ProductBuild=26100, ) + + # Update that token with the customs one if self.NTLM_VALUES: - # Update that token with the customs one for key in [ "NegotiateFlags", "ProductMajorVersion", "ProductMinorVersion", "ProductBuild", + "DomainName", + "WorkstationName", ]: if key in self.NTLM_VALUES: setattr(tok, key, self.NTLM_VALUES[key]) @@ -1613,6 +1621,7 @@ def GSS_Init_sec_context( elif Context.state == self.STATE.CLI_SENT_NEGO: # Client: auth (token=challenge) chall_tok = input_token + if self.UPN is None or self.HASHNT is None: raise ValueError( "Must provide a 'UPN' and a 'HASHNT' or 'PASSWORD' when " @@ -1654,11 +1663,12 @@ def GSS_Init_sec_context( NegotiateFlags=chall_tok.NegotiateFlags, ProductMajorVersion=10, ProductMinorVersion=0, - ProductBuild=19041, + ProductBuild=26100, ) - tok.LmChallengeResponse = LMv2_RESPONSE() # Populate the token + tok.LmChallengeResponse = LMv2_RESPONSE() + # 1. Set username try: tok.UserName, realm = _parse_upn(self.UPN) diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py index 3b806d6fcdd..f2c70b04957 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -33,9 +33,10 @@ from scapy.layers.dcerpc import NDRUnion, find_dcerpc_interface from scapy.layers.gssapi import ( + GSS_C_FLAGS, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, - GSS_C_FLAGS, + GSS_S_DEFECTIVE_TOKEN, ) from scapy.layers.msrpce.raw.ms_srvs import ( LPSHARE_ENUM_STRUCT, @@ -482,10 +483,20 @@ def update_smbheader(self, pkt): # DEV: add a condition on NEGOTIATED with prio=0 @ATMT.condition(NEGOTIATED, prio=1) + def should_retry_without_blob(self, ssp_tuple): + _, _, status = ssp_tuple + if status == GSS_S_DEFECTIVE_TOKEN: + # Token was defective. This could be that we passed a SPNEGO initial token + # to a NTLM SSP (not using SPNEGO). Retry using no input blob + raise self.NEGOTIATED() + + @ATMT.condition(NEGOTIATED, prio=2) def should_send_session_setup_request(self, ssp_tuple): _, _, status = ssp_tuple if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: - raise ValueError("Internal error: the SSP completed with an error.") + raise ValueError( + "Internal error: the SSP completed with error: %s" % status + ) raise self.SENT_SESSION_REQUEST().action_parameters(ssp_tuple) @ATMT.state() @@ -619,7 +630,9 @@ def AUTHENTICATED(self, ssp_blob=None): target_name="cifs/" + self.HOST if self.HOST else None, ) if status != GSS_S_COMPLETE: - raise ValueError("Internal error: the SSP completed with an error.") + raise ValueError( + "Internal error: the SSP completed with error: %s" % status + ) # Authentication was successful self.session.computeSMBSessionKeys(IsClient=True) diff --git a/scapy/layers/tls/crypto/hash.py b/scapy/layers/tls/crypto/hash.py index cb54b127cdc..05c653538ab 100644 --- a/scapy/layers/tls/crypto/hash.py +++ b/scapy/layers/tls/crypto/hash.py @@ -14,7 +14,19 @@ if conf.crypto_valid: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.hashes import ( + MD5, + SHA1, + SHA224, + SHA256, + SHA384, + SHA512, + SHAKE256, + ) from cryptography.hazmat.primitives.hashes import HashAlgorithm +else: + MD5 = SHA1 = SHA224 = SHA256 = SHA384 = SHA512 = SHAKE256 = None + HashAlgorithm = object _tls_hash_algs = {} @@ -59,12 +71,12 @@ def digest(self, tbd): class Hash_MD5(_GenericHash): - hash_cls = hashes.MD5 + hash_cls = MD5 hash_len = 16 class Hash_SHA(_GenericHash): - hash_cls = hashes.SHA1 + hash_cls = SHA1 hash_len = 20 @@ -72,22 +84,22 @@ class Hash_SHA(_GenericHash): class Hash_SHA224(_GenericHash): - hash_cls = hashes.SHA224 + hash_cls = SHA224 hash_len = 28 class Hash_SHA256(_GenericHash): - hash_cls = hashes.SHA256 + hash_cls = SHA256 hash_len = 32 class Hash_SHA384(_GenericHash): - hash_cls = hashes.SHA384 + hash_cls = SHA384 hash_len = 48 class Hash_SHA512(_GenericHash): - hash_cls = hashes.SHA512 + hash_cls = SHA512 hash_len = 64 @@ -107,7 +119,7 @@ class Hash_MD5SHA1(_GenericHash): class Hash_SHAKE256(_GenericHash): - hash_cls = hashes.SHAKE256 + hash_cls = SHAKE256 def __init__(self, digest_size: int): self.hash_len = digest_size diff --git a/test/scapy/layers/ntlm.uts b/test/scapy/layers/ntlm.uts index 8bc38dceb66..6b7f9766a72 100644 --- a/test/scapy/layers/ntlm.uts +++ b/test/scapy/layers/ntlm.uts @@ -186,8 +186,8 @@ assert ntlm_nego.NegotiateFlags.NEGOTIATE_UNICODE and ntlm_nego.NegotiateFlags.N assert ntlm_nego.NegotiateFlags == 0xe2898235 assert ntlm_nego.ProductMajorVersion == 10 assert ntlm_nego.ProductMinorVersion == 0 -assert ntlm_nego.ProductBuild == 19041 -assert bytes(ntlm_nego) == b'NTLMSSP\x00\x01\x00\x00\x005\x82\x89\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00aJ\x00\x00\x00\x0f' +assert ntlm_nego.ProductBuild == 26100 +assert bytes(ntlm_nego) == b'NTLMSSP\x00\x01\x00\x00\x005\x82\x89\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\xf4e\x00\x00\x00\x0f' = GSS_Accept_sec_context (SPNEGO_negTokenResp: NTLM_NEGOTIATE->NTLM_CHALLENGE) From 4521887e7017498e9043dce62d2ffd43867c0251 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:47:09 +0200 Subject: [PATCH 2/2] Add WinSSP test --- test/scapy/layers/smbclientserver.uts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/scapy/layers/smbclientserver.uts b/test/scapy/layers/smbclientserver.uts index 101843cbdc6..b770a5f4424 100644 --- a/test/scapy/layers/smbclientserver.uts +++ b/test/scapy/layers/smbclientserver.uts @@ -480,3 +480,19 @@ with run_smbserver(readonly=False, encryptshare=True): raise finally: cli.close() + + ++ Windows-only SMB tests +~ windows + += smbclient: use WinSSP to connect to the loopback + +from scapy.arch.windows.sspi import WinSSP + +try: + cli = smbclient("127.0.0.1", ssp=WinSSP(), cli=False) + results = cli.shares() + print(results) + assert any(x[0] == "IPC$" for x in results) +finally: + cli.close()