ronin-post_ex 0.1.0.beta1

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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.document +6 -0
  3. data/.github/workflows/ruby.yml +31 -0
  4. data/.gitignore +13 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +1 -0
  8. data/API_SPEC.md +235 -0
  9. data/COPYING.txt +165 -0
  10. data/ChangeLog.md +23 -0
  11. data/Gemfile +36 -0
  12. data/README.md +245 -0
  13. data/Rakefile +34 -0
  14. data/examples/bind_shell.rb +19 -0
  15. data/gemspec.yml +25 -0
  16. data/lib/ronin/post_ex/cli/shell_shell.rb +66 -0
  17. data/lib/ronin/post_ex/cli/system_shell.rb +811 -0
  18. data/lib/ronin/post_ex/remote_dir.rb +190 -0
  19. data/lib/ronin/post_ex/remote_file/stat.rb +174 -0
  20. data/lib/ronin/post_ex/remote_file.rb +417 -0
  21. data/lib/ronin/post_ex/remote_process.rb +170 -0
  22. data/lib/ronin/post_ex/resource.rb +144 -0
  23. data/lib/ronin/post_ex/sessions/bind_shell.rb +60 -0
  24. data/lib/ronin/post_ex/sessions/remote_shell_session.rb +48 -0
  25. data/lib/ronin/post_ex/sessions/reverse_shell.rb +67 -0
  26. data/lib/ronin/post_ex/sessions/rpc_session.rb +779 -0
  27. data/lib/ronin/post_ex/sessions/session.rb +73 -0
  28. data/lib/ronin/post_ex/sessions/shell_session.rb +618 -0
  29. data/lib/ronin/post_ex/system/fs.rb +650 -0
  30. data/lib/ronin/post_ex/system/process.rb +422 -0
  31. data/lib/ronin/post_ex/system/shell.rb +1037 -0
  32. data/lib/ronin/post_ex/system.rb +191 -0
  33. data/lib/ronin/post_ex/version.rb +26 -0
  34. data/lib/ronin/post_ex.rb +22 -0
  35. data/ronin-post_ex.gemspec +61 -0
  36. data/spec/sessions/bind_shell_spec.rb +31 -0
  37. data/spec/sessions/remote_shell_session_spec.rb +28 -0
  38. data/spec/sessions/reverse_shell_spec.rb +49 -0
  39. data/spec/sessions/rpc_session_spec.rb +500 -0
  40. data/spec/sessions/session_spec.rb +61 -0
  41. data/spec/sessions/shell_session_spec.rb +482 -0
  42. data/spec/spec_helper.rb +9 -0
  43. data/spec/system_spec.rb +66 -0
  44. metadata +155 -0
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-post_ex - a Ruby API for Post-Exploitation.
4
+ #
5
+ # Copyright (c) 2007-2022 Hal Brodigan (postmodern.mod3 at gmail.com)
6
+ #
7
+ # ronin-post_ex is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-post_ex is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-post_ex. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/post_ex/remote_file/stat'
22
+ require 'ronin/post_ex/resource'
23
+
24
+ require 'fake_io'
25
+ require 'set'
26
+
27
+ module Ronin
28
+ module PostEx
29
+ #
30
+ # The {RemoteFile} class represents files on a remote system. {RemoteFile}
31
+ # requires the API object to define either `file_read` and/or `file_write`.
32
+ # Additionally, {RemoteFile} can optionally use the `file_open`,
33
+ # `file_close`, `file_tell`, `file_seek` and `file_stat` methods.
34
+ #
35
+ # ## Supported API Methods
36
+ #
37
+ # * `file_open(path : String, mode : String) -> Integer`
38
+ # * `file_read(fd : Integer, length : Integer) -> String | nil`
39
+ # * `file_write(fd : Integer, pos : Integer, data : String) -> Integer`
40
+ # * `file_seek(fd : Integer, new_pos : Integer, whence : String)`
41
+ # * `file_tell(fd : Integer) -> Integer`
42
+ # * `file_ioctl(fd : Integer, command : String | Array[Integer], argument : Object) -> Integer`
43
+ # * `file_fcntl(fd : Integer, command : String | Array[Integer], argument : Object) -> Integer`
44
+ # * `file_stat(fd : Integer) => Hash[Symbol, Object] | nil`
45
+
46
+ # * `file_close(fd : Integer)`
47
+ # * `fs_readfile(path : String) -> String | nil`
48
+ # * `fs_stat(path : String) => Hash[Symbol, Object] | nil`
49
+ #
50
+ class RemoteFile < Resource
51
+
52
+ include FakeIO
53
+
54
+ #
55
+ # Creates a new remote controlled File object.
56
+ #
57
+ # @param [Sessions::Session#file_read, Sessions::Session#file_write] session
58
+ # The session object that defines the `file_read` and `file_write`
59
+ # methods.
60
+ #
61
+ # @param [String] path
62
+ # The path of the remote file.
63
+ #
64
+ # @param [String] mode
65
+ # The mode to open the file in.
66
+ #
67
+ # @note
68
+ # This method may use the `file_open` method, if it is defined by
69
+ # {#session}.
70
+ #
71
+ def initialize(session,path,mode='r')
72
+ @session = session
73
+
74
+ @path = path.to_s
75
+ @mode = mode.to_s
76
+
77
+ super()
78
+ end
79
+
80
+ #
81
+ # Opens a file.
82
+ #
83
+ # @param [Sessions::Session#file_read, Sessions::Session#file_write] session
84
+ # The session object controlling remote files.
85
+ #
86
+ # @param [String] path
87
+ # The path of the remote file.
88
+ #
89
+ # @yield [file]
90
+ # The given block will be passed the newly created file object.
91
+ # When the block has returned, the File object will be closed.
92
+ #
93
+ # @yieldparam [RemoteFile]
94
+ # The newly created file object.
95
+ #
96
+ # @return [RemoteFile, nil]
97
+ # If no block is given, then the newly opened remote file object will be
98
+ # returned. If a block was given, then `nil` will be returned.
99
+ #
100
+ def self.open(session,path)
101
+ io = new(session,path)
102
+
103
+ if block_given?
104
+ yield(io)
105
+ io.close
106
+ return
107
+ else
108
+ return io
109
+ end
110
+ end
111
+
112
+ # Seeks from beginning of file.
113
+ SEEK_SET = File::SEEK_SET
114
+
115
+ # Seeks from current position.
116
+ SEEK_CUR = File::SEEK_CUR
117
+
118
+ # Seeks from end of file.
119
+ SEEK_END = File::SEEK_END
120
+
121
+ # Seeks to next data.
122
+ SEEK_DATA = (defined?(File::SEEK_DATA) && File::SEEK_DATA) || 3
123
+
124
+ # Seeks to next hole.
125
+ SEEK_HOLE = (defined?(File::SEEK_HOLE) && File::SEEK_HOLE) || 4
126
+
127
+ # Mapping of `SEEK_*` constants to their String values.
128
+ #
129
+ # @api private
130
+ WHENCE = {
131
+ SEEK_SET => 'SEEK_SET',
132
+ SEEK_CUR => 'SEEK_CUR',
133
+ SEEK_END => 'SEEK_END',
134
+ SEEK_DATA => 'SEEK_DATA',
135
+ SEEK_HOLE => 'SEEK_HOLE'
136
+ }
137
+
138
+ #
139
+ # Sets the position in the file to read.
140
+ #
141
+ # @param [Integer] new_pos
142
+ # The new position to read from.
143
+ #
144
+ # @param [Integer] whence
145
+ # The origin point to seek from.
146
+ #
147
+ # @return [Integer]
148
+ # The new position within the file.
149
+ #
150
+ # @raise [ArgumentError]
151
+ # An invalid whence value was given.
152
+ #
153
+ # @note This method may use the `file_seek` API method, if it is defined
154
+ # by {#session}.
155
+ #
156
+ def seek(new_pos,whence=SEEK_SET)
157
+ clear_buffer!
158
+
159
+ unless WHENCE.has_key?(whence)
160
+ raise(ArgumentError,"invalid whence value: #{whence.inspect}")
161
+ end
162
+
163
+ if @session.respond_to?(:file_seek)
164
+ @session.file_seek(@fd,new_pos,WHENCE[whence])
165
+ end
166
+
167
+ @pos = new_pos
168
+ end
169
+ resource_method :seek, [:file_seek]
170
+
171
+ #
172
+ # The current offset in the file.
173
+ #
174
+ # @return [Integer]
175
+ # The current offset in bytes.
176
+ #
177
+ # @note
178
+ # This method may use the `file_tell` API method, if it is defined by
179
+ # {#session}.
180
+ #
181
+ def tell
182
+ if @session.respond_to?(:file_tell)
183
+ @pos = @session.file_tell(@fd)
184
+ else
185
+ @pos
186
+ end
187
+ end
188
+ resource_method :tell, [:file_tell]
189
+
190
+ #
191
+ # Executes a low-level command to control or query the IO stream.
192
+ #
193
+ # @param [String, Array<Integer>] command
194
+ # The IOCTL command.
195
+ #
196
+ # @param [Object] argument
197
+ # Argument of the command.
198
+ #
199
+ # @return [Integer]
200
+ # The return value from the `ioctl`.
201
+ #
202
+ # @raise [NotImplementedError]
203
+ # The API object does not define `file_ioctl`.
204
+ #
205
+ # @raise [RuntimeError]
206
+ # The `file_ioctl` method requires a file-descriptor.
207
+ #
208
+ # @note This method requires the `file_ioctl` API method.
209
+ #
210
+ def ioctl(command,argument)
211
+ unless @session.respond_to?(:file_ioctl)
212
+ raise(NotImplementedError,"#{@session.inspect} does not define file_ioctl")
213
+ end
214
+
215
+ if @fd == nil
216
+ raise(RuntimeError,"file_ioctl requires a file-descriptor")
217
+ end
218
+
219
+ return @session.file_ioctl(@fd,command,argument)
220
+ end
221
+ resource_method :ioctl, [:file_ioctl]
222
+
223
+ #
224
+ # Executes a low-level command to control or query the file stream.
225
+ #
226
+ # @param [String, Array<Integer>] command
227
+ # The FCNTL command.
228
+ #
229
+ # @param [Object] argument
230
+ # Argument of the command.
231
+ #
232
+ # @return [Integer]
233
+ # The return value from the `fcntl`.
234
+ #
235
+ # @raise [NotImplementedError]
236
+ # The API object does not define `file_fcntl`.
237
+ #
238
+ # @note This method requires the `file_fnctl` API method.
239
+ #
240
+ def fcntl(command,argument)
241
+ unless @session.respond_to?(:file_fcntl)
242
+ raise(NotImplementedError,"#{@session.inspect} does not define file_fcntl")
243
+ end
244
+
245
+ if @fd == nil
246
+ raise(RuntimeError,"file_ioctl requires a file-descriptor")
247
+ end
248
+
249
+ return @session.file_fcntl(@fd,command,argument)
250
+ end
251
+ resource_method :fcntl, [:file_fcntl]
252
+
253
+ #
254
+ # Re-opens the file.
255
+ #
256
+ # @param [String] path
257
+ # The new path for the file.
258
+ #
259
+ # @return [RemoteFile]
260
+ # The re-opened the file.
261
+ #
262
+ # @note
263
+ # This method may use the `file_close` and `file_open` API methods,
264
+ # if they are defined by {#session}.
265
+ #
266
+ def reopen(path)
267
+ close
268
+
269
+ @path = path.to_s
270
+ return open
271
+ end
272
+ resource_method :reopen, [:file_close, :file_open]
273
+
274
+ #
275
+ # The status information for the file.
276
+ #
277
+ # @return [Stat]
278
+ # The status information.
279
+ #
280
+ # @note This method relies on the `fs_stat` API method.
281
+ #
282
+ def stat
283
+ if @fd
284
+ Stat.new(@session, fd: @fd)
285
+ else
286
+ Stat.new(@session, path: @path)
287
+ end
288
+ end
289
+ resource_method :stat, [:file_stat]
290
+
291
+ #
292
+ # Flushes the file.
293
+ #
294
+ # @return [self]
295
+ #
296
+ # @note This method may use the `file_flush` API method, if it is defined
297
+ # by {#session}.
298
+ #
299
+ def flush
300
+ if @session.respond_to?(:file_flush)
301
+ @session.file_flush
302
+ end
303
+
304
+ return self
305
+ end
306
+
307
+ #
308
+ # Flushes the file before closing it.
309
+ #
310
+ # @return [nil]
311
+ #
312
+ def close
313
+ flush if @mode.include?('w')
314
+ super()
315
+ end
316
+
317
+ #
318
+ # Inspects the open file.
319
+ #
320
+ # @return [String]
321
+ # The inspected open file.
322
+ #
323
+ def inspect
324
+ "#<#{self.class}:#{@path}>"
325
+ end
326
+
327
+ private
328
+
329
+ #
330
+ # Attempts calling `file_open` from the API object to open the remote
331
+ # file.
332
+ #
333
+ # @return [Object]
334
+ # The file descriptor returned by `file_open`.
335
+ #
336
+ # @note
337
+ # This method may use the `file_open` API method, if {#session} defines
338
+ # it.
339
+ #
340
+ def io_open
341
+ if @session.respond_to?(:file_open)
342
+ @session.file_open(@path,@mode)
343
+ end
344
+ end
345
+ resource_method :open
346
+
347
+ # Default block size to read file data with.
348
+ BLOCK_SIZE = 4096
349
+
350
+ #
351
+ # Reads a block from the remote file by calling `file_read` or
352
+ # `fs_readfile` from the API object.
353
+ #
354
+ # @return [String, nil]
355
+ # A block of data from the file or `nil` if there is no more data to be
356
+ # read.
357
+ #
358
+ # @raise [IOError]
359
+ # The API object does not define `file_read` or `fs_readfile`.
360
+ #
361
+ # @note
362
+ # This method requires either the `fs_readfile` or `file_read` API
363
+ # methods.
364
+ #
365
+ def io_read
366
+ if @session.respond_to?(:file_read)
367
+ @session.file_read(@fd,BLOCK_SIZE)
368
+ elsif @api.respond_to?(:fs_readfile)
369
+ @eof = true
370
+ @api.fs_readfile(@path)
371
+ else
372
+ raise(IOError,"#{@session.inspect} does not support reading")
373
+ end
374
+ end
375
+ resource_method :read, [:file_read]
376
+
377
+ #
378
+ # Writes data to the remote file by calling `file_write` from the
379
+ # API object.
380
+ #
381
+ # @param [String] data
382
+ # The data to write.
383
+ #
384
+ # @return [Integer]
385
+ # The number of bytes writen.
386
+ #
387
+ # @raise [IOError]
388
+ # The API object does not define `file_write`.
389
+ #
390
+ # @note This method requires the `file_write` API method.
391
+ #
392
+ def io_write(data)
393
+ if @session.respond_to?(:file_write)
394
+ @pos += @session.file_write(@fd,@pos,data)
395
+ else
396
+ raise(IOError,"#{@session.inspect} does not support writing to files")
397
+ end
398
+ end
399
+ resource_method :write, [:file_write]
400
+
401
+ #
402
+ # Attempts calling `file_close` from the API object to close
403
+ # the file.
404
+ #
405
+ # @note This method may use the `file_close` method, if {#session} defines
406
+ # it.
407
+ #
408
+ def io_close
409
+ if @session.respond_to?(:file_close)
410
+ @session.file_close(@fd)
411
+ end
412
+ end
413
+ resource_method :close
414
+
415
+ end
416
+ end
417
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-post_ex - a Ruby API for Post-Exploitation.
4
+ #
5
+ # Copyright (c) 2007-2022 Hal Brodigan (postmodern.mod3 at gmail.com)
6
+ #
7
+ # ronin-post_ex is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-post_ex is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-post_ex. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/post_ex/resource'
22
+
23
+ require 'fake_io'
24
+
25
+ module Ronin
26
+ module PostEx
27
+ #
28
+ # The {RemoteProcess} class represents a command being executed on a remote
29
+ # system. The {RemoteProcess} class wraps around the `process_popen` and
30
+ # `process_read`, `process_write`, and `process_close` methods defined in
31
+ # the API object.
32
+ #
33
+ class RemoteProcess < Resource
34
+
35
+ include FakeIO
36
+ include Enumerable
37
+
38
+ # The command string.
39
+ #
40
+ # @return [String]
41
+ attr_reader :command
42
+
43
+ #
44
+ # Creates a new remote process.
45
+ #
46
+ # @param [Sessions::Session] session
47
+ # The object controlling command execution.
48
+ #
49
+ # @param [String] command
50
+ # The command to run.
51
+ #
52
+ # @raise [NotImplementedError]
53
+ # The session object does not define `process_popen`.
54
+ #
55
+ def initialize(session,command)
56
+ unless session.respond_to?(:process_popen)
57
+ raise(NotImplementedError,"#{session.inspect} must define #process_popen for #{self.class}")
58
+ end
59
+
60
+ @session = session
61
+ @command = command
62
+
63
+ super()
64
+ end
65
+
66
+ #
67
+ # Reopens the command.
68
+ #
69
+ # @param [String] command
70
+ # The new command to run.
71
+ #
72
+ # @return [RemoteProcess]
73
+ # The new command.
74
+ #
75
+ def reopen(command)
76
+ close
77
+
78
+ @command = command
79
+
80
+ return open
81
+ end
82
+ resource_method :reopen, [:process_popen]
83
+
84
+ #
85
+ # Converts the command to a `String`.
86
+ #
87
+ # @return [String]
88
+ # The process'es command.
89
+ #
90
+ def to_s
91
+ @command
92
+ end
93
+
94
+ #
95
+ # Inspects the command.
96
+ #
97
+ # @return [String]
98
+ # The inspected command listing the program name and arguments.
99
+ #
100
+ def inspect
101
+ "#<#{self.class}: #{self}>"
102
+ end
103
+
104
+ private
105
+
106
+ #
107
+ # Executes and opens the command for reading.
108
+ #
109
+ # @return [Enumerator]
110
+ # The enumerator that wraps around `process_popen`.
111
+ #
112
+ def io_open
113
+ @session.enum_for(:process_popen,@command)
114
+ end
115
+ resource_method :open, [:process_popen]
116
+
117
+ # Default block size to read process output with.
118
+ BLOCK_SIZE = 4096
119
+
120
+ #
121
+ # Reads a line of output from the command.
122
+ #
123
+ # @return [String]
124
+ # A line of output.
125
+ #
126
+ # @raise [EOFError]
127
+ # The end of the output stream has been reached.
128
+ #
129
+ def io_read
130
+ if @session.respond_to?(:process_read)
131
+ @session.process_write(@fd,BLOCK_SIZE)
132
+ end
133
+ end
134
+ resource_method :read
135
+
136
+ #
137
+ # Writes data to the shell.
138
+ #
139
+ # @param [String] data
140
+ # The data to write to the shell.
141
+ #
142
+ # @return [Integer]
143
+ # The number of bytes writen.
144
+ #
145
+ def io_write(data)
146
+ if @session.respond_to?(:process_write)
147
+ @session.process_write(@fd,data)
148
+ else
149
+ raise(IOError,"#{@session.inspect} does not support writing to the shell")
150
+ end
151
+ end
152
+ resource_method :write, [:process_write]
153
+
154
+ #
155
+ # Attempts calling `process_close` from the API object to close
156
+ # the file.
157
+ #
158
+ # @note
159
+ # This method may use the `process_close` method, if {#session} defines it.
160
+ #
161
+ def io_close
162
+ if @session.respond_to?(:process_close)
163
+ @session.process_close(@fd)
164
+ end
165
+ end
166
+ resource_method :close
167
+
168
+ end
169
+ end
170
+ end