본문 바로가기

SSRF 본문

개념 정리

SSRF

Seongjun_You 2022. 6. 2. 01:38

서비스 간 HTTP 통신이 이뤄질 때 요청 내에 이용자의 입력값이 포함될 수 있습니다.

이용자의 입력값으로 포함되면 개발자가 의도하지 않은 요청이 전송될 수 있습니다.

SSRF는 웹 서비스의 요청을 변조하는 쥐약점으로, 브라우저가 변조된 요청을 보내는

CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청을 보낼 수 있습니다.

 

웹 서비스는 외부에서 접근할 수 없는 내부망의 기능을 사용할 때가 있습니다.

예를 들어 백오피스 서비스가 있습니다. 백오피스 서비스는 관리자 페이지라고도 불리며,

이용자의 행위가 의심스러울 때 해당 계정을 정지시키거나 삭제하는 등 관리자만이

수행할 수 있는 모든 기능을 구현한 서비스입니다. 이러한 서비스는 관리자만 이용할 수 있어야

하기 때문에 외부에서 접근할 수 없는 내부망에 위치합니다.

 

다시  말해, 웹 서비스는 외부에서 직접 접근할 수 없는 내부망 서비스와 통신할 수 있습니다.

만약 공격자가 SSRF 취약점을 통해 웹 서비스의 권한으로 요청을 보낼 수 있다면

공격자는 외부에서 간접적으로 내부망 서비스를 이용할 수 있고, 이는 곧 막대한 피해를 입힐 수 있습니다.

 

웹 서비스가 보내는 요청을 변조하기 위해서는 요청 내에 이용자의 입력값이 포함돼야 합니다.

입력값이 포함되는 예시로는 웹 서비스가 이용자가 입력한 URL에 요청을 보내거나 요청을 보낼 URLDP

이용자 번호화 같은 내용 시 사용되는 경우, 그리고 이용자가 입력한 값이 HTTP Body에 포함되는 경우로

나누어볼 수 있습니다.

 

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

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)

/image_downloader

이용자가 입력한 image_url정보를 GET메소드로 HTTP 요청을 보내고 응답을 반환합니다.

http://127.0.0.1:8000/image_downloader?image_url=https://dreamhack.io/assets/dreamhack_logo.png

위와 같이 URL입력시 드림핵 페이지에 요청을 보내고 응답을 반환합니다.

 

/request_info

웹 페이지에 접속한 브라우저의 정보를 반환합니다.

브라우저를 통해 해당 엔드포인트에 접근하면 접속하는 데에 사용된 브라우저의 정보가 출력됩니다.

 

image_url에 request_info 경로를 입력하게되면???

반환 값을 확인하면 python-requests가 출력됩니다. 이유는 웹 서비스에서 http요청을 보냈기 때문입니다.

이처럼 이용자가 웹 서비스에서 사용하는 마이크로서비스의 API 주소를 알아내고,

image_url에 주소를 전달하면 외부에서 직접 접근할 수 없는 마이크로서비스의 기능을 사용할 수 있습니다.

 

 

2.  웹 서비스의 요청 URL에 이용자의 입력값이 포함된 경우

INTERNAL_API = "http://api.internal/"
# INTERNAL_API = "http://172.17.0.3/"
@app.route("/v1/api/user/information")
def user_info():
	user_idx = request.args.get("user_idx", "")
	response = requests.get(f"{INTERNAL_API}/user/{user_idx}")
@app.route("/v1/api/user/search")
def user_search():
	user_name = request.args.get("user_name", "")
	user_type = "public"
	response = requests.get(f"{INTERNAL_API}/user/search?user_name={user_name}&user_type={user_type}")

/user_info

이용자가 전달한 user_idx값을 내부 APU의 URL 경로로 사용합니다.

http://x.x.x.x/v1/api/user/information?user_idx=1

위와 같이 user_idx를 1로 설정하고 요청을 보내면 웹 서비스는 다음과 같은 주소에 요청을 보냅니다.

http://api.internal/user/1

 

/user_search

이용자가 전달한 user_name 값을 내부 API의 쿼리로 사용합니다.

user_name을 hello로 설정하고 요청을 보냅니다.

http://api.internal/user/search?user_name=hello&user_type=public

문제점 확인

웹 서비스가 요청하는 URL에 이용자의 입력값이 포함되면 요청을 변조할 수 있습니다.

user_info함수에서 user_idx에 ../search를 입력할 경우 웹 서비스는 다음과 같은 URL에

요청을 보냅니다.

http://api.internal/search

#문자를 입력해 경로를 조작할 수 있습니다.

#은 Fragment Identifier 구분자로, 뒤에 붙는 문자열은 API경로에서 생략됩니다.

http://api.internal/search?user_name=secret&user_type=private#&user_type=public

 

3. 웹 서비스의 요청 Body에 이용자의 입력값이 포함되는 경우

from flask import Flask, request, session
import requests
from os import urandom
app = Flask(__name__)
app.secret_key = urandom(32)
INTERNAL_API = "http://127.0.0.1:8000/"
header = {"Content-Type": "application/x-www-form-urlencoded"}
@app.route("/v1/api/board/write", methods=["POST"])
def board_write():
    session["idx"] = "guest" # session idx를 guest로 설정합니다.
    title = request.form.get("title", "") # title 값을 form 데이터에서 가져옵니다.
    body = request.form.get("body", "") # body 값을 form 데이터에서 가져옵니다.
    data = f"title={title}&body={body}&user={session['idx']}" # 전송할 데이터를 구성합니다.
    response = requests.post(f"{INTERNAL_API}/board/write", headers=header, data=data) # INTERNAL API 에 이용자가 입력한 값을 HTTP BODY 데이터로 사용해서 요청합니다.
    return response.content # INTERNAL API 의 응답 결과를 반환합니다.
@app.route("/board/write", methods=["POST"])
def internal_board_write():
    # form 데이터로 입력받은 값을 JSON 형식으로 반환합니다.
    title = request.form.get("title", "")
    body = request.form.get("body", "")
    user = request.form.get("user", "")
    info = {
        "title": title,
        "body": body,
        "user": user,
    }
    return info
@app.route("/")
def index():
    # board_write 기능을 호출하기 위한 페이지입니다.
    return """
        <form action="/v1/api/board/write" method="POST">
            <input type="text" placeholder="title" name="title"/><br/>
            <input type="text" placeholder="body" name="body"/><br/>
            <input type="submit"/>
        </form>
    """
app.run(host="127.0.0.1", port=8000, debug=True)

/board_write

이용자의 입력값을 HTTP Body에 포함하고 내부 API로 요청을 보냅니다.

전송할 데이터를 구성할 때 세션 정보를 guest 계정으로 설정합니다.

 

/internal_board_write

board_write함수에서 요청하는 내부 API를 구현한 기능입니다.

전달된 title, body 그리고 계정 이름을 JSON 형식으로 변환 후 반환합니다.

 

/index

board_write 기능을 호출하기 위한 인덱스 페이지입니다.

 

문제점 확인

URL에서 파라미터를 구분하기 위해 사용하는 구분 문자인 &를 이용해

data의 값을 변조할 수 있습니다.

title=title&user=admin&body=body&user=guest

다음과 같이 전달 시

{ "body": "body", "title": "title", "user": "admin" }

user가 변조된 것을 알 수 있습니다.

'개념 정리' 카테고리의 다른 글

XSS Filtering Bypass - 1  (0) 2022.06.04
NoSQL Injection  (0) 2022.06.02
File Vulnerability  (0) 2022.05.31
CSRF  (0) 2022.05.30
XSS  (0) 2022.05.30
Comments