From 196cb7fec9b186cdbef44f0d99a1d3c759930624 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Tue, 14 Feb 2017 23:00:52 +1000 Subject: [PATCH 1/4] added support for NTLM message encryption --- CHANGELOG.md | 3 + README.md | 51 ++++++++++-- appveyor.yml | 5 ++ winrm/encryption.py | 106 +++++++++++++++++++++++ winrm/protocol.py | 6 +- winrm/tests/test_encryption.py | 148 +++++++++++++++++++++++++++++++++ winrm/transport.py | 65 +++++++++++---- 7 files changed, 362 insertions(+), 22 deletions(-) create mode 100644 winrm/encryption.py create mode 100644 winrm/tests/test_encryption.py diff --git a/CHANGELOG.md b/CHANGELOG.md index db82903a..0caff5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +### Developing +- Added support for message encryption over HTTP when using NTLM + ### Version 0.2.2 - Added support for CredSSP authenication (via requests-credssp) - Improved README, see 'Valid transport options' section diff --git a/README.md b/README.md index dcc8ae2f..85f47cb3 100644 --- a/README.md +++ b/README.md @@ -130,21 +130,58 @@ pywinrm supports various transport methods in order to authenticate with the Win * `certificate`: Authentication is done through a certificate that is mapped to a local Windows account on the server. * `ssl`: When used in conjunction with `cert_pem` and `cert_key_pem` it will use a certificate as above. If not will revert to basic auth over HTTPS. * `kerberos`: Will use Kerberos authentication for domain accounts which only works when the client is in the same domain as the server and the required dependencies are installed. Currently a Kerberos ticket needs to be initiliased outside of pywinrm using the kinit command. -* `ntlm`: Will use NTLM authentication for both domain and local accounts. Currently no support for NTLMv2 auth and other features included in that version (WIP). +* `ntlm`: Will use NTLM authentication for both domain and local accounts. * `credssp`: Will use CredSSP authentication for both domain and local accounts. Allows double hop authentication. This only works over a HTTPS endpoint and not HTTP. -### HTTP or HTTPS endpoint +### Encryption -While either a HTTP or HTTPS endpoint can be used as the transport method, using HTTPS is prefered as the messages are encrypted using SSL. To use HTTPS either a self signed certificate or one from a CA can be used. You can use this [guide](http://www.joseph-streeter.com/?p=1086) to set up a HTTPS endpoint with a self signed certificate. +By default WinRM will not accept unencrypted messages from a client and Pywinrm +currently has 2 ways to do this. + +1. Using a HTTPS endpoint instead of HTTP (Recommended) +2. Use NTLM as the transport auth and setting `message_encryption` to `auto` or `always` + +Using a HTTPS endpoint is recommended as it will encrypt all the data sent +through to the server including the credentials and works with all transport +auth types. You can use [this script](https://github.com/ansible/ansible/blob/devel/examples/scripts/ConfigureRemotingForAnsible.ps1) +to easily set up a HTTPS endpoint on WinRM with a self signed certificate but +in a production environment this should be hardened with your own process. + +The second option is to use NTLM and set the `message_encryption` arg to +protocol to `auto` or `always`. This will use the authention GSS-API Wrap and +Unwrap methods if available to encrypt the message contents sent to the server. +This form of encryption is independent from the transport layer like TLS and is +currently only supported by the NTLM transport auth. Kerberos and CredSSP do +have the methods available but currently are not supported in Pywinrm. + +To configure message encryption you can use the `message_encryption` argument +when initialising protocol. This option has 3 values that can be set as shown +below. + +* `auto`: Default, Will only use message encryption if it is available for the auth method and HTTPS isn't used. +* `never`: Will never use message encryption even when not over HTTPS. +* `always`: Will always use message encryption evevn when running over HTTPS. + +If you set the value to `always` and the transport opt doesn't support message +encryption i.e. Basic auth, Pywinrm will throw an exception. + +If you do not use a HTTPS endpoint or message encryption then the Windows +server will automatically reject Pywinrm. You can change the settings on the +Windows server to allow unencrypted messages and credentials but this highly +insecure and shouldn't be used unless necessary. To allow unencrypted message +run the following command either from cmd or powershell -If you still wish to use a HTTP endpoint and loose confidentiality in your messages you will need to enable unencrypted messages in the server by running the following command ``` -# from cmd: +# from cmd winrm set winrm/config/service @{AllowUnencrypted="true"} + +# or from powershell +Set-Item -Path "WSMan:\localhost\Service\AllowUnencrypted" -Value $true ``` -As a repeat this should definitely not be used as your credentials and messages will allow anybody to see what is sent over the wire. -There are plans in place to allow message encryption for messages sent with Kerberos or NTLM messages in the future. +As a repeat this should definitely not be used as your credentials and messages +will allow anybody to see what is sent over the wire. + ### Enabling WinRM on remote host Enable WinRM over HTTP and HTTPS with self-signed certificate (includes firewall rules): diff --git a/appveyor.yml b/appveyor.yml index b5f5bc63..da156e83 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -49,5 +49,10 @@ test_script: py.test -v --cov-report=term-missing --cov=. + # Run integration tests with NTLM to check message encryption + $env:WINRM_TRANSPORT="ntlm" + Set-Item WSMan:\localhost\Service\AllowUnencrypted $false + py.test -v winrm/tests/test_integration_protocol.py winrm/tests/test_integration_session.py + after_test: - echo after_test diff --git a/winrm/encryption.py b/winrm/encryption.py new file mode 100644 index 00000000..7acfe6e9 --- /dev/null +++ b/winrm/encryption.py @@ -0,0 +1,106 @@ +import requests +import struct + +from winrm.exceptions import WinRMError + +class Encryption(object): + def __init__(self, session, protocol): + """ + [MS-WSMV] v30.0 2016-07-14 + + 2.2.9.1 Encrypted Message Types + When using Encryption, there are three options available + 1. Negotiate/SPNEGO + 2. Kerberos + 3. CredSSP + Details for each implementation can be found in this document under this section + + This init sets the following values to use to encrypt and decrypt. This is to help generify + the methods used in the body of the class. + wrap: A method that will return the encrypted message and a signature + unwrap: A method that will return an unencrypted message and verify the signature + protocol_string: The protocol string used for the particular auth protocol + + :param session: The handle of the session to get GSS-API wrap and unwrap methods + :param protocol: The auth protocol used, will determine the wrapping and unwrapping method plus + the protocol string to use. Currently only NTLM is supported + """ + if protocol == 'ntlm': # Details under Negotiate [2.2.9.1.1] in MS-WSMV + self.wrap = session.auth.session_security.wrap + self.unwrap = session.auth.session_security.unwrap + self.protocol_string = b"application/HTTP-SPNEGO-session-encrypted" + # TODO: Add support for Kerberos and CredSSP encryption + else: + raise WinRMError("Encryption for protocol '%s' not yet supported in pywinrm" % protocol) + + def prepare_encrypted_request(self, session, endpoint, message): + """ + Creates a prepared request to send to the server with an encrypted message + and correct headers + + :param session: The handle of the session to prepare requests with + :param endpoint: The endpoint/server to prepare requests to + :param message: The unencrypted message to send to the server + :return: A prepared request that has an encrypted message + """ + encrypted_message = self._encrypt_message(message) + request = requests.Request('POST', endpoint, data=encrypted_message) + prepared_request = session.prepare_request(request) + prepared_request.headers['Content-Length'] = str(len(prepared_request.body)) + prepared_request.headers['Content-Type'] = 'multipart/encrypted;protocol="{0}";boundary="Encrypted Boundary"'\ + .format(self.protocol_string.decode()) + + return prepared_request + + def parse_encrypted_response(self, response): + """ + Takes in the encrypted response from the server and decrypts it + + :param response: The response that needs to be decrytped + :return: The unencrypted message from the server + """ + content_type = response.headers['Content-Type'] + if 'protocol="{0}"'.format(self.protocol_string.decode()) in content_type: + msg = self._decrypt_response(response) + else: + msg = response.text + + return msg + + def _encrypt_message(self, message): + sealed_message, signature = self.wrap(message) + message_length = str(len(message)).encode() + signature_length = struct.pack("= read_timeout_sec or operation_timeout_sec < 1: @@ -65,7 +67,9 @@ def __init__( server_cert_validation=server_cert_validation, kerberos_delegation=kerberos_delegation, kerberos_hostname_override=kerberos_hostname_override, - auth_method=transport) + auth_method=transport, + message_encryption=message_encryption + ) self.username = username self.password = password diff --git a/winrm/tests/test_encryption.py b/winrm/tests/test_encryption.py new file mode 100644 index 00000000..4d6c86b7 --- /dev/null +++ b/winrm/tests/test_encryption.py @@ -0,0 +1,148 @@ +import base64 +import pytest +import struct + +from winrm.encryption import Encryption +from winrm.exceptions import WinRMError + + +def test_init_with_invalid_protocol(): + with pytest.raises(WinRMError) as excinfo: + Encryption(None, 'invalid_protocol') + + assert "Encryption for protocol 'invalid_protocol' not yet supported in pywinrm" in str(excinfo.value) + + +def test_encrypt_message(): + test_session = TestSession() + test_message = b"unencrypted message" + test_endpoint = b"endpoint" + + encryption = Encryption(test_session, 'ntlm') + + actual = encryption.prepare_encrypted_request(test_session, test_endpoint, test_message) + expected_encrypted_message = b"dW5lbmNyeXB0ZWQgbWVzc2FnZQ==" + expected_signature = b"1234" + signature_length = struct.pack(" Date: Fri, 7 Jul 2017 10:35:59 +1000 Subject: [PATCH 2/4] added support for credssp encryption --- CHANGELOG.md | 2 +- README.md | 16 ++--- winrm/encryption.py | 115 +++++++++++++++++++++++++-------- winrm/tests/test_encryption.py | 6 +- winrm/transport.py | 29 ++++++--- 5 files changed, 119 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0caff5ed..893ccfaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ### Developing -- Added support for message encryption over HTTP when using NTLM +- Added support for message encryption over HTTP when using NTLM and CredSSP ### Version 0.2.2 - Added support for CredSSP authenication (via requests-credssp) diff --git a/README.md b/README.md index 85f47cb3..5f880c82 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ By default WinRM will not accept unencrypted messages from a client and Pywinrm currently has 2 ways to do this. 1. Using a HTTPS endpoint instead of HTTP (Recommended) -2. Use NTLM as the transport auth and setting `message_encryption` to `auto` or `always` +2. Use NTLM or CredSSP as the transport auth and setting `message_encryption` to `auto` or `always` Using a HTTPS endpoint is recommended as it will encrypt all the data sent through to the server including the credentials and works with all transport @@ -147,12 +147,12 @@ auth types. You can use [this script](https://github.com/ansible/ansible/blob/de to easily set up a HTTPS endpoint on WinRM with a self signed certificate but in a production environment this should be hardened with your own process. -The second option is to use NTLM and set the `message_encryption` arg to -protocol to `auto` or `always`. This will use the authention GSS-API Wrap and -Unwrap methods if available to encrypt the message contents sent to the server. -This form of encryption is independent from the transport layer like TLS and is -currently only supported by the NTLM transport auth. Kerberos and CredSSP do -have the methods available but currently are not supported in Pywinrm. +The second option is to use NTLM or CredSSP and set the `message_encryption` +arg to protocol to `auto` or `always`. This will use the authentication GSS-API +Wrap and Unwrap methods if available to encrypt the message contents sent to +the server. This form of encryption is independent from the transport layer +like TLS and is currently only supported by the NTLM and CredSSP transport +auth. Kerberos currently does not have the methods available to achieve this. To configure message encryption you can use the `message_encryption` argument when initialising protocol. This option has 3 values that can be set as shown @@ -160,7 +160,7 @@ below. * `auto`: Default, Will only use message encryption if it is available for the auth method and HTTPS isn't used. * `never`: Will never use message encryption even when not over HTTPS. -* `always`: Will always use message encryption evevn when running over HTTPS. +* `always`: Will always use message encryption even when running over HTTPS. If you set the value to `always` and the transport opt doesn't support message encryption i.e. Basic auth, Pywinrm will throw an exception. diff --git a/winrm/encryption.py b/winrm/encryption.py index 7acfe6e9..6f89f7ed 100644 --- a/winrm/encryption.py +++ b/winrm/encryption.py @@ -4,6 +4,10 @@ from winrm.exceptions import WinRMError class Encryption(object): + + SIXTEN_KB = 16384 + MIME_BOUNDARY = b'--Encrypted Boundary' + def __init__(self, session, protocol): """ [MS-WSMV] v30.0 2016-07-14 @@ -23,13 +27,20 @@ def __init__(self, session, protocol): :param session: The handle of the session to get GSS-API wrap and unwrap methods :param protocol: The auth protocol used, will determine the wrapping and unwrapping method plus - the protocol string to use. Currently only NTLM is supported + the protocol string to use. Currently only NTLM and CredSSP is supported """ + self.protocol = protocol + self.session = session + if protocol == 'ntlm': # Details under Negotiate [2.2.9.1.1] in MS-WSMV - self.wrap = session.auth.session_security.wrap - self.unwrap = session.auth.session_security.unwrap self.protocol_string = b"application/HTTP-SPNEGO-session-encrypted" - # TODO: Add support for Kerberos and CredSSP encryption + self._build_message = self._build_ntlm_message + self._decrypt_message = self._decrypt_ntlm_message + elif protocol == 'credssp': # Details under CredSSP [2.2.9.1.3] in MS-WSMV + self.protocol_string = b"application/HTTP-CredSSP-session-encrypted" + self._build_message = self._build_credssp_message + self._decrypt_message = self._decrypt_credssp_message + # TODO: Add support for Kerberos encryption else: raise WinRMError("Encryption for protocol '%s' not yet supported in pywinrm" % protocol) @@ -43,12 +54,23 @@ def prepare_encrypted_request(self, session, endpoint, message): :param message: The unencrypted message to send to the server :return: A prepared request that has an encrypted message """ - encrypted_message = self._encrypt_message(message) + if self.protocol == 'credssp' and len(message) > self.SIXTEN_KB: + content_type = 'multipart/x-multi-encrypted' + encrypted_message = b'' + message_chunks = [message[i:i+self.SIXTEN_KB] for i in range(0, len(message), self.SIXTEN_KB)] + for message_chunk in message_chunks: + encrypted_chunk = self._encrypt_message(message_chunk) + encrypted_message += encrypted_chunk + else: + content_type = 'multipart/encrypted' + encrypted_message = self._encrypt_message(message) + encrypted_message += self.MIME_BOUNDARY + b"--\r\n" + request = requests.Request('POST', endpoint, data=encrypted_message) prepared_request = session.prepare_request(request) prepared_request.headers['Content-Length'] = str(len(prepared_request.body)) - prepared_request.headers['Content-Type'] = 'multipart/encrypted;protocol="{0}";boundary="Encrypted Boundary"'\ - .format(self.protocol_string.decode()) + prepared_request.headers['Content-Type'] = '{0};protocol="{1}";boundary="Encrypted Boundary"'\ + .format(content_type, self.protocol_string.decode()) return prepared_request @@ -68,39 +90,78 @@ def parse_encrypted_response(self, response): return msg def _encrypt_message(self, message): - sealed_message, signature = self.wrap(message) message_length = str(len(message)).encode() - signature_length = struct.pack(" Date: Fri, 7 Jul 2017 15:32:26 +1000 Subject: [PATCH 3/4] Added tests for CredSSP encryption --- winrm/tests/test_encryption.py | 89 ++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/winrm/tests/test_encryption.py b/winrm/tests/test_encryption.py index 81f8587d..94e1a649 100644 --- a/winrm/tests/test_encryption.py +++ b/winrm/tests/test_encryption.py @@ -38,6 +38,45 @@ def test_encrypt_message(): b"--Encrypted Boundary--\r\n" +def test_encrypt_large_credssp_message(): + test_session = TestSession() + test_message = b"unencrypted message " * 2048 + test_endpoint = b"endpoint" + message_chunks = [test_message[i:i + 16384] for i in range(0, len(test_message), 16384)] + + encryption = Encryption(test_session, 'credssp') + + actual = encryption.prepare_encrypted_request(test_session, test_endpoint, test_message) + expected_encrypted_message1 = base64.b64encode(message_chunks[0]) + expected_encrypted_message2 = base64.b64encode(message_chunks[1]) + expected_encrypted_message3 = base64.b64encode(message_chunks[2]) + + assert actual.headers == { + "Content-Length": "55303", + "Content-Type": 'multipart/x-multi-encrypted;protocol="application/HTTP-CredSSP-session-encrypted";boundary="Encrypted Boundary"' + } + + assert actual.body == b"--Encrypted Boundary\r\n" \ + b"\tContent-Type: application/HTTP-CredSSP-session-encrypted\r\n" \ + b"\tOriginalContent: type=application/soap+xml;charset=UTF-8;Length=16384\r\n" \ + b"--Encrypted Boundary\r\n" \ + b"\tContent-Type: application/octet-stream\r\n" + \ + struct.pack(" Date: Thu, 20 Jul 2017 17:51:37 +1000 Subject: [PATCH 4/4] Fixed up credssp message encryption --- CHANGELOG.md | 1 + setup.cfg | 2 +- winrm/encryption.py | 53 +++++++++++++++++--- winrm/protocol.py | 7 ++- winrm/tests/test_encryption.py | 89 ++++++++++++++++++++++++++-------- winrm/transport.py | 7 ++- 6 files changed, 127 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 893ccfaa..e7f99d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Developing - Added support for message encryption over HTTP when using NTLM and CredSSP +- Added parameter to disable TLSv1.2 when using CredSSP for Server 2008 support ### Version 0.2.2 - Added support for CredSSP authenication (via requests-credssp) diff --git a/setup.cfg b/setup.cfg index 536d2dee..84afeed8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,6 @@ requires = python-xmltodict [bdist_wheel] universal = 1 -[pytest] +[tool:pytest] norecursedirs = .git .idea env pep8ignore = tests/*.py E501 diff --git a/winrm/encryption.py b/winrm/encryption.py index 6f89f7ed..73341fd5 100644 --- a/winrm/encryption.py +++ b/winrm/encryption.py @@ -1,4 +1,5 @@ import requests +import re import struct from winrm.exceptions import WinRMError @@ -157,11 +158,51 @@ def _build_ntlm_message(self, message): def _build_credssp_message(self, message): sealed_message = self.session.auth.wrap(message) - # not really sure why I need to take a further 21 from the below the - # length of the --Encrypted Boundary\r\n is 22 but this seems to be - # the magic number - message_length_difference = len(sealed_message) - len(message) - 21 + trailer_length = self._get_credssp_trailer_length(len(message), self.session.auth.cipher_negotiated) - trailer_length = struct.pack("