SSRF는 웹 서비스의 요청을 변조하는 취약점으로, 브라우저가 변조된 요청을 보내는 CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청을 보낼 수 있다.
웹 서비스가 보내는 요청을 변조하기 위해서는 요청 내에 이용자의 입력값이 포함돼야 한다. 입력값이 포함되는 예시로는 웹 서비스가 이용자가 입력한 URL에 요청을 보내거나 요청을 보낼 URL에 이용자 번호와 같은 내용이 사용되는 경우, 그리고 이용자가 입력한 값이 HTTP Body에 포함되는 경우로 나눠볼 수 있다.
<이용자가 전달한 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_url을 requests.get 함수를 사용해 GET 메소드로 HTTP 요청을 보내고 응답을 반환한다.
http://127.0.0.1:8000/image_downloader?image_url= http://127.0.0.1:8000/request_info
이렇게 request_info와 같은 url로 요청을 보내면 외부에서 접근할 수 없는 마이크로서비스의 기능을 사용할 수 있다.
<웹 서비스의 요청 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_idx 값을 url에 포함시킨다.
http://x.x.x.x/v1/api/user/information?user_idx=1
여기서 user_idx에 ../search 같은 것을 입력하면 주소를 임의로 변경시킬 수 있다.
<웹 서비스의 요청 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)
글을 작성하고 보낼 때
data = f"title={title}&body={body}&user={session['idx']}
이런 식으로 보낸다.
title에서 title&user=admin 이런식으로 입력을 해버리면 user가 admin으로 전송된다.
'웹해킹' 카테고리의 다른 글
드림핵 Carve Party 롸업 (0) | 2023.11.05 |
---|---|
드림핵 web-ssrf 롸업 (0) | 2023.11.05 |
드림핵 file-download-1 롸업 (0) | 2023.11.05 |
드림핵 image-storage 롸업 (0) | 2023.11.05 |
ServerSide: File Vulnerability (0) | 2023.11.04 |