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,191 @@
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
+ require 'ronin/post_ex/system/fs'
23
+ require 'ronin/post_ex/system/process'
24
+ require 'ronin/post_ex/system/shell'
25
+ require 'ronin/post_ex/cli/system_shell'
26
+
27
+ module Ronin
28
+ module PostEx
29
+ #
30
+ # Represents a successfully compromised system. The {System} class will
31
+ # wraps around a session object which defines syscall-like post-exploitation
32
+ # API for reading/writing files, run commands, etc.
33
+ #
34
+ # ## Supported API Functions
35
+ #
36
+ # * `sys_time -> Integer`
37
+ # * `sys_hostname -> String`
38
+ #
39
+ # ## Example
40
+ #
41
+ # Define the session class which defines the Post-Exploitation API methods:
42
+ #
43
+ # require 'base64'
44
+ #
45
+ # class SimpleRATSession < Ronin::PostEx::Sessions::Session
46
+ #
47
+ # def initialize(socket)
48
+ # @socket = socket
49
+ # end
50
+ #
51
+ # def call(name,*args)
52
+ # @socket.puts("#{name} #{args.join(' ')}")
53
+ #
54
+ # Base64.strict_decode64(@socket.gets(chomp: true)(
55
+ # end
56
+ #
57
+ # def shell_exec(command)
58
+ # call('EXEC',command)
59
+ # end
60
+ #
61
+ # def fs_readfile(path)
62
+ # call('READ',path)
63
+ # end
64
+ #
65
+ # def process_pid
66
+ # call('PID').to_i
67
+ # end
68
+ #
69
+ # def process_getuid
70
+ # call('UID').to_i
71
+ # end
72
+ #
73
+ # def process_environ
74
+ # Hash[
75
+ # call('ENV').each_line(chomp: true).map { |line|
76
+ # line.split('=',2)
77
+ # }
78
+ # ]
79
+ # end
80
+ #
81
+ # end
82
+ #
83
+ # Initialize a new {System} object that wraps around the client:
84
+ #
85
+ # session = SimpleRATSession.new(socket)
86
+ # system = Ronin::PostEx::System.new(session)
87
+ #
88
+ # Interact with the system's remote files as if they were local files:
89
+ #
90
+ # file = system.fs.open('/etc/passwd')
91
+ # file.each_line do |line|
92
+ # user, x, uid, gid, name, home_dir, shell = line.split(':')
93
+ #
94
+ # puts "User Detected: #{user} (id=#{uid})"
95
+ # end
96
+ #
97
+ # Get information about the current process:
98
+ #
99
+ # system.process.pid
100
+ # # => 1234
101
+ # system.process.getuid
102
+ # # => 1001
103
+ # system.process.environ
104
+ # # => {"HOME"=>"...", "PATH"=>"...", ...}
105
+ #
106
+ # Execute commands on the remote system:
107
+ #
108
+ # system.shell.ls('/')
109
+ # # => "bin\nboot\ndev\netc\nhome\nlib\nlib64\nlost+found\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsnap\nsrv\nsys\ntmp\nusr\nvar\n"
110
+ # system.shell.exec("find -type f -name '*.xls' /srv") do |path|
111
+ # puts "Found XLS file: #{path}"
112
+ # end
113
+ #
114
+ class System < Resource
115
+
116
+ # The File-System resource.
117
+ #
118
+ # @return [System::FS]
119
+ attr_reader :fs
120
+
121
+ # The Process resource.
122
+ #
123
+ # @return [System::Process]
124
+ attr_reader :process
125
+
126
+ # The Shell resource.
127
+ #
128
+ # @return [System::Shell]
129
+ attr_reader :shell
130
+
131
+ #
132
+ # Initializes the system.
133
+ #
134
+ # @param [Object] session
135
+ # The object which defines the Post-Exploitation API methods.
136
+ #
137
+ def initialize(session)
138
+ super(session)
139
+
140
+ @fs = FS.new(session)
141
+ @process = Process.new(session)
142
+ @shell = Shell.new(session)
143
+ end
144
+
145
+ #
146
+ # Gets the current time.
147
+ #
148
+ # @return [Time]
149
+ # The current time.
150
+ #
151
+ # @note
152
+ # Requires the `sys_time` method be defined by the {#session} object.
153
+ #
154
+ def time
155
+ Time.at(@session.sys_time.to_i)
156
+ end
157
+ resource_method :time, [:sys_time]
158
+
159
+ #
160
+ # Gets the system's hostname.
161
+ #
162
+ # @return [String]
163
+ # The system's local hostname.
164
+ #
165
+ # @note
166
+ # Requires the `sys_hostname` method be defined by the {#session}
167
+ # object.
168
+ #
169
+ def hostname
170
+ @session.sys_hostname
171
+ end
172
+
173
+ #
174
+ # Starts an interactive post-exploitation system shell.
175
+ #
176
+ def interact
177
+ CLI::SystemShell.start(self)
178
+ end
179
+
180
+ #
181
+ # Exits the process.
182
+ #
183
+ # @see Process#exit
184
+ #
185
+ def exit
186
+ @process.exit
187
+ end
188
+
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,26 @@
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
+ module Ronin
22
+ module PostEx
23
+ # ronin-post_ex version
24
+ VERSION = '0.1.0.beta1'
25
+ end
26
+ end
@@ -0,0 +1,22 @@
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
+ require 'ronin/post_ex/version'
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gemspec = YAML.load_file('gemspec.yml')
7
+
8
+ gem.name = gemspec.fetch('name')
9
+ gem.version = gemspec.fetch('version') do
10
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
11
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ require 'ronin/post_ex/version'
14
+ Ronin::PostEx::VERSION
15
+ end
16
+
17
+ gem.summary = gemspec['summary']
18
+ gem.description = gemspec['description']
19
+ gem.licenses = Array(gemspec['license'])
20
+ gem.authors = Array(gemspec['authors'])
21
+ gem.email = gemspec['email']
22
+ gem.homepage = gemspec['homepage']
23
+ gem.metadata = gemspec['metadata'] if gemspec['metadata']
24
+
25
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
26
+
27
+ gem.files = `git ls-files`.split($/)
28
+ gem.files = glob[gemspec['files']] if gemspec['files']
29
+ gem.files += Array(gemspec['generated_files'])
30
+
31
+ gem.executables = gemspec.fetch('executables') do
32
+ glob['bin/*'].map { |path| File.basename(path) }
33
+ end
34
+
35
+ gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
36
+ gem.test_files = glob[gemspec['test_files'] || 'spec/{**/}*_spec.rb']
37
+ gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
38
+
39
+ gem.require_paths = Array(gemspec.fetch('require_paths') {
40
+ %w[ext lib].select { |dir| File.directory?(dir) }
41
+ })
42
+
43
+ gem.requirements = gemspec['requirements']
44
+ gem.required_ruby_version = gemspec['required_ruby_version']
45
+ gem.required_rubygems_version = gemspec['required_rubygems_version']
46
+ gem.post_install_message = gemspec['post_install_message']
47
+
48
+ split = lambda { |string| string.split(/,\s*/) }
49
+
50
+ if gemspec['dependencies']
51
+ gemspec['dependencies'].each do |name,versions|
52
+ gem.add_dependency(name,split[versions])
53
+ end
54
+ end
55
+
56
+ if gemspec['development_dependencies']
57
+ gemspec['development_dependencies'].each do |name,versions|
58
+ gem.add_development_dependency(name,split[versions])
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'ronin/post_ex/sessions/bind_shell'
3
+
4
+ describe Ronin::PostEx::Sessions::BindShell do
5
+ it "must inherit from Ronin::PostEx::Sessions::RemoteShellSession" do
6
+ expect(described_class).to be < Ronin::PostEx::Sessions::RemoteShellSession
7
+ end
8
+
9
+ let(:host) { 'example.com' }
10
+ let(:port) { 1337 }
11
+ let(:addrinfo) { Addrinfo.tcp(host,port) }
12
+
13
+ describe ".connect" do
14
+ let(:host) { 'example.com' }
15
+ let(:port) { 1337 }
16
+
17
+ let(:socket) { double('TCPSocket') }
18
+
19
+ before { allow(socket).to receive(:remote_address).and_return(addrinfo) }
20
+ subject { described_class }
21
+
22
+ it "must connect to the remote host and port and return a #{described_class} object" do
23
+ expect(TCPSocket).to receive(:new).with(host,port).and_return(socket)
24
+
25
+ bind_shell = subject.connect(host,port)
26
+
27
+ expect(bind_shell).to be_kind_of(described_class)
28
+ expect(bind_shell.io).to be(socket)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'ronin/post_ex/sessions/bind_shell'
3
+
4
+ describe Ronin::PostEx::Sessions::BindShell do
5
+ it "must inherit from Ronin::PostEx::Sessions::ShellSession" do
6
+ expect(described_class).to be < Ronin::PostEx::Sessions::ShellSession
7
+ end
8
+
9
+ let(:host) { 'example.com' }
10
+ let(:port) { 1337 }
11
+ let(:addrinfo) { Addrinfo.tcp(host,port) }
12
+ let(:socket) { double('TCPSocket') }
13
+
14
+ before { allow(socket).to receive(:remote_address).and_return(addrinfo) }
15
+ subject { described_class.new(socket) }
16
+
17
+ describe "#initialize" do
18
+ it "must set #io" do
19
+ expect(subject.io).to be(socket)
20
+ end
21
+
22
+ let(:ip) { addrinfo.ip_address }
23
+
24
+ it "musst set #name to \"ip:port\"" do
25
+ expect(subject.name).to eq("#{ip}:#{port}")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+ require 'ronin/post_ex/sessions/reverse_shell'
3
+
4
+ describe Ronin::PostEx::Sessions::ReverseShell do
5
+ it "must inherit from Ronin::PostEx::Sessions::RemoteShellSession" do
6
+ expect(described_class).to be < Ronin::PostEx::Sessions::RemoteShellSession
7
+ end
8
+
9
+ let(:host) { 'localhost' }
10
+ let(:port) { 1337 }
11
+ let(:addrinfo) { Addrinfo.tcp(host,port) }
12
+
13
+ describe ".listen" do
14
+ let(:server_socket) { double('TCPServer') }
15
+ let(:client_socket) { double('TCPSocket') }
16
+
17
+ before { allow(client_socket).to receive(:remote_address).and_return(addrinfo) }
18
+ subject { described_class }
19
+
20
+ it "must listen on a local port, accept a connection, return a #{described_class} object, and close the server socket" do
21
+ expect(TCPServer).to receive(:new).with(port,nil).and_return(server_socket)
22
+ expect(server_socket).to receive(:listen).with(1)
23
+ expect(server_socket).to receive(:accept).and_return(client_socket)
24
+ expect(server_socket).to receive(:close)
25
+
26
+ reverse_shell = subject.listen(port)
27
+
28
+ expect(reverse_shell).to be_kind_of(described_class)
29
+ expect(reverse_shell.io).to be(client_socket)
30
+ end
31
+
32
+ context "and a host argument is given" do
33
+ let(:host) { '127.0.0.1' }
34
+
35
+ it "must listen on a local host and port, accept a connection, return a #{described_class} object, and close the server socket" do
36
+ expect(TCPServer).to receive(:new).with(port,host).and_return(server_socket)
37
+ expect(server_socket).to receive(:listen).with(1)
38
+ expect(server_socket).to receive(:accept).and_return(client_socket)
39
+ expect(server_socket).to receive(:close)
40
+ allow(client_socket).to receive(:local_address).and_return(addrinfo)
41
+
42
+ reverse_shell = subject.listen(host,port)
43
+
44
+ expect(reverse_shell).to be_kind_of(described_class)
45
+ expect(reverse_shell.io).to be(client_socket)
46
+ end
47
+ end
48
+ end
49
+ end