웹 개발 언어는 HTTP 요청을 전송하는 라이브러리를 제공한다. 각 언어별 HTTP 라이브러리는 PHP의 php-curl, NodeJS는 http, 파이썬은 urllib, requests를 예로 들 수 있다. 이러한 라이브러리는 HTTP 요청을 보낼 클라이언트 뿐만 아니라 서버와 서버간 통신을 위해 사용되기도 한다. 일반적으로 다른 웹 애플리케이션에 존재하는 리소스를 사용하기 위한 목적으로 통신한다. 예를 들어, 마이크로서비스 간 통신, 외부 API 호출, 외부 웹 리소스 다운로드 등이 있다.

기존의 웹 서비스는 단일 서비스로 구현할 수 있었지만 최근의 웹 서비스는 지원하는 기능이 증가함에 따라 구성요소가 증가했다. 이에 따라 관리 및 코드의 복잡도를 낮추기 위해 마이크로서비스들로 웹서비스를 구현하는 추세이다. 이때 각 마이크로서비스는 주로 HTTP, GRPC 등을 사용해 API 통신을 한다.

8662fcd667ad46cf5f63a47076ea9cd94ecc201dfd9126c360a3ce0a820c1375.png

이처럼 서비스 간 HTTP 통신이 이뤄질 때 요청 내에 이용자의 입력 값이 포함될 수 있다. 이용자의 입력 값으로 포함되면 개발자가 의도하지 않은 요청이 전송될 수 있다. **Server-Side Request Forgery (SSRF)**는 웹 서비스의 요청을 변조하는 취약점으로, 브라우저가 변조된 요청을 보내는 CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청을 보낼 수 있다.

최근의 대다수 서비스들은 마이크로서비스로 구조를 많이 바꾸고, 새롭게 개발하는 추세이기 때문에 SSRF 취약점의 파급력이 더욱 높아지고 있다.

마이크로서비스란? 마이크로서비스는 소프트웨어가 잘 정의된 API를 통해 통신하는 소규모의 독립적인 서비스로 구성되어 있는 경우의 소프트웨어 개발을 위한 아키텍처 및 조직적 접근 방법이다. 이러한 서비스는 독립적인 소규모 팀에서 보유한다.


Server-Side Request Forgery (SSRF)

Server-Side Request Forgery (SSRF)

웹 서비스는 외부에서 접근할 수 없는 내부망의 기능을 사용할 때가 있다. 내부망의 기능은 백오피스 서비스를 예로 들 수 있다. 백오피스 서비스는 관리자 페이지라고도 불리며, 이용자의 행위가 의심스러울 때 해당 계정을 정지시키거나 삭제하는 등 관리자만이 수행할 수 있는 모든 기능을 구현한 서비스이다. 이러한 서비스는 관리자만 이용할 수 있어야 하기 때문에 외부에서 접근할 수 없는 내부망에 위치한다.

웹 서비스는 의심스러운 행위를 탐지하고 실시간으로 대응하기 위해 백오피스의 기능을 실행할 수 있다. 다시 말해, 웹 서비스는 외부에서 직접 접근할 수 없는 내부망 서비스와 통신할 수 있다. 만약 공격자가 SSRF 취약점을 통해 웹 서비스의 권한으로 요청을 보낼 수 있다면 공격자는 외부에서 간접적으로 내부망 서비스를 이용할 수 있고, 이는 곧 기업에 막대한 피해를 입힐 수 있다.

웹 서비스가 보내는 요청을 변조하기 위해서는 요청 내에 이용자의 입력 값이 포함되어야 한다. 입력 값이 포함되는 예시로는 웹 서비스가 이용자가 입력한 URL에 요청을 보내거나 요청을 보낼 URL에 이용자 번호와 같은 내용이 사용되는 경우, 그리고 이용자가 입력한 값이 HTTP Body에 포함되는 경우로 나눠볼 수 있다.


이용자가 입력한 URL에 요청을 보내는 경우

분석

아래 코드는 이용자가 전달한 URL에 요청을 보내는 예제 코드이다. 코드를 살펴보면, 두 가지의 엔드포인트가 존재한다.

# pip3 install flask requests # 파이썬 flask, requests 라이브러리를 설치하는 명령입니다.
# python3 main.py # 파이썬 코드를 실행하는 명령입니다.

from flask import Flask, request
import requests

app = Flask(__name__)

@app.route("/image_downloader")
def image_downloader():
    # 이용자가 입력한 URL에 HTTP 요청을 보내고 응답을 반환하는 페이지 입니다.
    image_url = request.args.get("image_url", "") # URL 파라미터에서 image_url 값을 가져옵니다.
    response = requests.get(image_url) # requests 라이브러리를 사용해서 image_url URL에 HTTP GET 메소드 요청을 보내고 결과를 response에 저장합니다.
    return ( # 아래의 3가지 정보를 반환합니다.
        response.content, # HTTP 응답으로 온 데이터
        200, # HTTP 응답 코드
        {"Content-Type": response.headers.get("Content-Type", "")}, # HTTP 응답으로 온 헤더 중 Content-Type(응답 내용의 타입)
    )

@app.route("/request_info")
def request_info():
    # 접속한 브라우저(User-Agent)의 정보를 출력하는 페이지 입니다.
    return request.user_agent.string
    
    
app.run(host="127.0.0.1", port=8000)