Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/fuzzers/ntp/ntp_protocol_fuzzer.rb
Views: 11655
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'securerandom'67class MetasploitModule < Msf::Auxiliary8include Msf::Auxiliary::Fuzzer9include Msf::Exploit::Remote::Udp10include Msf::Auxiliary::Scanner1112def initialize13super(14'Name' => 'NTP Protocol Fuzzer',15'Description' => %q(16A simplistic fuzzer for the Network Time Protocol that sends the17following probes to understand NTP and look for anomalous NTP behavior:1819* All possible combinations of NTP versions and modes, even if not20allowed or specified in the RFCs21* Short versions of the above22* Short, invalid datagrams23* Full-size, random datagrams24* All possible NTP control messages25* All possible NTP private messages2627This findings of this fuzzer are not necessarily indicative of bugs,28let alone vulnerabilities, rather they point out interesting things29that might deserve more attention. Furthermore, this module is not30particularly intelligent and there are many more areas of NTP that31could be explored, including:3233* Warn if the response is 100% identical to the request34* Warn if the "mode" (if applicable) doesn't align with what we expect,35* Filter out the 12-byte mode 6 unsupported opcode errors.36* Fuzz the control message payload offset/size/etc. There be bugs37),38'Author' => 'Jon Hart <jon_hart[at]rapid7.com>',39'License' => MSF_LICENSE40)4142register_options(43[44Opt::RPORT(123),45OptInt.new('SLEEP', [true, 'Sleep for this many ms between requests', 0]),46OptInt.new('WAIT', [true, 'Wait this many ms for responses', 250])47])4849register_advanced_options(50[51OptString.new('VERSIONS', [false, 'Specific versions to fuzz (csv)', '2,3,4']),52OptString.new('MODES', [false, 'Modes to fuzz (csv)']),53OptString.new('MODE_6_OPERATIONS', [false, 'Mode 6 operations to fuzz (csv)']),54OptString.new('MODE_7_IMPLEMENTATIONS', [false, 'Mode 7 implementations to fuzz (csv)']),55OptString.new('MODE_7_REQUEST_CODES', [false, 'Mode 7 request codes to fuzz (csv)'])56])57end5859def sleep_time60datastore['SLEEP'] / 1000.061end6263def check_and_set(setting)64thing = setting.upcase65const_name = thing.to_sym66var_name = thing.downcase67if datastore[thing]68instance_variable_set("@#{var_name}", datastore[thing].split(/[^\d]/).select { |v| !v.empty? }.map { |v| v.to_i })69unsupported_things = instance_variable_get("@#{var_name}") - Rex::Proto::NTP.const_get(const_name)70fail "Unsupported #{thing}: #{unsupported_things}" unless unsupported_things.empty?71else72instance_variable_set("@#{var_name}", Rex::Proto::NTP.const_get(const_name))73end74end7576def run_host(ip)77# check and set the optional advanced options78check_and_set('VERSIONS')79check_and_set('MODES')80check_and_set('MODE_6_OPERATIONS')81check_and_set('MODE_7_IMPLEMENTATIONS')82check_and_set('MODE_7_REQUEST_CODES')8384connect_udp85fuzz_version_mode(ip, true)86fuzz_version_mode(ip, false)87fuzz_short(ip)88fuzz_random(ip)89fuzz_control(ip) if @modes.include?(6)90fuzz_private(ip) if @modes.include?(7)91disconnect_udp92end9394# Sends a series of NTP control messages95def fuzz_control(host)96@versions.each do |version|97print_status("#{host}:#{rport} fuzzing version #{version} control messages (mode 6)")98@mode_6_operations.each do |op|99request = Rex::Proto::NTP.ntp_control(version, op).to_binary_s100what = "#{request.size}-byte version #{version} mode 6 op #{op} message"101vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")102responses = probe(host, datastore['RPORT'].to_i, request)103handle_responses(host, request, responses, what)104Rex.sleep(sleep_time)105end106end107end108109# Sends a series of NTP private messages110def fuzz_private(host)111@versions.each do |version|112print_status("#{host}:#{rport} fuzzing version #{version} private messages (mode 7)")113@mode_7_implementations.each do |implementation|114@mode_7_request_codes.each do |request_code|115request = Rex::Proto::NTP.ntp_private(version, implementation, request_code, "\0" * 188).to_binary_s116what = "#{request.size}-byte version #{version} mode 7 imp #{implementation} req #{request_code} message"117vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")118responses = probe(host, datastore['RPORT'].to_i, request)119handle_responses(host, request, responses, what)120Rex.sleep(sleep_time)121end122end123end124end125126# Sends a series of small, short datagrams, looking for a reply127def fuzz_short(host)128print_status("#{host}:#{rport} fuzzing short messages")1290.upto(4) do |size|130request = SecureRandom.random_bytes(size)131what = "short #{request.size}-byte random message"132vprint_status("#{host}:#{rport} probing with #{what}")133responses = probe(host, datastore['RPORT'].to_i, request)134handle_responses(host, request, responses, what)135Rex.sleep(sleep_time)136end137end138139# Sends a series of random, full-sized datagrams, looking for a reply140def fuzz_random(host)141print_status("#{host}:#{rport} fuzzing random messages")1420.upto(5) do143# TODO: is there a better way to pick this size? Should more than one be tried?144request = SecureRandom.random_bytes(48)145what = "random #{request.size}-byte message"146vprint_status("#{host}:#{rport} probing with #{what}")147responses = probe(host, datastore['RPORT'].to_i, request)148handle_responses(host, request, responses, what)149Rex.sleep(sleep_time)150end151end152153# Sends a series of different version + mode combinations154def fuzz_version_mode(host, short)155print_status("#{host}:#{rport} fuzzing #{short ? 'short ' : nil}version and mode combinations")156@versions.each do |version|157@modes.each do |mode|158request = Rex::Proto::NTP::NTPGeneric.new159request.version = version160request.mode = mode161unless short162# TODO: is there a better way to pick this size? Should more than one be tried?163request.payload = SecureRandom.random_bytes(16)164end165request = request.to_binary_s166what = "#{request.size}-byte #{short ? 'short ' : nil}version #{version} mode #{mode} message"167vprint_status("#{host}:#{rport} probing with #{what}")168responses = probe(host, datastore['RPORT'].to_i, request)169handle_responses(host, request, responses, what)170Rex.sleep(sleep_time)171end172end173end174175# Sends +message+ to +host+ on UDP port +port+, returning all replies176def probe(host, port, message)177message = message.to_binary_s if message.respond_to?('to_binary_s')178replies = []179begin180udp_sock.sendto(message, host, port, 0)181rescue ::Errno::EISCONN182udp_sock.write(message)183end184reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0)185while reply && reply[1]186replies << reply187reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0)188end189replies190end191192def handle_responses(host, request, responses, what)193problems = []194descriptions = []195request = request.to_binary_s if request.respond_to?('to_binary_s')196responses.select! { |r| r[1] }197return if responses.empty?198responses.each do |response|199data = response[0]200descriptions << Rex::Proto::NTP.describe(data)201problems << 'large response' if request.size < data.size202ntp_req = Rex::Proto::NTP::NTPGeneric.new.read(request)203ntp_resp = Rex::Proto::NTP::NTPGeneric.new.read(data)204problems << 'version mismatch' if ntp_req.version != ntp_resp.version205end206207problems << 'multiple responses' if responses.size > 1208problems.sort!209problems.uniq!210211description = descriptions.join(',')212if problems.empty?213vprint_status("#{host}:#{rport} -- Received '#{description}' to #{what}")214else215print_good("#{host}:#{rport} -- Received '#{description}' to #{what}: #{problems.join(',')}")216end217end218end219220221