awinrm 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- awinrm-0.0.1/LICENSE +45 -0
- awinrm-0.0.1/MANIFEST.in +2 -0
- awinrm-0.0.1/PKG-INFO +20 -0
- awinrm-0.0.1/README.md +1 -0
- awinrm-0.0.1/awinrm/__init__.py +213 -0
- awinrm-0.0.1/awinrm/_version.py +7 -0
- awinrm-0.0.1/awinrm/encryption.py +229 -0
- awinrm-0.0.1/awinrm/examples/__init__.py +0 -0
- awinrm-0.0.1/awinrm/examples/authcheck.py +33 -0
- awinrm-0.0.1/awinrm/examples/runcmd.py +48 -0
- awinrm-0.0.1/awinrm/examples/shell.py +62 -0
- awinrm-0.0.1/awinrm/exceptions.py +51 -0
- awinrm-0.0.1/awinrm/factory.py +20 -0
- awinrm-0.0.1/awinrm/protocol.py +485 -0
- awinrm-0.0.1/awinrm/transport.py +131 -0
- awinrm-0.0.1/awinrm.egg-info/PKG-INFO +20 -0
- awinrm-0.0.1/awinrm.egg-info/SOURCES.txt +23 -0
- awinrm-0.0.1/awinrm.egg-info/dependency_links.txt +1 -0
- awinrm-0.0.1/awinrm.egg-info/entry_points.txt +3 -0
- awinrm-0.0.1/awinrm.egg-info/not-zip-safe +1 -0
- awinrm-0.0.1/awinrm.egg-info/requires.txt +5 -0
- awinrm-0.0.1/awinrm.egg-info/top_level.txt +1 -0
- awinrm-0.0.1/pyproject.toml +3 -0
- awinrm-0.0.1/setup.cfg +4 -0
- awinrm-0.0.1/setup.py +59 -0
awinrm-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Tamas Jos (@skelsec)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
##############################################################################
|
|
24
|
+
Original project (also MIT) was taken from https://github.com/diyan/pywinrm
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Copyright (c) 2013 Alexey Diyan
|
|
28
|
+
|
|
29
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
30
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
31
|
+
in the Software without restriction, including without limitation the rights
|
|
32
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
33
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
34
|
+
furnished to do so, subject to the following conditions:
|
|
35
|
+
|
|
36
|
+
The above copyright notice and this permission notice shall be included in
|
|
37
|
+
all copies or substantial portions of the Software.
|
|
38
|
+
|
|
39
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
40
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
41
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
42
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
43
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
44
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
45
|
+
THE SOFTWARE.
|
awinrm-0.0.1/MANIFEST.in
ADDED
awinrm-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: awinrm
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Asynchronous Python library for Windows Remote Management
|
|
5
|
+
Home-page: https://github.com/skelsec/awinrm
|
|
6
|
+
Author: Tamas Jos
|
|
7
|
+
Author-email: info@skelsecprojects.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: unicrypto==0.0.10
|
|
15
|
+
Requires-Dist: asysocks>=0.2.10
|
|
16
|
+
Requires-Dist: asyauth>=0.0.14
|
|
17
|
+
Requires-Dist: six
|
|
18
|
+
Requires-Dist: xmltodict
|
|
19
|
+
|
|
20
|
+
Asynchronous Python library for Windows Remote Management
|
awinrm-0.0.1/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# awinrm
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import unicode_literals
|
|
2
|
+
import logging
|
|
3
|
+
logger = logging.getLogger('awinrm')
|
|
4
|
+
handler = logging.StreamHandler()
|
|
5
|
+
formatter = logging.Formatter(
|
|
6
|
+
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
|
|
7
|
+
handler.setFormatter(formatter)
|
|
8
|
+
logger.addHandler(handler)
|
|
9
|
+
logger.setLevel(logging.INFO)
|
|
10
|
+
logger.propagate = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from base64 import b64encode
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
import warnings
|
|
17
|
+
import asyncio
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from asysocks.unicomm.protocol.client.http.commons.factory import HTTPConnectionFactory
|
|
21
|
+
from awinrm.protocol import Protocol
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Session:
|
|
25
|
+
def __init__(self, url:str, ssl_ctx = None, authtype='auto', factory:HTTPConnectionFactory = None, **kwargs):
|
|
26
|
+
if factory is None:
|
|
27
|
+
if url is None:
|
|
28
|
+
raise Exception('Either url or factory parameter is required')
|
|
29
|
+
factory = HTTPConnectionFactory.from_url(url)
|
|
30
|
+
cred = factory.get_credential()
|
|
31
|
+
target = factory.get_target()
|
|
32
|
+
self.url = self._build_url(target.get_url(), kwargs.get('transport', 'plaintext'))
|
|
33
|
+
self.protocol = Protocol(self.url, cred, ssl_ctx = ssl_ctx, authtype=authtype, proxies=target.proxies, **kwargs)
|
|
34
|
+
self.__shells = []
|
|
35
|
+
|
|
36
|
+
async def __aenter__(self):
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
40
|
+
for shell in self.__shells:
|
|
41
|
+
await shell.close()
|
|
42
|
+
await self.protocol.close()
|
|
43
|
+
|
|
44
|
+
def create_shell(self, working_directory=None, env_vars=None, noprofile=False,
|
|
45
|
+
codepage=437, lifetime=None, idle_timeout=None):
|
|
46
|
+
shell = WinRMShell(self, working_directory=working_directory, env_vars=env_vars, noprofile=noprofile,
|
|
47
|
+
codepage=codepage, lifetime=lifetime, idle_timeout=idle_timeout)
|
|
48
|
+
self.__shells.append(shell)
|
|
49
|
+
return shell
|
|
50
|
+
|
|
51
|
+
async def run_cmd(self, command, args=()):
|
|
52
|
+
shell_id = await self.protocol.open_shell()
|
|
53
|
+
command_id = await self.protocol.run_command(shell_id, command, args)
|
|
54
|
+
stdout_buff = b''
|
|
55
|
+
stderr_buff = b''
|
|
56
|
+
return_code = -1
|
|
57
|
+
async for stdout, stderr, return_code in self.protocol.get_command_output(shell_id, command_id):
|
|
58
|
+
stdout_buff += stdout
|
|
59
|
+
stderr_buff += stderr
|
|
60
|
+
return_code = return_code
|
|
61
|
+
await self.protocol.cleanup_command(shell_id, command_id)
|
|
62
|
+
await self.protocol.close_shell(shell_id)
|
|
63
|
+
return stdout_buff, stderr_buff, return_code
|
|
64
|
+
|
|
65
|
+
async def run_ps(self, script):
|
|
66
|
+
"""base64 encodes a Powershell script and executes the powershell
|
|
67
|
+
encoded script command
|
|
68
|
+
"""
|
|
69
|
+
# must use utf16 little endian on windows
|
|
70
|
+
encoded_ps = b64encode(script.encode('utf_16_le')).decode('ascii')
|
|
71
|
+
rs = self.run_cmd('powershell -encodedcommand {0}'.format(encoded_ps))
|
|
72
|
+
if len(rs.std_err):
|
|
73
|
+
# if there was an error message, clean it it up and make it human
|
|
74
|
+
# readable
|
|
75
|
+
rs.std_err = self._clean_error_msg(rs.std_err)
|
|
76
|
+
return rs
|
|
77
|
+
|
|
78
|
+
def _clean_error_msg(self, msg):
|
|
79
|
+
"""converts a Powershell CLIXML message to a more human readable string
|
|
80
|
+
"""
|
|
81
|
+
# TODO prepare unit test, beautify code
|
|
82
|
+
# if the msg does not start with this, return it as is
|
|
83
|
+
if msg.startswith(b"#< CLIXML\r\n"):
|
|
84
|
+
# for proper xml, we need to remove the CLIXML part
|
|
85
|
+
# (the first line)
|
|
86
|
+
msg_xml = msg[11:]
|
|
87
|
+
try:
|
|
88
|
+
# remove the namespaces from the xml for easier processing
|
|
89
|
+
msg_xml = self._strip_namespace(msg_xml)
|
|
90
|
+
root = ET.fromstring(msg_xml)
|
|
91
|
+
# the S node is the error message, find all S nodes
|
|
92
|
+
nodes = root.findall("./S")
|
|
93
|
+
new_msg = ""
|
|
94
|
+
for s in nodes:
|
|
95
|
+
# append error msg string to result, also
|
|
96
|
+
# the hex chars represent CRLF so we replace with newline
|
|
97
|
+
new_msg += s.text.replace("_x000D__x000A_", "\n")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
# if any of the above fails, the msg was not true xml
|
|
100
|
+
# print a warning and return the original string
|
|
101
|
+
warnings.warn(
|
|
102
|
+
"There was a problem converting the Powershell error "
|
|
103
|
+
"message: %s" % (e))
|
|
104
|
+
else:
|
|
105
|
+
# if new_msg was populated, that's our error message
|
|
106
|
+
# otherwise the original error message will be used
|
|
107
|
+
if len(new_msg):
|
|
108
|
+
# remove leading and trailing whitespace while we are here
|
|
109
|
+
return new_msg.strip().encode('utf-8')
|
|
110
|
+
|
|
111
|
+
# either failed to decode CLIXML or there was nothing to decode
|
|
112
|
+
# just return the original message
|
|
113
|
+
return msg
|
|
114
|
+
|
|
115
|
+
def _strip_namespace(self, xml):
|
|
116
|
+
"""strips any namespaces from an xml string"""
|
|
117
|
+
p = re.compile(b"xmlns=*[\"\"][^\"\"]*[\"\"]")
|
|
118
|
+
allmatches = p.finditer(xml)
|
|
119
|
+
for match in allmatches:
|
|
120
|
+
xml = xml.replace(match.group(), b"")
|
|
121
|
+
return xml
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _build_url(target, transport):
|
|
125
|
+
match = re.match(
|
|
126
|
+
r'(?i)^((?P<scheme>http[s]?)://)?(?P<host>[0-9a-z-_.]+)(:(?P<port>\d+))?(?P<path>(/)?(wsman)?)?', target) # NOQA
|
|
127
|
+
scheme = match.group('scheme')
|
|
128
|
+
if not scheme:
|
|
129
|
+
# TODO do we have anything other than HTTP/HTTPS
|
|
130
|
+
scheme = 'https' if transport == 'ssl' else 'http'
|
|
131
|
+
host = match.group('host')
|
|
132
|
+
port = match.group('port')
|
|
133
|
+
if not port:
|
|
134
|
+
port = 5986 if transport == 'ssl' else 5985
|
|
135
|
+
path = match.group('path')
|
|
136
|
+
if not path:
|
|
137
|
+
path = 'wsman'
|
|
138
|
+
return '{0}://{1}:{2}/{3}'.format(scheme, host, port, path.lstrip('/'))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class WinRMShell:
|
|
143
|
+
def __init__(self, session:Session, working_directory:str=None, env_vars=None, noprofile:bool=False,
|
|
144
|
+
codepage:int=437, lifetime:int=None, idle_timeout:int=None):
|
|
145
|
+
self.session = session
|
|
146
|
+
self.working_directory = working_directory
|
|
147
|
+
self.env_vars = env_vars
|
|
148
|
+
self.noprofile = noprofile
|
|
149
|
+
self.codepage = codepage
|
|
150
|
+
self.lifetime = lifetime
|
|
151
|
+
self.idle_timeout = idle_timeout
|
|
152
|
+
self.shell_cmd = 'cmd.exe'
|
|
153
|
+
self.command_id = None
|
|
154
|
+
self.closed_evt = asyncio.Event()
|
|
155
|
+
|
|
156
|
+
self.shell_id = None
|
|
157
|
+
self.stdin = asyncio.Queue()
|
|
158
|
+
self.stdout = asyncio.Queue()
|
|
159
|
+
self.stderr = asyncio.Queue()
|
|
160
|
+
self.return_code = None
|
|
161
|
+
|
|
162
|
+
async def __aenter__(self):
|
|
163
|
+
try:
|
|
164
|
+
self.shell_id = await self.session.protocol.open_shell(
|
|
165
|
+
working_directory = self.working_directory,
|
|
166
|
+
env_vars = self.env_vars,
|
|
167
|
+
noprofile = self.noprofile,
|
|
168
|
+
codepage = self.codepage,
|
|
169
|
+
lifetime = self.lifetime,
|
|
170
|
+
idle_timeout = self.idle_timeout
|
|
171
|
+
)
|
|
172
|
+
self.command_id = await self.session.protocol.run_command(self.shell_id, self.shell_cmd)
|
|
173
|
+
stdout, stderr, return_code, command_done = await self.session.protocol._raw_get_command_output(self.shell_id, self.command_id)
|
|
174
|
+
await self.stdout.put(stdout)
|
|
175
|
+
await self.stderr.put(stderr)
|
|
176
|
+
self.return_code = return_code
|
|
177
|
+
|
|
178
|
+
return self
|
|
179
|
+
except Exception as e:
|
|
180
|
+
await self.__aexit__(None, None, None)
|
|
181
|
+
raise e
|
|
182
|
+
|
|
183
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
184
|
+
await self.close()
|
|
185
|
+
|
|
186
|
+
async def close(self):
|
|
187
|
+
if self.closed_evt.is_set():
|
|
188
|
+
return
|
|
189
|
+
await self.session.protocol.cleanup_command(self.shell_id, self.command_id)
|
|
190
|
+
await self.session.protocol.close_shell(self.shell_id)
|
|
191
|
+
self.closed_evt.set()
|
|
192
|
+
|
|
193
|
+
async def send_input(self, data):
|
|
194
|
+
await self.session.protocol.send_command_input(self.shell_id, self.command_id, data)
|
|
195
|
+
await self.read_output()
|
|
196
|
+
|
|
197
|
+
async def read_output(self):
|
|
198
|
+
stdout, stderr, return_code, command_done = await self.session.protocol._raw_get_command_output(self.shell_id, self.command_id)
|
|
199
|
+
await self.stdout.put(stdout)
|
|
200
|
+
await self.stderr.put(stderr)
|
|
201
|
+
self.return_code = return_code
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def decode_bytes(data:bytes, hint:str='cp437'):
|
|
205
|
+
encodings = ['utf-8', 'cp1252', 'cp1251', 'cp932', 'cp936']
|
|
206
|
+
if hint is not None and len(hint) > 0:
|
|
207
|
+
encodings.insert(0, hint)
|
|
208
|
+
for encoding in encodings:
|
|
209
|
+
try:
|
|
210
|
+
return data.decode(encoding)
|
|
211
|
+
except UnicodeDecodeError:
|
|
212
|
+
continue
|
|
213
|
+
raise UnicodeDecodeError("Unable to decode the input data with the provided encodings.")
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import struct
|
|
3
|
+
from awinrm import logger
|
|
4
|
+
from awinrm.exceptions import WinRMError
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
from asyauth.common.credentials.ntlm import NTLMCredential
|
|
8
|
+
from asyauth.common.credentials.kerberos import KerberosCredential
|
|
9
|
+
from asyauth.common.credentials.spnego import SPNEGOCredential
|
|
10
|
+
from asyauth.common.credentials.credssp import CREDSSPCredential
|
|
11
|
+
|
|
12
|
+
class Encryption(object):
|
|
13
|
+
|
|
14
|
+
SIXTEN_KB = 16384
|
|
15
|
+
MIME_BOUNDARY = b'--Encrypted Boundary'
|
|
16
|
+
|
|
17
|
+
def __init__(self, session, protocol:str):
|
|
18
|
+
"""
|
|
19
|
+
[MS-WSMV] v30.0 2016-07-14
|
|
20
|
+
|
|
21
|
+
2.2.9.1 Encrypted Message Types
|
|
22
|
+
When using Encryption, there are three options available
|
|
23
|
+
1. Negotiate/SPNEGO
|
|
24
|
+
2. Kerberos
|
|
25
|
+
3. CredSSP
|
|
26
|
+
Details for each implementation can be found in this document under this section
|
|
27
|
+
|
|
28
|
+
This init sets the following values to use to encrypt and decrypt. This is to help generify
|
|
29
|
+
the methods used in the body of the class.
|
|
30
|
+
wrap: A method that will return the encrypted message and a signature
|
|
31
|
+
unwrap: A method that will return an unencrypted message and verify the signature
|
|
32
|
+
protocol_string: The protocol string used for the particular auth protocol
|
|
33
|
+
|
|
34
|
+
:param session: The handle of the session to get GSS-API wrap and unwrap methods
|
|
35
|
+
:param protocol: The auth protocol used, will determine the wrapping and unwrapping method plus
|
|
36
|
+
the protocol string to use. Currently only NTLM and CredSSP is supported
|
|
37
|
+
"""
|
|
38
|
+
self.protocol = protocol.lower()
|
|
39
|
+
self.session = session
|
|
40
|
+
self.sequence_number = 0
|
|
41
|
+
logger.debug('Using encryption protocol: %s' % self.protocol)
|
|
42
|
+
cred = self.session.authmanager.authobj.get_active_credential()
|
|
43
|
+
if self.protocol == 'spnego': # Details under Negotiate [2.2.9.1.1] in MS-WSMV
|
|
44
|
+
self.protocol_string = b"application/HTTP-SPNEGO-session-encrypted"
|
|
45
|
+
if isinstance(cred, NTLMCredential):
|
|
46
|
+
self._build_message = self._build_ntlm_message
|
|
47
|
+
self._decrypt_message = self._decrypt_ntlm_message
|
|
48
|
+
elif isinstance(cred, KerberosCredential):
|
|
49
|
+
self._build_message = self._build_kerberos_message
|
|
50
|
+
self._decrypt_message = self._decrypt_kerberos_message
|
|
51
|
+
|
|
52
|
+
elif self.protocol == 'credssp': # Details under CredSSP [2.2.9.1.3] in MS-WSMV
|
|
53
|
+
self.protocol_string = b"application/HTTP-CredSSP-session-encrypted"
|
|
54
|
+
self._build_message = self._build_credssp_message
|
|
55
|
+
self._decrypt_message = self._decrypt_credssp_message
|
|
56
|
+
|
|
57
|
+
else:
|
|
58
|
+
raise WinRMError("Encryption for protocol '%s' not supported in awinrm" % self.protocol)
|
|
59
|
+
|
|
60
|
+
async def prepare_encrypted_request(self, endpoint, message):
|
|
61
|
+
"""
|
|
62
|
+
Creates a prepared request to send to the server with an encrypted message
|
|
63
|
+
and correct headers
|
|
64
|
+
|
|
65
|
+
:param endpoint: The endpoint/server to prepare requests to
|
|
66
|
+
:param message: The unencrypted message to send to the server
|
|
67
|
+
:return: A prepared request that has an encrypted message
|
|
68
|
+
"""
|
|
69
|
+
host = urlsplit(endpoint).hostname
|
|
70
|
+
|
|
71
|
+
if self.protocol == 'credssp' and len(message) > self.SIXTEN_KB:
|
|
72
|
+
content_type = 'multipart/x-multi-encrypted'
|
|
73
|
+
encrypted_message = b''
|
|
74
|
+
message_chunks = [message[i:i+self.SIXTEN_KB] for i in range(0, len(message), self.SIXTEN_KB)]
|
|
75
|
+
for message_chunk in message_chunks:
|
|
76
|
+
encrypted_chunk = await self._encrypt_message(message_chunk, host)
|
|
77
|
+
encrypted_message += encrypted_chunk
|
|
78
|
+
else:
|
|
79
|
+
content_type = 'multipart/encrypted'
|
|
80
|
+
encrypted_message = await self._encrypt_message(message, host)
|
|
81
|
+
encrypted_message += self.MIME_BOUNDARY + b"--\r\n"
|
|
82
|
+
|
|
83
|
+
headers = []
|
|
84
|
+
headers.append(('Content-Type', '{0};protocol="{1}";boundary="Encrypted Boundary"'\
|
|
85
|
+
.format(content_type, self.protocol_string.decode())))
|
|
86
|
+
|
|
87
|
+
return encrypted_message, headers
|
|
88
|
+
|
|
89
|
+
async def parse_encrypted_response(self, response, data):
|
|
90
|
+
"""
|
|
91
|
+
Takes in the encrypted response from the server and decrypts it
|
|
92
|
+
|
|
93
|
+
:param response: The response that needs to be decrypted
|
|
94
|
+
:return: The unencrypted message from the server
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
content_type = response.getheaders('content-type')
|
|
98
|
+
if content_type is None:
|
|
99
|
+
return data
|
|
100
|
+
content_type = content_type[0]
|
|
101
|
+
if 'protocol="{0}"'.format(self.protocol_string.decode()) in content_type:
|
|
102
|
+
host = urlsplit(response.url).hostname
|
|
103
|
+
msg = await self._decrypt_response(host, data)
|
|
104
|
+
else:
|
|
105
|
+
msg = data
|
|
106
|
+
|
|
107
|
+
return msg
|
|
108
|
+
|
|
109
|
+
async def _encrypt_message(self, message, host):
|
|
110
|
+
message_length = str(len(message)).encode()
|
|
111
|
+
encrypted_stream = await self._build_message(message, host)
|
|
112
|
+
|
|
113
|
+
message_payload = self.MIME_BOUNDARY + b"\r\n" \
|
|
114
|
+
b"\tContent-Type: " + self.protocol_string + b"\r\n" \
|
|
115
|
+
b"\tOriginalContent: type=application/soap+xml;charset=UTF-8;Length=" + message_length + b"\r\n" + \
|
|
116
|
+
self.MIME_BOUNDARY + b"\r\n" \
|
|
117
|
+
b"\tContent-Type: application/octet-stream\r\n" + \
|
|
118
|
+
encrypted_stream
|
|
119
|
+
|
|
120
|
+
return message_payload
|
|
121
|
+
|
|
122
|
+
async def _decrypt_response(self, host, data):
|
|
123
|
+
parts = data.split(self.MIME_BOUNDARY + b'\r\n')
|
|
124
|
+
parts = list(filter(None, parts)) # filter out empty parts of the split
|
|
125
|
+
message = b''
|
|
126
|
+
|
|
127
|
+
for i in range(0, len(parts)):
|
|
128
|
+
if i % 2 == 1:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
header = parts[i].strip()
|
|
132
|
+
payload = parts[i + 1]
|
|
133
|
+
|
|
134
|
+
expected_length = int(header.split(b'Length=')[1])
|
|
135
|
+
|
|
136
|
+
# remove the end MIME block if it exists
|
|
137
|
+
if payload.endswith(self.MIME_BOUNDARY + b'--\r\n'):
|
|
138
|
+
payload = payload[:len(payload) - 24]
|
|
139
|
+
|
|
140
|
+
encrypted_data = payload.replace(b'\tContent-Type: application/octet-stream\r\n', b'')
|
|
141
|
+
decrypted_message = await self._decrypt_message(encrypted_data, host)
|
|
142
|
+
actual_length = len(decrypted_message)
|
|
143
|
+
|
|
144
|
+
if actual_length != expected_length:
|
|
145
|
+
raise WinRMError('Encrypted length from server does not match the '
|
|
146
|
+
'expected size, message has been tampered with')
|
|
147
|
+
message += decrypted_message
|
|
148
|
+
|
|
149
|
+
return message
|
|
150
|
+
|
|
151
|
+
async def _decrypt_ntlm_message(self, encrypted_data, host):
|
|
152
|
+
message = await self.session.authmanager.authobj.decrypt(encrypted_data[4:], None)
|
|
153
|
+
return message[0]
|
|
154
|
+
|
|
155
|
+
async def _decrypt_credssp_message(self, encrypted_data, host):
|
|
156
|
+
message, signature = await self.session.authmanager.authobj.decrypt(encrypted_data[4:], None)
|
|
157
|
+
return message
|
|
158
|
+
|
|
159
|
+
async def _decrypt_kerberos_message(self, encrypted_data, host):
|
|
160
|
+
signature_length = struct.unpack("<i", encrypted_data[:4])[0]
|
|
161
|
+
signature = b'\x00'*8 + encrypted_data[4:signature_length + 4]
|
|
162
|
+
encrypted_message = encrypted_data[signature_length + 4:]
|
|
163
|
+
|
|
164
|
+
message, _ = await self.session.authmanager.authobj.decrypt(encrypted_message, None, auth_data=signature)
|
|
165
|
+
return message
|
|
166
|
+
|
|
167
|
+
async def _build_ntlm_message(self, message, host):
|
|
168
|
+
sealed_message, signature = await self.session.authmanager.authobj.encrypt(message, self.sequence_number)
|
|
169
|
+
signature_length = struct.pack("<i", len(signature))
|
|
170
|
+
self.sequence_number += 1
|
|
171
|
+
return signature_length + signature + sealed_message
|
|
172
|
+
|
|
173
|
+
async def _build_credssp_message(self, message, host):
|
|
174
|
+
sealed_message, _ = await self.session.authmanager.authobj.encrypt(message, self.sequence_number) ##TODO: Check if this is correct
|
|
175
|
+
cipher_negotiated = self.session.authmanager.authobj.get_cipher_name()
|
|
176
|
+
trailer_length = self._get_credssp_trailer_length(len(message), cipher_negotiated)
|
|
177
|
+
return struct.pack("<i", trailer_length) + sealed_message
|
|
178
|
+
|
|
179
|
+
async def _build_kerberos_message(self, message, host):
|
|
180
|
+
self.sequence_number = 0
|
|
181
|
+
sealed_message, signature = await self.session.authmanager.authobj.encrypt(message, self.sequence_number) ##TODO: Check if this is correct
|
|
182
|
+
self.sequence_number += 1
|
|
183
|
+
signature_length = struct.pack("<i", len(signature))
|
|
184
|
+
return signature_length + signature + sealed_message
|
|
185
|
+
|
|
186
|
+
def _get_credssp_trailer_length(self, message_length, cipher_suite):
|
|
187
|
+
# I really don't like the way this works but can't find a better way, MS
|
|
188
|
+
# allows you to get this info through the struct SecPkgContext_StreamSizes
|
|
189
|
+
# but there is no GSSAPI/OpenSSL equivalent so we need to calculate it
|
|
190
|
+
# ourselves
|
|
191
|
+
|
|
192
|
+
if re.match(r'^.*-GCM-[\w\d]*$', cipher_suite):
|
|
193
|
+
# We are using GCM for the cipher suite, GCM has a fixed length of 16
|
|
194
|
+
# bytes for the TLS trailer making it easy for us
|
|
195
|
+
trailer_length = 16
|
|
196
|
+
else:
|
|
197
|
+
# We are not using GCM so need to calculate the trailer size. The
|
|
198
|
+
# trailer length is equal to the length of the hmac + the length of the
|
|
199
|
+
# padding required by the block cipher
|
|
200
|
+
hash_algorithm = cipher_suite.split('-')[-1]
|
|
201
|
+
|
|
202
|
+
# while there are other algorithms, SChannel doesn't support them
|
|
203
|
+
# as of yet https://msdn.microsoft.com/en-us/library/windows/desktop/aa374757(v=vs.85).aspx
|
|
204
|
+
if hash_algorithm == 'MD5':
|
|
205
|
+
hash_length = 16
|
|
206
|
+
elif hash_algorithm == 'SHA':
|
|
207
|
+
hash_length = 20
|
|
208
|
+
elif hash_algorithm == 'SHA256':
|
|
209
|
+
hash_length = 32
|
|
210
|
+
elif hash_algorithm == 'SHA384':
|
|
211
|
+
hash_length = 48
|
|
212
|
+
else:
|
|
213
|
+
hash_length = 0
|
|
214
|
+
|
|
215
|
+
pre_pad_length = message_length + hash_length
|
|
216
|
+
|
|
217
|
+
if "RC4" in cipher_suite:
|
|
218
|
+
# RC4 is a stream cipher so no padding would be added
|
|
219
|
+
padding_length = 0
|
|
220
|
+
elif "DES" in cipher_suite or "3DES" in cipher_suite:
|
|
221
|
+
# 3DES is a 64 bit block cipher
|
|
222
|
+
padding_length = 8 - (pre_pad_length % 8)
|
|
223
|
+
else:
|
|
224
|
+
# AES is a 128 bit block cipher
|
|
225
|
+
padding_length = 16 - (pre_pad_length % 16)
|
|
226
|
+
|
|
227
|
+
trailer_length = (pre_pad_length + padding_length) - message_length
|
|
228
|
+
|
|
229
|
+
return trailer_length
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from awinrm import Session
|
|
3
|
+
from asysocks.unicomm.protocol.client.http.client import ClientSession
|
|
4
|
+
|
|
5
|
+
async def check_auth(url):
|
|
6
|
+
async with ClientSession() as session:
|
|
7
|
+
async with session.post(url) as resp:
|
|
8
|
+
if resp.status == 401:
|
|
9
|
+
return resp.get_all('www-authenticate', [])
|
|
10
|
+
return ['NOAUTH']
|
|
11
|
+
|
|
12
|
+
async def amain(url):
|
|
13
|
+
transport = 'ssl'
|
|
14
|
+
if url.startswith('http://'):
|
|
15
|
+
transport = 'plaintext'
|
|
16
|
+
url = Session._build_url(url, transport)
|
|
17
|
+
print('[+] Testing URL: ' + url)
|
|
18
|
+
headers = await check_auth(url)
|
|
19
|
+
for entry in headers:
|
|
20
|
+
print('[+] Authenthod: %s' % entry)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
import argparse
|
|
25
|
+
|
|
26
|
+
parser = argparse.ArgumentParser(description='WinRM - Enumerate authentication types supported by the remote server')
|
|
27
|
+
parser.add_argument('url', type=str, help = 'URL to connect to. Must start with http:// or https://')
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
asyncio.run(amain(args.url))
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import asyncio
|
|
3
|
+
import traceback
|
|
4
|
+
from awinrm import Session, decode_bytes
|
|
5
|
+
from awinrm import logger
|
|
6
|
+
|
|
7
|
+
async def amain(url, command, authtype):
|
|
8
|
+
async with Session(url, authtype=authtype) as session:
|
|
9
|
+
try:
|
|
10
|
+
stdout, stderr, return_code = await session.run_cmd(command)
|
|
11
|
+
if len(stdout) > 0:
|
|
12
|
+
print(decode_bytes(stdout))
|
|
13
|
+
if len(stderr) > 0:
|
|
14
|
+
print(decode_bytes(stderr), file=sys.stderr)
|
|
15
|
+
sys.exit(return_code)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
traceback.print_exc()
|
|
18
|
+
print('[-] Error: %s' % str(e))
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
import argparse
|
|
23
|
+
import logging
|
|
24
|
+
from asyauth import logger as authlogger
|
|
25
|
+
from asysocks import logger as sockslogger
|
|
26
|
+
|
|
27
|
+
parser = argparse.ArgumentParser(description='WinRM - Execute a single shell command remotely')
|
|
28
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity')
|
|
29
|
+
parser.add_argument('-c', '--command', type=str, default = 'ipconfig /all', help = 'Shell command to execute')
|
|
30
|
+
parser.add_argument('-a', '--authproto', choices=['spnego', 'credssp'], default = 'spnego', help = 'Authentication protocol to use')
|
|
31
|
+
parser.add_argument('url', type=str, help = 'URL to connect to')
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
if args.verbose == 0:
|
|
35
|
+
logging.basicConfig(level=logging.INFO)
|
|
36
|
+
logger.setLevel(logging.ERROR)
|
|
37
|
+
authlogger.setLevel(logging.ERROR)
|
|
38
|
+
sockslogger.setLevel(logging.ERROR)
|
|
39
|
+
else:
|
|
40
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
41
|
+
logger.setLevel(logging.DEBUG)
|
|
42
|
+
authlogger.setLevel(logging.DEBUG)
|
|
43
|
+
sockslogger.setLevel(logging.DEBUG)
|
|
44
|
+
|
|
45
|
+
asyncio.run(amain(args.url, args.command, args.authproto))
|
|
46
|
+
|
|
47
|
+
if __name__ == '__main__':
|
|
48
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import asyncio
|
|
3
|
+
import traceback
|
|
4
|
+
from awinrm import Session, decode_bytes
|
|
5
|
+
from awinrm import logger
|
|
6
|
+
|
|
7
|
+
import aioconsole
|
|
8
|
+
|
|
9
|
+
async def print_output(shell):
|
|
10
|
+
while True:
|
|
11
|
+
try:
|
|
12
|
+
stdout = await shell.stdout.get()
|
|
13
|
+
if len(stdout) > 0:
|
|
14
|
+
print(decode_bytes(stdout), end='')
|
|
15
|
+
stderr = await shell.stderr.get()
|
|
16
|
+
if len(stderr) > 0:
|
|
17
|
+
print(decode_bytes(stderr), end='', file=sys.stderr)
|
|
18
|
+
except asyncio.CancelledError:
|
|
19
|
+
break
|
|
20
|
+
except Exception as e:
|
|
21
|
+
traceback.print_exc()
|
|
22
|
+
print('[-] Error: %s' % str(e))
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
async def amain(url, authtype):
|
|
26
|
+
async with Session(url, authtype=authtype) as session:
|
|
27
|
+
async with session.create_shell() as shell:
|
|
28
|
+
try:
|
|
29
|
+
x = asyncio.create_task(print_output(shell))
|
|
30
|
+
while True:
|
|
31
|
+
user_input = await aioconsole.ainput("")
|
|
32
|
+
await shell.send_input(user_input + '\r\n')
|
|
33
|
+
except Exception as e:
|
|
34
|
+
traceback.print_exc()
|
|
35
|
+
print('[-] Error: %s' % str(e))
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
import argparse
|
|
41
|
+
import logging
|
|
42
|
+
from asyauth import logger as authlogger
|
|
43
|
+
|
|
44
|
+
parser = argparse.ArgumentParser(description='WinRM - Execute a single shell command remotely')
|
|
45
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity')
|
|
46
|
+
parser.add_argument('-a', '--authproto', choices=['spnego', 'credssp'], default = 'spnego', help = 'Authentication protocol to use')
|
|
47
|
+
parser.add_argument('url', type=str, help = 'URL to connect to')
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
|
|
50
|
+
if args.verbose == 0:
|
|
51
|
+
logging.basicConfig(level=logging.INFO)
|
|
52
|
+
logger.setLevel(logging.ERROR)
|
|
53
|
+
authlogger.setLevel(logging.ERROR)
|
|
54
|
+
else:
|
|
55
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
56
|
+
logger.setLevel(logging.DEBUG)
|
|
57
|
+
authlogger.setLevel(logging.DEBUG)
|
|
58
|
+
|
|
59
|
+
asyncio.run(amain(args.url, args.authproto))
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
main()
|