ronin-post_ex 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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