FlawOpen

flawopen.com/Teardowns/cve-2026-104286-fortimail-ibe-path-traversal

● CVE-2026-104286 · CVSS 9.8[7] · 심각
보안 연구 · FlawOpen

CVE-2026-104286: FortiMail IBE 경로 순회

CVE-2026-104286은 인증되지 않은 HTTP(S) 요청의 경로 순회를 통해 취약한 FortiMail 시스템에 파일을 쓸 수 있게 합니다. Fortinet은 실제 악용을 보고했습니다. IBE를 끄거나 웹 접근을 제한하고 조사 후 업데이트하세요.[1], [5]

💡 알기 쉬운 설명 (ELI5)

직원이 첨부 파일을 정리하는 우편실을 떠올려 보세요. 이 결함은 요청이 잘못된 폴더 경로를 지정해 파일을 예상된 위치 밖에 쓰게 할 수 있습니다. 임시 조치는 IBE를 끄거나 웹 인터페이스 접근을 제한하는 것이며, 이후 업데이트하고 과거 침입 흔적도 조사해야 합니다.[1], [5]

핵심 개념 및 용어

경로 순회 (CWE-22)
신뢰할 수 없는 경로 구성 요소가 파일 작업의 허용 디렉터리를 벗어납니다. Fortinet은 영향받는 FortiMail에 임의 파일을 쓸 수 있다고 설명합니다.[1], [6]
Identity-Based Encryption (IBE)
FortiMail의 암호화 메일 워크플로 기능입니다. 공급업체의 임시 완화책은 IBE 비활성화이며 운영 영향을 검토해야 합니다.[1], [5]
CISA KEV
CISA는 2026년 10월 1일 CVE-2026-104286을 KEV에 추가했습니다. 이는 알려진 악용을 뜻하며 특정 장비의 침해를 증명하지는 않습니다.[2], [5]
버전 범위 불일치
Fortinet 및 정부 요약은 7.2, 7.4, 7.6, 8.0을 나열하지만 CNA 구조 데이터에는 7.0도 있습니다. 7.0은 Fortinet에 확인하세요.[6], [5], [4]

프레임워크 매핑

근본 원인 분석 (Root Cause)

Fortinet은 CVE-2026-104286을 인증 없이 HTTP 또는 HTTPS 요청으로 임의 파일을 쓸 수 있는 경로 순회 취약점으로 설명합니다. FortiMail 펌웨어는 비공개이며 공개 자문에는 취약한 소스 코드나 패치 diff가 없습니다. 이 글은 이를 추측하지 않습니다. 공개 정부 자문은 임의 파일 쓰기 영향, 악용 및 버전 안내를 확인하지만 FortiMail 내부 핸들러는 설명하지 않습니다. IBE 비활성화 또는 웹 접근 제한 임시 조치와 공개 IOC는 Fortinet 자문을 근거로 합니다. 장비 접근을 제한해 조사한 뒤 해당 수정 버전을 적용하세요. IOC가 없다고 안전한 것은 아닙니다. CVE 레코드의 설명문은 7.2.0–7.2.9, 7.4.0–7.4.8, 7.6.0–7.6.6, 8.0.0–8.0.1을 나열합니다. 구조화 버전 표는 7.4와 7.6의 상한을 더 낮게 표시하고 7.0.0–7.0.9도 추가합니다. 같은 레코드의 solutions 항목은 더 이른 “예정” 버전인 7.4.8, 7.6.6, 8.0.1, 7.2.10을 제시해 영향 버전 데이터 및 10월 1일 캐나다 권고와 충돌합니다. 날짜가 명시된 정부 권고를 공개된 운영 지침으로 사용하고, 현재 제공 여부와 7.0 범위는 Fortinet에 확인하세요. 상충하는 CNA 항목만으로 수정 버전을 추정하지 마세요.[1], [3], [5], [4], [6], [7]

운영 매트릭스: 버전 범위와 초기 대응

영역공식 출처의 설명방어자 조치
FortiMail 7.0NVD/CNA 데이터에는 7.0.0–7.0.9가 포함되지만 캐나다와 프랑스의 공식 요약에는 7.0 분기가 없습니다. 같은 레코드의 설명과 표는 7.4 및 7.6의 상한도 서로 다릅니다. solutions 항목도 영향 범위와 캐나다 권고에 어긋나는 더 낮은 “예정” 빌드(7.4.8, 7.6.6, 8.0.1, 7.2.10)를 나열합니다.Fortinet에 해당 분기 상태를 확인하고, 요약에 없다는 이유로 안전하다고 추정하지 마세요.
FortiMail 7.2캐나다 권고는 7.4 분기 이상으로 업그레이드하도록 안내합니다.지원되는 수정 완료 분기로 이전을 계획하고 7.2 패치가 있다고 가정하지 마세요.
FortiMail 7.4 / 7.6 / 8.0캐나다 권고는 각각 7.4.9, 7.6.7, 8.0.2를 수정 버전으로 제시합니다.공개된 빌드 지침으로 캐나다 권고 AV26-989를 사용하고, 업데이트 전에 Fortinet 최신 자문에서 제공 여부와 지원을 확인하세요. 확인 없이 더 낮은 CNA 기준을 적용하지 마세요.
차단 및 조사Fortinet은 악용을 보고하고 침해 지표를 공개했습니다. 임시 조치는 IBE 비활성화 또는 웹 접근 제한입니다.증거를 보존하고 임시 조치를 적용한 뒤 지표를 확인하고, 패치와 침해 여부 조사를 진행하세요.

단계별 공격 실행 흐름

단계 1

인증되지 않은 요청이 웹 흐름에 도달

Fortinet은 특수 제작된 HTTP 또는 HTTPS 요청이 인증 없이 결함을 유발할 수 있다고 설명합니다. 공개 자문에는 내부 핸들러가 공개되지 않았습니다.[1], [5], [4]

단계 2

경로가 의도된 경계를 벗어남

공급업체는 이를 경로 순회(CWE-22)로 분류하며 영향을 받은 FortiMail에 파일을 쓸 수 있다고 설명합니다. CVE 설명만으로 구체적 지속성 방식을 단정하지 마세요.[1], [6], [5]

단계 3

기록된 파일이 추가 영향을 일으킬 수 있음

Fortinet은 CVSS 3.1 9.8 Critical로 평가하고 실제 악용을 보고했습니다. 조사에는 최신 공급업체 IOC를 사용하세요.[1], [5], [4]

단계 4

완화하고 증거를 보존한 뒤 업데이트

IBE를 끄거나 웹 접근을 신뢰 네트워크로 제한하세요. 로그와 구성을 보존하고 IOC를 점검한 뒤 제품 분기에 맞는 수정 버전을 설치하세요.[1], [5]

소스 코드 비교: 취약한 구현 vs 보안 패치

FortiMail 펌웨어 소스와 공급업체 패치 diff는 공개되지 않았으므로, 이 코드는 일반적인 Python 방어 패턴이며 Fortinet 코드나 취약점 재현이 아닙니다. 신뢰할 수 있는 디렉터리를 한 번 열고 서버가 생성한 불투명한 파일명을 사용하며, 배타적 생성 및 링크 미추적 플래그를 지정해 해당 디렉터리 디스크립터를 기준으로 새 파일을 만듭니다. POSIX 전용이며 필요한 플래그가 없으면 안전하게 실패합니다. 중첩 경로는 디렉터리 디스크립터로 검증된 구성요소를 하나씩 탐색하고, 문자열 경로를 검사한 뒤 이름으로 다시 여는 방식을 피하세요. CVE-2026-104286은 Fortinet이 지원하는 업데이트 또는 공식 임시 완화책으로 해결하세요. 설정된 루트 경로의 모든 구성요소와 상위 디렉터리는 관리자 통제하에 있어야 합니다. 루트를 열 때 O_NOFOLLOW는 마지막 구성요소만 보호합니다.

취약한 구현
from pathlib import Path

def destination(root: Path, request_path: str) -> Path:
    # 취약한 설계 예시: 요청 입력을 결합한 뒤
    # 허용된 루트 안에 있는지 확인하지 않습니다.
    return root / request_path
보안 강화 패치
import os
import uuid

# 신뢰할 수 있는 서비스 디렉터리를 한 번 엽니다. 상위 경로 디렉터리도 관리자가 통제해야 합니다.

def open_upload_root(root):
    required = ("O_DIRECTORY", "O_NOFOLLOW")
    if not all(hasattr(os, flag) for flag in required) or os.open not in os.supports_dir_fd:
        raise RuntimeError("POSIX directory protections unavailable")
    flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
    return os.open(root, flags)

# 요청 경로를 파일 경로로 바꾸지 말고 불투명한 이름을 생성합니다.
# 이름이 이미 있으면 O_EXCL이 실패하고 O_NOFOLLOW가 마지막 심볼릭 링크를 거부합니다.
ROOT_FD = open_upload_root("/srv/app-private/uploads")


def create_upload_file():
    name = uuid.uuid4().hex
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
    fd = os.open(name, flags, 0o600, dir_fd=ROOT_FD)
    return name, fd

엔지니어링 및 시스템 보안 강화 체크리스트

출처