commit
ffafd4c01f
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'rex/proto/ntp/constants'
|
||||
require 'rex/proto/ntp/modes'
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: binary -*-
|
||||
module Rex
|
||||
module Proto
|
||||
module NTP
|
||||
VERSIONS = (0..7).to_a
|
||||
MODES = (0..7).to_a
|
||||
MODE_6_OPERATIONS = (0..31).to_a
|
||||
MODE_7_IMPLEMENTATIONS = (0..255).to_a
|
||||
MODE_7_REQUEST_CODES = (0..255).to_a
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,130 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'bit-struct'
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
module NTP
|
||||
|
||||
# A very generic NTP message
|
||||
#
|
||||
# Uses the common/similar parts from versions 1-4 and considers everything
|
||||
# after to be just one big field. For the particulars on the different versions,
|
||||
# see:
|
||||
# http://tools.ietf.org/html/rfc958#appendix-B
|
||||
# http://tools.ietf.org/html/rfc1059#appendix-B
|
||||
# pages 45/48 of http://tools.ietf.org/pdf/rfc1119.pdf
|
||||
# http://tools.ietf.org/html/rfc1305#appendix-D
|
||||
# http://tools.ietf.org/html/rfc5905#page-19
|
||||
class NTPGeneric < BitStruct
|
||||
# 0 1 2 3
|
||||
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
# |LI | VN | mode| Stratum | Poll | Precision |
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
unsigned :li, 2, default: 0
|
||||
unsigned :version, 3, default: 0
|
||||
unsigned :mode, 3, default: 0
|
||||
unsigned :stratum, 8, default: 0
|
||||
unsigned :poll, 8, default: 0
|
||||
unsigned :precision, 8, default: 0
|
||||
rest :payload
|
||||
end
|
||||
|
||||
# An NTP control message. Control messages are only specified for NTP
|
||||
# versions 2-4, but this is a fuzzer so why not try them all...
|
||||
class NTPControl < BitStruct
|
||||
# 0 1 2 3
|
||||
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
# |00 | VN | 6 |R E M| op | Sequence |
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
# | status | association id |
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
# | offset | count |
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
unsigned :reserved, 2, default: 0
|
||||
unsigned :version, 3, default: 0
|
||||
unsigned :mode, 3, default: 6
|
||||
unsigned :response, 1, default: 0
|
||||
unsigned :error, 1, default: 0
|
||||
unsigned :more, 1, default: 0
|
||||
unsigned :operation, 5, default: 0
|
||||
unsigned :sequence, 16, default: 0
|
||||
unsigned :status, 16, default: 0
|
||||
unsigned :association_id, 16, default: 0
|
||||
# TODO: there *must* be bugs in the handling of these next two fields!
|
||||
unsigned :payload_offset, 16, default: 0
|
||||
unsigned :payload_size, 16, default: 0
|
||||
rest :payload
|
||||
end
|
||||
|
||||
# An NTP "private" message. Private messages are only specified for NTP
|
||||
# versions 2-4, but this is a fuzzer so why not try them all...
|
||||
class NTPPrivate < BitStruct
|
||||
# 0 1 2 3
|
||||
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
# |R M| VN | 7 |A| Sequence | Implementation| Req code |
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
# | err | Number of data items | MBZ | Size of data item |
|
||||
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
unsigned :response, 1, default: 0
|
||||
unsigned :more, 1, default: 0
|
||||
unsigned :version, 3, default: 0
|
||||
unsigned :mode, 3, default: 7
|
||||
unsigned :auth, 1, default: 0
|
||||
unsigned :sequence, 7, default: 0
|
||||
unsigned :implementation, 8, default: 0
|
||||
unsigned :request_code, 8, default: 0
|
||||
unsigned :error, 4, default: 0
|
||||
unsigned :record_count, 12, default: 0
|
||||
unsigned :mbz, 4, default: 0
|
||||
unsigned :record_size, 12, default: 0
|
||||
rest :payload
|
||||
|
||||
def records
|
||||
records = []
|
||||
1.upto(record_count) do |record_num|
|
||||
records << payload[record_size*(record_num-1), record_size]
|
||||
end
|
||||
records
|
||||
end
|
||||
end
|
||||
|
||||
def self.ntp_control(version, operation, payload = nil)
|
||||
n = NTPControl.new
|
||||
n.version = version
|
||||
n.operation = operation
|
||||
if payload
|
||||
n.payload_offset = 0
|
||||
n.payload_size = payload.size
|
||||
n.payload = payload
|
||||
end
|
||||
n.to_s
|
||||
end
|
||||
|
||||
def self.ntp_private(version, implementation, request_code, payload = nil)
|
||||
n = NTPPrivate.new
|
||||
n.version = version
|
||||
n.implementation = implementation
|
||||
n.request_code = request_code
|
||||
n.payload = payload if payload
|
||||
n.to_s
|
||||
end
|
||||
|
||||
def self.ntp_generic(version, mode)
|
||||
n = NTPGeneric.new
|
||||
n.version = version
|
||||
n.mode = mode
|
||||
n.to_s
|
||||
end
|
||||
|
||||
# Parses the given message and provides a description about the NTP message inside
|
||||
def self.describe(message)
|
||||
ntp = NTPGeneric.new(message)
|
||||
"#{message.size}-byte version #{ntp.version} mode #{ntp.mode} reply"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,215 @@
|
|||
# encoding: UTF-8
|
||||
##
|
||||
# This module requires Metasploit: http//metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'rex/proto/ntp'
|
||||
require 'securerandom'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
include Msf::Auxiliary::Fuzzer
|
||||
include Msf::Exploit::Remote::Udp
|
||||
include Msf::Auxiliary::Scanner
|
||||
|
||||
def initialize
|
||||
super(
|
||||
'Name' => 'NTP Protocol Fuzzer',
|
||||
'Description' => %q(
|
||||
A simplistic fuzzer for the Network Time Protocol that sends the
|
||||
following probes to understand NTP and look for anomalous NTP behavior:
|
||||
|
||||
* All possible combinations of NTP versions and modes, even if not
|
||||
allowed or specified in the RFCs
|
||||
* Short versions of the above
|
||||
* Short, invalid datagrams
|
||||
* Full-size, random datagrams
|
||||
* All possible NTP control messages
|
||||
* All possible NTP private messages
|
||||
|
||||
This findings of this fuzzer are not necessarily indicative of bugs,
|
||||
let alone vulnerabilities, rather they point out interesting things
|
||||
that might deserve more attention. Furthermore, this module is not
|
||||
particularly intelligent and there are many more areas of NTP that
|
||||
could be explored, including:
|
||||
|
||||
* Warn if the response is 100% identical to the request
|
||||
* Warn if the "mode" (if applicable) doesn't align with what we expect,
|
||||
* Filter out the 12-byte mode 6 unsupported opcode errors.
|
||||
* Fuzz the control message payload offset/size/etc. There be bugs
|
||||
),
|
||||
'Author' => 'Jon Hart <jon_hart[at]rapid7.com>',
|
||||
'License' => MSF_LICENSE
|
||||
)
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(123),
|
||||
OptInt.new('SLEEP', [true, 'Sleep for this many ms between requests', 0]),
|
||||
OptInt.new('WAIT', [true, 'Wait this many ms for responses', 250])
|
||||
], self.class)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptString.new('VERSIONS', [false, 'Specific versions to fuzz (csv)', '2,3,4']),
|
||||
OptString.new('MODES', [false, 'Modes to fuzz (csv)', nil]),
|
||||
OptString.new('MODE_6_OPERATIONS', [false, 'Mode 6 operations to fuzz (csv)', nil]),
|
||||
OptString.new('MODE_7_IMPLEMENTATIONS', [false, 'Mode 7 implementations to fuzz (csv)', nil]),
|
||||
OptString.new('MODE_7_REQUEST_CODES', [false, 'Mode 7 request codes to fuzz (csv)', nil])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def sleep_time
|
||||
datastore['SLEEP'] / 1000.0
|
||||
end
|
||||
|
||||
def check_and_set(setting)
|
||||
thing = setting.upcase
|
||||
const_name = thing.to_sym
|
||||
var_name = thing.downcase
|
||||
if datastore.key?(thing)
|
||||
instance_variable_set("@#{var_name}", datastore[thing].split(/[^\d]/).select { |v| !v.empty? }.map { |v| v.to_i })
|
||||
unsupported_things = instance_variable_get("@#{var_name}") - Rex::Proto::NTP.const_get(const_name)
|
||||
fail "Unsupported #{thing}: #{unsupported_things}" unless unsupported_things.empty?
|
||||
else
|
||||
instance_variable_set("@#{var_name}", Rex::Proto::NTP.const_get(const_name))
|
||||
end
|
||||
end
|
||||
|
||||
def run_host(ip)
|
||||
# check and set the optional advanced options
|
||||
check_and_set('VERSIONS')
|
||||
check_and_set('MODES')
|
||||
check_and_set('MODE_6_OPERATIONS')
|
||||
check_and_set('MODE_7_IMPLEMENTATIONS')
|
||||
check_and_set('MODE_7_REQUEST_CODES')
|
||||
|
||||
connect_udp
|
||||
fuzz_version_mode(ip, true)
|
||||
fuzz_version_mode(ip, false)
|
||||
fuzz_short(ip)
|
||||
fuzz_random(ip)
|
||||
fuzz_control(ip) if @modes.include?(6)
|
||||
fuzz_private(ip) if @modes.include?(7)
|
||||
disconnect_udp
|
||||
end
|
||||
|
||||
# Sends a series of NTP control messages
|
||||
def fuzz_control(host)
|
||||
@versions.each do |version|
|
||||
print_status("#{host}:#{rport} fuzzing version #{version} control messages (mode 6)")
|
||||
@mode_6_operations.each do |op|
|
||||
request = Rex::Proto::NTP.ntp_control(version, op)
|
||||
what = "#{request.size}-byte version #{version} mode 6 op #{op} message"
|
||||
vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")
|
||||
responses = probe(host, datastore['RPORT'].to_i, request)
|
||||
handle_responses(host, request, responses, what)
|
||||
Rex.sleep(sleep_time)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sends a series of NTP private messages
|
||||
def fuzz_private(host)
|
||||
@versions.each do |version|
|
||||
print_status("#{host}:#{rport} fuzzing version #{version} private messages (mode 7)")
|
||||
@mode_7_implementations.each do |implementation|
|
||||
@mode_7_request_codes.each do |request_code|
|
||||
request = Rex::Proto::NTP.ntp_private(version, implementation, request_code, "\x00" * 188)
|
||||
what = "#{request.size}-byte version #{version} mode 7 imp #{implementation} req #{request_code} message"
|
||||
vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")
|
||||
responses = probe(host, datastore['RPORT'].to_i, request)
|
||||
handle_responses(host, request, responses, what)
|
||||
Rex.sleep(sleep_time)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sends a series of small, short datagrams, looking for a reply
|
||||
def fuzz_short(host)
|
||||
print_status("#{host}:#{rport} fuzzing short messages")
|
||||
0.upto(4) do |size|
|
||||
request = SecureRandom.random_bytes(size)
|
||||
what = "short #{request.size}-byte random message"
|
||||
vprint_status("#{host}:#{rport} probing with #{what}")
|
||||
responses = probe(host, datastore['RPORT'].to_i, request)
|
||||
handle_responses(host, request, responses, what)
|
||||
Rex.sleep(sleep_time)
|
||||
end
|
||||
end
|
||||
|
||||
# Sends a series of random, full-sized datagrams, looking for a reply
|
||||
def fuzz_random(host)
|
||||
print_status("#{host}:#{rport} fuzzing random messages")
|
||||
0.upto(5) do
|
||||
# TODO: is there a better way to pick this size? Should more than one be tried?
|
||||
request = SecureRandom.random_bytes(48)
|
||||
what = "random #{request.size}-byte message"
|
||||
vprint_status("#{host}:#{rport} probing with #{what}")
|
||||
responses = probe(host, datastore['RPORT'].to_i, request)
|
||||
handle_responses(host, request, responses, what)
|
||||
Rex.sleep(sleep_time)
|
||||
end
|
||||
end
|
||||
|
||||
# Sends a series of different version + mode combinations
|
||||
def fuzz_version_mode(host, short)
|
||||
print_status("#{host}:#{rport} fuzzing #{short ? 'short ' : nil}version and mode combinations")
|
||||
@versions.each do |version|
|
||||
@modes.each do |mode|
|
||||
request = Rex::Proto::NTP::NTPGeneric.new
|
||||
request.version = version
|
||||
request.mode = mode
|
||||
unless short
|
||||
# TODO: is there a better way to pick this size? Should more than one be tried?
|
||||
request.payload = SecureRandom.random_bytes(16)
|
||||
end
|
||||
what = "#{request.size}-byte #{short ? 'short ' : nil}version #{version} mode #{mode} message"
|
||||
vprint_status("#{host}:#{rport} probing with #{what}")
|
||||
responses = probe(host, datastore['RPORT'].to_i, request)
|
||||
handle_responses(host, request, responses, what)
|
||||
Rex.sleep(sleep_time)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sends +message+ to +host+ on UDP port +port+, returning all replies
|
||||
def probe(host, port, message)
|
||||
replies = []
|
||||
udp_sock.sendto(message, host, port, 0)
|
||||
reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0)
|
||||
while reply && reply[1]
|
||||
replies << reply
|
||||
reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0)
|
||||
end
|
||||
replies
|
||||
end
|
||||
|
||||
def handle_responses(host, request, responses, what)
|
||||
problems = []
|
||||
descriptions = []
|
||||
responses.select! { |r| r[1] }
|
||||
return if responses.empty?
|
||||
responses.each do |response|
|
||||
data = response[0]
|
||||
descriptions << Rex::Proto::NTP.describe(data)
|
||||
problems << 'large response' if request.size < data.size
|
||||
ntp_req = Rex::Proto::NTP::NTPGeneric.new(request)
|
||||
ntp_resp = Rex::Proto::NTP::NTPGeneric.new(data)
|
||||
problems << 'version mismatch' if ntp_req.version != ntp_resp.version
|
||||
end
|
||||
|
||||
problems << 'multiple responses' if responses.size > 1
|
||||
problems.sort!
|
||||
problems.uniq!
|
||||
|
||||
description = descriptions.join(',')
|
||||
if problems.empty?
|
||||
vprint_status("#{host}:#{rport} -- Received '#{description}' to #{what}")
|
||||
else
|
||||
print_good("#{host}:#{rport} -- Received '#{description}' to #{what}: #{problems.join(',')}")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,83 @@
|
|||
# -*- coding: binary -*-
|
||||
#
|
||||
require 'rex/proto/ntp/modes'
|
||||
|
||||
describe "Rex::Proto::NTP mode message handling" do
|
||||
before do
|
||||
@payload = 'R7' * 7
|
||||
end
|
||||
|
||||
describe Rex::Proto::NTP::NTPControl do
|
||||
before do
|
||||
@control_raw = "\x1e\x05\x12\x34\x12\x34\x12\x34\x00\x00\x00\x0e" + @payload
|
||||
@control = Rex::Proto::NTP::NTPControl.new
|
||||
@control.version = 3
|
||||
@control.response = 0
|
||||
@control.more = 0
|
||||
@control.operation = 5
|
||||
@control.sequence = 0x1234
|
||||
@control.association_id = 0x1234
|
||||
@control.status = 0x1234
|
||||
@control.payload_offset = 0
|
||||
@control.payload_size = 14
|
||||
@control.payload = @payload
|
||||
end
|
||||
|
||||
it 'Generates control NTP messages correctly' do
|
||||
@control_raw.should == @control.to_s
|
||||
end
|
||||
|
||||
it 'Parses control NTP messages correctly' do
|
||||
parsed_raw = Rex::Proto::NTP::NTPControl.new(@control_raw)
|
||||
@control.should == parsed_raw
|
||||
end
|
||||
end
|
||||
|
||||
describe Rex::Proto::NTP::NTPGeneric do
|
||||
before do
|
||||
@generic_raw = "\xcc\x12\x34\x56" + @payload
|
||||
@generic = Rex::Proto::NTP::NTPGeneric.new
|
||||
@generic.li = 3
|
||||
@generic.version = 1
|
||||
@generic.mode = 4
|
||||
@generic.stratum = 0x12
|
||||
@generic.poll = 0x34
|
||||
@generic.precision = 0x56
|
||||
@generic.payload = @payload
|
||||
end
|
||||
|
||||
it 'Generates generic NTP messages correctly' do
|
||||
@generic_raw.should == @generic.to_s
|
||||
end
|
||||
|
||||
it 'Parses generic NTP messages correctly' do
|
||||
parsed_raw = Rex::Proto::NTP::NTPGeneric.new(@generic_raw)
|
||||
@generic.should == parsed_raw
|
||||
end
|
||||
end
|
||||
|
||||
describe Rex::Proto::NTP::NTPPrivate do
|
||||
before do
|
||||
@private_raw = "\x1f\x5a\x01\x99\x00\x00\x00\x00" + @payload
|
||||
@private = Rex::Proto::NTP::NTPPrivate.new
|
||||
@private.response = 0
|
||||
@private.more = 0
|
||||
@private.version = 3
|
||||
@private.mode = 7
|
||||
@private.auth = 0
|
||||
@private.sequence = 90
|
||||
@private.implementation = 1
|
||||
@private.request_code = 153
|
||||
@private.payload = @payload
|
||||
end
|
||||
|
||||
it 'Generates private NTP messages correctly' do
|
||||
@private_raw.should == @private.to_s
|
||||
end
|
||||
|
||||
it 'Parses private NTP messages correctly' do
|
||||
parsed_raw = Rex::Proto::NTP::NTPPrivate.new(@private_raw)
|
||||
@private.should == parsed_raw
|
||||
end
|
||||
end
|
||||
end
|
|
@ -133,9 +133,7 @@ class Msftidy
|
|||
break
|
||||
end
|
||||
else
|
||||
if line =~ /^\s*(require|load)\s+['"]nokogiri['"]/
|
||||
has_nokogiri = true
|
||||
end
|
||||
has_nokogiri = line_has_require?(line, 'nokogiri')
|
||||
end
|
||||
end
|
||||
error(msg) if has_nokogiri_xml_parser
|
||||
|
@ -201,6 +199,23 @@ class Msftidy
|
|||
end
|
||||
end
|
||||
|
||||
# See if 'require "rubygems"' or equivalent is used, and
|
||||
# warn if so. Since Ruby 1.9 this has not been necessary and
|
||||
# the framework only suports 1.9+
|
||||
def check_rubygems
|
||||
@source.each_line do |line|
|
||||
if line_has_require?(line, 'rubygems')
|
||||
warn("Explicitly requiring/loading rubygems is not necessary")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Does the given line contain a require/load of the specified library?
|
||||
def line_has_require?(line, lib)
|
||||
line =~ /^\s*(require|load)\s+['"]#{lib}['"]/
|
||||
end
|
||||
|
||||
def check_snake_case_filename
|
||||
sep = File::SEPARATOR
|
||||
good_name = Regexp.new "^[a-z0-9_#{sep}]+\.rb$"
|
||||
|
@ -578,6 +593,7 @@ def run_checks(full_filepath)
|
|||
tidy.check_mode
|
||||
tidy.check_shebang
|
||||
tidy.check_nokogiri
|
||||
tidy.check_rubygems
|
||||
tidy.check_ref_identifiers
|
||||
tidy.check_old_keywords
|
||||
tidy.check_verbose_option
|
||||
|
|
Loading…
Reference in New Issue