← 목록으로

SSRF (Server-Side Request Forgery): 서버가 대신 요청하게 만들기

[Web] Web · 작성: 2026-07-19 19:15:17 · 수정: 2026-07-21 22:53:06 · 조회 21

SSRF를 "서버가 대신 요청을 보내는 취약점"이라고만 알면, 왜 방화벽으로 외부망을 다 막아둔 서버에서도 SSRF가 치명적인지, 왜 169.254.169.254라는 이상한 IP 하나가 클라우드 전체를 무너뜨리는 단초가 되는지를 설명할 수 없다. 이 글은 DNS 조회부터 커널의 소켓 연결(connect) 시스템 콜, 그리고 클라우드 메타데이터 서비스까지 파고든다.

왜 SSRF가 성립하는가: URL 하나가 거치는 두 번의 신뢰 전환

서버가 사용자 입력을 받아 "대신 요청"을 보내는 기능(웹훅 URL 등록, 이미지 프록시, PDF 렌더링을 위한 외부 리소스 가져오기, URL 미리보기)이 있다면 대략 이런 코드가 된다.

// 예시: URL 미리보기 기능 (이 블로그엔 없음, 설명용 예시)
const res = await axios.get(userProvidedUrl);

여기서 신뢰가 두 번 전환된다. 첫 번째는 "사용자 입력(문자열)"이 "서버가 실행할 네트워크 요청"으로 바뀌는 지점 — SQLi가 "데이터→명령"으로 바뀌는 것과 같은 구조다. 두 번째가 SSRF만의 고유한 지점인데, 이 요청이 서버 자신의 네트워크 위치(내부망, 클라우드 메타데이터, localhost)에서 실행된다는 것 — 즉 공격자는 자기 브라우저가 아니라 서버의 네트워크 신뢰 위치를 그대로 빌려 쓰는 셈이다.

런타임 레벨: axios.get(url) 한 줄이 실제로 거치는 4단계

  1. URL 파싱http://169.254.169.254/latest/meta-data/ 문자열을 스킴/호스트/포트/경로로 분해
  2. DNS 조회 (getaddrinfo 시스템 콜) — 호스트가 도메인이면 OS의 리졸버가 IP로 변환. IP를 직접 넣으면 이 단계가 생략된다는 게 중요한 우회 포인트가 된다(아래 참고)
  3. TCP 연결 (connect() 시스템 콜) — Node의 libuv가 커널에게 "이 IP:포트로 3-way handshake를 시작하라"고 요청. 이 시점부터는 완전히 커널 네트워크 스택의 라우팅 테이블이 개입한다 — 목적지 IP가 링크-로컬(169.254.0.0/16)이면, 게이트웨이를 거치지 않고 같은 네트워크 인터페이스 안에서 바로 응답하는 서비스로 연결된다. AWS/GCP/Azure의 메타데이터 서비스가 정확히 이 대역(169.254.169.254)을 쓰는 이유가 여기 있다 — 일반적인 아웃바운드 방화벽 규칙(0.0.0.0/0 차단 등)은 대개 "외부로 나가는" 트래픽을 겨냥하지, 인터페이스 로컬 스코프인 링크-로컬 주소는 애초에 라우팅되어 나가는 트래픽이 아니라서 방화벽 정책 대상에서 빠지는 경우가 많다.
  4. HTTP 요청/응답 — 연결이 수립되면 평문 HTTP 요청을 보내고 응답을 그대로 애플리케이션에 반환

왜 블랙리스트 필터링이 구조적으로 실패하는가: DNS Rebinding (TOCTOU)

"127.0.0.1, 169.254.169.254, 10.0.0.0/8 같은 사설 IP는 막아야지"라는 접근은 아래 두 가지로 쉽게 뚫린다.

① 인코딩 우회 — IP는 여러 표기법이 있다. 127.0.0.1을 10진수 정수로 쓰면 2130706433이고, 8진수로는 0177.0.0.1, 축약형으로는 127.1도 유효한 IP 표기다. 문자열 매칭 필터는 이 모든 변형을 다 알아야 하는데, getaddrinfo/inet_aton 같은 OS 레벨 파서는 이 표기법들을 전부 정상적으로 127.0.0.1로 해석해버린다 — 애플리케이션의 필터는 문자열을 보고, 실제 연결은 커널의 파서가 그 문자열을 재해석한 값으로 이뤄지기 때문에 둘 사이에 괴리가 생긴다.

② DNS Rebinding (진짜 TOCTOU) — 이게 더 근본적이다.

  1. 애플리케이션이 evil.com을 검사 시점에 조회 → 정상 공인 IP(1.2.3.4) 응답 → "안전하다" 판정
  2. 판정과 실제 연결 사이의 아주 짧은 시간차 동안, 공격자가 자기 DNS 서버의 TTL을 0으로 설정해뒀기 때문에 애플리케이션이 실제로 connect()를 호출하는 시점엔 DNS가 다시 조회되며 이번엔 127.0.0.1을 반환
  3. 검사는 1번 IP로, 실제 연결은 2번 IP로 — Time-Of-Check-Time-Of-Use(TOCTOU) 레이스 컨디션이 네트워크 계층에서 그대로 재현된 것이다. pwntools 실전이나 Race Condition 글에서 다룬 TOCTOU와 본질적으로 동일한 취약점 패턴이 SSRF 방어에서도 그대로 나타난다는 게 이 취약점의 핵심 통찰이다.

페이로드가 서버에서 실제로 작동하는 순서 (클라우드 메타데이터 탈취)

  1. 공격자가 SSRF 가능한 파라미터에 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 삽입
  2. 서버 프로세스가 axios.get() 호출 → DNS 조회 생략(이미 IP) → 커널이 링크-로컬 스코프로 즉시 connect()
  3. AWS IMDSv1(토큰 없는 구버전)은 별도 인증 없이 이 요청만으로 응답 — 이 서버에 붙어있는 IAM 역할 이름을 반환
  4. 이어서 .../iam/security-credentials/<역할이름> 요청 → 임시 AccessKey/SecretKey/SessionToken이 평문 JSON으로 반환
  5. 애플리케이션이 이 응답 바디를 사용자에게 그대로 돌려주거나(응답 반영형 SSRF), 에러 메시지 등으로 일부라도 노출되면 공격자는 이 자격증명을 그대로 자기 AWS CLI에 넣어 그 IAM 역할이 가진 모든 권한(S3 버킷, 다른 인스턴스, 시크릿 매니저)을 그대로 행사할 수 있다

여기서 끝나지 않는다: SSRF 체이닝

1) 클라우드 메타데이터 → 전체 계정 장악

위 시나리오처럼 얻은 IAM 임시 자격증명으로 s3:ListBucket, ec2:DescribeInstances, 심하면 iam:* 권한까지 있다면 웹 애플리케이션 하나의 SSRF가 클라우드 계정 전체의 침해로 확장된다. 실제 대형 침해 사고(2019년 Capital One 사건 등)의 전형적인 패턴이 정확히 이 체인이다.

2) 내부망 포트 스캔 → Redis 무인증 RCE

SSRF는 서버가 도달 가능한 내부망 전체를 "대신 스캔"할 수 있게 해준다. http://10.0.0.5:6379/처럼 다양한 IP:포트를 순회시켜 응답 시간/에러 패턴 차이로 열린 포트를 찾아낸 뒤, 인증 없이 열려있는 Redis를 발견하면 SSRF로 만든 HTTP 요청 스킴을 살짝 비틀어(gopher:// 스킴 지원 클라이언트라면) Redis 프로토콜 명령을 직접 주입할 수 있다.

gopher://internal-redis:6379/_CONFIG%20SET%20dir%20/var/www/html%0d%0aCONFIG%20SET%20dbfilename%20shell.php%0d%0aSET%20x%20%22%3C%3Fphp%20system(%24_GET%5B%27c%27%5D)%3B%3F%3E%22%0d%0aSAVE%0d%0a

Redis가 SAVE 시점에 디스크에 실제로 write() 시스템 콜을 호출해 웹 루트에 PHP 웹쉘 파일을 그대로 써버리면, SSRF 하나가 내부망에 격리되어 외부에서 직접 닿지 않던 서버의 원격 코드 실행으로 끝난다 — 방화벽이 "외부→내부"는 막아도 "내부 서버 자신이 만드는 내부 요청"은 막지 못한다는 SSRF의 본질이 여기서 극명하게 드러난다.

3) file:// 스킴과 결합 — SSRF가 LFI가 되는 순간

URL을 받는 라이브러리가 http/https뿐 아니라 file:// 스킴까지 지원하도록 구현되어 있다면, file:///etc/passwdfile:///Users/martina/webapp/.env처럼 네트워크 요청이 아니라 로컬 파일 읽기로 전환된다. 이 순간 SSRF는 사실상 LFI 글에서 다룬 Path Traversal/LFI와 동일한 결과를 만든다 — 같은 "서버가 사용자 대신 리소스를 가져온다"는 원리가 스킴만 바뀌었을 뿐 그대로 재현된 것.

4) 리다이렉트 체이닝으로 필터 우회

http://공격자-서버.com/redirect처럼 필터를 통과하는 정상 URL을 최초 요청으로 넣고, 그 서버가 302 Location: http://169.254.169.254/...로 리다이렉트하게 만들면, HTTP 클라이언트 라이브러리가 리다이렉트를 자동으로 따라가는 기본 동작을 이용해 최초 검증을 통째로 우회할 수 있다. axios/node-fetch가 기본적으로 maxRedirects를 자동 처리한다는 점이 여기서 공격 표면이 된다.

탐지와 방어 (요약)

SSRF를 "서버가 대신 요청하는 문제"가 아니라 **"애플리케이션의 신뢰 판단(필터)과 커널의 실제 네트워크 연결(라우팅 테이블, connect 시스템 콜) 사이에 시간차와 해석 차이가 존재한다는 구조적 틈"**으로 이해하면, 왜 화이트리스트만이 근본 해법인지, 그리고 왜 이 취약점 하나가 클라우드 계정 전체 장악부터 내부망 RCE까지 이어지는 가장 파급력 큰 체이닝의 출발점이 되는지가 같은 논리로 설명된다.

Web 카테고리의 글 (5/12)

  1. SQL Injection: 원리, 탐지, 방어
  2. XSS (Cross-Site Scripting): Stored / Reflected / DOM 차이와 방어
  3. CSRF (Cross-Site Request Forgery): 원리와 방어
  4. IDOR / BOLA: 권한 검증이 빠졌을 때
  5. SSRF (Server-Side Request Forgery): 서버가 대신 요청하게 만들기
  6. 파일 업로드 취약점: 웹쉘로 이어지는 경로
  7. 안전하지 않은 역직렬화 (Insecure Deserialization)
  8. SSTI (Server-Side Template Injection): 원리와 탐지
  9. OS Command Injection: 원리와 필터 우회
  10. JWT 취약점: alg=none부터 알고리즘 컨퓨전까지
  11. Race Condition (TOCTOU) 취약점
  12. Path Traversal / LFI (Local File Inclusion): 커널 경로 탐색부터 RCE 체이닝까지
← IDOR / BOLA: 권한 검증이 빠졌을 때 파일 업로드 취약점: 웹쉘로 이어지는 경로 →