Skip to content

Commit bc274b3

Browse files
committed
Move NTP message code to Rex::Proto::NTP, simplify option handling
1 parent b9925bb commit bc274b3

File tree

4 files changed

+163
-144
lines changed

4 files changed

+163
-144
lines changed

lib/rex/proto/ntp.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- coding: binary -*-
2+
require 'rex/proto/ntp/constants'
3+
require 'rex/proto/ntp/modes'

lib/rex/proto/ntp/constants.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# -*- coding: binary -*-
2+
module Rex
3+
module Proto
4+
module NTP
5+
VERSIONS = (0..7).to_a
6+
MODES = (0..7).to_a
7+
MODE_6_OPERATIONS = (0..31).to_a
8+
MODE_7_IMPLEMENTATIONS = (0..255).to_a
9+
MODE_7_REQUEST_CODES = (0..255).to_a
10+
end
11+
end
12+
end

lib/rex/proto/ntp/modes.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# -*- coding: binary -*-
2+
module Rex
3+
module Proto
4+
module NTP
5+
6+
# A very generic NTP message
7+
#
8+
# Uses the common/similar parts from versions 1-4 and considers everything
9+
# after to be just one big field. For the particulars on the different versions,
10+
# see:
11+
# http://tools.ietf.org/html/rfc958#appendix-B
12+
# http://tools.ietf.org/html/rfc1059#appendix-B
13+
# pages 45/48 of http://tools.ietf.org/pdf/rfc1119.pdf
14+
# http://tools.ietf.org/html/rfc1305#appendix-D
15+
# http://tools.ietf.org/html/rfc5905#page-19
16+
class NTPGeneric < BitStruct
17+
# 0 1 2 3
18+
# 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
19+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20+
# |LI | VN | mode| Stratum | Poll | Precision |
21+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22+
unsigned :li, 2, default: 0
23+
unsigned :version, 3, default: 0
24+
unsigned :mode, 3, default: 0
25+
unsigned :stratum, 8, default: 0
26+
unsigned :poll, 8, default: 0
27+
unsigned :precision, 8, default: 0
28+
char :payload, 352
29+
end
30+
31+
# An NTP control message. Control messages are only specified for NTP
32+
# versions 2-4, but this is a fuzzer so why not try them all...
33+
class NTPControl < BitStruct
34+
# 0 1 2 3
35+
# 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
36+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37+
# |00 | VN | 6 |R E M| op | Sequence |
38+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
39+
# | status | association id |
40+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
41+
# | offset | count |
42+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
43+
unsigned :reserved, 2, default: 0
44+
unsigned :version, 3, default: 0
45+
unsigned :mode, 3, default: 6
46+
unsigned :response, 1, default: 0
47+
unsigned :error, 1, default: 0
48+
unsigned :more, 1, default: 0
49+
unsigned :operation, 5, default: 0
50+
unsigned :sequence, 16, default: 0
51+
unsigned :status, 16, default: 0
52+
unsigned :association_id, 16, default: 0
53+
# TODO: there *must* be bugs in the handling of these next two fields!
54+
unsigned :payload_offset, 16, default: 0
55+
unsigned :payload_size, 16, default: 0
56+
rest :payload
57+
end
58+
59+
# An NTP "private" message. Private messages are only specified for NTP
60+
# versions 2-4, but this is a fuzzer so why not try them all...
61+
class NTPPrivate < BitStruct
62+
# 0 1 2 3
63+
# 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
64+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
65+
# |00 | VN | 7 |A| Sequence |
66+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
67+
# | Implementation| request code |
68+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
69+
unsigned :reserved, 2, default: 0
70+
unsigned :version, 3, default: 0
71+
unsigned :mode, 3, default: 7
72+
unsigned :auth, 1, default: 0
73+
unsigned :sequence, 7, default: 0
74+
unsigned :implementation, 8, default: 0
75+
unsigned :request_code, 8, default: 0
76+
rest :payload
77+
end
78+
79+
def self.ntp_control(version, operation, payload = nil)
80+
n = NTPControl.new
81+
n.version = version
82+
n.operation = operation
83+
if payload
84+
n.payload_offset = 0
85+
n.payload_size = payload.size
86+
n.payload = payload
87+
end
88+
n.to_s
89+
end
90+
91+
def self.ntp_private(version, implementation, request_code, payload = nil)
92+
n = NTPPrivate.new
93+
n.version = version
94+
n.implementation = implementation
95+
n.request_code = request_code
96+
n.payload = payload if payload
97+
n.to_s
98+
end
99+
100+
def self.ntp_generic(version, mode)
101+
n = NTPGeneric.new
102+
n.version = version
103+
n.mode = mode
104+
n.to_s
105+
end
106+
107+
# Parses the given message and provides a description about the NTP message inside
108+
def self.describe(message)
109+
ntp = NTPGeneric.new(message)
110+
"#{message.size}-byte version #{ntp.version} mode #{ntp.mode} reply"
111+
end
112+
end
113+
end
114+
end

modules/auxiliary/fuzzers/ntp/ntp_protocol_fuzzer.rb

Lines changed: 34 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
##
55

66
require 'msf/core'
7-
require 'bit-struct'
7+
require 'rex/proto/ntp'
88
require 'securerandom'
99

1010
class Metasploit3 < Msf::Auxiliary
@@ -13,12 +13,6 @@ class Metasploit3 < Msf::Auxiliary
1313
include Msf::Exploit::Remote::Udp
1414
include Msf::Auxiliary::Scanner
1515

16-
NTP_SUPPORTED_VERSIONS = (0..7).to_a
17-
NTP_SUPPORTED_MODES = (0..7).to_a
18-
NTP_SUPPORTED_MODE_6_OPERATIONS = (0..31).to_a
19-
NTP_SUPPORTED_MODE_7_IMPLEMENTATIONS = (0..255).to_a
20-
NTP_SUPPORTED_MODE_7_REQUEST_CODES = (0..255).to_a
21-
2216
def initialize
2317
super(
2418
'Name' => 'NTP Protocol Fuzzer',
@@ -52,142 +46,44 @@ def initialize
5246
register_options(
5347
[
5448
Opt::RPORT(123),
55-
OptString.new('VERSIONS', [true, 'Versions to fuzz', [3,2,4]]),
56-
OptString.new('MODES', [true, 'Modes to fuzz', NTP_SUPPORTED_MODES]),
57-
OptString.new('MODE_6_OPERATIONS', [true, 'Mode 6 operations to fuzz', NTP_SUPPORTED_MODE_6_OPERATIONS]),
58-
OptString.new('MODE_7_IMPLEMENTATIONS', [true, 'Mode 7 implementations to fuzz', [3,2,0]]),
59-
OptString.new('MODE_7_REQUEST_CODES', [true, 'Mode 7 request codes to fuzz', NTP_SUPPORTED_MODE_7_REQUEST_CODES]),
6049
OptInt.new('SLEEP', [true, 'Sleep for this many ms between requests', 0]),
61-
OptInt.new('WAIT', [true, 'Wait this many ms for responses', 500])
50+
OptInt.new('WAIT', [true, 'Wait this many ms for responses', 250])
6251
], self.class)
63-
end
6452

65-
# A very generic NTP message
66-
#
67-
# Uses the common/similar parts from versions 1-4 and considers everything
68-
# after to be just one big field. For the particulars on the different versions,
69-
# see:
70-
# http://tools.ietf.org/html/rfc958#appendix-B
71-
# http://tools.ietf.org/html/rfc1059#appendix-B
72-
# pages 45/48 of http://tools.ietf.org/pdf/rfc1119.pdf
73-
# http://tools.ietf.org/html/rfc1305#appendix-D
74-
# http://tools.ietf.org/html/rfc5905#page-19
75-
class NTPGeneric < BitStruct
76-
# 0 1 2 3
77-
# 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
78-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
79-
# |LI | VN | mode| Stratum | Poll | Precision |
80-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
81-
unsigned :li, 2, default: 0
82-
unsigned :version, 3, default: 0
83-
unsigned :mode, 3, default: 0
84-
unsigned :stratum, 8, default: 0
85-
unsigned :poll, 8, default: 0
86-
unsigned :precision, 8, default: 0
87-
char :payload, 352
88-
end
89-
90-
# An NTP control message. Control messages are only specified for NTP
91-
# versions 2-4, but this is a fuzzer so why not try them all...
92-
class NTPControl < BitStruct
93-
# 0 1 2 3
94-
# 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
95-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
96-
# |00 | VN | 6 |R E M| op | Sequence |
97-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
98-
# | status | association id |
99-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
100-
# | offset | count |
101-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
102-
unsigned :reserved, 2, default: 0
103-
unsigned :version, 3, default: 0
104-
unsigned :mode, 3, default: 6
105-
unsigned :response, 1, default: 0
106-
unsigned :error, 1, default: 0
107-
unsigned :more, 1, default: 0
108-
unsigned :operation, 5, default: 0
109-
unsigned :sequence, 16, default: 0
110-
unsigned :status, 16, default: 0
111-
unsigned :association_id, 16, default: 0
112-
# TODO: there *must* be bugs in the handling of these next two fields!
113-
unsigned :payload_offset, 16, default: 0
114-
unsigned :payload_size, 16, default: 0
115-
rest :payload
53+
register_advanced_options(
54+
[
55+
OptString.new('VERSIONS', [false, 'Specific versions to fuzz (csv)', nil]),
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)
11661
end
11762

118-
# An NTP "private" message. Private messages are only specified for NTP
119-
# versions 2-4, but this is a fuzzer so why not try them all...
120-
class NTPPrivate < BitStruct
121-
# 0 1 2 3
122-
# 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
123-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
124-
# |00 | VN | 7 |A| Sequence |
125-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
126-
# | Implementation| request code |
127-
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
128-
unsigned :reserved, 2, default: 0
129-
unsigned :version, 3, default: 0
130-
unsigned :mode, 3, default: 7
131-
unsigned :auth, 1, default: 0
132-
unsigned :sequence, 7, default: 0
133-
unsigned :implementation, 8, default: 0
134-
unsigned :request_code, 8, default: 0
135-
rest :payload
63+
def sleep_time
64+
datastore['SLEEP'] / 1000.0
13665
end
13766

138-
def build_ntp_control(version, operation, payload = nil)
139-
n = NTPControl.new
140-
n.version = version
141-
n.operation = operation
142-
if payload
143-
n.payload_offset = 0
144-
n.payload_size = payload.size
145-
n.payload = payload
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))
14677
end
147-
n.to_s
148-
end
149-
150-
def build_ntp_private(version, implementation, request_code, payload = nil)
151-
n = NTPPrivate.new
152-
n.version = version
153-
n.implementation = implementation
154-
n.request_code = request_code
155-
n.payload = payload if payload
156-
n.to_s
157-
end
158-
159-
def build_ntp_generic(version, mode)
160-
n = NTPGeneric.new
161-
n.version = version
162-
n.mode = mode
163-
n.to_s
164-
end
165-
166-
def sleep_time
167-
datastore['SLEEP'] / 1000.0
16878
end
16979

17080
def run_host(ip)
171-
# parse and sanity check versions
172-
@versions = datastore['VERSIONS'].split(/[^\d]/).select { |v| !v.empty? }.map { |v| v.to_i }
173-
unsupported_versions = @versions - NTP_SUPPORTED_VERSIONS
174-
fail "Unsupported NTP versions: #{unsupported_versions}" unless unsupported_versions.empty?
175-
# parse and sanity check modes
176-
@modes = datastore['MODES'].split(/[^\d]/).select { |m| !m.empty? }.map { |v| v.to_i }
177-
unsupported_modes = @modes - NTP_SUPPORTED_MODES
178-
fail "Unsupported NTP modes: #{unsupported_modes}" unless unsupported_modes.empty?
179-
# parse and sanity check mode 6 operations
180-
@mode_6_operations = datastore['MODE_6_OPERATIONS'].split(/[^\d]/).select { |m| !m.empty? }.map { |v| v.to_i }
181-
unsupported_ops = @mode_6_operations - NTP_SUPPORTED_MODE_6_OPERATIONS
182-
fail "Unsupported NTP mode 6 operations: #{unsupported_ops}" unless unsupported_ops.empty?
183-
# parse and sanity check mode 7 implementations
184-
@mode_7_implementations = datastore['MODE_7_IMPLEMENTATIONS'].split(/[^\d]/).select { |m| !m.empty? }.map { |v| v.to_i }
185-
unsupported_implementations = @mode_7_implementations - NTP_SUPPORTED_MODE_7_IMPLEMENTATIONS
186-
fail "Unsupported NTP mode 7 implementations: #{unsupported_implementations}" unless unsupported_implementations.empty?
187-
# parse and sanity check mode 7 request codes
188-
@mode_7_request_codes = datastore['MODE_7_REQUEST_CODES'].split(/[^\d]/).select { |m| !m.empty? }.map { |v| v.to_i }
189-
unsupported_request_codes = @mode_7_request_codes - NTP_SUPPORTED_MODE_7_REQUEST_CODES
190-
fail "Unsupported NTP mode 7 request codes: #{unsupported_request_codes}" unless unsupported_request_codes.empty?
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')
19187

19288
connect_udp
19389
fuzz_version_mode(ip)
@@ -204,7 +100,7 @@ def fuzz_control(host)
204100
@versions.each do |version|
205101
print_status("#{host}:#{rport} fuzzing version #{version} control messages (mode 6)")
206102
@mode_6_operations.each do |op|
207-
request = build_ntp_control(version, op)
103+
request = Rex::Proto::NTP.ntp_control(version, op)
208104
what = "#{request.size}-byte version #{version} mode 6 op #{op} message"
209105
vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")
210106
responses = probe(host, datastore['RPORT'].to_i, request)
@@ -220,7 +116,7 @@ def fuzz_private(host)
220116
print_status("#{host}:#{rport} fuzzing version #{version} private messages (mode 7)")
221117
@mode_7_implementations.each do |implementation|
222118
@mode_7_request_codes.each do |request_code|
223-
request = build_ntp_private(version, implementation, request_code, "\x00"*188)
119+
request = Rex::Proto::NTP.ntp_private(version, implementation, request_code, "\x00"*188)
224120
what = "#{request.size}-byte version #{version} mode 7 imp #{implementation} req #{request_code} message"
225121
vprint_status("#{host}:#{rport} probing with #{request.size}-byte #{what}")
226122
responses = probe(host, datastore['RPORT'].to_i, request)
@@ -262,7 +158,7 @@ def fuzz_version_mode(host, short=false)
262158
print_status("#{host}:#{rport} fuzzing #{short ? 'short ' : nil}version and mode combinations")
263159
@versions.each do |version|
264160
@modes.each do |mode|
265-
request = build_ntp_generic(version, mode)
161+
request = Rex::Proto::NTP.ntp_generic(version, mode)
266162
request = request[0, 4] if short
267163
what = "#{request.size}-byte #{short ? 'short ' : nil}version #{version} mode #{mode} message"
268164
vprint_status("#{host}:#{rport} probing with #{what}")
@@ -283,23 +179,17 @@ def probe(host, port, message)
283179
replies
284180
end
285181

286-
# Parses the given message and provides a description about the NTP message inside
287-
def describe(message)
288-
ntp = NTPGeneric.new(message)
289-
"#{message.size}-byte version #{ntp.version} mode #{ntp.mode} reply"
290-
end
291-
292182
def handle_responses(host, request, responses, what)
293183
problems = []
294184
descriptions = []
295185
responses.select! { |r| r[1] }
296186
return if responses.empty?
297187
responses.each do |response|
298188
data = response[0]
299-
descriptions << describe(data)
189+
descriptions << Rex::Proto::NTP.describe(data)
300190
problems << 'large response' if request.size < data.size
301-
ntp_req = NTPGeneric.new(request)
302-
ntp_resp = NTPGeneric.new(data)
191+
ntp_req = Rex::Proto::NTP::NTPGeneric.new(request)
192+
ntp_resp = Rex::Proto::NTP::NTPGeneric.new(data)
303193
problems << 'version mismatch' if ntp_req.version != ntp_resp.version
304194
end
305195

0 commit comments

Comments
 (0)