Skip to content

Commit 4401c6f

Browse files
committed
Land rapid7#6178, rsync modules_list improvements
2 parents 9a0f0a7 + f408bca commit 4401c6f

File tree

1 file changed

+203
-39
lines changed

1 file changed

+203
-39
lines changed

modules/auxiliary/scanner/rsync/modules_list.rb

Lines changed: 203 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,228 @@
66
require 'msf/core'
77

88
class Metasploit3 < Msf::Auxiliary
9-
109
include Msf::Exploit::Remote::Tcp
1110
include Msf::Auxiliary::Scanner
1211
include Msf::Auxiliary::Report
1312

13+
RSYNC_HEADER = '@RSYNCD:'
14+
HANDLED_EXCEPTIONS = [
15+
Rex::AddressInUse, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused,
16+
::Errno::ETIMEDOUT, ::Timeout::Error, ::EOFError
17+
]
18+
1419
def initialize
1520
super(
16-
'Name' => 'Rsync Unauthenticated List Command',
17-
'Description' => 'List all (listable) modules from a rsync daemon',
18-
'Author' => 'ikkini',
21+
'Name' => 'List Rsync Modules',
22+
'Description' => %q(
23+
An rsync module is essentially a directory share. These modules can
24+
optionally be protected by a password. This module connects to and
25+
negotiates with an rsync server, lists the available modules and,
26+
optionally, determines if the module requires a password to access.
27+
),
28+
'Author' => [
29+
'ikkini', # original metasploit module
30+
'Jon Hart <jon_hart[at]rapid7.com>', # improved metasploit module
31+
'Nixawk' # improved metasploit module
32+
],
1933
'References' =>
2034
[
2135
['URL', 'http://rsync.samba.org/ftp/rsync/rsync.html']
2236
],
2337
'License' => MSF_LICENSE
2438
)
39+
2540
register_options(
2641
[
42+
OptBool.new('TEST_AUTHENTICATION',
43+
[ true, 'Test if the rsync module requires authentication', true ]),
2744
Opt::RPORT(873)
28-
], self.class)
45+
]
46+
)
47+
48+
register_advanced_options(
49+
[
50+
OptBool.new('SHOW_MOTD',
51+
[ true, 'Show the rsync motd, if found', false ]),
52+
OptBool.new('SHOW_VERSION',
53+
[ true, 'Show the rsync version', false ]),
54+
OptInt.new('READ_TIMEOUT', [ true, 'Seconds to wait while reading rsync responses', 2 ])
55+
]
56+
)
57+
end
58+
59+
def peer
60+
"#{rhost}:#{rport}"
61+
end
62+
63+
def read_timeout
64+
datastore['READ_TIMEOUT']
65+
end
66+
67+
def get_rsync_auth_status(rmodule)
68+
sock.puts("#{rmodule}\n")
69+
res = sock.get_once(-1, read_timeout)
70+
if res
71+
res.strip!
72+
if res =~ /^#{RSYNC_HEADER} AUTHREQD \S+$/
73+
'required'
74+
elsif res =~ /^#{RSYNC_HEADER} OK$/
75+
'not required'
76+
else
77+
vprint_error("#{peer} - unexpected response when connecting to #{rmodule}: #{res}")
78+
"unexpected response '#{res}'"
79+
end
80+
else
81+
vprint_error("#{peer} - no response when connecting to #{rmodule}")
82+
'no response'
83+
end
84+
end
85+
86+
def rsync_list
87+
sock.puts("#list\n")
88+
89+
modules_metadata = []
90+
# the module listing is the module name and comment separated by a tab, each module
91+
# on its own line, lines separated with a newline
92+
sock.get(read_timeout).split(/\n/).map(&:strip).map do |module_line|
93+
break if module_line =~ /^#{RSYNC_HEADER} EXIT$/
94+
name, comment = module_line.split(/\t/).map(&:strip)
95+
next unless name
96+
modules_metadata << { name: name, comment: comment }
97+
end
98+
99+
modules_metadata
100+
end
101+
102+
# Attempts to negotiate the rsync protocol with the endpoint.
103+
def rsync_negotiate
104+
# rsync is promiscuous and will send the negotitation and motd
105+
# upon connecting. abort if we get nothing
106+
return unless (greeting = sock.get_once(-1, read_timeout))
107+
108+
# parse the greeting control and data lines. With some systems, the data
109+
# lines at this point will be the motd.
110+
greeting_control_lines, greeting_data_lines = rsync_parse_lines(greeting)
111+
112+
# locate the rsync negotiation and complete it by just echo'ing
113+
# back the same rsync version that it sent us
114+
version = nil
115+
greeting_control_lines.map do |greeting_control_line|
116+
if /^#{RSYNC_HEADER} (?<version>\d+(\.\d+)?)$/ =~ greeting_control_line
117+
version = Regexp.last_match('version')
118+
sock.puts("#{RSYNC_HEADER} #{version}\n")
119+
end
120+
end
121+
122+
unless version
123+
vprint_error("#{peer} - no rsync negotiation found")
124+
return
125+
end
126+
127+
_, post_neg_data_lines = rsync_parse_lines(sock.get_once(-1, read_timeout))
128+
motd_lines = greeting_data_lines + post_neg_data_lines
129+
[ version, motd_lines.empty? ? nil : motd_lines.join("\n") ]
130+
end
131+
132+
# parses the control and data lines from the provided response data
133+
def rsync_parse_lines(response_data)
134+
control_lines = []
135+
data_lines = []
136+
137+
if response_data
138+
response_data.strip!
139+
response_data.split(/\n/).map do |line|
140+
if line =~ /^#{RSYNC_HEADER}/
141+
control_lines << line
142+
else
143+
data_lines << line
144+
end
145+
end
146+
end
147+
148+
[ control_lines, data_lines ]
29149
end
30150

31151
def run_host(ip)
32-
connect
33-
version = sock.get_once
34-
35-
return if version.blank?
36-
37-
print_good("#{ip}:#{rport} - rsync #{version.strip} found")
38-
report_service(:host => ip, :port => rport, :proto => 'tcp', :name => 'rsync')
39-
report_note(
40-
:host => ip,
41-
:proto => 'tcp',
42-
:port => rport,
43-
:type => 'rsync_version',
44-
:data => version.strip
45-
)
152+
begin
153+
connect
154+
version, motd = rsync_negotiate
155+
unless version
156+
vprint_error("#{peer} - does not appear to be rsync")
157+
disconnect
158+
return
159+
end
160+
rescue *HANDLED_EXCEPTIONS => e
161+
vprint_error("#{peer} - error while connecting and negotiating: #{e}")
162+
disconnect
163+
return
164+
end
46165

47-
# making sure we match the version of the server
48-
sock.puts("#{version}")
49-
# the listing command
50-
sock.puts("\n")
51-
listing = sock.get(20)
52-
disconnect
53-
54-
return if listing.blank?
55-
56-
print_good("#{ip}:#{rport} - rsync listing found")
57-
listing.gsub!('@RSYNCD: EXIT', '') # not interested in EXIT message
58-
listing_sanitized = Rex::Text.to_hex_ascii(listing.strip)
59-
60-
vprint_status("#{ip}:#{rport} - #{version.rstrip} #{listing_sanitized}")
61-
report_note(
62-
:host => ip,
63-
:proto => 'tcp',
64-
:port => rport,
65-
:type => 'rsync_listing',
66-
:data => listing_sanitized
166+
info = "rsync protocol version #{version}"
167+
info += ", MOTD '#{motd}'" if motd
168+
report_service(
169+
host: ip,
170+
port: rport,
171+
proto: 'tcp',
172+
name: 'rsync',
173+
info: info
67174
)
175+
print_status("#{peer} - rsync version: #{version}") if datastore['SHOW_VERSION']
176+
print_status("#{peer} - rsync MOTD: #{motd}") if motd && datastore['SHOW_MOTD']
177+
178+
modules_metadata = {}
179+
begin
180+
modules_metadata = rsync_list
181+
rescue *HANDLED_EXCEPTIONS => e
182+
vprint_error("#{peer} -- error while listing modules: #{e}")
183+
return
184+
ensure
185+
disconnect
186+
end
187+
188+
if modules_metadata.empty?
189+
print_status("#{peer} - no rsync modules found")
190+
else
191+
modules = modules_metadata.map { |m| m[:name] }
192+
print_good("#{peer} - #{modules.size} rsync modules found: #{modules.join(', ')}")
193+
194+
table_columns = %w(Name Comment)
195+
if datastore['TEST_AUTHENTICATION']
196+
table_columns << 'Authentication'
197+
modules_metadata.each do |module_metadata|
198+
begin
199+
connect
200+
rsync_negotiate
201+
module_metadata[:authentication] = get_rsync_auth_status(module_metadata[:name])
202+
rescue *HANDLED_EXCEPTIONS => e
203+
vprint_error("#{peer} - error while testing authentication on #{module_metadata[:name]}: #{e}")
204+
break
205+
ensure
206+
disconnect
207+
end
208+
end
209+
end
210+
211+
# build a table to store the module listing in
212+
listing_table = Msf::Ui::Console::Table.new(
213+
Msf::Ui::Console::Table::Style::Default,
214+
'Header' => "rsync modules for #{peer}",
215+
'Columns' => table_columns,
216+
'Rows' => modules_metadata.map(&:values)
217+
)
218+
vprint_line(listing_table.to_s)
219+
220+
report_note(
221+
host: ip,
222+
proto: 'tcp',
223+
port: rport,
224+
type: 'rsync_modules',
225+
data: { modules: modules_metadata }
226+
)
227+
end
228+
end
229+
230+
def setup
231+
fail_with(Failure::BadConfig, 'READ_TIMEOUT must be > 0') if read_timeout <= 0
68232
end
69233
end

0 commit comments

Comments
 (0)