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,73 @@
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/system'
22
+
23
+ module Ronin
24
+ module PostEx
25
+ module Sessions
26
+ #
27
+ # Base class for all post-exploitation session classes.
28
+ #
29
+ class Session
30
+
31
+ #
32
+ # The session name.
33
+ #
34
+ # @return [String]
35
+ #
36
+ # @raise [NotImplementedError]
37
+ # The session class did not set `@name`.
38
+ #
39
+ def name
40
+ @name || raise(NotImplementedError,"#{self.class}#name was not set")
41
+ end
42
+
43
+ #
44
+ # The remote system connected to the session.
45
+ #
46
+ # @return [System]
47
+ #
48
+ def system
49
+ @system ||= System.new(self)
50
+ end
51
+
52
+ #
53
+ # Closes the session.
54
+ #
55
+ # @abstract
56
+ #
57
+ def close
58
+ end
59
+
60
+ #
61
+ # Converts the session to a String.
62
+ #
63
+ # @return [String]
64
+ # The session's {#name}.
65
+ #
66
+ def to_s
67
+ name
68
+ end
69
+
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,618 @@
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/sessions/session'
22
+
23
+ require 'shellwords'
24
+ require 'base64'
25
+
26
+ module Ronin
27
+ module PostEx
28
+ module Sessions
29
+ #
30
+ # Base class for all interactive shell based post-exploitation sessions.
31
+ #
32
+ # ## Features
33
+ #
34
+ # * Supports Bash, Zsh, and all POSIX shells.
35
+ # * Emulates most of the post-exploitation API via shell commands.
36
+ #
37
+ class ShellSession < Session
38
+
39
+ # The IO object used to communicate with the shell.
40
+ #
41
+ # @return [Socket, IO]
42
+ #
43
+ # @api private
44
+ attr_reader :io
45
+
46
+ #
47
+ # Initializes the shell session.
48
+ #
49
+ # @param [Socet, IO] io
50
+ # The IO object used to communicate with the shell.
51
+ #
52
+ def initialize(io)
53
+ @io = io
54
+ end
55
+
56
+ #
57
+ # @group Shell Methods
58
+ #
59
+
60
+ #
61
+ # Writes a line to the shell.
62
+ #
63
+ # @param [String] line
64
+ # The line to write.
65
+ #
66
+ # @api private
67
+ #
68
+ def shell_puts(line)
69
+ @io.write("#{line}\n")
70
+ end
71
+
72
+ #
73
+ # Reads a line from the shell.
74
+ #
75
+ # @return [String, nil]
76
+ #
77
+ # @api private
78
+ #
79
+ def shell_gets
80
+ @io.gets
81
+ end
82
+
83
+ # Deliminator line to indicate the beginning and end of output
84
+ DELIMINATOR = '---'
85
+
86
+ #
87
+ # Executes a shell command and returns it's output.
88
+ #
89
+ # @param [String] command
90
+ # The shell command to execute.
91
+ #
92
+ # @return [String]
93
+ # The output of the shell command.
94
+ #
95
+ def shell_exec(command)
96
+ shell_puts("echo #{DELIMINATOR}; #{command} 2>/dev/null | base64; echo #{DELIMINATOR}")
97
+
98
+ # consume any leading output before the command output
99
+ while (line = shell_gets)
100
+ if line.chomp == DELIMINATOR
101
+ break
102
+ end
103
+ end
104
+
105
+ output = String.new
106
+
107
+ while (line = shell_gets)
108
+ if line.chomp == DELIMINATOR
109
+ break
110
+ end
111
+
112
+ output << line
113
+ end
114
+
115
+ return Base64.decode64(output)
116
+ end
117
+
118
+ #
119
+ # Joins a command with arguments into a single command String.
120
+ #
121
+ # @param [String] command
122
+ # The command name to execute.
123
+ #
124
+ # @param [Array<String>] arguments
125
+ # Additional arguments for the command.
126
+ #
127
+ # @return [String]
128
+ # The command string.
129
+ #
130
+ # @api private
131
+ #
132
+ def command_join(command,*arguments)
133
+ Shellwords.join([command,*arguments])
134
+ end
135
+
136
+ #
137
+ # Invokes a specific command with arguments.
138
+ #
139
+ # @param [String] command
140
+ # The command name to execute.
141
+ #
142
+ # @param [Array<String>] arguments
143
+ # Additional arguments for the command.
144
+ #
145
+ # @return [String, nil]
146
+ # The command's output or `nil` if there was no output.
147
+ #
148
+ # @api private
149
+ #
150
+ def command_exec(command,*arguments)
151
+ output = shell_exec(command_join(command,*arguments))
152
+
153
+ if output.empty? then nil
154
+ else output
155
+ end
156
+ end
157
+
158
+ #
159
+ # @group System Methods
160
+ #
161
+
162
+ #
163
+ # Gets the current time and returns the UNIX timestamp.
164
+ #
165
+ # @return [Integer]
166
+ # The current time as a UNIX timestamp.
167
+ #
168
+ # @note executes the `date +%s` command.
169
+ #
170
+ def sys_time
171
+ shell_exec('date +%s').to_i
172
+ end
173
+
174
+ #
175
+ # Gets the system's hostname.
176
+ #
177
+ # @return [String]
178
+ #
179
+ # @note executes the `echo $HOSTNAME` command.
180
+ #
181
+ def sys_hostname
182
+ shell_exec("echo $HOSTNAME").chomp
183
+ end
184
+
185
+ #
186
+ # @group File-System methods
187
+ #
188
+
189
+ #
190
+ # Gets the current working directory and returns the directory path.
191
+ #
192
+ # @return [String]
193
+ # The remote current working directory.
194
+ #
195
+ # @note executes the `pwd` command.
196
+ #
197
+ def fs_getcwd
198
+ shell_exec('pwd').chomp
199
+ end
200
+
201
+ #
202
+ # Changes the current working directory.
203
+ #
204
+ # @param [String] path
205
+ # The new remote current working directory.
206
+ #
207
+ # @note executes the `cd <path>` command.
208
+ #
209
+ def fs_chdir(path)
210
+ shell_puts("cd #{Shellwords.escape(path)} 2>/dev/null")
211
+ end
212
+
213
+ #
214
+ # Reads the entire file at the given path and returns the full file's
215
+ # contents.
216
+ #
217
+ # @param [String] path
218
+ # The remote path to read.
219
+ #
220
+ # @return [String, nil]
221
+ # The contents of the remote file or `nil` if the file could not be
222
+ # read.
223
+ #
224
+ # @note executes the `cat <path>` command.
225
+ #
226
+ def fs_readfile(path)
227
+ command_exec('cat',path)
228
+ end
229
+
230
+ #
231
+ # Reads the destination path of a remote symbolic link.
232
+ #
233
+ # @param [String] path
234
+ # The remote path to read.
235
+ #
236
+ # @return [String, nil]
237
+ # The destination of the remote symbolic link or `nil` if the symbolic
238
+ # link could not be read.
239
+ #
240
+ # @note executes the `readlink -f <path>` command.
241
+ #
242
+ def fs_readlink(path)
243
+ command_exec('readlink','-f',path).chomp
244
+ end
245
+
246
+ #
247
+ # Reads the contents of a remote directory and returns an Array of
248
+ # directory entry names.
249
+ #
250
+ # @param [String] path
251
+ # The path of the remote directory to read.
252
+ #
253
+ # @return [Array<String>]
254
+ # The entities within the remote directory.
255
+ #
256
+ # @note executes the `ls <path>` command.
257
+ #
258
+ def fs_readdir(path)
259
+ command_exec('ls',path).lines(chomp: true)
260
+ end
261
+
262
+ #
263
+ # Evaluates a directory glob pattern and returns all matching paths.
264
+ #
265
+ # @param [String] pattern
266
+ # The glob pattern to search for remotely.
267
+ #
268
+ # @return [Array<String>]
269
+ # The matching paths.
270
+ #
271
+ # @note executes the `ls <pattern>` command.
272
+ #
273
+ def fs_glob(pattern,&block)
274
+ shell_exec("ls #{pattern}").lines(chomp: true)
275
+ end
276
+
277
+ #
278
+ # Creates a remote temporary file with the given file basename.
279
+ #
280
+ # @param [String] basename
281
+ # The basename for the new temporary file.
282
+ #
283
+ # @return [String]
284
+ # The path of the newly created temporary file.
285
+ #
286
+ # @note executes the `mktemp <basename>` command.
287
+ #
288
+ def fs_mktemp(basename)
289
+ command_exec('mktemp',basename).chomp
290
+ end
291
+
292
+ #
293
+ # Creates a new remote directory at the given path.
294
+ #
295
+ # @param [String] new_path
296
+ # The new remote directory to create.
297
+ #
298
+ # @note executes the `mkdir <path>` command.
299
+ #
300
+ def fs_mkdir(new_path)
301
+ command_exec('mkdir',new_path)
302
+ end
303
+
304
+ #
305
+ # Copies a source file to the destination path.
306
+ #
307
+ # @param [String] path
308
+ # The source file.
309
+ #
310
+ # @param [String] new_path
311
+ # The destination path.
312
+ #
313
+ # @note executes the `cp -r <path> <new_path>` command.
314
+ #
315
+ def fs_copy(path,new_path)
316
+ command_exec('cp','-r',path,new_path)
317
+ end
318
+
319
+ #
320
+ # Removes a file at the given path.
321
+ #
322
+ # @param [String] path
323
+ # The remote path to remove.
324
+ #
325
+ # @note executes the `rm <path>` command.
326
+ #
327
+ def fs_unlink(path)
328
+ command_exec('rm',path)
329
+ end
330
+
331
+ #
332
+ # Removes an empty directory at the given path.
333
+ #
334
+ # @param [String] path
335
+ # The remote directory path to remove.
336
+ #
337
+ # @note executes the `rmdir <path>` command.
338
+ #
339
+ def fs_rmdir(path)
340
+ command_exec('rmdir',path)
341
+ end
342
+
343
+ #
344
+ # Moves or renames a remote source file to a new destination path.
345
+ #
346
+ # @param [String] path
347
+ # The source file path.
348
+ #
349
+ # @param [String] new_path
350
+ # The destination file path.
351
+ #
352
+ # @note executes the `mv <path> <new_path>` command.
353
+ #
354
+ def fs_move(path,new_path)
355
+ command_exec('mv',path,new_path)
356
+ end
357
+
358
+ #
359
+ # Creates a remote symbolic link at the destination path pointing to the
360
+ # source path.
361
+ #
362
+ # @param [String] src
363
+ # The source file path for the new symbolic link.
364
+ #
365
+ # @param [String] dest
366
+ # The remote path of the new symbolic link.
367
+ #
368
+ # @note executes the `ln -s <src> <dest>` command.
369
+ #
370
+ def fs_link(src,dest)
371
+ command_exec('ln','-s',src,dest)
372
+ end
373
+
374
+ #
375
+ # Changes the group ownership of a remote file or directory.
376
+ #
377
+ # @param [String] group
378
+ # The new group name for the remote file or directory.
379
+ #
380
+ # @param [String] path
381
+ # The path of the remote file or directory.
382
+ #
383
+ # @note executes the `chgrp <group> <path>` command.
384
+ #
385
+ def fs_chgrp(group,path)
386
+ command_exec('chgrp',group,path)
387
+ end
388
+
389
+ #
390
+ # Changes the user ownership of remote a file or directory.
391
+ #
392
+ # @param [String] user
393
+ # The new user for the remote file or directory.
394
+ #
395
+ # @param [String] path
396
+ # The path of the remote file or directory.
397
+ #
398
+ # @note executes the `chown <user> <path>` command.
399
+ #
400
+ def fs_chown(user,path)
401
+ command_exec('chown',user,path)
402
+ end
403
+
404
+ #
405
+ # Changes the permissions on a remote file or directory.
406
+ #
407
+ # @param [Integer] mode
408
+ # The permissions mode for the remote file or directory.
409
+ #
410
+ # @param [String] path
411
+ # The path of the remote file or directory.
412
+ #
413
+ # @note executes the `chmod <umask> <path>` command.
414
+ #
415
+ def fs_chmod(mode,path)
416
+ umask = "%.4o" % mode
417
+
418
+ command_exec('chmod',umask,path)
419
+ end
420
+
421
+ #
422
+ # Queries file information for the given remote path and returns a Hash
423
+ # of file metadata.
424
+ #
425
+ # @param [String] path
426
+ # The path to the remote file or directory.
427
+ #
428
+ # @return [Hash{Symbol => Object}, nil]
429
+ # The metadata for the remote file.
430
+ #
431
+ # @note executes the `stat -t <path>` command.
432
+ #
433
+ def fs_stat(path)
434
+ fields = command_exec('stat','-t',path).strip.split(' ')
435
+
436
+ return {
437
+ path: path,
438
+ size: fields[1].to_i,
439
+ blocks: fields[2].to_i,
440
+ uid: fields[4].to_i,
441
+ gid: fields[5].to_i,
442
+ inode: fields[7].to_i,
443
+ links: fields[8].to_i,
444
+ atime: Time.at(fields[11].to_i),
445
+ mtime: Time.at(fields[12].to_i),
446
+ ctime: Time.at(fields[13].to_i),
447
+ blocksize: fields[14].to_i
448
+ }
449
+ end
450
+
451
+ #
452
+ # @group Process methods
453
+ #
454
+
455
+ #
456
+ # Gets the current process's Process ID (PID).
457
+ #
458
+ # @return [Integer]
459
+ # The current process's PID.
460
+ #
461
+ # @note executes the `echo $$` command.
462
+ #
463
+ def process_getpid
464
+ shell_exec('echo $$').to_i
465
+ end
466
+
467
+ #
468
+ # Gets the current process's parent Process ID (PPID).
469
+ #
470
+ # @return [Integer]
471
+ # The current process's PPID.
472
+ #
473
+ # @note executes the `echo $PPID` command.
474
+ #
475
+ def process_getppid
476
+ shell_exec('echo $PPID').to_i
477
+ end
478
+
479
+ #
480
+ # Gets the current process's user ID (UID).
481
+ #
482
+ # @return [Integer]
483
+ # The current process's UID.
484
+ #
485
+ # @note executes the `id -u` command.
486
+ #
487
+ def process_getuid
488
+ command_exec('id','-u').to_i
489
+ end
490
+
491
+ #
492
+ # Gets the current process's group ID (GID).
493
+ #
494
+ # @return [Integer]
495
+ # The group ID (GID) for the current process.
496
+ #
497
+ # @note executes the `id -g` command.
498
+ #
499
+ def process_getgid
500
+ command_exec('id','-g').to_i
501
+ end
502
+
503
+ #
504
+ # Queries all environment variables of the current process. Returns a
505
+ # Hash of the env variable names and values.
506
+ #
507
+ # @return [Hash{String => String}]
508
+ # The Hash of environment variables.
509
+ #
510
+ # @note executes the `env` command.
511
+ #
512
+ def process_environ
513
+ Hash[command_exec('env').each_line(chomp: true).map { |line|
514
+ line.split('=',2)
515
+ }]
516
+ end
517
+
518
+ #
519
+ # Gets an individual environment variable. If the environment variable
520
+ # has not been set, `nil` will be returned.
521
+ #
522
+ # @param [String] name
523
+ # The environment variable name to get.
524
+ #
525
+ # @return [String]
526
+ # The environment variable value.
527
+ #
528
+ # @note executes the `echo $<name>` command.
529
+ #
530
+ def process_getenv(name)
531
+ shell_exec("echo $#{name}").chomp
532
+ end
533
+
534
+ #
535
+ # Sets an environment variable to the given value.
536
+ #
537
+ # @param [String] name
538
+ # The environment variable name to set.
539
+ #
540
+ # @param [String] value
541
+ # The new value for the environment variable.
542
+ #
543
+ # @note executes the `export <name>=<value>` command.
544
+ #
545
+ def process_setenv(name,value)
546
+ shell_puts("export #{name}=#{value}")
547
+ end
548
+
549
+ #
550
+ # Un-sets an environment variable.
551
+ #
552
+ # @param [String] name
553
+ # The environment variable to unset.
554
+ #
555
+ # @note executes the `unset <name>` command.
556
+ #
557
+ def process_unsetenv(name)
558
+ shell_puts("unset #{name}")
559
+ end
560
+
561
+ #
562
+ # Kills another process using the given Process ID (POD) and the signal
563
+ # number.
564
+ #
565
+ # @param [Integer] pid
566
+ # The process ID (PID) to kill.
567
+ #
568
+ # @param [Integer] signal
569
+ # The signal to send the process ID (PID).
570
+ #
571
+ # @note executes the `kill -s <signal> <pid>` command.
572
+ #
573
+ def process_kill(pid,signal)
574
+ command_exec('kill','-s',signal,pid)
575
+ end
576
+
577
+ #
578
+ # Spawns a new process using the given program and additional arguments.
579
+ #
580
+ # @param [String] command
581
+ # The command name to spawn.
582
+ #
583
+ # @param [Array<String>] arguments
584
+ # Additional arguments for the program.
585
+ #
586
+ # @return [Integer]
587
+ # The process ID (PID) of the spawned process.
588
+ #
589
+ # @note
590
+ # executes the command with additional arguments as a background
591
+ # process.
592
+ #
593
+ def process_spawn(command,*arguments)
594
+ command = command_join(command,*arguments)
595
+
596
+ shell_exec("#{command} 2>&1 >/dev/null &; echo $!").to_i
597
+ end
598
+
599
+ #
600
+ # Exits the current process.
601
+ #
602
+ # @note executes the `exit` command.
603
+ #
604
+ def process_exit
605
+ shell_puts('exit')
606
+ end
607
+
608
+ #
609
+ # Closes the remote shell.
610
+ #
611
+ def close
612
+ @io.close
613
+ end
614
+
615
+ end
616
+ end
617
+ end
618
+ end