Skip to content

Commit 1ee83ff

Browse files
committed
Land rapid7#3696, pile of NTP DRDoS 0days
Dr. DoS in da house?
2 parents a39f7b9 + 7a76efa commit 1ee83ff

File tree

11 files changed

+615
-116
lines changed

11 files changed

+615
-116
lines changed

lib/msf/core/auxiliary/drdos.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -*- coding: binary -*-
2+
module Msf
3+
4+
###
5+
#
6+
# This module provides methods for Distributed Reflective Denial of Service (DRDoS) attacks
7+
#
8+
###
9+
module Auxiliary::DRDoS
10+
11+
def prove_drdos(response_map)
12+
vulnerable = false
13+
proofs = []
14+
response_map.each do |request, responses|
15+
responses ||= []
16+
this_proof = ''
17+
18+
# compute packet amplification
19+
if responses.size > 1
20+
vulnerable = true
21+
this_proof += "#{responses.size}x packet amplification"
22+
else
23+
this_proof += 'No packet amplification'
24+
end
25+
26+
this_proof += ' and '
27+
28+
# compute bandwidth amplification
29+
total_size = responses.map(&:size).reduce(:+)
30+
bandwidth_amplification = total_size - request.size
31+
if bandwidth_amplification > 0
32+
vulnerable = true
33+
this_proof += "a #{bandwidth_amplification}-byte bandwidth amplification"
34+
else
35+
this_proof += 'no bandwidth amplification'
36+
end
37+
38+
# TODO (maybe): show the request and responses in more detail?
39+
proofs << this_proof
40+
end
41+
42+
[ vulnerable, proofs.join(', ') ]
43+
end
44+
45+
end
46+
end

lib/msf/core/auxiliary/mixins.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#
66
require 'msf/core/auxiliary/auth_brute'
77
require 'msf/core/auxiliary/dos'
8+
require 'msf/core/auxiliary/drdos'
89
require 'msf/core/auxiliary/fuzzer'
910
require 'msf/core/auxiliary/report'
1011
require 'msf/core/auxiliary/scanner'
@@ -20,4 +21,5 @@
2021
require 'msf/core/auxiliary/cisco'
2122
require 'msf/core/auxiliary/nmap'
2223
require 'msf/core/auxiliary/iax2'
24+
require 'msf/core/auxiliary/ntp'
2325
require 'msf/core/auxiliary/pii'

lib/msf/core/auxiliary/ntp.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: binary -*-
2+
require 'rex/proto/ntp'
3+
4+
module Msf
5+
6+
###
7+
#
8+
# This module provides methods for working with NTP
9+
#
10+
###
11+
module Auxiliary::NTP
12+
13+
include Auxiliary::Scanner
14+
15+
#
16+
# Initializes an instance of an auxiliary module that uses NTP
17+
#
18+
19+
def initialize(info = {})
20+
super
21+
register_options(
22+
[
23+
Opt::RPORT(123),
24+
], self.class)
25+
26+
register_advanced_options(
27+
[
28+
OptInt.new('VERSION', [true, 'Use this NTP version', 2]),
29+
OptInt.new('IMPLEMENTATION', [true, 'Use this NTP mode 7 implementation', 3])
30+
], self.class)
31+
end
32+
end
33+
end

lib/rex/proto/ntp/modes.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def self.ntp_control(version, operation, payload = nil)
101101
n.payload_size = payload.size
102102
n.payload = payload
103103
end
104-
n.to_s
104+
n
105105
end
106106

107107
def self.ntp_private(version, implementation, request_code, payload = nil)
@@ -110,14 +110,14 @@ def self.ntp_private(version, implementation, request_code, payload = nil)
110110
n.implementation = implementation
111111
n.request_code = request_code
112112
n.payload = payload if payload
113-
n.to_s
113+
n
114114
end
115115

116116
def self.ntp_generic(version, mode)
117117
n = NTPGeneric.new
118118
n.version = version
119119
n.mode = mode
120-
n.to_s
120+
n
121121
end
122122

123123
# Parses the given message and provides a description about the NTP message inside

modules/auxiliary/scanner/ntp/ntp_monlist.rb

Lines changed: 81 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
class Metasploit3 < Msf::Auxiliary
99

1010
include Msf::Auxiliary::Report
11-
include Msf::Auxiliary::Scanner
11+
include Msf::Exploit::Remote::Udp
12+
include Msf::Auxiliary::UDPScanner
13+
include Msf::Auxiliary::NTP
14+
include Msf::Auxiliary::DRDoS
1215

1316
def initialize
1417
super(
@@ -33,10 +36,7 @@ def initialize
3336

3437
register_options(
3538
[
36-
Opt::RPORT(123),
37-
Opt::CHOST,
3839
OptInt.new('RETRY', [false, "Number of tries to query the NTP server", 3]),
39-
OptInt.new('BATCHSIZE', [true, 'The number of hosts to probe in each set', 256]),
4040
OptBool.new('SHOW_LIST', [false, 'Show the recent clients list', 'false'])
4141
], self.class)
4242

@@ -46,134 +46,112 @@ def initialize
4646
], self.class)
4747
end
4848

49-
# Define our batch size
50-
def run_batch_size
51-
datastore['BATCHSIZE'].to_i
49+
# Called for each IP in the batch
50+
def scan_host(ip)
51+
scanner_send(@probe, ip, datastore['RPORT'])
5252
end
5353

54-
# Fingerprint a single host
55-
def run_batch(batch)
54+
# Called for each response packet
55+
def scanner_process(data, shost, sport)
56+
@results[shost] ||= { messages: [], peers: [] }
57+
@results[shost][:messages] << Rex::Proto::NTP::NTPPrivate.new(data)
58+
@results[shost][:peers] << extract_peer_tuples(data)
59+
end
5660

61+
# Called before the scan block
62+
def scanner_prescan(batch)
5763
@results = {}
5864
@aliases = {}
65+
@probe = Rex::Proto::NTP.ntp_private(datastore['VERSION'], datastore['IMPLEMENTATION'], 42)
66+
end
5967

60-
vprint_status("Sending probes to #{batch[0]}->#{batch[-1]} (#{batch.length} hosts)")
61-
62-
begin
63-
udp_sock = nil
64-
idx = 0
65-
66-
# Create an unbound UDP socket if no CHOST is specified, otherwise
67-
# create a UDP socket bound to CHOST (in order to avail of pivoting)
68-
udp_sock = Rex::Socket::Udp.create({
69-
'LocalHost' => datastore['CHOST'] || nil,
70-
'Context' => {'Msf' => framework, 'MsfExploit' => self}
71-
})
72-
add_socket(udp_sock)
73-
74-
# Try more times since NTP servers can be a bit busy
75-
1.upto(datastore['RETRY'].to_i) do
76-
batch.each do |ip|
77-
next if @results[ip]
78-
79-
begin
80-
data = probe_pkt_ntp
81-
udp_sock.sendto(data, ip, datastore['RPORT'].to_i, 0)
82-
rescue ::Interrupt
83-
raise $!
84-
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
85-
nil
86-
end
87-
88-
if (idx % 30 == 0)
89-
while (r = udp_sock.recvfrom(65535, 0.1) and r[1])
90-
parse_reply(r)
91-
end
92-
end
93-
94-
idx += 1
95-
end
96-
end
97-
98-
while (r = udp_sock.recvfrom(65535, 10) and r[1])
99-
parse_reply(r)
100-
end
101-
102-
rescue ::Interrupt
103-
raise $!
104-
rescue ::Exception => e
105-
print_error("Unknown error: #{e.class} #{e}")
106-
end
107-
68+
# Called after the scan block
69+
def scanner_postscan(batch)
10870
@results.keys.each do |k|
71+
response_map = { @probe => @results[k][:messages] }
72+
peer = "#{k}:#{rport}"
10973

74+
# TODO: check to see if any of the responses are actually NTP before reporting
11075
report_service(
11176
:host => k,
11277
:proto => 'udp',
113-
:port => datastore['RPORT'].to_i,
78+
:port => rport,
11479
:name => 'ntp'
11580
)
11681

117-
report_note(
118-
:host => k,
119-
:proto => 'udp',
120-
:port => datastore['RPORT'].to_i,
121-
:type => 'ntp.monlist',
122-
:data => {:monlist => @results[k]}
123-
)
124-
125-
if (@aliases[k] and @aliases[k].keys[0] != k)
126-
print_good("#{k}:#{datastore['RPORT'].to_i} NTP monlist request permitted (#{@results[k].length} entries)")
82+
peers = @results[k][:peers].flatten(1)
83+
unless peers.empty?
84+
print_good("#{peer} NTP monlist request permitted (#{peers.length} entries)")
85+
# store the peers found from the monlist
86+
report_note(
87+
:host => k,
88+
:proto => 'udp',
89+
:port => rport,
90+
:type => 'ntp.monlist',
91+
:data => {:monlist => peers}
92+
)
93+
# print out peers if desired
94+
if datastore['SHOW_LIST']
95+
peers.each do |ntp_peer|
96+
print_status("#{peer} #{ntp_peer}")
97+
end
98+
end
99+
# store any aliases for our target
127100
report_note(
128101
:host => k,
129102
:proto => 'udp',
130-
:port => datastore['RPORT'].to_i,
103+
:port => rport,
131104
:type => 'ntp.addresses',
132-
:data => {:addresses => @aliases[k].keys}
105+
:data => {:addresses => peers.map { |p| p.last }.sort.uniq }
133106
)
134-
end
135107

136-
if (datastore['StoreNTPClients'])
137-
print_status("#{k} Storing #{@results[k].length} NTP client hosts in the database...")
138-
@results[k].each do |r|
139-
maddr,mport,mserv = r
140-
report_note(
141-
:host => maddr,
142-
:type => 'ntp.client.history',
143-
:data => {
144-
:address => maddr,
145-
:port => mport,
146-
:server => mserv
147-
}
148-
)
108+
if (datastore['StoreNTPClients'])
109+
print_status("#{peer} Storing #{peers.length} NTP client hosts in the database...")
110+
peers.each do |r|
111+
maddr,mport,mserv = r
112+
next if maddr == '127.0.0.1' # some NTP servers peer with themselves..., but we can't store loopback
113+
report_note(
114+
:host => maddr,
115+
:type => 'ntp.client.history',
116+
:data => {
117+
:address => maddr,
118+
:port => mport,
119+
:server => mserv
120+
}
121+
)
122+
end
149123
end
150124
end
151-
end
152-
153-
end
154-
155-
def parse_reply(pkt)
156125

157-
# Ignore "empty" packets
158-
return if not pkt[1]
159-
160-
if(pkt[1] =~ /^::ffff:/)
161-
pkt[1] = pkt[1].sub(/^::ffff:/, '')
126+
vulnerable, proof = prove_drdos(response_map)
127+
what = 'NTP Mode 7 monlist DRDoS (CVE-2013-5211)'
128+
if vulnerable
129+
print_good("#{peer} - Vulnerable to #{what}: #{proof}")
130+
report_vuln({
131+
:host => k,
132+
:port => rport,
133+
:proto => 'udp',
134+
:name => what,
135+
:refs => self.references
136+
})
137+
else
138+
vprint_status("#{peer} - Not vulnerable to #{what}: #{proof}")
139+
end
162140
end
163141

164-
data = pkt[0]
165-
host = pkt[1]
166-
port = pkt[2]
142+
end
167143

168-
return if pkt[0].length < (72 + 16)
144+
# Examine the monlist reponse +data+ and extract all peer tuples (saddd, dport, daddr)
145+
def extract_peer_tuples(data)
146+
return [] if data.length < 76
169147

170148
# NTP headers 8 bytes
171149
ntp_flags, ntp_auth, ntp_vers, ntp_code = data.slice!(0,4).unpack('C*')
172-
vprint_status("#{host}:#{port} - ntp_auth: #{ntp_auth}, ntp_vers: #{ntp_vers}")
173150
pcnt, plen = data.slice!(0,4).unpack('nn')
174-
return if plen != 72
151+
return [] if plen != 72
175152

176153
idx = 0
154+
peer_tuples = []
177155
1.upto(pcnt) do
178156
#u_int32 firsttime; /* first time we received a packet */
179157
#u_int32 lasttime; /* last packet from this host */
@@ -184,21 +162,11 @@ def parse_reply(pkt)
184162
#u_int32 flags; /* flags about destination */
185163
#u_short port; /* port number of last reception */
186164

187-
firsttime,lasttime,restr,count,saddr,daddr,flags,dport = data[idx, 30].unpack("NNNNNNNn")
165+
_,_,_,_,saddr,daddr,_,dport = data[idx, 30].unpack("NNNNNNNn")
188166

189-
@results[host] ||= []
190-
@aliases[host] ||= {}
191-
@results[host] << [ Rex::Socket.addr_itoa(daddr), dport, Rex::Socket.addr_itoa(saddr) ]
192-
@aliases[host][Rex::Socket.addr_itoa(saddr)] = true
193-
if datastore['SHOW_LIST']
194-
print_status("#{host}:#{port} #{Rex::Socket.addr_itoa(saddr)} (lst: #{lasttime}sec., cnt: #{count})")
195-
end
167+
peer_tuples << [ Rex::Socket.addr_itoa(saddr), dport, Rex::Socket.addr_itoa(daddr) ]
196168
idx += plen
197169
end
170+
peer_tuples
198171
end
199-
200-
def probe_pkt_ntp
201-
"\x17\x00\x03\x2a" + "\x00" * 188
202-
end
203-
204172
end

0 commit comments

Comments
 (0)