def test_unwrap_small_spnego_without_end_hyphens(self): expected = b"plaintext" encryption = WinRMEncryption(MockAuthSPNEGO(), WinRMEncryption.SPNEGO) bwrapped = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-SPNEGO-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=9\r\n" \ b"--Encrypted Boundary\r\n\tContent-Type: application/" \ b"octet-stream\r\n\x10\x00\x00\x00reallylongheaderplaintext-" \ b"encrypted--Encrypted Boundary\r\n" actual = encryption.unwrap_message(bwrapped, "Encrypted Boundary") assert expected == actual
def test_unwrap_large_kerberos(self): expected = b"a" * 20000 encryption = WinRMEncryption(MockAuth(), WinRMEncryption.KERBEROS) bwrapped = (b"--Encrypted Boundary\r\n\tContent-Type: application" b"/HTTP-Kerberos-session-encrypted\r\n\tOriginalContent: " b"type=application/soap+xml;charset=UTF-8;Length=20000" b"\r\n--Encrypted Boundary\r\n\tContent-Type: application" b"/octet-stream\r\n\x10\x00\x00\x00reallylongheader" + expected + b"-encrypted--Encrypted Boundary--\r\n") actual = encryption.unwrap_message(bwrapped, "Encrypted Boundary") assert expected == actual
def test_unwrap_large_spnego(self): expected = b"a" * 20000 encryption = WinRMEncryption(MockAuthSPNEGO(), WinRMEncryption.SPNEGO) bwrapped = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-SPNEGO-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=20000" \ b"\r\n--Encrypted Boundary\r\n\tContent-Type: application" \ b"/octet-stream\r\n\x10\x00\x00\x00reallylongheader" + expected + \ b"-encrypted--Encrypted Boundary--\r\n" actual = encryption.unwrap_message(bwrapped, "Encrypted Boundary") assert expected == actual
def test_unwrap_small_credsp(self): expected = b"plaintext" encryption = WinRMEncryption(MockAuthCREDSSP(), WinRMEncryption.CREDSSP) bwrapped = b"--Encrypted Boundary2\r\n\tContent-Type: application" \ b"/HTTP-CredSSP-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=9\r\n" \ b"--Encrypted Boundary2\r\n\tContent-Type: application/" \ b"octet-stream\r\n\x10\x00\x00\x00plaintext-encrypted" \ b"--Encrypted Boundary2--\r\n" actual = encryption.unwrap_message(bwrapped, "Encrypted Boundary2") assert expected == actual
def test_wrap_large_spnego(self): plaintext = b"a" * 20000 encryption = WinRMEncryption(MockAuthSPNEGO(), WinRMEncryption.SPNEGO) expected = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-SPNEGO-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=20000" \ b"\r\n--Encrypted Boundary\r\n\tContent-Type: application" \ b"/octet-stream\r\n\x10\x00\x00\x00reallylongheader" + plaintext + \ b"-encrypted--Encrypted Boundary--\r\n" actual_type, actual = encryption.wrap_message(plaintext) assert "multipart/encrypted" == actual_type assert expected == actual
def test_unwrap_small_kerberos(self): expected = b"plaintext" encryption = WinRMEncryption(MockAuthSPNEGO(), WinRMEncryption.KERBEROS) # The spaces after -- on each boundary is on purpose, some MS implementations do this. bwrapped = b"-- Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-Kerberos-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=9\r\n" \ b"-- Encrypted Boundary\r\n\tContent-Type: application/" \ b"octet-stream\r\n\x10\x00\x00\x00reallylongheaderplaintext-" \ b"encrypted-- Encrypted Boundary--\r\n" actual = encryption.unwrap_message(bwrapped, "Encrypted Boundary") assert expected == actual
def test_wrap_small_credsp(self): plaintext = b"plaintext" encryption = WinRMEncryption(MockAuthCREDSSP(), WinRMEncryption.CREDSSP) expected = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-CredSSP-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=9\r\n" \ b"--Encrypted Boundary\r\n\tContent-Type: application/" \ b"octet-stream\r\n\x10\x00\x00\x00plaintext-encrypted" \ b"--Encrypted Boundary--\r\n" actual_type, actual = encryption.wrap_message(plaintext) assert "multipart/encrypted" == actual_type assert expected == actual
def test_wrap_small_spnego(self): plaintext = b"plaintext" encryption = WinRMEncryption(MockAuth(MockAuthSPNEGO()), WinRMEncryption.SPNEGO) expected = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-SPNEGO-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=9\r\n" \ b"--Encrypted Boundary\r\n\tContent-Type: application/" \ b"octet-stream\r\n\x07\x00\x00\x00header plaintext-" \ b"encrypted--Encrypted Boundary--\r\n" actual_type, actual = encryption.wrap_message(plaintext, "hostname") assert "multipart/encrypted" == actual_type assert expected == actual
def test_wrap_small_kerberos(self): plaintext = b"plaintext" encryption = WinRMEncryption(MockAuthSPNEGO(), WinRMEncryption.KERBEROS) expected = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-Kerberos-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=9\r\n" \ b"--Encrypted Boundary\r\n\tContent-Type: application/" \ b"octet-stream\r\n\x10\x00\x00\x00reallylongheaderplaintext-" \ b"encrypted--Encrypted Boundary--\r\n" actual_type, actual = encryption.wrap_message(plaintext) assert "multipart/encrypted" == actual_type assert expected == actual
def test_wrap_spnego_padded(self): plaintext = b"plaintext" encryption = WinRMEncryption(MockAuth(padding=True), WinRMEncryption.SPNEGO) expected = ( b"--Encrypted Boundary\r\n\tContent-Type: application" b"/HTTP-SPNEGO-session-encrypted\r\n\tOriginalContent: " b"type=application/soap+xml;charset=UTF-8;Length=10\r\n" b"--Encrypted Boundary\r\n\tContent-Type: application/" b"octet-stream\r\n\x10\x00\x00\x00reallylongheaderplaintext-" b"encrypted--Encrypted Boundary--\r\n") actual_type, actual = encryption.wrap_message(plaintext) assert "multipart/encrypted" == actual_type assert expected == actual
def test_unwrap_length_mismatch(self): encryption = WinRMEncryption(MockAuthSPNEGO(), WinRMEncryption.SPNEGO) bwrapped = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-SPNEGO-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=9\r\n" \ b"--Encrypted Boundary\r\n\tContent-Type: application/" \ b"octet-stream\r\n\x10\x00\x00\x00reallylongheaderplain-" \ b"encrypted--Encrypted Boundary--\r\n" with pytest.raises(WinRMError) as err: encryption.unwrap_message(bwrapped, "Encrypted Boundary") assert str(err.value) == \ "The encrypted length from the server does not match the " \ "expected length, decryption failed, actual: 5 != expected: 9"
def send(self, message): hostname = get_hostname(self.endpoint) if self.session is None: self.session = self._build_session() # need to send an initial blank message to setup the security # context required for encryption if self.wrap_required: request = requests.Request('POST', self.endpoint, data=None) prep_request = self.session.prepare_request(request) self._send_request(prep_request) protocol = WinRMEncryption.SPNEGO if isinstance(self.session.auth, HttpCredSSPAuth): protocol = WinRMEncryption.CREDSSP elif self.session.auth.contexts[ hostname].response_auth_header == 'kerberos': # When Kerberos (not Negotiate) was used, we need to send a special protocol value and not SPNEGO. protocol = WinRMEncryption.KERBEROS self.encryption = WinRMEncryption( self.session.auth.contexts[hostname], protocol) log.debug("Sending message: %s" % message) # for testing, keep commented out # self._test_messages.append({"request": message.decode('utf-8'), # "response": None}) headers = self.session.headers if self.wrap_required: content_type, payload = self.encryption.wrap_message(message) type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' \ % (content_type, self.encryption.protocol) headers.update({ 'Content-Type': type_header, 'Content-Length': str(len(payload)), }) else: payload = message headers['Content-Type'] = "application/soap+xml;charset=UTF-8" request = requests.Request('POST', self.endpoint, data=payload, headers=headers) prep_request = self.session.prepare_request(request) return self._send_request(prep_request)
def send(self, message: bytes) -> bytes: hostname = get_hostname(self.endpoint) if self.session is None: self.session = self._build_session() # need to send an initial blank message to setup the security # context required for encryption if self.wrap_required: request = requests.Request("POST", self.endpoint, data=None) prep_request = self.session.prepare_request(request) self._send_request(prep_request) protocol = WinRMEncryption.SPNEGO if isinstance(self.session.auth, HttpCredSSPAuth): protocol = WinRMEncryption.CREDSSP elif self.session.auth.contexts[hostname].response_auth_header == "kerberos": # type: ignore[union-attr] # This should not happen # When Kerberos (not Negotiate) was used, we need to send a special protocol value and not SPNEGO. protocol = WinRMEncryption.KERBEROS self.encryption = WinRMEncryption(self.session.auth.contexts[hostname], protocol) # type: ignore[union-attr] # This should not happen if log.isEnabledFor(logging.DEBUG): log.debug("Sending message: %s" % message.decode("utf-8")) # for testing, keep commented out # self._test_messages.append({"request": message.decode('utf-8'), # "response": None}) headers = self.session.headers if self.wrap_required: content_type, payload = self.encryption.wrap_message(message) # type: ignore[union-attr] # This should not happen protocol = self.encryption.protocol if self.encryption else WinRMEncryption.SPNEGO type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' % (content_type, protocol) headers.update( { "Content-Type": type_header, "Content-Length": str(len(payload)), } ) else: payload = message headers["Content-Type"] = "application/soap+xml;charset=UTF-8" request = requests.Request("POST", self.endpoint, data=payload, headers=headers) prep_request = self.session.prepare_request(request) return self._send_request(prep_request)
def _build_auth_negotiate(self, session, auth_provider="auto"): kwargs = self._get_auth_kwargs('negotiate') session.auth = HTTPNegotiateAuth(username=self.username, password=self.password, auth_provider=auth_provider, wrap_required=self.wrap_required, **kwargs) self.encryption = WinRMEncryption(session.auth, WinRMEncryption.SPNEGO)
def test_unwrap_large_credsp(self): expected = b"a" * 20000 encryption = WinRMEncryption(MockAuthCREDSSP(), WinRMEncryption.CREDSSP) bwrapped = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-CredSSP-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=16384" \ b"\r\n--Encrypted Boundary\r\n\tContent-Type: application" \ b"/octet-stream\r\n\x10\x00\x00\x00" + b"a" * 16384 + \ b"-encrypted--Encrypted Boundary\r\n\tContent-Type: " \ b"application/HTTP-CredSSP-session-encrypted\r\n" \ b"\tOriginalContent: type=application/soap+xml;" \ b"charset=UTF-8;Length=3616\r\n--Encrypted Boundary\r\n" \ b"\tContent-Type: application/octet-stream\r\n" \ b"\x10\x00\x00\x00" + b"a" * 3616 + \ b"-encrypted--Encrypted Boundary--\r\n" actual = encryption.unwrap_message(bwrapped, "Encrypted Boundary") assert expected == actual
def test_wrap_large_credsp(self): plaintext = b"a" * 20000 encryption = WinRMEncryption(MockAuth(MockAuthCREDSSP()), WinRMEncryption.CREDSSP) expected = b"--Encrypted Boundary\r\n\tContent-Type: application" \ b"/HTTP-CredSSP-session-encrypted\r\n\tOriginalContent: " \ b"type=application/soap+xml;charset=UTF-8;Length=16384" \ b"\r\n--Encrypted Boundary\r\n\tContent-Type: application" \ b"/octet-stream\r\n\x10\x00\x00\x00" + b"a" * 16384 + \ b"-encrypted--Encrypted Boundary\r\n\tContent-Type: " \ b"application/HTTP-CredSSP-session-encrypted\r\n" \ b"\tOriginalContent: type=application/soap+xml;" \ b"charset=UTF-8;Length=3616\r\n--Encrypted Boundary\r\n" \ b"\tContent-Type: application/octet-stream\r\n" \ b"\x10\x00\x00\x00" + b"a" * 3616 + \ b"-encrypted--Encrypted Boundary--\r\n" actual_type, actual = encryption.wrap_message(plaintext, "hostname") assert "multipart/x-multi-encrypted" == actual_type assert expected == actual
def _build_auth_credssp(self, session): if not HAS_CREDSSP: raise ImportError("Cannot use CredSSP auth as requests-credssp is " "not installed: %s" % str(CREDSSP_IMP_ERR)) if self.username is None: raise ValueError("For credssp auth, the username must be " "specified") if self.password is None: raise ValueError("For credssp auth, the password must be " "specified") kwargs = self._get_auth_kwargs('credssp') session.auth = HttpCredSSPAuth(username=self.username, password=self.password, **kwargs) self.encryption = WinRMEncryption(session.auth, WinRMEncryption.CREDSSP)
def test_get_credssp_trailer_length(self, cipher, expected): encryption = WinRMEncryption(None, WinRMEncryption.CREDSSP) actual = encryption._credssp_trailer(30, cipher) assert expected == actual
class _TransportHTTP(object): def __init__(self, server, port=None, username=None, password=None, ssl=True, path="wsman", auth="negotiate", cert_validation=True, connection_timeout=30, encryption='auto', proxy=None, no_proxy=False, read_timeout=30, reconnection_retries=0, reconnection_backoff=2.0, **kwargs): self.server = server self.port = port if port is not None else (5986 if ssl else 5985) self.username = username self.password = password self.ssl = ssl self.path = path if auth not in SUPPORTED_AUTHS: raise ValueError("The specified auth '%s' is not supported, " "please select one of '%s'" % (auth, ", ".join(SUPPORTED_AUTHS))) self.auth = auth self.cert_validation = cert_validation self.connection_timeout = connection_timeout self.read_timeout = read_timeout self.reconnection_retries = reconnection_retries self.reconnection_backoff = reconnection_backoff # determine the message encryption logic if encryption not in ["auto", "always", "never"]: raise ValueError("The encryption value '%s' must be auto, " "always, or never" % encryption) enc_providers = ["credssp", "kerberos", "negotiate", "ntlm"] if ssl: # msg's are automatically encrypted with TLS, we only want message # encryption if always was specified self.wrap_required = encryption == "always" if self.wrap_required and self.auth not in enc_providers: raise ValueError( "Cannot use message encryption with auth '%s', either set " "encryption='auto' or use one of the following auth " "providers: %s" % (self.auth, ", ".join(enc_providers))) else: # msg's should always be encrypted when not using SSL, unless the # user specifies to never encrypt self.wrap_required = not encryption == "never" if self.wrap_required and self.auth not in enc_providers: raise ValueError( "Cannot use message encryption with auth '%s', either set " "encryption='never', use ssl=True or use one of the " "following auth providers: %s" % (self.auth, ", ".join(enc_providers))) self.encryption = None self.proxy = proxy self.no_proxy = no_proxy for kwarg_list in AUTH_KWARGS.values(): for kwarg in kwarg_list: setattr(self, kwarg, kwargs.get(kwarg, None)) self.endpoint = self._create_endpoint(self.ssl, self.server, self.port, self.path) log.debug("Initialising HTTP transport for endpoint: %s, auth: %s, " "user: %s" % (self.endpoint, self.username, self.auth)) self.session = None # used when building tests, keep commented out # self._test_messages = [] def close(self): if self.session: self.session.close() def send(self, message): hostname = get_hostname(self.endpoint) if self.session is None: self.session = self._build_session() # need to send an initial blank message to setup the security # context required for encryption if self.wrap_required: request = requests.Request('POST', self.endpoint, data=None) prep_request = self.session.prepare_request(request) self._send_request(prep_request) protocol = WinRMEncryption.SPNEGO if isinstance(self.session.auth, HttpCredSSPAuth): protocol = WinRMEncryption.CREDSSP elif self.session.auth.contexts[ hostname].response_auth_header == 'kerberos': # When Kerberos (not Negotiate) was used, we need to send a special protocol value and not SPNEGO. protocol = WinRMEncryption.KERBEROS self.encryption = WinRMEncryption( self.session.auth.contexts[hostname], protocol) log.debug("Sending message: %s" % message) # for testing, keep commented out # self._test_messages.append({"request": message.decode('utf-8'), # "response": None}) headers = self.session.headers if self.wrap_required: content_type, payload = self.encryption.wrap_message(message) type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' \ % (content_type, self.encryption.protocol) headers.update({ 'Content-Type': type_header, 'Content-Length': str(len(payload)), }) else: payload = message headers['Content-Type'] = "application/soap+xml;charset=UTF-8" request = requests.Request('POST', self.endpoint, data=payload, headers=headers) prep_request = self.session.prepare_request(request) return self._send_request(prep_request) def _send_request(self, request): response = self.session.send(request, timeout=(self.connection_timeout, self.read_timeout)) content_type = response.headers.get('content-type', "") if content_type.startswith( "multipart/encrypted;") or content_type.startswith( "multipart/x-multi-encrypted;"): boundary = re.search('boundary=[' '|\\"](.*)[' '|\\"]', response.headers['content-type']).group(1) response_content = self.encryption.unwrap_message( response.content, to_unicode(boundary)) response_text = to_string(response_content) else: response_content = response.content response_text = response.text if response_content else '' log.debug("Received message: %s" % response_text) # for testing, keep commented out # self._test_messages[-1]['response'] = response_text try: response.raise_for_status() except requests.HTTPError as err: response = err.response if response.status_code == 401: raise AuthenticationError("Failed to authenticate the user %s " "with %s" % (self.username, self.auth)) else: code = response.status_code raise WinRMTransportError('http', code, response_text) return response_content def _build_session(self): log.debug("Building requests session with auth %s" % self.auth) self._suppress_library_warnings() session = requests.Session() session.headers['User-Agent'] = "Python PSRP Client" # requests defaults to 'Accept-Encoding: gzip, default' which normally doesn't matter on vanila WinRM but for # Exchange endpoints hosted on IIS they actually compress it with 1 of the 2 algorithms. By explicitly setting # identity we are telling the server not to transform (compress) the data using the HTTP methods which we don't # support. https://tools.ietf.org/html/rfc7231#section-5.3.4 session.headers['Accept-Encoding'] = 'identity' # get the env requests settings session.trust_env = True settings = session.merge_environment_settings(url=self.endpoint, proxies={}, stream=None, verify=None, cert=None) # set the proxy config orig_proxy = session.proxies session.proxies = settings['proxies'] if self.proxy is not None: proxy_key = 'https' if self.ssl else 'http' session.proxies = {proxy_key: self.proxy} elif self.no_proxy: session.proxies = orig_proxy # Retry on connection errors, with a backoff factor retry_kwargs = { 'total': self.reconnection_retries, 'connect': self.reconnection_retries, 'status': self.reconnection_retries, 'read': 0, 'backoff_factor': self.reconnection_backoff, 'status_forcelist': (425, 429, 503), } try: retries = Retry(**retry_kwargs) except TypeError: # Status was added in urllib3 >= 1.21 (Requests >= 2.14.0), remove # the status retry counter and try again. The user should upgrade # to a newer version log.warning( "Using an older requests version that without support " "for status retries, ignoring.", exc_info=True) del retry_kwargs['status'] retries = Retry(**retry_kwargs) session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries)) session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries)) # set cert validation config session.verify = self.cert_validation # if cert_validation is a bool (no path specified), not False and there # are env settings for verification, set those env settings if isinstance(self.cert_validation, bool) and self.cert_validation \ and settings['verify'] is not None: session.verify = settings['verify'] build_auth = getattr(self, "_build_auth_%s" % self.auth) build_auth(session) return session def _build_auth_basic(self, session): if self.username is None: raise ValueError("For basic auth, the username must be specified") if self.password is None: raise ValueError("For basic auth, the password must be specified") session.auth = requests.auth.HTTPBasicAuth(username=self.username, password=self.password) def _build_auth_certificate(self, session): if self.certificate_key_pem is None: raise ValueError("For certificate auth, the path to the " "certificate key pem file must be specified with " "certificate_key_pem") if self.certificate_pem is None: raise ValueError("For certificate auth, the path to the " "certificate pem file must be specified with " "certificate_pem") if self.ssl is False: raise ValueError("For certificate auth, SSL must be used") session.cert = (self.certificate_pem, self.certificate_key_pem) session.headers['Authorization'] = "http://schemas.dmtf.org/wbem/" \ "wsman/1/wsman/secprofile/" \ "https/mutual" def _build_auth_credssp(self, session): if self.username is None: raise ValueError("For credssp auth, the username must be " "specified") if self.password is None: raise ValueError("For credssp auth, the password must be " "specified") kwargs = self._get_auth_kwargs('credssp') session.auth = HttpCredSSPAuth(username=self.username, password=self.password, **kwargs) def _build_auth_kerberos(self, session): self._build_auth_negotiate(session, "kerberos") def _build_auth_negotiate(self, session, auth_provider="negotiate"): kwargs = self._get_auth_kwargs('negotiate') session.auth = HTTPNegotiateAuth(username=self.username, password=self.password, auth_provider=auth_provider, wrap_required=self.wrap_required, **kwargs) def _build_auth_ntlm(self, session): self._build_auth_negotiate(session, "ntlm") def _get_auth_kwargs(self, auth_provider): kwargs = {} for kwarg in AUTH_KWARGS[auth_provider]: kwarg_value = getattr(self, kwarg, None) if kwarg_value is not None: kwarg_key = kwarg[len(auth_provider) + 1:] kwargs[kwarg_key] = kwarg_value return kwargs def _suppress_library_warnings(self): # try to suppress known warnings from requests if possible try: from requests.packages.urllib3.exceptions import \ InsecurePlatformWarning warnings.simplefilter('ignore', category=InsecurePlatformWarning) except: # NOQA: E722; # pragma: no cover pass try: from requests.packages.urllib3.exceptions import SNIMissingWarning warnings.simplefilter('ignore', category=SNIMissingWarning) except: # NOQA: E722; # pragma: no cover pass # if we're explicitly ignoring validation, try to suppress # InsecureRequestWarning, since the user opted-in if self.cert_validation is False: try: from requests.packages.urllib3.exceptions import \ InsecureRequestWarning warnings.simplefilter('ignore', category=InsecureRequestWarning) except: # NOQA: E722; # pragma: no cover pass @staticmethod def _create_endpoint(ssl, server, port, path): scheme = "https" if ssl else "http" # Check if the server is an IPv6 Address, enclose in [] if it is try: address = ipaddress.IPv6Address(to_unicode(server)) except ipaddress.AddressValueError: pass else: server = "[%s]" % address.compressed endpoint = "%s://%s:%s/%s" % (scheme, server, port, path) return endpoint