|
6 | 6 | require 'msf/core'
|
7 | 7 |
|
8 | 8 | class Metasploit3 < Msf::Auxiliary
|
9 |
| - |
10 | 9 | include Msf::Exploit::Remote::Tcp
|
11 | 10 | include Msf::Auxiliary::Scanner
|
12 | 11 | include Msf::Auxiliary::Report
|
13 | 12 |
|
| 13 | + RSYNC_HEADER = '@RSYNCD:' |
| 14 | + HANDLED_EXCEPTIONS = [ |
| 15 | + Rex::AddressInUse, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, |
| 16 | + ::Errno::ETIMEDOUT, ::Timeout::Error, ::EOFError |
| 17 | + ] |
| 18 | + |
14 | 19 | def initialize
|
15 | 20 | 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 | + ], |
19 | 33 | 'References' =>
|
20 | 34 | [
|
21 | 35 | ['URL', 'http://rsync.samba.org/ftp/rsync/rsync.html']
|
22 | 36 | ],
|
23 | 37 | 'License' => MSF_LICENSE
|
24 | 38 | )
|
| 39 | + |
25 | 40 | register_options(
|
26 | 41 | [
|
| 42 | + OptBool.new('TEST_AUTHENTICATION', |
| 43 | + [ true, 'Test if the rsync module requires authentication', true ]), |
27 | 44 | 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 ] |
29 | 149 | end
|
30 | 150 |
|
31 | 151 | 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 |
46 | 165 |
|
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 |
67 | 174 | )
|
| 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 |
68 | 232 | end
|
69 | 233 | end
|
0 commit comments