CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/fuzzers/ntp/ntp_protocol_fuzzer.rb
Views: 11655
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
)
42
43
register_options(
44
[
45
Opt::RPORT(123),
46
OptInt.new('SLEEP', [true, 'Sleep for this many ms between requests', 0]),
47
OptInt.new('WAIT', [true, 'Wait this many ms for responses', 250])
48
])
49
50
register_advanced_options(
51
[
52
OptString.new('VERSIONS', [false, 'Specific versions to fuzz (csv)', '2,3,4']),
53
OptString.new('MODES', [false, 'Modes to fuzz (csv)']),
54
OptString.new('MODE_6_OPERATIONS', [false, 'Mode 6 operations to fuzz (csv)']),
55
OptString.new('MODE_7_IMPLEMENTATIONS', [false, 'Mode 7 implementations to fuzz (csv)']),
56
OptString.new('MODE_7_REQUEST_CODES', [false, 'Mode 7 request codes to fuzz (csv)'])
57
])
58
end
59
60
def sleep_time
61
datastore['SLEEP'] / 1000.0
62
end
63
64
def check_and_set(setting)
65
thing = setting.upcase
66
const_name = thing.to_sym
67
var_name = thing.downcase
68
if datastore[thing]
69
instance_variable_set("@#{var_name}", datastore[thing].split(/[^\d]/).select { |v| !v.empty? }.map { |v| v.to_i })
70
unsupported_things = instance_variable_get("@#{var_name}") - Rex::Proto::NTP.const_get(const_name)
71
fail "Unsupported #{thing}: #{unsupported_things}" unless unsupported_things.empty?
72
else
73
instance_variable_set("@#{var_name}", Rex::Proto::NTP.const_get(const_name))
74
end
75
end
76
77
def run_host(ip)
78
# check and set the optional advanced options
79
check_and_set('VERSIONS')
80
check_and_set('MODES')
81
check_and_set('MODE_6_OPERATIONS')
82
check_and_set('MODE_7_IMPLEMENTATIONS')
83
check_and_set('MODE_7_REQUEST_CODES')
84
85
connect_udp
86
fuzz_version_mode(ip, true)
87
fuzz_version_mode(ip, false)
88
fuzz_short(ip)
89
fuzz_random(ip)
90
fuzz_control(ip) if @modes.include?(6)
91
fuzz_private(ip) if @modes.include?(7)
92
disconnect_udp
93
end
94
95
# Sends a series of NTP control messages
96
def fuzz_control(host)
97
@versions.each do |version|
98
print_status("#{host}:#{rport} fuzzing version #{version} control messages (mode 6)")
99
@mode_6_operations.each do |op|
100
request = Rex::Proto::NTP.ntp_control(version, op).to_binary_s
101
what = "#{request.size}-byte version #{version} mode 6 op #{op} message"
102
vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")
103
responses = probe(host, datastore['RPORT'].to_i, request)
104
handle_responses(host, request, responses, what)
105
Rex.sleep(sleep_time)
106
end
107
end
108
end
109
110
# Sends a series of NTP private messages
111
def fuzz_private(host)
112
@versions.each do |version|
113
print_status("#{host}:#{rport} fuzzing version #{version} private messages (mode 7)")
114
@mode_7_implementations.each do |implementation|
115
@mode_7_request_codes.each do |request_code|
116
request = Rex::Proto::NTP.ntp_private(version, implementation, request_code, "\0" * 188).to_binary_s
117
what = "#{request.size}-byte version #{version} mode 7 imp #{implementation} req #{request_code} message"
118
vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")
119
responses = probe(host, datastore['RPORT'].to_i, request)
120
handle_responses(host, request, responses, what)
121
Rex.sleep(sleep_time)
122
end
123
end
124
end
125
end
126
127
# Sends a series of small, short datagrams, looking for a reply
128
def fuzz_short(host)
129
print_status("#{host}:#{rport} fuzzing short messages")
130
0.upto(4) do |size|
131
request = SecureRandom.random_bytes(size)
132
what = "short #{request.size}-byte random message"
133
vprint_status("#{host}:#{rport} probing with #{what}")
134
responses = probe(host, datastore['RPORT'].to_i, request)
135
handle_responses(host, request, responses, what)
136
Rex.sleep(sleep_time)
137
end
138
end
139
140
# Sends a series of random, full-sized datagrams, looking for a reply
141
def fuzz_random(host)
142
print_status("#{host}:#{rport} fuzzing random messages")
143
0.upto(5) do
144
# TODO: is there a better way to pick this size? Should more than one be tried?
145
request = SecureRandom.random_bytes(48)
146
what = "random #{request.size}-byte message"
147
vprint_status("#{host}:#{rport} probing with #{what}")
148
responses = probe(host, datastore['RPORT'].to_i, request)
149
handle_responses(host, request, responses, what)
150
Rex.sleep(sleep_time)
151
end
152
end
153
154
# Sends a series of different version + mode combinations
155
def fuzz_version_mode(host, short)
156
print_status("#{host}:#{rport} fuzzing #{short ? 'short ' : nil}version and mode combinations")
157
@versions.each do |version|
158
@modes.each do |mode|
159
request = Rex::Proto::NTP::NTPGeneric.new
160
request.version = version
161
request.mode = mode
162
unless short
163
# TODO: is there a better way to pick this size? Should more than one be tried?
164
request.payload = SecureRandom.random_bytes(16)
165
end
166
request = request.to_binary_s
167
what = "#{request.size}-byte #{short ? 'short ' : nil}version #{version} mode #{mode} message"
168
vprint_status("#{host}:#{rport} probing with #{what}")
169
responses = probe(host, datastore['RPORT'].to_i, request)
170
handle_responses(host, request, responses, what)
171
Rex.sleep(sleep_time)
172
end
173
end
174
end
175
176
# Sends +message+ to +host+ on UDP port +port+, returning all replies
177
def probe(host, port, message)
178
message = message.to_binary_s if message.respond_to?('to_binary_s')
179
replies = []
180
begin
181
udp_sock.sendto(message, host, port, 0)
182
rescue ::Errno::EISCONN
183
udp_sock.write(message)
184
end
185
reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0)
186
while reply && reply[1]
187
replies << reply
188
reply = udp_sock.recvfrom(65535, datastore['WAIT'] / 1000.0)
189
end
190
replies
191
end
192
193
def handle_responses(host, request, responses, what)
194
problems = []
195
descriptions = []
196
request = request.to_binary_s if request.respond_to?('to_binary_s')
197
responses.select! { |r| r[1] }
198
return if responses.empty?
199
responses.each do |response|
200
data = response[0]
201
descriptions << Rex::Proto::NTP.describe(data)
202
problems << 'large response' if request.size < data.size
203
ntp_req = Rex::Proto::NTP::NTPGeneric.new.read(request)
204
ntp_resp = Rex::Proto::NTP::NTPGeneric.new.read(data)
205
problems << 'version mismatch' if ntp_req.version != ntp_resp.version
206
end
207
208
problems << 'multiple responses' if responses.size > 1
209
problems.sort!
210
problems.uniq!
211
212
description = descriptions.join(',')
213
if problems.empty?
214
vprint_status("#{host}:#{rport} -- Received '#{description}' to #{what}")
215
else
216
print_good("#{host}:#{rport} -- Received '#{description}' to #{what}: #{problems.join(',')}")
217
end
218
end
219
end
220
221