Land #18122, rocketmq version lib

This commit is contained in:
Christophe De La Fuente 2023-07-05 18:11:25 +02:00
commit ae48236d07
No known key found for this signature in database
GPG Key ID: 9E350956EA00352A
3 changed files with 210 additions and 60 deletions

View File

@ -0,0 +1,126 @@
# -*- coding: binary -*-
module Msf
###
#
# This module provides methods for working with Apache RocketMQ
#
###
module Auxiliary::Rocketmq
include Msf::Exploit::Remote::Tcp
def initialize(info = {})
super
register_options([ Opt::RPORT(9876) ], Msf::Auxiliary::Rocketmq)
end
# Sends a version request to the service, and returns the data as a list of hashes or nil on error
#
# @see https://github.com/Malayke/CVE-2023-33246_RocketMQ_RCE_EXPLOIT/blob/e27693a854a8e3b2863dc366f36002107e3595de/check.py#L68
# @return [String, nil] The data as a list of hashes or nil on error.
def send_version_request
data = '{"code":105,"extFields":{"Signature":"/u5P/wZUbhjanu4LM/UzEdo2u2I=","topic":"TBW102","AccessKey":"rocketmq2"},"flag":0,"language":"JAVA","opaque":1,"serializeTypeCurrentRPC":"JSON","version":401}'
data_length = "\x00\x00\x00" + [data.length].pack('C')
header = "\x00\x00\x00" + [data.length + data_length.length].pack('C')
begin
connect
sock.send(header + data_length + data, 0)
res = sock.recv(1024)
rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
print_error("Unable to connect: #{e.class} #{e.message}\n#{e.backtrace * "\n"}")
elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
ensure
disconnect
end
if res.nil?
vprint_error('No response received')
return nil
end
unless res.include?('{')
vprint_error('Response contains unusable data')
return nil
end
res
end
# This function takes an ID (number) and looks through rocketmq's index of version numbers to find the real version number
# Errors will result in "UNKNOWN_VERSION_ID_<id>" and may be caused by needing to update the version table
# from https://github.com/apache/rocketmq/blob/develop/common/src/4d82b307ef50f5cba5717d0ebafeb3cabf336873/java/org/apache/rocketmq/common/MQVersion.java
#
# @param [Integer] id The version id found in the NameServer response.
# @return [String] The Apache RocketMQ version string.
def get_rocketmq_version(id)
version_list = JSON.parse(File.read(::File.join(Msf::Config.data_directory, 'rocketmq_versions_list.json'), mode: 'rb'))
version_list.fetch(id, "UNKNOWN_VERSION_ID_#{id}").gsub('_', '.')
end
# This function takes a response from the send_version_request function and parses as it doesn't get returned as
# proper json. It returns a Hash including RocketMQ versions info and Broker info if found
#
# @param [String] res Response form the send_version_request request
# @return [Hash] Hash including RocketMQ versions info and Broker info if found
def parse_rocketmq_data(res)
# remove a response header so we have json-ish data
res = res[8..]
# we have 2 json objects appended to eachother, so we now need to split that out and make it usable
res = res.split('}{')
jsonable = []
# patch back in the { and }
res.each do |r|
r += '}' unless r.end_with?('}')
r = '{' + r unless r.start_with?('{')
jsonable.append(r)
end
result = []
jsonable.each do |j|
res = JSON.parse(j)
result.append(res)
rescue JSON::ParserError
vprint_error("Unable to parse json data: #{j}")
next
end
parsed_data = {}
result.each do |j|
parsed_data['version'] = get_rocketmq_version(j['version']) if j['version']
parsed_data['brokerDatas'] = j['brokerDatas'] if j['brokerDatas']
end
if parsed_data == {} || parsed_data['version'].nil?
vprint_error('Unable to find version or other data within response.')
return
end
parsed_data
end
# This function takes the broker data from the name server response, the rhost address and a default Broker port
# number. The function searches the broker data for a broker instance listening on the rhost address and if found it
# returns the port found. If the search is unsuccessful it returns the default broker port.
#
# @param [Array] broker_datas An array containing a hash of Broker info
# @param [String] rhosts The RHOST address
# @param [Integer] default_broker_port The default broker port
# @return [Integer] the determined broker port
def get_broker_port(broker_datas, rhost, default_broker_port: 10911)
# Example of brokerData:
# [{"brokerAddrs"=>{"0"=>"172.16.199.135:10911"}, "brokerName"=>"DESKTOP-8ATHH6O", "cluster"=>"DefaultCluster"}]
broker_datas['brokerDatas'].each do |broker_data|
broker_data['brokerAddrs'].values.each do |broker_endpoint|
next unless broker_endpoint.start_with?("#{rhost}:")
return broker_endpoint.match(/\A#{rhost}:(\d+)\z/)[1].to_i
end
end
print_status("autodetection failed, assuming default port of #{default_broker_port}")
default_broker_port
end
end
end

View File

@ -6,6 +6,7 @@ class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
include Msf::Auxiliary::Rocketmq
def initialize(info = {})
super(
@ -31,74 +32,20 @@ class MetasploitModule < Msf::Auxiliary
}
)
)
register_options([ Opt::RPORT(9876) ])
end
def get_version(id)
# from https://github.com/apache/rocketmq/blob/develop/common/src/4d82b307ef50f5cba5717d0ebafeb3cabf336873/java/org/apache/rocketmq/common/MQVersion.java
version_list = JSON.parse(File.read(::File.join(Msf::Config.data_directory, 'rocketmq_versions_list.json'), mode: 'rb'))
version_list.fetch(id, "UNKNOWN_VERSION_ID_#{id})")
end
def run_host(_ip)
# https://github.com/Malayke/CVE-2023-33246_RocketMQ_RCE_EXPLOIT/blob/e27693a854a8e3b2863dc366f36002107e3595de/check.py#L68
data = '{"code":105,"extFields":{"Signature":"/u5P/wZUbhjanu4LM/UzEdo2u2I=","topic":"TBW102","AccessKey":"rocketmq2"},"flag":0,"language":"JAVA","opaque":1,"serializeTypeCurrentRPC":"JSON","version":401}'
data_length = "\x00\x00\x00" + [data.length].pack('C')
header = "\x00\x00\x00" + [data.length + data_length.length].pack('C')
begin
connect
vprint_status('Sending request')
sock.send(header + data_length + data, 0)
res = sock.recv(1024)
rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
print_error("Unable to connect: #{e.class} #{e.message}\n#{e.backtrace * "\n"}")
elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
ensure
disconnect
end
res = send_version_request
if res.nil?
vprint_error('No response received')
print_error('Invalid or no response received')
return
end
unless res.include?('{')
vprint_error('Response contains unusable data')
return
end
# remove a response header so we have json-ish data
res = res[8..]
# we have 2 json objects appended to eachother, so we now need to split that out and make it usable
res = res.split('}{')
jsonable = []
# patch back in the { and }
res.each do |r|
r += '}' unless r.end_with?('}')
r = '{' + r unless r.start_with?('{')
jsonable.append(r)
end
parsed_data = {}
parsed_data = parse_rocketmq_data(res)
# grab some data that we need/want out of the response
jsonable.each do |j|
begin
res = JSON.parse(j)
rescue JSON::ParserError
vprint_error("Unable to parse json data: #{j}")
next
end
parsed_data['version'] = get_version(res['version']).gsub('_', '.') if res['version']
parsed_data['brokerDatas'] = res['brokerDatas'] if res['brokerDatas']
end
if parsed_data == {}
vprint_error('Unable to find version or other data within response.')
return
end
print_good("RocketMQ version #{parsed_data['version']} found with brokers: #{res['brokerDatas']}")
output = "RocketMQ version #{parsed_data['version']}"
output += " found with brokers: #{parsed_data['brokerDatas']}" if parsed_data['brokerDatas']
print_good(output)
end
end

View File

@ -0,0 +1,77 @@
require 'spec_helper'
RSpec.describe Msf::Auxiliary::Rocketmq do
subject do
mod = Msf::Auxiliary.new
mod.extend(Msf::Exploit::Remote::Tcp)
mod.extend(Msf::Auxiliary::Rocketmq)
mod
end
let(:mock_name_server_response) do
"\x00\x00\x00\xC7\x00\x00\x00\xC3{\"code\":105,\"extFields\":{\"Signature\":\"/u5P/wZUbhjanu4LM/UzEdo2u2I=\",\"topic\":\"TBW102\",\"AccessKey\":\"rocketmq2\"},\"flag\":0,\"language\":\"JAVA\",\"opaque\":1,\"serializeTypeCurrentRPC\":\"JSON\",\"version\":401}".b
end
let(:expected_name_server_response) do
"\x00\x00\x01a\x00\x00\x00_{\"code\":0,\"flag\":1,\"language\":\"JAVA\",\"opaque\":1,\"serializeTypeCurrentRPC\":\"JSON\",\"version\":403}{\"brokerDatas\":[{\"brokerAddrs\":{\"0\":\"172.16.199.135:10911\"},\"brokerName\":\"DESKTOP-8ATHH6O\",\"cluster\":\"DefaultCluster\"}],\"filterServerTable\":{},\"queueDatas\":[{\"brokerName\":\"DESKTOP-8ATHH6O\",\"perm\":7,\"readQueueNums\":8,\"topicSysFlag\":0,\"writeQueueNums\":8}]}".b
end
let(:expected_parsed_data_response) do
{
'brokerDatas' => [
{
'brokerAddrs' => {
'0' => '172.16.199.135:10911'
},
'brokerName' => 'DESKTOP-8ATHH6O',
'cluster' => 'DefaultCluster'
}
],
'version' => 'V4.9.5'
}
end
let(:mock_sock) { double :'Rex::Socket::Tcp', send: nil, recv: expected_name_server_response, close: nil, shutdown: nil }
before(:each) do
allow(subject).to receive(:connect).and_return(mock_sock)
allow(subject).to receive(:sock).and_return(mock_sock)
end
describe '#get_rocketmq_version' do
context 'correctly looks up id 401 as V4.9.4' do
it 'returns that version' do
expect(subject.get_rocketmq_version(401)).to eql('V4.9.4')
end
end
context 'correctly looks up id 99999 as UNKNOWN.VERSION.ID.99999' do
it 'returns that version' do
expect(subject.get_rocketmq_version(99999)).to eql('UNKNOWN.VERSION.ID.99999')
end
end
end
describe '#send_version_request' do
it 'returns version info' do
expect(mock_sock).to receive(:send).with(mock_name_server_response, 0)
expect(subject.send_version_request).to eq(expected_name_server_response)
end
end
describe '#parse_rocketmq_data' do
it 'correctly parses the response from the name server into version and brokeDatas info' do
expect(subject.parse_rocketmq_data(expected_name_server_response)).to eq(expected_parsed_data_response)
end
end
describe '#get_broker_port' do
it 'returns the broker port associated with the given rport in the name server response ' do
expect(subject.get_broker_port(expected_parsed_data_response, '172.16.199.135')).to eq(10911)
end
it 'returns the default broker port when rhost is not found in the name server response' do
expect(subject.get_broker_port(expected_parsed_data_response, '172.16.199.1', default_broker_port: 10000)).to eq(10000)
end
end
end