-
Notifications
You must be signed in to change notification settings - Fork 119
/
Copy pathpower_shell.rb
202 lines (179 loc) · 7.74 KB
/
power_shell.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# Copyright 2016 Shawn Neal <sneal@sneal.net>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'securerandom' unless defined?(SecureRandom)
require_relative 'base'
require_relative '../psrp/message_fragmenter'
require_relative '../psrp/receive_response_reader'
require_relative '../wsmv/configuration'
require_relative '../wsmv/create_pipeline'
require_relative '../wsmv/send_data'
require_relative '../wsmv/init_runspace_pool'
require_relative '../wsmv/keep_alive'
module WinRM
module Shells
# Proxy to a remote PowerShell instance
class Powershell < Base
include WinRM::WSMV::SOAP
class << self
def close_shell(connection_opts, transport, shell_id)
msg = WinRM::WSMV::CloseShell.new(
connection_opts,
shell_id: shell_id,
shell_uri: WinRM::WSMV::Header::RESOURCE_URI_POWERSHELL
)
transport.send_request(msg.build)
end
end
# Create a new powershell shell
# @param connection_opts [ConnectionOpts] The WinRM connection options
# @param transport [HttpTransport] The WinRM SOAP transport
# @param logger [Logger] The logger to log diagnostic messages to
def initialize(connection_opts, transport, logger)
super
@shell_uri = WinRM::WSMV::Header::RESOURCE_URI_POWERSHELL
end
# Runs the specified command
# @param command [String] The powershell script to run
# @param block [&block] The optional callback for any realtime output
# @yield [Message] PSRP Message in response
# @yieldreturn [Array<Message>] All messages in response
def send_pipeline_command(command, &block)
with_command_shell(command) do |shell, cmd|
response_reader.read_message(command_output_message(shell, cmd), true, &block)
end
end
# calculate the maimum fragment size so that they will be as large as possible yet
# no greater than the max_envelope_size_kb on the end point. To calculate this
# threshold, we:
# - determine the maximum number of bytes accepted on the endpoint
# - subtract the non-fragment characters in the SOAP envelope
# - determine the number of bytes that could be base64 encded to the above length
# - subtract the fragment header bytes (ids, length, etc)
def max_fragment_blob_size
@max_fragment_blob_size ||= begin
fragment_header_length = 21
begin
max_fragment_bytes = (max_envelope_size_kb * 1024) - empty_pipeline_envelope.length
base64_deflated(max_fragment_bytes) - fragment_header_length
rescue WinRMWSManFault => e
# A non administrator user will encounter an access denied
# error attempting to query winrm configuration.
# we will assin a small default and adjust to a protocol
# appropriate max length when that info is available
raise unless e.fault_code == '5'
WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
rescue WinRMSoapFault
WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
end
end
end
protected
def response_reader
@response_reader ||= WinRM::PSRP::ReceiveResponseReader.new(transport, logger)
end
def send_command(command, _arguments)
command_id = SecureRandom.uuid.to_s.upcase
command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
message = PSRP::MessageFactory.create_pipeline_message(@runspace_id, command_id, command)
fragmenter.fragment(message) do |fragment|
command_args = [connection_opts, shell_id, command_id, fragment]
if fragment.start_fragment
resp_doc = transport.send_request(WinRM::WSMV::CreatePipeline.new(*command_args).build)
command_id = REXML::XPath.first(resp_doc, "//*[local-name() = 'CommandId']").text
else
transport.send_request(WinRM::WSMV::SendData.new(*command_args).build)
end
end
logger.debug("[WinRM] Command created for #{command} with id: #{command_id}")
command_id
end
def open_shell
@runspace_id = SecureRandom.uuid.to_s.upcase
runspace_msg = WinRM::WSMV::InitRunspacePool.new(
connection_opts,
@runspace_id,
open_shell_payload(@runspace_id)
)
resp_doc = transport.send_request(runspace_msg.build)
shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
wait_for_running(shell_id)
shell_id
end
def out_streams
%w[stdout]
end
private
def base64_deflated(inflated_length)
inflated_length / 4 * 3
end
def empty_pipeline_envelope
WinRM::WSMV::CreatePipeline.new(
connection_opts,
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000000'
).build
end
def max_envelope_size_kb
@max_envelope_size_kb ||= begin
config_msg = WinRM::WSMV::Configuration.new(connection_opts)
msg = config_msg.build
resp_doc = transport.send_request(msg)
REXML::XPath.first(resp_doc, "//*[local-name() = 'MaxEnvelopeSizekb']").text.to_i
rescue REXML::ParseException
logger.debug("[WinRM] Endpoint doesn't support config request for MaxEnvelopeSizekb")
raise
end
end
def open_shell_payload(shell_id)
[
WinRM::PSRP::MessageFactory.session_capability_message(shell_id),
WinRM::PSRP::MessageFactory.init_runspace_pool_message(shell_id)
].map do |message|
fragmenter.fragment(message).bytes
end.flatten
end
def wait_for_running(shell_id)
state = WinRM::PSRP::MessageData::RunspacepoolState::OPENING
keepalive_msg = WinRM::WSMV::KeepAlive.new(connection_opts, shell_id)
# 2 is "openned". if we start issuing commands while in "openning" the runspace
# seems to hang
until state == WinRM::PSRP::MessageData::RunspacepoolState::OPENED
response_reader.read_message(keepalive_msg) do |message|
logger.debug("[WinRM] polling for pipeline state. message: #{message.inspect}")
parsed = message.parsed_data
case parsed
when WinRM::PSRP::MessageData::RunspacepoolState
state = parsed.runspace_state
when WinRM::PSRP::MessageData::SessionCapability
# if the user lacks admin privileges, we cannot query the MaxEnvelopeSizeKB
# on the server and will assign to a "best effort" default based on protocol version
if fragmenter.max_blob_length == WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
fragmenter.max_blob_length = default_protocol_envelope_size(parsed.protocol_version)
end
end
end
end
end
# Powershell v2.0 has a protocol version of 2.1
# which defaults to a 150 MaxEnvelopeSizeKB
# later versions default to 500
def default_protocol_envelope_size(protocol_version)
protocol_version > '2.1' ? 512000 : 153600
end
def fragmenter
@fragmenter ||= WinRM::PSRP::MessageFragmenter.new(max_fragment_blob_size)
end
end
end
end