본문 바로가기

클라우드·마이크로서비스 환경에서의 SSRF 탐지 및 방어 아키텍처 디자인

728x90

— URL 파싱 우회·메타데이터 노출·내부서비스 접근을 막기 위한 설계·운영·탐지 가이드

개요 · 배경

  • 정의: SSRF(Server-Side Request Forgery)는 애플리케이션이 사용자 입력 URL을 서버 측에서 요청하도록 허용할 때, 공격자가 이를 악용해 내부망·관리용 엔드포인트·링크로컬 메타데이터 등에 접근하게 되는 취약점입니다.
  • 주요 목표
    1. 클라우드/호스트 메타데이터 노출(예: 링크로컬 169.254.169.254, fe80::/10 대역 등)
    2. 내부 관리용 서비스 스캔 및 상호작용(예: 오케스트레이션/런타임/키-밸류 스토어/모니터링 에이전트/DB/메일·큐·캐시 등)
    3. 파일/토큰/자격증명 노출(file:// 등 비HTTP 스킴 허용 시)
    4. 프로토콜 스머글링(예: gopher://, dict://, FastCGI 등)로 RCE/상태 변조로 확장

공격면(Attack Surface) · 빈출 시나리오

  • URL 입력/프록시형 기능: 이미지 프록시, 웹훅 테스트, 미리보기(Preview), RSS/웹캠/건강체크, 서버-사이드 렌더링 등
  • 메타데이터 엔드포인트: 링크로컬(예: 169.254.169.254) 또는 내부 도메인으로 노출된 인스턴스/작업/자격증명/구성 정보
  • 내부 관리용 API: 오케스트레이션(제어 포트), 런타임 데몬(관리 소켓/포트), 키-밸류 스토어(기본 포트), 모니터링 에이전트(명령 실행 옵션), 메일/큐/캐시/검색 등
  • URL 파서 불일치: 언어/라이브러리별 @, #, \ 처리 차이, 인증정보/프래그먼트/역슬래시 혼동
  • 스킴 남용: file://, ftp://, ldap://, dict://, tftp://, gopher://, jar:, netdoc:// 등 런타임이 지원 시 악용

필터 우회 기법(요약 · 방어 관점에서 이해해야 할 포인트)

아래는 “이런 기법이 존재한다”는 수준의 인지용이며, 방어 설계 시 정규화·재검증이 필요한 이유를 설명합니다. (악용 목적의 상세 페이로드는 제외)

  1. 대상 표기 변조
    • localhost/127.0.0.1/::1/0.0.0.0 변형, 127.0.0.0/8 내 임의 IP, 축약형(http://0/, 127.1)
    • 십진(도트·무도트), 8진, 16진, 혼합 인코딩, 이중/삼중 URL 인코딩
  2. 리다이렉트 체인 악용
    • Whitelist 도메인 → 30x → 내부/메타데이터로 전환
    • 307/308로 메서드/바디 유지 리다이렉트
  3. DNS 기법
    • 동적 해석/재바인딩(한 번은 외부, 다음은 내부로 해석)
    • 서브도메인 패턴으로 내부 IP를 가리키는 서비스(예: <IP>.domain) 악용
  4. 파서 불일치·희귀 문자
    • \, @ 이중 사용, 프래그먼트 # 혼동, 유니코드 숫자/동그라미 문자
  5. 비HTTP 스킴
    • 런타임/프레임워크가 지원할 경우 파일 읽기/로컬 소켓/저수준 프로토콜 상호작용 발생

방어 설계(핵심 원칙 5가지)

  1. 아키텍처 우선: 서버가 임의 URL을 대신 호출하지 않게 설계
    • 가능하면 ID(식별자)만 입력받고, 서버 내부 매핑으로 실제 대상 결정
  2. 강력한 허용목록(Allowlist)
    • 스킴: http/https만. 나머지 전부 불허
    • 호스트: 필요한 정확 일치 FQDN만 허용(와일드카드 지양)
    • 포트: 80/443 등 필요한 범위만
  3. 정규화 후 검증(Normalize-then-Validate)
    • IDNA/Punycode, IPv6·포트 기본값, @/#/\ 제거, 이중 인코딩/유니코드 숫자 해제 후 검사
    • 리다이렉트 매 hop 최종 목적지까지 동일 정책 재검증
  4. 요청 제한
    • GET만(필요 시), 임의 헤더/바디 전송 금지, 타임아웃·재시도 제한
    • 응답 크기/타입 제한, 캐시/리소스 폭주 방지
  5. 네트워크 차단
    • 애플리케이션에서 내부망·메타데이터 대역으로 직접 이그레스 불가
    • Egress 전용 게이트웨이/프록시만 통과 → 게이트웨이에서 최종 도메인 재검증
300x250

안전한 구현 예시(요지) — 정규화·허용목록·리다이렉트 재검증

아래 코드는 개념 예시입니다. (사내 표준 라이브러리/헬퍼로 공통화 권장)

1. Python (요지)

import ipaddress, socket, urllib.parse, requests

ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_HOSTS   = {"api.example.corp", "assets.example.corp"}
ALLOWED_PORTS   = {80, 443}
BLOCKED_NETS = [
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("169.254.0.0/16"), # 링크로컬(메타데이터)
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),       # ULA
    ipaddress.ip_network("fe80::/10"),      # 링크로컬(IPv6)
]

def is_private(host):
    addrs = socket.getaddrinfo(host.encode("idna").decode(), None)
    for _, _, _, _, sa in addrs:
        ip = ipaddress.ip_address(sa[0])
        if any(ip in n for n in BLOCKED_NETS):
            return True
    return False

def safe_fetch(input_url, max_redirects=3):
    p = urllib.parse.urlsplit(input_url)
    if p.scheme.lower() not in ALLOWED_SCHEMES: raise ValueError("scheme")
    host = (p.hostname or "").lower()
    if host not in ALLOWED_HOSTS:               raise ValueError("host")
    port = p.port or (443 if p.scheme=="https" else 80)
    if port not in ALLOWED_PORTS:               raise ValueError("port")
    if is_private(host):                        raise ValueError("private")

    url = urllib.parse.urlunsplit((p.scheme, f"{host}:{port}", p.path or "/", "", ""))
    s = requests.Session()
    for _ in range(max_redirects+1):
        r = s.get(url, allow_redirects=False, timeout=5)
        if 300 <= r.status_code < 400 and "Location" in r.headers:
            loc = urllib.parse.urljoin(url, r.headers["Location"])
            q = urllib.parse.urlsplit(loc)
            h = (q.hostname or "").lower()
            prt = q.port or (443 if q.scheme=="https" else 80)
            if q.scheme not in ALLOWED_SCHEMES: raise ValueError("redir-scheme")
            if h not in ALLOWED_HOSTS:          raise ValueError("redir-host")
            if prt not in ALLOWED_PORTS:        raise ValueError("redir-port")
            if is_private(h):                   raise ValueError("redir-private")
            url = urllib.parse.urlunsplit((q.scheme, f"{h}:{prt}", q.path or "/", "", ""))
            continue
        return r
    raise ValueError("too-many-redirects")

2. 게이트웨이/리버스 프록시(아이디어)

  • 스킴 차단: file|ftp|ldap|dict|tftp|gopher|jar|netdoc
  • 내부 IP/링크로컬 패턴 차단: 127.*, 10.*, 172.16-31.*, 192.168.*, 169.254.*, ::1, fc00::/7, fe80::/10
  • 인코딩/역슬래시/이중 %25 탐지 시 고강도 검사 또는 차단
  • 리다이렉트 체인 최종 목적지 재검증(게이트웨이 레벨에서 강제)

네트워크·플랫폼 레벨 차단

1. 이그레스 기본 DENY(호스트 방화벽 개념 예시)

# 링크로컬(메타데이터)·루프백·사설 대역으로의 아웃바운드 차단
iptables -A OUTPUT -d 169.254.169.254 -j DROP
iptables -A OUTPUT -d 127.0.0.0/8     -j DROP
iptables -A OUTPUT -d 10.0.0.0/8      -j DROP
iptables -A OUTPUT -d 172.16.0.0/12   -j DROP
iptables -A OUTPUT -d 192.168.0.0/16  -j DROP

2. Egress 게이트웨이/프록시 경유 강제

  • 애플리케이션은 직접 외부로 나가지 못하고, 지정 게이트웨이만 통과
  • 게이트웨이에서 허용 도메인 집합·최종 목적지 재검증·로깅 수행

3. 메타데이터 접근 보호(일반 원칙)

  • 가능하면 비활성화 또는 토큰·헤더 기반 방어·홉 제한강제 정책
  • 앱 레벨에서 임의 헤더 주입 금지(메타데이터 보호 헤더 우회 차단)
  • 네트워크에서 링크로컬 경로 차단 및 필요한 워크로드만 프록시 경유

내부 서비스 악용(고급 기법) 대비

특정 제품명이 아닌 유형·행동 중심으로 방어 포인트만 제시합니다.

  • 키-밸류/캐시/검색: 기본 포트로 무인증 쓰기/명령이 가능한 경우 존재 → 네트워크 접근 차단, 바인드 주소 제한, 인증/ACL 필수
  • 메일/큐/전송 프로토콜: 저수준 프로토콜을 gopher:// 등으로 직접 구사 가능 → 스킴 차단 + 게이트웨이 레벨 프로토콜 필터링
  • 실행 게이트웨이/런타임: FastCGI/WSGI와 같은 로컬 실행 인터페이스 노출 시 RCE 전개 → 내부망 바인드 + 소켓 권한 + 외부 경로 차단
  • DNS: 내부 DNS로 존 전송 유도(AXFR) 가능성 → 권한 제한·전송 ACL, 로깅 및 비정상 트랜잭션 탐지

탐지·모니터링(벤더 중립)

1. SIEM/로그 룰 아이디어(개념 표현)

  • 목적지 IP/대역
    • 127.0.0.0/8, 169.254.0.0/16, 사설 대역 10/8, 172.16/12, 192.168/16 이그레스 시 경고
  • 도메인 패턴
    • 내부 IP를 지시하는 동적 서브도메인 패턴 또는 테스트 도메인 악용 징후
  • 포트/서비스
    • 관리용/제어용 포트(예: 오케스트레이션, 런타임, K/V 스토어, 모니터링 에이전트 등) 접근 탐지
  • 문자열 패턴
    • 무도트 십진/8진/16진 IP, \, @, #, 과도한 %25(이중 인코딩), 유니코드 숫자
  • 행위 패턴
    • 짧은 시간 내 30x 연속 리다이렉트 후 차단/실패 반복
    • 동일 파라미터에서 인코딩 변종 다수 시도

2. 엔드포인트/에이전트·프록시 로그

  • 링크로컬/사설 대역으로의 아웃바운드DROP하는 방화벽/프록시 로그를 상승도(중/높음)로 티켓화
  • 게이트웨이에서 허용목록 불일치/리다이렉트 최종지 차단 발생 시 알림

안전한 검증 절차

  1. 샌드박스 네트워크에서만 수행(격리·가짜 메타데이터·더미 내부서비스)
  2. 우회기법 카탈로그(표기/인코딩/리다이렉트/파서 불일치/비HTTP 스킴)를 전수 테스트
  3. 성공 기준
    • 비HTTP·내부·메타데이터 모두 차단
    • 리다이렉트 최종지 재검증 통과 실패
    • 게이트웨이 로그에 정상 차단·알림 생성

운영 체크리스트(역할별)

  1. 개발자
    • 외부 입력 URL 대신 리소스 ID 매핑
    • http/https·허용 FQDN·허용 포트만
    • 정규화 후 검증, 리다이렉트 매 hop 재검증
    • 임의 헤더·바디 금지, 타임아웃/응답 크기 제한
    • 에러/차단 사유 명확 로깅
  2. 플랫폼/인프라
    • Egress 기본 DENY, 게이트웨이 강제 경유
    • 링크로컬/사설 대역 라우팅·방화벽 차단
    • 메타데이터 접근 비활성화/제한/토큰·헤더 강제
    • 내부 관리용 서비스 로컬 바인드·인증·TLS/ACL
  3. 보안팀
    • SIEM 룰: 내부 대역 이그레스·리다이렉트 이상·인코딩 변종 탐지
    • 분기별 회귀 테스트: 런타임/라이브러리 업데이트 시 URL 파서 불일치 점검
    • 보안 게이트: 코드리뷰/아키텍처 리뷰에 SSRF 방어 항목 포함

예시(방어 검증용 · 안전한 방향의 샘플)

  1. 입력 정규화: IDNA, 디코딩 재귀 1회, 역슬래시 → 슬래시 치환 후 재파싱
  2. 목적지 검증: 스킴·호스트·포트 허용목록, DNS 해석된 모든 A/AAAA가 사설/링크로컬인지 검사
  3. 리다이렉트: 자동따라가기 금지 → 매 hop 수동 검증 후 허용 시에만 진행
  4. 네트워크: 애플리케이션 서브넷에서 링크로컬·사설 경로 불가, 게이트웨이만 통과
  5. 로그: 차단 사유(사설대역, 비허용 스킴, 리다이렉트 내부유도 등)와 파라미터 해시값만 기록(민감정보 최소화)

즉시 적용 스피드런

  1. 앱에 정규화→허용목록→리다이렉트 재검증 패턴 삽입
  2. 호스트/프록시에서 링크로컬·사설 대역 이그레스 DROP
  3. 메타데이터 접근 비활성/제한 + 헤더/토큰 강제
  4. 탐지 룰 배포(내부 대역 이그레스·인코딩 변종·리다이렉트 이상)
  5. 샌드박스 전수 테스트로 우회 기법 차단 확인
728x90
그리드형(광고전용)

댓글