psrp 0.0.1

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.
@@ -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
+