psrp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright 2016 Shawn Neal <sneal@sneal.net>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'base'
18
+
19
+ module PSRP
20
+ module WSMV
21
+ # WSMV message to close a remote shell
22
+ class CloseShell < Base
23
+ def initialize(session_opts, shell_id)
24
+ @session_opts = session_opts
25
+ @shell_id = shell_id
26
+ end
27
+
28
+ protected
29
+
30
+ def create_header(header)
31
+ header << Gyoku.xml(close_header)
32
+ end
33
+
34
+ def create_body(_body)
35
+ # no body
36
+ end
37
+
38
+ private
39
+
40
+ def close_header
41
+ merge_headers(shared_headers(@session_opts),
42
+ resource_uri_shell(RESOURCE_URI_POWERSHELL),
43
+ action_delete,
44
+ selector_shell_id(@shell_id))
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright 2016 Matt Wrock <matt@mattwrock.com>
4
+ # Copyright 2016 Sam Oluwalana <soluwalana@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative 'base'
19
+
20
+ module PSRP
21
+ module WSMV
22
+ # WSMV message to execute a command via psrp
23
+ class CreatePipeline < Base
24
+ attr_accessor :shell_id
25
+
26
+ def initialize(session_opts, shell_id, pipeline)
27
+ @session_opts = session_opts
28
+ @shell_id = shell_id
29
+ @pipeline = pipeline
30
+ end
31
+
32
+ protected
33
+
34
+ def create_header(header)
35
+ header << Gyoku.xml(command_headers)
36
+ end
37
+
38
+ def create_body(body)
39
+ body.tag!("#{NS_WIN_SHELL}:CommandLine") do |cl|
40
+ cl << Gyoku.xml(command_body)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # MS-PSRP 3.1.5.3.3
47
+ # MS-PSRP 2.2.2.10
48
+ def command_body
49
+ {
50
+ "#{NS_WIN_SHELL}:Command" => '',
51
+ "#{NS_WIN_SHELL}:Arguments" => encode_bytes(@pipeline)
52
+ }
53
+ end
54
+
55
+
56
+ def command_headers
57
+ merge_headers(shared_headers(@session_opts),
58
+ resource_uri_shell(RESOURCE_URI_POWERSHELL),
59
+ action_command,
60
+ selector_shell_id(shell_id))
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,92 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright 2016 Matt Wrock <matt@mattwrock.com>
4
+ # Copyright 2016 Sam Oluwalana <soluwalana@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative 'base'
19
+
20
+ module PSRP
21
+ module WSMV
22
+
23
+
24
+ # WSMV message to create a remote shell
25
+ class InitRunspacePool < Base
26
+ attr_accessor :shell_id
27
+
28
+ def initialize(session_opts)
29
+ @session_opts = session_opts
30
+ @shell_id = SecureRandom.uuid.to_s.upcase
31
+ end
32
+
33
+ protected
34
+
35
+ def create_header(header)
36
+ header << Gyoku.xml(shell_headers)
37
+ end
38
+
39
+ def create_body(body)
40
+ body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body) }
41
+ end
42
+
43
+ private
44
+
45
+ def shell_body
46
+ body = {
47
+ "#{NS_WIN_SHELL}:InputStreams" => 'stdin pr',
48
+ "#{NS_WIN_SHELL}:OutputStreams" => 'stdout'
49
+ }
50
+ session_capabilities = PSRP::MessageEncoder.new(shell_id, nil, :SESSION_CAPABILITY)
51
+ runspace_init = PSRP::MessageEncoder.new(shell_id, nil, :INIT_RUNSPACEPOOL)
52
+ body['creationXml'] = encode_bytes(session_capabilities.fragments[0] + runspace_init.fragments[0])
53
+ body[:attributes!] = {
54
+ 'creationXml' => {
55
+ 'xmlns' => 'http://schemas.microsoft.com/powershell'
56
+ }
57
+ }
58
+ body
59
+ end
60
+
61
+ def header_opts
62
+ {
63
+ "#{NS_WSMAN_DMTF}:OptionSet" => {
64
+ "#{NS_WSMAN_DMTF}:Option" => 2.1,
65
+ :attributes! => {
66
+ "#{NS_WSMAN_DMTF}:Option" => {
67
+ 'Name' => 'protocolversion',
68
+ 'MustComply' => 'true'
69
+ }
70
+ }
71
+ },
72
+ :attributes! => {
73
+ "#{NS_WSMAN_DMTF}:OptionSet" => {
74
+ 'env:mustUnderstand' => 'true'
75
+ }
76
+ }
77
+ }
78
+ end
79
+
80
+ def shell_headers
81
+ merge_headers(shared_headers(@session_opts),
82
+ resource_uri_shell(RESOURCE_URI_POWERSHELL),
83
+ header_opts,
84
+ action_create,
85
+ selector_shell_id(shell_id))
86
+
87
+ end
88
+
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright 2016 Shawn Neal <sneal@sneal.net>
4
+ # Copyright 2016 Sam Oluwalana <soluwalana@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative 'base'
19
+
20
+ module PSRP
21
+ module WSMV
22
+ # WSMV message to get output from a remote shell
23
+ class ReceiveOutput < Base
24
+ def initialize(session_opts, command_out_opts)
25
+ @session_opts = session_opts
26
+ @shell_id = command_out_opts[:shell_id]
27
+ @command_id = command_out_opts[:command_id]
28
+ @shell_uri = command_out_opts[:shell_uri] || RESOURCE_URI_POWERSHELL
29
+ @out_streams = command_out_opts[:out_streams] || %w(stdout)
30
+ end
31
+
32
+ protected
33
+
34
+ def create_header(header)
35
+ header << Gyoku.xml(output_header)
36
+ end
37
+
38
+ def create_body(body)
39
+ body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(output_body) }
40
+ end
41
+
42
+ private
43
+
44
+ def output_header
45
+ merge_headers(shared_headers(@session_opts),
46
+ resource_uri_shell(@shell_uri),
47
+ action_receive,
48
+ header_opts,
49
+ selector_shell_id(@shell_id)
50
+ )
51
+ end
52
+
53
+ def header_opts
54
+ {
55
+ "#{NS_WSMAN_DMTF}:OptionSet" => {
56
+ "#{NS_WSMAN_DMTF}:Option" => 'TRUE', :attributes! => {
57
+ "#{NS_WSMAN_DMTF}:Option" => {
58
+ 'Name' => 'WSMAN_CMDSHELL_OPTION_KEEPALIVE'
59
+ }
60
+ }
61
+ },
62
+ "#{NS_WSMAN_DMTF}:OperationTimeout" => 'PT20.000S'
63
+ }
64
+ end
65
+
66
+ def output_body
67
+ if @command_id
68
+ {
69
+ "#{NS_WIN_SHELL}:DesiredStream" => @out_streams.join(' '), :attributes! => {
70
+ "#{NS_WIN_SHELL}:DesiredStream" => {
71
+ 'CommandId' => @command_id
72
+ }
73
+ }
74
+ }
75
+ else
76
+ { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout' }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright 2016 Sam Oluwalana <soluwalana@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'base'
18
+
19
+ module PSRP
20
+ module WSMV
21
+ class SendData < Base
22
+
23
+ def initialize(session_opts, shell_id, command_id, pipeline)
24
+ @session_opts = session_opts
25
+ @shell_id = shell_id
26
+ @command_id = command_id
27
+ @pipeline = pipeline
28
+ end
29
+
30
+ protected
31
+
32
+ def create_header(header)
33
+ header << Gyoku.xml(command_headers)
34
+ end
35
+
36
+ def create_body(body)
37
+ body.tag!("#{NS_WIN_SHELL}:Send") do |cl|
38
+ cl << Gyoku.xml(command_body)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def command_body
45
+ {
46
+ "#{NS_WIN_SHELL}:Stream" => encode_bytes(@pipeline),
47
+ :attributes! => {
48
+ "#{NS_WIN_SHELL}:Stream" => {
49
+ 'Name': 'stdin',
50
+ 'CommandId': @command_id
51
+ }
52
+ }
53
+ }
54
+ end
55
+
56
+ def command_headers
57
+ merge_headers(shared_headers(@session_opts),
58
+ resource_uri_shell(RESOURCE_URI_POWERSHELL),
59
+ action_send,
60
+ selector_shell_id(@shell_id))
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,289 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2015 Matt Wrock <matt@mattwrock.com>
4
+ # Copyright 2016 Shawn Neal <sneal@sneal.net>
5
+ # Copyright 2016 Sam Oluwalana <soluwalana@gmail.com>
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'erubis'
20
+
21
+
22
+ # PowerShell Remoting Protcol module
23
+ module PSRP
24
+
25
+
26
+ class Message
27
+ # Length of all the blob header fields:
28
+ # BOM, pipeline_id, runspace_pool_id, message_type, blob_destination
29
+ BLOB_HEADER_LEN = 43
30
+
31
+ # Maximum allowed length of the blob
32
+ BLOB_MAX_LEN = 32768 - BLOB_HEADER_LEN
33
+
34
+ # All known PSRP message types
35
+ MESSAGE_TYPES = {
36
+ SESSION_CAPABILITY: 0x00010002,
37
+ INIT_RUNSPACEPOOL: 0x00010004,
38
+ PUBLIC_KEY: 0x00010005,
39
+ ENCRYPTED_SESSION_KEY: 0x00010006,
40
+ PUBLIC_KEY_REQUEST: 0x00010007,
41
+ CONNECT_RUNSPACEPOOL: 0x00010008,
42
+ RUNSPACEPOOL_INIT_DATA: 0x0002100B,
43
+ RESET_RUNSPACE_STATE: 0x0002100C,
44
+ SET_MAX_RUNSPACES: 0x00021002,
45
+ SET_MIN_RUNSPACES: 0x00021003,
46
+ RUNSPACE_AVAILABILITY: 0x00021004,
47
+ RUNSPACEPOOL_STATE: 0x00021005,
48
+ CREATE_PIPELINE: 0x00021006,
49
+ GET_AVAILABLE_RUNSPACES: 0x00021007,
50
+ USER_EVENT: 0x00021008,
51
+ APPLICATION_PRIVATE_DATA: 0x00021009,
52
+ GET_COMMAND_METADATA: 0x0002100A,
53
+ RUNSPACEPOOL_HOST_CALL: 0x00021100,
54
+ RUNSPACEPOOL_HOST_RESPONSE: 0x00021101,
55
+ PIPELINE_INPUT:0x00041002,
56
+ END_OF_PIPELINE_INPUT: 0x00041003,
57
+ PIPELINE_OUTPUT: 0x00041004,
58
+ ERROR_RECORD: 0x00041005,
59
+ PIPELINE_STATE: 0x00041006,
60
+ DEBUG_RECORD: 0x00041007,
61
+ VERBOSE_RECORD: 0x00041008,
62
+ WARNING_RECORD: 0x00041009,
63
+ PROGRESS_RECORD: 0x00041010,
64
+ INFORMATION_RECORD: 0x00041011,
65
+ PIPELINE_HOST_CALL: 0x00041100,
66
+ PIPELINE_HOST_RESPONSE: 0x00041101
67
+ }
68
+
69
+
70
+ # Format a UUID into a GUID compatible byte array for Windows
71
+ #
72
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa373931(v=vs.85).aspx
73
+ # typedef struct _GUID {
74
+ # DWORD Data1;
75
+ # WORD Data2;
76
+ # WORD Data3;
77
+ # BYTE Data4[8];
78
+ # } GUID;
79
+ #
80
+ # @param uuid [String] Canonical hex format with hypens.
81
+ # @return [Array<byte>] UUID in a Windows GUID compatible byte array layout.
82
+ def uuid_to_windows_guid_bytes(uuid)
83
+ return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] unless uuid
84
+ b = uuid.scan(/[0-9a-fA-F]{2}/).map { |x| x.to_i(16) }
85
+ b[0..3].reverse + b[4..5].reverse + b[6..7].reverse + b[8..15]
86
+ end
87
+ end
88
+
89
+ class MessageDecoder < Message
90
+ attr :message_id, :fragment_id, :fragment_flags, :blob_length, :destination, :message_type, :client_runspace, :client_pid, :data
91
+
92
+ def initialize(raw_text)
93
+ # Message ID - 8bytes
94
+ # Fragment ID - 8bytes
95
+ # reserved - 6bits
96
+ # end fragment - 1bit
97
+ # start fragment - 1bit
98
+ # blob_length - 4bytes
99
+ # destination - 4bytes
100
+ # message_type - 4bytes
101
+ # client runspace GUID - 16bytes
102
+ # client PID GUID - 16bytes
103
+ # BOM 3bytes
104
+ #
105
+ # Data - blob_length - 43 bytes (40 blob bytes + BOM)
106
+ unencoded = Base64.decode64(raw_text)
107
+ fields = unencoded.unpack('Q>2CL>L<2h32h32C3A*')
108
+
109
+ @message_id = fields[0]
110
+ @fragment_id = fields[1]
111
+ @fragment_flags = fields[2]
112
+ @blob_length = fields[3]
113
+
114
+ if is_start_fragment?
115
+ @destination = fields[4]
116
+ @message_type = fields[5]
117
+ @client_runspace = fields[6]
118
+ @client_pid = fields[7]
119
+ @data = fields[11]
120
+ else
121
+ fields = unencoded.unpack('Q>2CL>A*')
122
+ @destination = nil
123
+ @message_type = nil
124
+ @client_runspace = nil
125
+ @client_pid = nil
126
+ @data = fields[4]
127
+ end
128
+ end
129
+
130
+ def is_end_fragment?
131
+ (@fragment_flags & 2) != 0
132
+ end
133
+
134
+ def is_start_fragment?
135
+ (@fragment_flags & 1) != 0
136
+ end
137
+ end
138
+
139
+ # PowerShell Remoting Protocol base message.
140
+ # http://download.microsoft.com/download/9/5/E/95EF66AF-9026-4BB0-A41D-A4F81802D92C/%5BMS-PSRP%5D.pdf
141
+ class MessageEncoder < Message
142
+
143
+ attr :fragments
144
+
145
+ TEMPLATES = {
146
+ SESSION_CAPABILITY: 'session_capability',
147
+ INIT_RUNSPACEPOOL: 'init_runspacepool',
148
+ CREATE_PIPELINE: 'create_pipeline',
149
+ RUNSPACE_AVAILABILITY: 'runspace_availability'
150
+
151
+ }
152
+
153
+ @@obj_id = 0
154
+
155
+ # Creates a new PSRP message instance
156
+ # @param id [Fixnum] The incrementing fragment id.
157
+ # @param shell_id [String] The UUID of the remote shell/runspace pool.
158
+ # @param command_id [String] The UUID to correlate the command/pipeline
159
+ # response.
160
+ # @param message_type [Fixnum] The PSRP MessageType. This is most commonly
161
+ # specified in hex, e.g. 0x00010002.
162
+ # @param payload [String] The PSRP payload as serialized XML
163
+ def initialize(shell_id, command_id, message_type, context = nil)
164
+ fail 'shell_id cannot be nil' if shell_id.nil?
165
+ @@obj_id += 1
166
+ @id = @@obj_id
167
+ @shell_id = shell_id
168
+ @command_id = command_id
169
+ @message_type = MESSAGE_TYPES[message_type]
170
+ @payload = render(TEMPLATES[message_type], context).force_encoding('utf-8').bytes
171
+
172
+ num_fragments = 1
173
+ num_fragments += (@payload.length / BLOB_MAX_LEN)
174
+ if (@payload.length % BLOB_MAX_LEN) == 0
175
+ num_fragments -= 1
176
+ end
177
+
178
+ @fragments = []
179
+
180
+ for i in 0..(num_fragments - 1)
181
+ start_pos = (i * BLOB_MAX_LEN)
182
+ end_pos = ((i + 1) * BLOB_MAX_LEN) - 1
183
+
184
+ if num_fragments == 1
185
+ flags = [3]
186
+ elsif i == 0
187
+ flags = [1]
188
+ elsif i == (num_fragments - 1)
189
+ flags = [2]
190
+ else
191
+ flags = [0]
192
+ end
193
+
194
+ @fragments.push(bytes(@payload[start_pos..end_pos], i, flags))
195
+ end
196
+ end
197
+
198
+ # Renders the specified template with the given context
199
+ # @param template [String] The base filename of the PSRP message template.
200
+ # @param context [Hash] Any options required for rendering the template.
201
+ # @return [String] The rendered XML PSRP message.
202
+ # @api private
203
+ def render(template, context = nil)
204
+ template_path = File.expand_path(
205
+ "#{File.dirname(__FILE__)}/templates/#{template}.xml.erb")
206
+ template = File.read(template_path)
207
+ Erubis::Eruby.new(template).result(context)
208
+ end
209
+
210
+ # Returns the raw PSRP message bytes ready for transfer to Windows inside a
211
+ # WinRM message.
212
+ # @return [Array<Byte>] Unencoded raw byte array of the PSRP message.
213
+ # rubocop:disable Metrics/AbcSize
214
+ def bytes(blob, frag_id, flags)
215
+
216
+ # Packet fragment headers
217
+ message = message_id
218
+ message += fragment_id(frag_id)
219
+ message += flags
220
+
221
+ if frag_id == 0
222
+ # packet fragment header
223
+ message += blob_length(blob, BLOB_HEADER_LEN)
224
+
225
+ # PSRP message header
226
+ message += blob_destination
227
+ message += message_type
228
+ message += runspace_pool_id
229
+ message += pipeline_id
230
+ message += byte_order_mark + blob
231
+
232
+ else
233
+ # packet fragment header
234
+ message += blob_length(blob, 0)
235
+ # PSRP fragment
236
+ message += blob
237
+ end
238
+ message
239
+ end
240
+
241
+ private
242
+
243
+ def message_id
244
+ int64be(@id)
245
+ end
246
+
247
+ def fragment_id(frag_id)
248
+ int64be(frag_id)
249
+ end
250
+
251
+ def blob_length(blob, header_len)
252
+ int16be(blob.length + header_len)
253
+ end
254
+
255
+ def blob_destination
256
+ [2, 0, 0, 0]
257
+ end
258
+
259
+ def message_type
260
+ int16le(@message_type)
261
+ end
262
+
263
+ def runspace_pool_id
264
+ uuid_to_windows_guid_bytes(@shell_id)
265
+ end
266
+
267
+ def pipeline_id
268
+ uuid_to_windows_guid_bytes(@command_id)
269
+ end
270
+
271
+ def byte_order_mark
272
+ [239, 187, 191]
273
+ end
274
+
275
+ def int64be(int64)
276
+ [int64 >> 32, int64 & 0x00000000ffffffff].pack('N2').unpack('C8')
277
+ end
278
+
279
+ def int16be(int16)
280
+ [int16].pack('N').unpack('C4')
281
+ end
282
+
283
+ def int16le(int16)
284
+ [int16].pack('N').unpack('C4').reverse
285
+ end
286
+
287
+ end
288
+ end
289
+