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