|
| 1 | +# encoding: UTF-8 |
| 2 | +## |
| 3 | +# This module requires Metasploit: http//metasploit.com/download |
| 4 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 5 | +## |
| 6 | + |
| 7 | +require 'msf/core' |
| 8 | +require 'rex/proto/ntp' |
| 9 | +require 'securerandom' |
| 10 | + |
| 11 | +class Metasploit3 < Msf::Auxiliary |
| 12 | + include Msf::Auxiliary::Fuzzer |
| 13 | + include Msf::Exploit::Remote::Udp |
| 14 | + include Msf::Auxiliary::Scanner |
| 15 | + |
| 16 | + def initialize |
| 17 | + super( |
| 18 | + 'Name' => 'NTP Protocol Fuzzer', |
| 19 | + 'Description' => %q( |
| 20 | + A simplistic fuzzer for the Network Time Protocol that sends the |
| 21 | + following probes to understand NTP and look for anomalous NTP behavior: |
| 22 | +
|
| 23 | + * All possible combinations of NTP versions and modes, even if not |
| 24 | + allowed or specified in the RFCs |
| 25 | + * Short versions of the above |
| 26 | + * Short, invalid datagrams |
| 27 | + * Full-size, random datagrams |
| 28 | + * All possible NTP control messages |
| 29 | + * All possible NTP private messages |
| 30 | +
|
| 31 | + This findings of this fuzzer are not necessarily indicative of bugs, |
| 32 | + let alone vulnerabilities, rather they point out interesting things |
| 33 | + that might deserve more attention. Furthermore, this module is not |
| 34 | + particularly intelligent and there are many more areas of NTP that |
| 35 | + could be explored, including: |
| 36 | +
|
| 37 | + * Warn if the response is 100% identical to the request |
| 38 | + * Warn if the "mode" (if applicable) doesn't align with what we expect, |
| 39 | + * Filter out the 12-byte mode 6 unsupported opcode errors. |
| 40 | + * Fuzz the control message payload offset/size/etc. There be bugs |
| 41 | + ), |
| 42 | + 'Author' => 'Jon Hart <jon_hart[at]rapid7.com>', |
| 43 | + 'License' => MSF_LICENSE |
| 44 | + ) |
| 45 | + |
| 46 | + register_options( |
| 47 | + [ |
| 48 | + Opt::RPORT(123), |
| 49 | + OptInt.new('SLEEP', [true, 'Sleep for this many ms between requests', 0]), |
| 50 | + OptInt.new('WAIT', [true, 'Wait this many ms for responses', 250]) |
| 51 | + ], self.class) |
| 52 | + |
| 53 | + register_advanced_options( |
| 54 | + [ |
| 55 | + OptString.new('VERSIONS', [false, 'Specific versions to fuzz (csv)', '2,3,4']), |
| 56 | + OptString.new('MODES', [false, 'Modes to fuzz (csv)', nil]), |
| 57 | + OptString.new('MODE_6_OPERATIONS', [false, 'Mode 6 operations to fuzz (csv)', nil]), |
| 58 | + OptString.new('MODE_7_IMPLEMENTATIONS', [false, 'Mode 7 implementations to fuzz (csv)', nil]), |
| 59 | + OptString.new('MODE_7_REQUEST_CODES', [false, 'Mode 7 request codes to fuzz (csv)', nil]) |
| 60 | + ], self.class) |
| 61 | + end |
| 62 | + |
| 63 | + def sleep_time |
| 64 | + datastore['SLEEP'] / 1000.0 |
| 65 | + end |
| 66 | + |
| 67 | + def check_and_set(setting) |
| 68 | + thing = setting.upcase |
| 69 | + const_name = thing.to_sym |
| 70 | + var_name = thing.downcase |
| 71 | + if datastore.key?(thing) |
| 72 | + instance_variable_set("@#{var_name}", datastore[thing].split(/[^\d]/).select { |v| !v.empty? }.map { |v| v.to_i }) |
| 73 | + unsupported_things = instance_variable_get("@#{var_name}") - Rex::Proto::NTP.const_get(const_name) |
| 74 | + fail "Unsupported #{thing}: #{unsupported_things}" unless unsupported_things.empty? |
| 75 | + else |
| 76 | + instance_variable_set("@#{var_name}", Rex::Proto::NTP.const_get(const_name)) |
| 77 | + end |
| 78 | + end |
| 79 | + |
| 80 | + def run_host(ip) |
| 81 | + # check and set the optional advanced options |
| 82 | + check_and_set('VERSIONS') |
| 83 | + check_and_set('MODES') |
| 84 | + check_and_set('MODE_6_OPERATIONS') |
| 85 | + check_and_set('MODE_7_IMPLEMENTATIONS') |
| 86 | + check_and_set('MODE_7_REQUEST_CODES') |
| 87 | + |
| 88 | + connect_udp |
| 89 | + fuzz_version_mode(ip, true) |
| 90 | + fuzz_version_mode(ip, false) |
| 91 | + fuzz_short(ip) |
| 92 | + fuzz_random(ip) |
| 93 | + fuzz_control(ip) if @modes.include?(6) |
| 94 | + fuzz_private(ip) if @modes.include?(7) |
| 95 | + disconnect_udp |
| 96 | + end |
| 97 | + |
| 98 | + # Sends a series of NTP control messages |
| 99 | + def fuzz_control(host) |
| 100 | + @versions.each do |version| |
| 101 | + print_status("#{host}:#{rport} fuzzing version #{version} control messages (mode 6)") |
| 102 | + @mode_6_operations.each do |op| |
| 103 | + request = Rex::Proto::NTP.ntp_control(version, op) |
| 104 | + what = "#{request.size}-byte version #{version} mode 6 op #{op} message" |
| 105 | + vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}") |
| 106 | + responses = probe(host, datastore['RPORT'].to_i, request) |
| 107 | + handle_responses(host, request, responses, what) |
| 108 | + Rex.sleep(sleep_time) |
| 109 | + end |
| 110 | + end |
| 111 | + end |
| 112 | + |
| 113 | + # Sends a series of NTP private messages |
| 114 | + def fuzz_private(host) |
| 115 | + @versions.each do |version| |
| 116 | + print_status("#{host}:#{rport} fuzzing version #{version} private messages (mode 7)") |
| 117 | + @mode_7_implementations.each do |implementation| |
| 118 | + @mode_7_request_codes.each do |request_code| |
| 119 | + request = Rex::Proto::NTP.ntp_private(version, implementation, request_code, "\x00" * 188) |
| 120 | + what = "#{request.size}-byte version #{version} mode 7 imp #{implementation} req #{request_code} message" |
| 121 | + vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}") |
| 122 | + responses = probe(host, datastore['RPORT'].to_i, request) |
| 123 | + handle_responses(host, request, responses, what) |
| 124 | + Rex.sleep(sleep_time) |
| 125 | + end |
| 126 | + end |
| 127 | + end |
| 128 | + end |
| 129 | + |
| 130 | + # Sends a series of small, short datagrams, looking for a reply |
| 131 | + def fuzz_short(host) |
| 132 | + print_status("#{host}:#{rport} fuzzing short messages") |
| 133 | + 0.upto(4) do |size| |
| 134 | + request = SecureRandom.random_bytes(size) |
| 135 | + what = "short #{request.size}-byte random message" |
| 136 | + vprint_status("#{host}:#{rport} probing with #{what}") |
| 137 | + responses = probe(host, datastore['RPORT'].to_i, request) |
| 138 | + handle_responses(host, request, responses, what) |
| 139 | + Rex.sleep(sleep_time) |
| 140 | + end |
| 141 | + end |
| 142 | + |
| 143 | + # Sends a series of random, full-sized datagrams, looking for a reply |
| 144 | + def fuzz_random(host) |
| 145 | + print_status("#{host}:#{rport} fuzzing random messages") |
| 146 | + 0.upto(5) do |
| 147 | + # TODO: is there a better way to pick this size? Should more than one be tried? |
| 148 | + request = SecureRandom.random_bytes(48) |
| 149 | + what = "random #{request.size}-byte message" |
| 150 | + vprint_status("#{host}:#{rport} probing with #{what}") |
| 151 | + responses = probe(host, datastore['RPORT'].to_i, request) |
| 152 | + handle_responses(host, request, responses, what) |
| 153 | + Rex.sleep(sleep_time) |
| 154 | + end |
| 155 | + end |
| 156 | + |
| 157 | + # Sends a series of different version + mode combinations |
| 158 | + def fuzz_version_mode(host, short) |
| 159 | + print_status("#{host}:#{rport} fuzzing #{short ? 'short ' : nil}version and mode combinations") |
| 160 | + @versions.each do |version| |
| 161 | + @modes.each do |mode| |
| 162 | + request = Rex::Proto::NTP::NTPGeneric.new |
| 163 | + request.version = version |
| 164 | + request.mode = mode |
| 165 | + unless short |
| 166 | + # TODO: is there a better way to pick this size? Should more than one be tried? |
| 167 | + request.payload = SecureRandom.random_bytes(16) |
| 168 | + end |
| 169 | + what = "#{request.size}-byte #{short ? 'short ' : nil}version #{version} mode #{mode} message" |
| 170 | + vprint_status("#{host}:#{rport} probing with #{what}") |
| 171 | + responses = probe(host, datastore['RPORT'].to_i, request) |
| 172 | + handle_responses(host, request, responses, what) |
| 173 | + Rex.sleep(sleep_time) |
| 174 | + end |
| 175 | + end |
| 176 | + end |
| 177 | + |
| 178 | + # Sends +message+ to +host+ on UDP port +port+, returning all replies |
| 179 | + def probe(host, port, message) |
| 180 | + replies = [] |
| 181 | + udp_sock.sendto(message, host, port, 0) |
| 182 | + reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0) |
| 183 | + while reply && reply[1] |
| 184 | + replies << reply |
| 185 | + reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0) |
| 186 | + end |
| 187 | + replies |
| 188 | + end |
| 189 | + |
| 190 | + def handle_responses(host, request, responses, what) |
| 191 | + problems = [] |
| 192 | + descriptions = [] |
| 193 | + responses.select! { |r| r[1] } |
| 194 | + return if responses.empty? |
| 195 | + responses.each do |response| |
| 196 | + data = response[0] |
| 197 | + descriptions << Rex::Proto::NTP.describe(data) |
| 198 | + problems << 'large response' if request.size < data.size |
| 199 | + ntp_req = Rex::Proto::NTP::NTPGeneric.new(request) |
| 200 | + ntp_resp = Rex::Proto::NTP::NTPGeneric.new(data) |
| 201 | + problems << 'version mismatch' if ntp_req.version != ntp_resp.version |
| 202 | + end |
| 203 | + |
| 204 | + problems << 'multiple responses' if responses.size > 1 |
| 205 | + problems.sort! |
| 206 | + problems.uniq! |
| 207 | + |
| 208 | + description = descriptions.join(',') |
| 209 | + if problems.empty? |
| 210 | + vprint_status("#{host}:#{rport} -- Received '#{description}' to #{what}") |
| 211 | + else |
| 212 | + print_good("#{host}:#{rport} -- Received '#{description}' to #{what}: #{problems.join(',')}") |
| 213 | + end |
| 214 | + end |
| 215 | +end |
0 commit comments