msfrpc-client 1.1.0 → 1.1.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile.lock +25 -25
- data/examples/msfrpc_irb.rb +1 -2
- data/examples/msfrpc_pro_discover.rb +207 -0
- data/examples/msfrpc_pro_exploit.rb +225 -0
- data/examples/msfrpc_pro_import.rb +91 -0
- data/examples/msfrpc_pro_nexpose.rb +148 -0
- data/examples/msfrpc_pro_report.rb +70 -70
- data/lib/msfrpc-client/client.rb +290 -204
- data/lib/msfrpc-client/constants.rb +30 -32
- data/lib/msfrpc-client/version.rb +1 -1
- data/msfrpc-client.gemspec +5 -5
- metadata +27 -23
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'optparse'
|
4
|
+
require 'msfrpc-client'
|
5
|
+
require 'rex/ui'
|
6
|
+
|
7
|
+
def usage(ropts)
|
8
|
+
$stderr.puts ropts
|
9
|
+
|
10
|
+
if @rpc and @rpc.token
|
11
|
+
wspaces = @rpc.call("pro.workspaces") rescue {}
|
12
|
+
if wspaces.keys.length > 0
|
13
|
+
$stderr.puts "Active Projects:"
|
14
|
+
wspaces.each_pair do |k,v|
|
15
|
+
$stderr.puts "\t#{k}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
exit(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
opts = {}
|
23
|
+
|
24
|
+
# Parse script-specific options
|
25
|
+
parser = Msf::RPC::Client.option_parser(opts)
|
26
|
+
parser.separator('Task Options:')
|
27
|
+
|
28
|
+
parser.on("--path PATH") do |path|
|
29
|
+
opts[:path] = path
|
30
|
+
end
|
31
|
+
|
32
|
+
parser.on("--project PROJECT") do |project|
|
33
|
+
opts[:project] = project
|
34
|
+
end
|
35
|
+
|
36
|
+
parser.on("--help") do
|
37
|
+
$stderr.puts parser
|
38
|
+
exit(1)
|
39
|
+
end
|
40
|
+
parser.separator('')
|
41
|
+
|
42
|
+
parser.parse!(ARGV)
|
43
|
+
@rpc = Msf::RPC::Client.new(opts)
|
44
|
+
|
45
|
+
if not @rpc.token
|
46
|
+
$stderr.puts "Error: Invalid RPC server options specified"
|
47
|
+
$stderr.puts parser
|
48
|
+
exit(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
project = opts[:project] || usage(parser)
|
52
|
+
path = opts[:path] || usage(parser)
|
53
|
+
user = @rpc.call("pro.default_admin_user")['username']
|
54
|
+
task = @rpc.call("pro.start_import", {
|
55
|
+
'workspace' => project,
|
56
|
+
'username' => user,
|
57
|
+
'DS_PATH' => path
|
58
|
+
})
|
59
|
+
|
60
|
+
if not task['task_id']
|
61
|
+
$stderr.puts "[-] Error starting the task: #{task.inspect}"
|
62
|
+
exit(0)
|
63
|
+
end
|
64
|
+
|
65
|
+
puts "[*] Creating Task ID #{task['task_id']}..."
|
66
|
+
while true
|
67
|
+
select(nil, nil, nil, 0.50)
|
68
|
+
|
69
|
+
stat = @rpc.call("pro.task_status", task['task_id'])
|
70
|
+
|
71
|
+
if stat['status'] == 'invalid'
|
72
|
+
$stderr.puts "[-] Error checking task status"
|
73
|
+
exit(0)
|
74
|
+
end
|
75
|
+
|
76
|
+
info = stat[ task['task_id'] ]
|
77
|
+
|
78
|
+
if not info
|
79
|
+
$stderr.puts "[-] Error finding the task"
|
80
|
+
exit(0)
|
81
|
+
end
|
82
|
+
|
83
|
+
if info['status'] == "error"
|
84
|
+
$stderr.puts "[-] Error generating report: #{info['error']}"
|
85
|
+
exit(0)
|
86
|
+
end
|
87
|
+
|
88
|
+
break if info['progress'] == 100
|
89
|
+
end
|
90
|
+
|
91
|
+
$stdout.puts "[+] Task Complete!"
|
@@ -0,0 +1,148 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'optparse'
|
4
|
+
require 'msfrpc-client'
|
5
|
+
require 'rex/ui'
|
6
|
+
|
7
|
+
def usage(ropts)
|
8
|
+
$stderr.puts ropts
|
9
|
+
|
10
|
+
if @rpc and @rpc.token
|
11
|
+
wspaces = @rpc.call("pro.workspaces") rescue {}
|
12
|
+
if wspaces.keys.length > 0
|
13
|
+
$stderr.puts "Active Projects:"
|
14
|
+
wspaces.each_pair do |k,v|
|
15
|
+
$stderr.puts "\t#{k}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
$stderr.puts ""
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
opts = {}
|
24
|
+
|
25
|
+
# Parse script-specific options
|
26
|
+
parser = Msf::RPC::Client.option_parser(opts)
|
27
|
+
parser.separator('NeXpose Specific Options:')
|
28
|
+
|
29
|
+
parser.on("--project PROJECT") do |x|
|
30
|
+
opts[:project] = x
|
31
|
+
end
|
32
|
+
|
33
|
+
parser.on("--targets TARGETS") do |x|
|
34
|
+
opts[:targets] = [x]
|
35
|
+
end
|
36
|
+
|
37
|
+
parser.on("--nexpose-host HOST") do |x|
|
38
|
+
opts[:nexpose_host] = x
|
39
|
+
end
|
40
|
+
|
41
|
+
parser.on("--nexpose-user USER") do |x|
|
42
|
+
opts[:nexpose_user] = x
|
43
|
+
end
|
44
|
+
|
45
|
+
parser.on("--nexpose-pass PASSWORD") do |x|
|
46
|
+
opts[:nexpose_pass] = x
|
47
|
+
end
|
48
|
+
|
49
|
+
parser.on("--nexpose-pass-file PATH") do |x|
|
50
|
+
opts[:nexpose_pass_file] = x
|
51
|
+
end
|
52
|
+
|
53
|
+
parser.on("--scan-template TEMPLATE (optional)") do |x|
|
54
|
+
opts[:scan_template] = x
|
55
|
+
end
|
56
|
+
|
57
|
+
parser.on("--nexpose-port PORT (optional)") do |x|
|
58
|
+
opts[:nexpose_port] = x
|
59
|
+
end
|
60
|
+
|
61
|
+
parser.on("--blacklist BLACKLIST (optional)") do |x|
|
62
|
+
opts[:blacklist] = x
|
63
|
+
end
|
64
|
+
|
65
|
+
parser.on("--help") do
|
66
|
+
$stderr.puts parser
|
67
|
+
exit(1)
|
68
|
+
end
|
69
|
+
|
70
|
+
parser.separator('')
|
71
|
+
parser.parse!(ARGV)
|
72
|
+
|
73
|
+
@rpc = Msf::RPC::Client.new(opts)
|
74
|
+
|
75
|
+
if not @rpc.token
|
76
|
+
$stderr.puts "Error: Invalid RPC server options specified"
|
77
|
+
$stderr.puts parser
|
78
|
+
exit(1)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the password from the file
|
82
|
+
if opts[:nexpose_pass_file]
|
83
|
+
nexpose_pass = File.open(opts[:nexpose_pass_file],"r").read.chomp!
|
84
|
+
else
|
85
|
+
nexpose_pass = opts[:nexpose_pass] || usage(parser)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Store the user's settings
|
89
|
+
project = opts[:project] || usage(parser),
|
90
|
+
targets = opts[:targets] || usage(parser),
|
91
|
+
blacklist = opts[:blacklist],
|
92
|
+
nexpose_host = opts[:nexpose_host] || usage(parser),
|
93
|
+
nexpose_port = opts[:nexpose_port] || "3780",
|
94
|
+
nexpose_user = opts[:nexpose_user] || "nxadmin"
|
95
|
+
scan_template = opts[:scan_template] || "pentest-audit"
|
96
|
+
|
97
|
+
# Get the default user
|
98
|
+
user = @rpc.call("pro.default_admin_user")['username']
|
99
|
+
|
100
|
+
options = {
|
101
|
+
'workspace' => project,
|
102
|
+
'username' => user,
|
103
|
+
'DS_WHITELIST_HOSTS' => targets,
|
104
|
+
'DS_NEXPOSE_HOST' => nexpose_host,
|
105
|
+
'DS_NEXPOSE_PORT' => nexpose_port,
|
106
|
+
'DS_NEXPOSE_USER' => nexpose_user,
|
107
|
+
'nexpose_pass' => nexpose_pass,
|
108
|
+
'DS_SCAN_TEMPLATE' => scan_template
|
109
|
+
}
|
110
|
+
|
111
|
+
puts "DEBUG: Running task with #{options}"
|
112
|
+
|
113
|
+
# Create the task object with all options
|
114
|
+
task = @rpc.call("pro.start_exploit", options)
|
115
|
+
|
116
|
+
|
117
|
+
if not task['task_id']
|
118
|
+
$stderr.puts "[-] Error starting the task: #{task.inspect}"
|
119
|
+
exit(0)
|
120
|
+
end
|
121
|
+
|
122
|
+
puts "[*] Creating Task ID #{task['task_id']}..."
|
123
|
+
while true
|
124
|
+
select(nil, nil, nil, 0.50)
|
125
|
+
|
126
|
+
stat = @rpc.call("pro.task_status", task['task_id'])
|
127
|
+
|
128
|
+
if stat['status'] == 'invalid'
|
129
|
+
$stderr.puts "[-] Error checking task status"
|
130
|
+
exit(0)
|
131
|
+
end
|
132
|
+
|
133
|
+
info = stat[ task['task_id'] ]
|
134
|
+
|
135
|
+
if not info
|
136
|
+
$stderr.puts "[-] Error finding the task"
|
137
|
+
exit(0)
|
138
|
+
end
|
139
|
+
|
140
|
+
if info['status'] == "error"
|
141
|
+
$stderr.puts "[-] Error generating report: #{info['error']}"
|
142
|
+
exit(0)
|
143
|
+
end
|
144
|
+
|
145
|
+
break if info['progress'] == 100
|
146
|
+
end
|
147
|
+
|
148
|
+
$stdout.puts "[+] Task Complete!"
|
@@ -6,43 +6,43 @@ require 'msfrpc-client'
|
|
6
6
|
require 'rex/ui'
|
7
7
|
|
8
8
|
def usage(ropts)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
9
|
+
$stderr.puts ropts
|
10
|
+
|
11
|
+
if @rpc and @rpc.token
|
12
|
+
wspaces = @rpc.call("pro.workspaces") rescue {}
|
13
|
+
if wspaces.keys.length > 0
|
14
|
+
$stderr.puts "Active Projects:"
|
15
|
+
wspaces.each_pair do |k,v|
|
16
|
+
$stderr.puts "\t#{k}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
$stderr.puts ""
|
21
|
+
exit(1)
|
22
22
|
end
|
23
23
|
|
24
24
|
opts = {
|
25
|
-
|
25
|
+
:format => 'PDF'
|
26
26
|
}
|
27
27
|
|
28
28
|
parser = Msf::RPC::Client.option_parser(opts)
|
29
29
|
|
30
30
|
parser.separator('Report Options:')
|
31
31
|
parser.on("--format FORMAT") do |v|
|
32
|
-
|
32
|
+
opts[:format] = v.upcase
|
33
33
|
end
|
34
34
|
|
35
35
|
parser.on("--project PROJECT") do |v|
|
36
|
-
|
36
|
+
opts[:project] = v
|
37
37
|
end
|
38
38
|
|
39
39
|
parser.on("--output OUTFILE") do |v|
|
40
|
-
|
40
|
+
opts[:output] = v
|
41
41
|
end
|
42
42
|
|
43
43
|
parser.on("--help") do
|
44
|
-
|
45
|
-
|
44
|
+
$stderr.puts ropts
|
45
|
+
exit(1)
|
46
46
|
end
|
47
47
|
parser.separator('')
|
48
48
|
|
@@ -50,9 +50,9 @@ parser.parse!(ARGV)
|
|
50
50
|
@rpc = Msf::RPC::Client.new(opts)
|
51
51
|
|
52
52
|
if not @rpc.token
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
$stderr.puts "Error: Invalid RPC server options specified"
|
54
|
+
$stderr.puts parser
|
55
|
+
exit(1)
|
56
56
|
end
|
57
57
|
|
58
58
|
wspace = opts[:project] || usage(parser)
|
@@ -61,66 +61,66 @@ rtype = opts[:format]
|
|
61
61
|
user = @rpc.call("pro.default_admin_user")['username']
|
62
62
|
|
63
63
|
task = @rpc.call("pro.start_report", {
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
64
|
+
'DS_WHITELIST_HOSTS' => "",
|
65
|
+
'DS_BLACKLIST_HOSTS' => "",
|
66
|
+
'workspace' => wspace,
|
67
|
+
'username' => user,
|
68
|
+
'DS_MaskPasswords' => false,
|
69
|
+
'DS_IncludeTaskLog' => false,
|
70
|
+
'DS_JasperDisplaySession' => true,
|
71
|
+
'DS_JasperDisplayCharts' => true,
|
72
|
+
'DS_LootExcludeScreenshots' => false,
|
73
|
+
'DS_LootExcludePasswords' => false,
|
74
|
+
'DS_JasperTemplate' => "msfxv3.jrxml",
|
75
|
+
'DS_REPORT_TYPE' => rtype.upcase,
|
76
|
+
'DS_UseJasper' => true,
|
77
|
+
'DS_UseCustomReporting' => true,
|
78
|
+
'DS_JasperProductName' => "Metasploit Pro",
|
79
|
+
'DS_JasperDbEnv' => "production",
|
80
|
+
'DS_JasperLogo' => '',
|
81
|
+
'DS_JasperDisplaySections' => "1,2,3,4,5,6,7,8",
|
82
|
+
'DS_EnablePCIReport' => true,
|
83
|
+
'DS_EnableFISMAReport' => true,
|
84
|
+
'DS_JasperDisplayWeb' => true,
|
85
85
|
})
|
86
86
|
|
87
87
|
|
88
88
|
if not task['task_id']
|
89
|
-
|
90
|
-
|
89
|
+
$stderr.puts "[-] Error generating the report: #{task.inspect}"
|
90
|
+
exit(0)
|
91
91
|
end
|
92
92
|
|
93
93
|
puts "[*] Report is generating with Task ID #{task['task_id']}..."
|
94
94
|
while true
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
95
|
+
select(nil, nil, nil, 0.50)
|
96
|
+
stat = @rpc.call("pro.task_status", task['task_id'])
|
97
|
+
if stat['status'] == 'invalid'
|
98
|
+
$stderr.puts "[-] Error checking task status"
|
99
|
+
exit(0)
|
100
|
+
end
|
101
|
+
|
102
|
+
info = stat[ task['task_id'] ]
|
103
|
+
|
104
|
+
if not info
|
105
|
+
$stderr.puts "[-] Error finding the task"
|
106
|
+
exit(0)
|
107
|
+
end
|
108
|
+
|
109
|
+
if info['status'] == "error"
|
110
|
+
$stderr.puts "[-] Error generating report: #{info['error']}"
|
111
|
+
exit(0)
|
112
|
+
end
|
113
|
+
|
114
|
+
break if info['progress'] == 100
|
115
115
|
end
|
116
116
|
|
117
117
|
report = @rpc.call('pro.report_download_by_task', task['task_id'])
|
118
118
|
if report and report['data']
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
119
|
+
::File.open(fname, "wb") do |fd|
|
120
|
+
fd.write(report['data'])
|
121
|
+
end
|
122
|
+
$stderr.puts "[-] Report saved to #{::File.expand_path(fname)}"
|
123
123
|
else
|
124
|
-
|
124
|
+
$stderr.puts "[-] Error downloading report: #{report.inspect}"
|
125
125
|
end
|
126
126
|
|
data/lib/msfrpc-client/client.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
1
3
|
# MessagePack for data encoding (http://www.msgpack.org/)
|
2
4
|
require 'msgpack'
|
3
5
|
|
4
6
|
# Standardize option parsing
|
5
|
-
require
|
7
|
+
require 'optparse'
|
6
8
|
|
7
9
|
# Parse configuration file
|
8
10
|
require 'yaml'
|
@@ -15,207 +17,291 @@ require 'rex/proto/http'
|
|
15
17
|
require 'msfrpc-client/constants'
|
16
18
|
|
17
19
|
module Msf
|
18
|
-
module RPC
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
20
|
+
module RPC
|
21
|
+
class Client
|
22
|
+
# @!attribute token
|
23
|
+
# @return [String] A login token.
|
24
|
+
attr_accessor :token
|
25
|
+
|
26
|
+
# @!attribute info
|
27
|
+
# @return [Hash] Login information.
|
28
|
+
attr_accessor :info
|
29
|
+
|
30
|
+
# Initializes the RPC client to connect to: https://127.0.0.1:3790 (TLS1)
|
31
|
+
# The connection information is overridden through the optional info hash.
|
32
|
+
#
|
33
|
+
# @param [Hash] info Information needed for the initialization.
|
34
|
+
# @option info [String] :token A token used by the client.
|
35
|
+
# @return [void]
|
36
|
+
|
37
|
+
def initialize(info = {})
|
38
|
+
@user = nil
|
39
|
+
@pass = nil
|
40
|
+
|
41
|
+
self.info = {
|
42
|
+
host: '127.0.0.1',
|
43
|
+
port: 3790,
|
44
|
+
uri: '/api/',
|
45
|
+
ssl: true,
|
46
|
+
ssl_version: 'TLS1.2',
|
47
|
+
context: {}
|
48
|
+
}.merge(info)
|
49
|
+
|
50
|
+
self.token = self.info[:token]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Logs in by calling the 'auth.login' API. The authentication token will
|
54
|
+
# expire after 5 minutes, but will automatically be rewnewed when you
|
55
|
+
# make a new RPC request.
|
56
|
+
#
|
57
|
+
# @param [String] user Username.
|
58
|
+
# @param [String] pass Password.
|
59
|
+
# @raise RuntimeError Indicating a failed authentication.
|
60
|
+
# @return [TrueClass] Indicating a successful login.
|
61
|
+
|
62
|
+
def login(user, pass)
|
63
|
+
@user = user
|
64
|
+
@pass = pass
|
65
|
+
res = self.call('auth.login', user, pass)
|
66
|
+
unless res && res['result'] == 'success'
|
67
|
+
raise Msf::RPC::Exception.new('Authentication failed')
|
68
|
+
end
|
69
|
+
self.token = res['token']
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Attempts to login again with the last known user name and password.
|
75
|
+
#
|
76
|
+
# @return [TrueClass] Indicating a successful login.
|
77
|
+
|
78
|
+
def re_login
|
79
|
+
login(@user, @pass)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Calls an API.
|
84
|
+
#
|
85
|
+
# @param [String] meth The RPC API to call.
|
86
|
+
# @param [Array<string>] args The arguments to pass.
|
87
|
+
# @raise [RuntimeError] Something is wrong while calling the remote API,
|
88
|
+
# including:
|
89
|
+
# * A missing token (your client needs to
|
90
|
+
# authenticate).
|
91
|
+
# * A unexpected response from the server, such as
|
92
|
+
# a timeout or unexpected HTTP code.
|
93
|
+
# @raise [Msf::RPC::ServerException] The RPC service returns an error.
|
94
|
+
# @return [Hash] The API response. It contains the following keys:
|
95
|
+
# * 'version' [String] Framework version.
|
96
|
+
# * 'ruby' [String] Ruby version.
|
97
|
+
# * 'api' [String] API version.
|
98
|
+
# @example
|
99
|
+
# # This will return something like this:
|
100
|
+
# # {"version"=>"4.11.0-dev",
|
101
|
+
# # "ruby"=>"2.1.5 x86_64-darwin14.0 2014-11-13", "api"=>"1.0"}
|
102
|
+
# rpc.call('core.version')
|
103
|
+
|
104
|
+
def call(meth, *args)
|
105
|
+
if meth == 'auth.logout'
|
106
|
+
do_logout_cleanup
|
107
|
+
end
|
108
|
+
|
109
|
+
unless meth == 'auth.login'
|
110
|
+
unless self.token
|
111
|
+
raise Msf::RPC::Exception.new('Client not authenticated')
|
112
|
+
end
|
113
|
+
args.unshift(self.token)
|
114
|
+
end
|
115
|
+
|
116
|
+
args.unshift(meth)
|
117
|
+
|
118
|
+
begin
|
119
|
+
send_rpc_request(args)
|
120
|
+
rescue Msf::RPC::ServerException => e
|
121
|
+
if e.message =~ /Invalid Authentication Token/i &&
|
122
|
+
meth != 'auth.login' && @user && @pass
|
123
|
+
re_login
|
124
|
+
args[1] = self.token
|
125
|
+
retry
|
126
|
+
else
|
127
|
+
raise e
|
128
|
+
end
|
129
|
+
ensure
|
130
|
+
@cli.close if @cli
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Closes the client.
|
135
|
+
#
|
136
|
+
# @return [void]
|
137
|
+
def close
|
138
|
+
if @cli && @cli.conn?
|
139
|
+
@cli.close
|
140
|
+
end
|
141
|
+
@cli = nil
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# Class methods
|
146
|
+
#
|
147
|
+
|
148
|
+
#
|
149
|
+
# Provides a parser object that understands the
|
150
|
+
# RPC specific options
|
151
|
+
#
|
152
|
+
def self.option_parser(options)
|
153
|
+
parser = OptionParser.new
|
154
|
+
|
155
|
+
parser.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
156
|
+
parser.separator('')
|
157
|
+
parser.separator('RPC Options:')
|
158
|
+
|
159
|
+
parser.on('--rpc-host HOST') do |v|
|
160
|
+
options[:host] = v
|
161
|
+
end
|
162
|
+
|
163
|
+
parser.on('--rpc-port PORT') do |v|
|
164
|
+
options[:port] = v.to_i
|
165
|
+
end
|
166
|
+
|
167
|
+
parser.on('--rpc-ssl <true|false>') do |v|
|
168
|
+
options[:ssl] = v
|
169
|
+
end
|
170
|
+
|
171
|
+
parser.on('--rpc-uri URI') do |v|
|
172
|
+
options[:uri] = v
|
173
|
+
end
|
174
|
+
|
175
|
+
parser.on('--rpc-user USERNAME') do |v|
|
176
|
+
options[:user] = v
|
177
|
+
end
|
178
|
+
|
179
|
+
parser.on('--rpc-pass PASSWORD') do |v|
|
180
|
+
options[:pass] = v
|
181
|
+
end
|
182
|
+
|
183
|
+
parser.on('--rpc-token TOKEN') do |v|
|
184
|
+
options[:token] = v
|
185
|
+
end
|
186
|
+
|
187
|
+
parser.on('--rpc-config CONFIG-FILE') do |v|
|
188
|
+
options[:config] = v
|
189
|
+
end
|
190
|
+
|
191
|
+
parser.on('--rpc-help') do
|
192
|
+
$stderr.puts parser
|
193
|
+
exit(1)
|
194
|
+
end
|
195
|
+
|
196
|
+
parser.separator('')
|
197
|
+
|
198
|
+
parser
|
199
|
+
end
|
200
|
+
|
201
|
+
#
|
202
|
+
# Load options from the command-line, environment.
|
203
|
+
# and any configuration files specified
|
204
|
+
#
|
205
|
+
def self.option_handler(options = {})
|
206
|
+
options[:host] ||= ENV['MSFRPC_HOST']
|
207
|
+
options[:port] ||= ENV['MSFRPC_PORT']
|
208
|
+
options[:uri] ||= ENV['MSFRPC_URI']
|
209
|
+
options[:user] ||= ENV['MSFRPC_USER']
|
210
|
+
options[:pass] ||= ENV['MSFRPC_PASS']
|
211
|
+
options[:ssl] ||= ENV['MSFRPC_SSL']
|
212
|
+
options[:token] ||= ENV['MSFRPC_TOKEN']
|
213
|
+
options[:config] ||= ENV['MSFRPC_CONFIG']
|
214
|
+
|
215
|
+
empty_keys = options.keys.select { |k| options[k].nil? }
|
216
|
+
empty_keys.each { |k| options.delete(k) }
|
217
|
+
|
218
|
+
config_file = options.delete(:config)
|
219
|
+
|
220
|
+
if config_file
|
221
|
+
yaml_data = ::File.read(config_file) rescue nil
|
222
|
+
if yaml_data
|
223
|
+
yaml = ::YAML.load(yaml_data) rescue nil
|
224
|
+
if yaml && yaml.is_a?(::Hash) && yaml['options']
|
225
|
+
yaml['options'].each_pair do |k, v|
|
226
|
+
case k
|
227
|
+
when 'ssl'
|
228
|
+
options[k.intern] = !!(v.to_s =~ /^(t|y|1)/i)
|
229
|
+
when 'port'
|
230
|
+
options[k.intern] = v.to_i
|
231
|
+
else
|
232
|
+
options[k.intern] = v
|
233
|
+
end
|
234
|
+
end
|
235
|
+
else
|
236
|
+
$stderr.puts "Could not parse configuration file: #{config_file}"
|
237
|
+
exit(1)
|
238
|
+
end
|
239
|
+
else
|
240
|
+
$stderr.puts "Could not read configuration file: #{config_file}"
|
241
|
+
exit(1)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
options[:port] = options[:port].to_i if options[:port]
|
246
|
+
|
247
|
+
options[:ssl] = !!(options[:ssl].to_s =~ /^(t|y|1)/i) if options[:ssl]
|
248
|
+
|
249
|
+
options
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
def send_rpc_request(args)
|
255
|
+
unless @cli
|
256
|
+
@cli = Rex::Proto::Http::Client.new(info[:host], info[:port], info[:context], info[:ssl], info[:ssl_version])
|
257
|
+
@cli.set_config(
|
258
|
+
vhost: info[:host],
|
259
|
+
agent: "Metasploit RPC Client/#{API_VERSION}",
|
260
|
+
read_max_data: 1024 * 1024 * 512
|
261
|
+
)
|
262
|
+
end
|
263
|
+
|
264
|
+
req = @cli.request_cgi(
|
265
|
+
'method' => 'POST',
|
266
|
+
'uri' => self.info[:uri],
|
267
|
+
'ctype' => 'binary/message-pack',
|
268
|
+
'data' => args.to_msgpack
|
269
|
+
)
|
270
|
+
|
271
|
+
begin
|
272
|
+
res = @cli.send_recv(req)
|
273
|
+
rescue => e
|
274
|
+
raise Msf::RPC::ServerException.new(000, e.message, e.class)
|
275
|
+
end
|
276
|
+
|
277
|
+
if res && [200, 401, 403, 500].include?(res.code)
|
278
|
+
resp = MessagePack.unpack(res.body)
|
279
|
+
|
280
|
+
# Boolean true versus truthy check required here;
|
281
|
+
# RPC responses such as { "error" => "Here I am" } and
|
282
|
+
# { "error" => # "" } must be accommodated.
|
283
|
+
if resp && resp.is_a?(::Hash) && resp['error'] == true
|
284
|
+
raise Msf::RPC::ServerException.new(
|
285
|
+
resp['error_code'] || res.code,
|
286
|
+
resp['error_message'] || resp['error_string'],
|
287
|
+
resp['error_class'], resp['error_backtrace']
|
288
|
+
)
|
289
|
+
end
|
290
|
+
|
291
|
+
return resp
|
292
|
+
else
|
293
|
+
if res
|
294
|
+
raise Msf::RPC::Exception.new(res.inspect)
|
295
|
+
else
|
296
|
+
raise Msf::RPC::Exception.new('Unknown error parsing or sending response')
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def do_logout_cleanup
|
302
|
+
@user = nil
|
303
|
+
@pass = nil
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
219
307
|
end
|
220
|
-
end
|
221
|
-
|