Skip to content

Commit

Permalink
Add Windows SSPI support and use newer NTLM class (#10)
Browse files Browse the repository at this point in the history
* Use simpler token generator for NTLM auth

* Fix up auth after recent changes

* Added Windows SSPI support

* Satisfy pep8
  • Loading branch information
jborean93 authored Aug 19, 2019
1 parent a3fbd1e commit 4239f27
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 59 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

* Fix issue where timeout was not being applied to the new connection
* Fix various deprecated regex escape patterns
* Added support for Windows Kerberos and implicit credential support through the optional extra library [pywin32](https://github.com/mhammond/pywin32)
* Simplified the fallback NTLM context object


## 0.1.1 - 2018-09-14
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ backlog for features that would be nice to have in this library.
## Requirements

* Python 2.6, 2.7, 3.4+
* For Kerberos auth [python-gssapi](https://github.com/pythongssapi/python-gssapi)
* For Kerberos auth
* [python-gssapi](https://github.com/pythongssapi/python-gssapi) on Linux
* [pywin32](https://github.com/mhammond/pywin32) on Windows

To use Kerberos authentication, further dependencies are required, to install
these dependencies run
Expand All @@ -46,12 +48,13 @@ sudo yum install gcc python-devel krb5-devel krb5-workstation python-devel
pip install smbprotocol[kerberos]
```

Currently Kerberos authentication is not supported on Windows. As part of this
optional extra, the python-gssapi library is installed and smbprotocol requires
a particular GSSAPI extension to be available to work. This extension should
be installed on the majority of MIT or Heimdall Kerberos installs but it isn't
guaranteed. To verify that Kerberos is available you can run the following
check in a Python console
Kerberos auth with Windows just requires the `pywin32` package to be installed
and the Windows host to be joined to that domain. On Linux the python-gssapi
library must be installed and smbprotocol requires a particular GSSAPI
extension to be available to work. This extension should be installed on the
majority of MIT or Heimdal Kerberos installs but it isn't guaranteed. To
verify that Kerberos is available on Linux you can run the following check in
a Python console:

```
try:
Expand All @@ -73,7 +76,7 @@ To install smbprotocol, simply run
```
pip install smbprotocol
# on a non Windows host, to install with Kerberos support
# To install with Kerberos support
pip install smbprotocol[kerberos]
```

Expand Down Expand Up @@ -204,7 +207,6 @@ docker run -d -p $SMB_PORT:445 -v $(pwd)/build-scripts:/app -w /app -e SMB_USER=
Here is a list of features that I would like to incorporate, PRs are welcome
if you want to implement them yourself;

* SSPI integration for Windows and Kerberos authentication
* Test and support DFS mounts and not just server shares
* Multiple channel support to speed up large data transfers
* Create an easier API on top of the `raw` SMB calls that currently exist
Expand Down
8 changes: 8 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ install:
- cmd: pip install -r requirements-test.txt
- cmd: pip install .

# test out pywin32 on some matrixes
- ps: |
$ErrorActionPreference = "SilentlyContinue"
if ($env:PYTHON -in @("Python27", "Python27-x64", "Python36", "Python36-x64")) {
pip install .[kerberos]
}
$ErrorActionPreference = "Stop"
build: off # Do not run MSBuild, build stuff at install step

test_script:
Expand Down
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@
packages=['smbprotocol'],
install_requires=[
'cryptography>=2.0',
'ntlm-auth',
'ntlm-auth>=1.2.0',
'pyasn1',
'six',
],
extras_require={
':python_version<"2.7"': [
'ordereddict'
],
'kerberos:sys_platform=="win32"': [],
'kerberos:sys_platform=="win32"': [
'pywin32'
],
'kerberos:sys_platform!="win32"': [
'gssapi>=1.4.1'
]
Expand Down
186 changes: 141 additions & 45 deletions smbprotocol/session.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import base64
import logging

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.kbkdf import CounterLocation, \
KBKDFHMAC, Mode
from ntlm_auth.ntlm import Ntlm
from ntlm_auth.ntlm import NtlmContext as Ntlm
from pyasn1.codec.der import decoder

from smbprotocol.connection import Capabilities, Commands, Dialects, \
Expand All @@ -17,7 +16,15 @@
Structure
from smbprotocol.structure import _bytes_to_hex

HAVE_SSPI = False # TODO: add support for Windows and SSPI
HAVE_SSPI = False
try: # pragma: no cover
import sspi
import sspicon
import win32security
HAVE_SSPI = True
except ImportError: # pragma: no cover
pass

HAVE_GSSAPI = False
try: # pragma: no cover
import gssapi
Expand Down Expand Up @@ -154,17 +161,21 @@ def __init__(self, connection, username=None, password=None,
3.2.1.3 Per Session
The Session object that is used to store the details for an
authenticated SMB session. There are 3 forms of authentication that are
authenticated SMB session. There are 4 forms of authentication that are
supported;
1. NTLM Auth, requires the username and password
2. Kerberos Auth, only available in certain circumstances
3. Guest Auth, the credentials were rejected but the server allows a
1. SSPI Auth, Windows only if pywin32 is installed. Uses either
Kerberos or NTLM auth depending on the environment setup and can
use the current user's credentials if none are provided here.
2. NTLM Auth, requires the username and password
3. Kerberos Auth, only available in certain circumstances
4. Guest Auth, the credentials were rejected but the server allows a
fallback to guest authentication (insecure and non-default)
NTLM Auth is the fallback as it should be available in most scenarios
while Kerberos only works on a system where python-gssapi is installed
and the GGF extension for inquire_sec_context_by_oid is available.
and the GGF extension for inquire_sec_context_by_oid is available
(Linux), or pywin32 is installed (Windows).
If using Kerberos Auth, the username and password can be omitted which
means the default user kerb ticket (if available) is used. If the
Expand Down Expand Up @@ -379,16 +390,20 @@ def _authenticate_session(self, mech):
server=self.connection.server_name)
elif mech in [MechTypes.KRB5, MechTypes.MS_KRB5, MechTypes.NTLMSSP] \
and HAVE_SSPI:
raise NotImplementedError("SSPI on Windows for authentication is "
"not yet implemented")
context = SSPIContext(username=self.username,
password=self.password,
server=self.connection.server_name)
elif mech == MechTypes.NTLMSSP:
context = NtlmContext(username=self.username,
password=self.password)
else:
raise NotImplementedError("Mech Type %s is not yet supported"
% mech)

for out_token in context.step():
response = None
token_gen = context.step()
out_token = next(token_gen)
while not context.complete or out_token is not None:
session_setup = SMB2SessionSetupRequest()
session_setup['security_mode'] = \
self.connection.client_security_mode
Expand All @@ -414,7 +429,11 @@ def _authenticate_session(self, mech):
session_resp = SMB2SessionSetupResponse()
session_resp.unpack(response['data'].get_value())

context.in_token = session_resp['buffer'].get_value()
in_token = session_resp['buffer'].get_value()
if not in_token:
break

out_token = token_gen.send(in_token)
status = response['status'].get_value()
if status == NtStatus.STATUS_MORE_PROCESSING_REQUIRED:
log.info("More processing is required for SMB2_SESSION_SETUP")
Expand Down Expand Up @@ -452,6 +471,14 @@ def _smb3kdf(self, ki, label, context):
return kdf.derive(ki)


def _split_username_and_domain(username):
try:
domain, username = username.split("\\", 1)
return domain, username
except ValueError:
return "", username


class NtlmContext(object):

def __init__(self, username, password):
Expand All @@ -462,39 +489,33 @@ def __init__(self, username, password):
raise SMBAuthenticationError("The password must be set when using "
"NTLM authentication")

# try and get the domain part from the username
log.info("Setting up NTLM Security Context for user %s" % username)
try:
self.domain, self.username = username.split("\\", 1)
except ValueError:
self.username = username
self.domain = ''
self.password = password
self.context = Ntlm()
self.in_token = None
self.domain, self.username = _split_username_and_domain(username)
self.context = Ntlm(self.username, password, domain=self.domain)

@property
def complete(self):
return self.context.complete

def step(self):
log.info("NTLM: Generating Negotiate message")
msg1 = self.context.create_negotiate_message(self.domain)
msg1 = base64.b64decode(msg1)
msg1 = self.context.step()
log.debug("NTLM: Negotiate message: %s" % _bytes_to_hex(msg1))
yield msg1

log.info("NTLM: Parsing Challenge message")
msg2 = base64.b64encode(self.in_token)
log.debug("NTLM: Challenge message: %s" % _bytes_to_hex(self.in_token))
self.context.parse_challenge_message(msg2)

log.info("NTLM: Generating Authenticate message")
msg3 = self.context.create_authenticate_message(
user_name=self.username,
password=self.password,
domain_name=self.domain
)
yield base64.b64decode(msg3)
msg2 = yield msg1

log.info("NTLM: Parsing Challenge message and generating Authentication message")
log.debug("NTLM: Challenge message: %s" % _bytes_to_hex(msg2))
msg3 = self.context.step(input_token=msg2)

yield msg3

def get_session_key(self):
return self.context.authenticate_message.exported_session_key
# The session_key was only recently added in ntlm-auth, we have the
# fallback to the non-public interface for older versions where we
# know this still works. This should be removed once ntlm-auth no
# longer requires these older versions (>=1.4.0).
return getattr(self.context, 'session_key',
self.context._session_security.exported_session_key)


class GSSAPIContext(object):
Expand All @@ -510,16 +531,17 @@ def __init__(self, username, password, server):
self.context = gssapi.SecurityContext(name=server_name,
creds=self.creds,
usage='initiate')
self.in_token = None

@property
def complete(self):
return self.context.complete

def step(self):
in_token = None
while not self.context.complete:
log.info("GSSAPI: gss_init_sec_context called")
out_token = self.context.step(self.in_token)
if out_token:
yield out_token
else:
log.info("GSSAPI: gss_init_sec_context complete")
out_token = self.context.step(in_token)
in_token = yield out_token

def get_session_key(self):
# GSS_C_INQ_SSPI_SESSION_KEY
Expand All @@ -530,7 +552,7 @@ def get_session_key(self):
return context_data[0]

def _acquire_creds(self, username, password):
# 3 use cases with Kerberos AUth
# 3 use cases with Kerberos Auth
# 1. Both the user and pass is supplied so we want to create a new
# ticket with the pass
# 2. Only the user is supplied so we will attempt to get the cred
Expand Down Expand Up @@ -585,3 +607,77 @@ def _acquire_creds(self, username, password):

log.info("GSSAPI: Acquired credentials for user %s" % str(user))
return creds


class SSPIContext(object):

def __init__(self, username, password, server):
log.info("Setting up SSPI Security Context for Windows auth")
self._call_counter = 0

flags = sspicon.ISC_REQ_INTEGRITY | \
sspicon.ISC_REQ_CONFIDENTIALITY | \
sspicon.ISC_REQ_REPLAY_DETECT | \
sspicon.ISC_REQ_SEQUENCE_DETECT | \
sspicon.ISC_REQ_MUTUAL_AUTH

domain, username = _split_username_and_domain(username)
# We could use the MECH to derive the package name but we are just
# better off using Negotiate and lettings Windows do all the heavy
# lifting.
self._context = sspi.ClientAuth(
pkg_name='Negotiate',
auth_info=(username, domain, password),
targetspn="cifs/%s" % server,
scflags=flags
)

@property
def complete(self):
return self._context.authenticated

def step(self):
in_token = None
while not self.complete:
log.info("SSPI: InitializeSecurityContext called")
out_token = self._step(in_token)
in_token = yield out_token if out_token != b"" else None

def get_session_key(self):
return self._context.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SESSION_KEY)

def _step(self, token):
success_codes = [
sspicon.SEC_E_OK,
sspicon.SEC_I_COMPLETE_AND_CONTINUE,
sspicon.SEC_I_COMPLETE_NEEDED,
sspicon.SEC_I_CONTINUE_NEEDED
]

if token:
sec_token = win32security.PySecBufferType(
self._context.pkg_info['MaxToken'],
sspicon.SECBUFFER_TOKEN
)
sec_token.Buffer = token

sec_buffer = win32security.PySecBufferDescType()
sec_buffer.append(sec_token)
else:
sec_buffer = None

rc, out_buffer = self._context.authorize(sec_buffer_in=sec_buffer)
self._call_counter += 1
if rc not in success_codes:
rc_name = "Unknown Error"
for name, value in vars(sspicon).items():
if isinstance(value, int) and name.startswith("SEC_") and \
value == rc:
rc_name = name
break
raise SMBAuthenticationError(
"InitializeSecurityContext failed on call %d: (%d) %s 0x%s"
% (self._call_counter, rc, rc_name, format(rc, 'x'))
)

return out_buffer[0].Buffer
3 changes: 0 additions & 3 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,16 @@ def test_username_without_domain(self):
actual = NtlmContext("username", "password")
assert actual.domain == ""
assert actual.username == "username"
assert actual.password == "password"

def test_username_in_netlogon_form(self):
actual = NtlmContext("DOMAIN\\username", "password")
assert actual.domain == "DOMAIN"
assert actual.username == "username"
assert actual.password == "password"

def test_username_in_upn_form(self):
actual = NtlmContext("[email protected]", "password")
assert actual.domain == ""
assert actual.username == "[email protected]"
assert actual.password == "password"


class TestSession(object):
Expand Down

0 comments on commit 4239f27

Please sign in to comment.