1. CSRF vs SSRF
์๋น์ค ๊ฐ HTTP ํต์ ์ด ์ด๋ค์ง ๋ ์์ฒญ ๋ด์ ์ด์ฉ์์ ์ ๋ ฅ๊ฐ์ด ํฌํจ๋ ์ ์๋๋ฐ, ์ด๋ ์ด์ฉ์์ ์ ๋ ฅ๊ฐ์ผ๋ก ์ธํด ๊ฐ๋ฐ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ด ์ ์ก๋ ์ ์๋ค.
Server-side Request Forgery(SSRF)๋ ์น ์๋น์ค์ ์์ฒญ์ ๋ณ์กฐํ๋ ์ทจ์ฝ์ ์ผ๋ก, ์๋ฒ ์ธก์์ ์์กฐ๋ HTTP ์์ฒญ์ ๋ฐ์์์ผ ์ง์ ์ ์ธ ์ ๊ทผ์ด ์ ํ๋ ์๋ฒ ๋ด๋ถ ์์์ ์ ๊ทผํ์ฌ ์ธ๋ถ๋ก ๋ฐ์ดํฐ ์ ์ถ ๋ฐ ์ค๋์์ ์ ๋ฐํ ์ ์๋ค.
๊ณต๊ฒฉํํ๋ง ๋ณด๋ฉด ์์กฐ๋ HTTP ์์ฒญ(Request Forgery)๋ฅผ ์ด์ฉํ ๊ณต๊ฒฉ์ด๊ธฐ ๋๋ฌธ์ CSRF(Cross Site Request Forgery)์ ์ ์ฌํ๋ค๊ณ ๋ณผ ์ ์์ผ๋ ๊ณต๊ฒฉ์์ ๊ณต๊ฒฉ์ด ๋ฐํ๋๋ ์ง์ ์ด ์๋ฒ ์ธก(Server Side)์ธ์ง ํด๋ผ์ด์ธํธ ์ธก(Client Side)์ธ์ง์ ์ฌ๋ถ์ ๋ฐ๋ผ์ ๊ณต๊ฒฉ ํํ๊ฐ ๊ตฌ๋ถ๋ ์ ์๋ค. CSRF๊ฐ ์ฌ์ฉ์์ ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ํ์ด์ฌํนํ์ฌ ์ฌ์ฉ์๋ก ํ์ฌ๊ธ ์ ์ฑ ์์ฒญ์ ์ํํ๊ฒ ๋ง๋ ๋ค๋ฉด, SSRF๋ ์ ๊ทผ์ด ์ ํ๋ ๋ด๋ถํ๊ฒฝ์ ์ถ๊ฐ ๊ณต๊ฒฉ(Post-Exploitation)์ด ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ ๊ณต๊ฒฉ์ ์ํฅ๋๊ฐ ๋์์ง ์๋ฐ์ ์๋ค.
์น ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ ํ๊ฒฝ์ด ํด๋ผ์ฐ๋ ์ธํ๋ผ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก MSA(๋ง์ดํฌ๋ก์๋น์ค)ํํ๋ก ๋ณํํ๋ฉด์ ์์คํ ๊ฐ ์ฐ๊ณ ๋ฐ ์ฌ์ฉ์ ๊ถํ๋ถ์ฌ ๋ฑ์ผ๋ก ์ธํด ๊ฐ๋ฐ ์ธํ๋ผ์ ์ํคํ ์ฒ๊ฐ ๋ณต์กํด์ง๋ฉด์ ๋ณด์ ์ด์๊ฐ ์ง์์ ์ผ๋ก ์ฆ๊ฐ๋์๋ค.
* MSA ๊ด๋ จ ๋ธ๋ก๊ทธ :https://wooaoe.tistory.com/57
( Monolithic Architecture๋ ์ํํธ์จ์ด์ ๋ชจ๋ ๊ตฌ์ฑ์์๊ฐ ํ ํ๋ก์ ํธ์ ํตํฉ๋์ด ์๋ ํํ๋ก, ํ๋๋ก ๋ฌถ์ฌ์๋? ๊ฐ๋ ์ด๋ฉด, MSA๋ ์๋น์ค๋ณ๋ก ๋ฐ๋ก๋ฐ๋ก ๋๋์ด์ ธ ์๊ณ API๋ฅผ ํตํด์๋ง ์ํธ์์ฉํ ์ ์๋ค. ์ ๋๋ก ์ดํดํ๋ฉด ๋ ๋ฏ.. ํ๋ ธ๋ค๋ฉด ์๋ ค์ฃผ์ธ์ )
์น ์๋น์ค๊ฐ ๋ณด๋ด๋ ์์ฒญ์ ๋ณ์กฐํ๊ธฐ ์ํด์๋ ์์ฒญ ๋ด์ ์ด์ฉ์์ ์ ๋ ฅ๊ฐ์ด ํฌํจ๋์ด์ผ ํ๋ค.
์ ๋ ฅ๊ฐ์ด ํฌํจ๋๋ ์์๋ก๋
1) ์น ์๋น์ค๊ฐ ์ด์ฉ์๊ฐ ์ ๋ ฅํ URL์ ์์ฒญ์ ๋ณด๋ด๊ฑฐ๋,
2) ์์ฒญ์ ๋ณด๋ผ URL์ ์ด์ฉ์ ๋ฒํธ์ ๊ฐ์ ๋ด์ฉ์ด ์ฌ์ฉ๋๋ ๊ฒฝ์ฐ,
3) ์ด์ฉ์๊ฐ ์ ๋ ฅํ ๊ฐ์ด HTTP Body์ ํฌํจ๋๋ ๊ฒฝ์ฐ๊ฐ ์๋ค.
2. ์ด์ฉ์๊ฐ ์ ๋ ฅํ 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)
image_downloader
(1) ์ด์ฉ์(ํด๋ผ์ด์ธํธ) - URL ์ ๋ ฅ
(2) ์๋ฒ - ์๋ต (์๋ต ๋ฐ์ดํฐ / HTTP ์๋ต ์ฝ๋)
์ด์ฉ์๊ฐ ์ ๋ ฅํ image_url์ requests.get ํจ์๋ฅผ ์ฌ์ฉํด GET ๋ฉ์๋๋ก HTTP ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐํํ๋ค.
๋ธ๋ผ์ฐ์ ์์ ๋ค์๊ณผ ๊ฐ์ URL์ ์ ๋ ฅํ๋ฉด ๋๋ฆผํต ํ์ด์ง์ ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐํํ๋ค.
http://127.0.0.1:8000/image_downloader?image_url=https://dreamhack.io/assets/dreamhack_logo.png
request_info
์น ํ์ด์ง์ ์ ์ํ ๋ธ๋ผ์ฐ์ ์ ์ ๋ณด(User-Agent)๋ฅผ ๋ฐํํ๋ค.
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4558.0 Safari/537.36
์ด๋, img_url์์ ์ ๋ ฅ๊ฐ์ผ๋ก request_info์ ๊ฒฝ๋ก๋ฅผ ์ ๋ ฅํ๋ค.
๊ทธ๋ผ image_downloader์์๋ http://127.0.0.1:8000/request_info URL์ HTTP ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐํํ๋ค.
http://127.0.0.1:8000/image_downloader?image_url=http://127.0.0.1:8000/request_info
๋ฐํํ ๊ฐ์ ํ์ธํด๋ณด๋ฉด ๋ธ๋ผ์ฐ์ ์ ๋ณด๊ฐ python-requests/<LIBRARY_VERSION>์ธ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ ์ํ ๋ธ๋ผ์ฐ์ ์ ๋ณด๋ก python-requests๊ฐ ์ถ๋ ฅ๋ ์ด์ ๋ ์น ์๋น์ค์์ HTTP ์์ฒญ์ ๋ณด๋๊ธฐ ๋๋ฌธ์ด๋ค.
์ด์ฒ๋ผ ์ด์ฉ์๊ฐ ์น ์๋น์ค์์ ์ฌ์ฉํ๋ ๋ง์ดํฌ๋ก์๋น์ค์ API ์ฃผ์๋ฅผ ์์๋ด๊ณ , image_url์ ์ฃผ์๋ฅผ ์ ๋ฌํ๋ฉด ์ธ๋ถ์์ ์ง์ ์ ๊ทผํ ์ ์๋ ๋ง์ดํฌ๋ก์๋น์ค์ ๊ธฐ๋ฅ์ ์์๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
3. ์น ์๋น์ค์ ์์ฒญ 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 ๊ฐ์ ๋ด๋ถ API์ URL ๊ฒฝ๋ก๋ก ์ฌ์ฉํ๋ค.
์ด์ฉ์๊ฐ ์์ ๊ฐ์ด user_idx๋ฅผ 1๋ก ์ค์ ํ๊ณ ์์ฒญ์ ๋ณด๋ผ ๋,
http://172.17.0.3/v1/api/user/information?user_idx=1
์น ์๋น์ค๋ ๋ค์๊ณผ ๊ฐ์ ์ฃผ์์ ์์ฒญ์ ๋ณด๋ธ๋ค.
http://172.17.0.3/user/1
user_search
์ด์ฉ์๊ฐ ์ ๋ฌํ user_name ๊ฐ์ ๋ด๋ถ API์ ์ฟผ๋ฆฌ๋ก ์ฌ์ฉํ๋ค.
์ด์ฉ์๊ฐ user_name์ “hello”๋ก ์ค์ ํ๊ณ ์์ฒญ์ ๋ณด๋ผ ๋,
http://172.17.0.3/v1/api/user/search?user_name=hello
์น ์๋น์ค๋ ๋ค์๊ณผ ๊ฐ์ ์ฃผ์์ ์์ฒญ์ ๋ณด๋ธ๋ค.
http://172.17.0.3/user/search?user_name=hello&user_type=public
์ด๋, ์น ์๋น์ค๊ฐ ์์ฒญํ๋ URL์ ์ด์ฉ์์ ์ ๋ ฅ๊ฐ์ด ํฌํจ๋๋ฉด ์์ฒญ์ ๋ณ์กฐํ ์ ์๋ค.
์ด์ฉ์์ ์ ๋ ฅ๊ฐ ์ค URL์ ๊ตฌ์ฑ ์์ ๋ฌธ์๋ฅผ ์ฝ์ ํ๋ฉด API ๊ฒฝ๋ก๋ฅผ ์กฐ์ํ ์ ์๋ค.
์๋ฅผ ๋ค์ด, ์์ ์ฝ๋์ user_info ํจ์์์ user_idx์ ../search๋ฅผ ์ ๋ ฅํ ๊ฒฝ์ฐ ์น ์๋น์ค๋ ๋ค์๊ณผ ๊ฐ์ URL์ ์์ฒญ์ ๋ณด๋ธ๋ค.
http://api.internal/search
..๋ ์์ ๊ฒฝ๋ก๋ก ์ด๋ํ๊ธฐ ์ํ ๊ตฌ๋ถ์๋ก, ํด๋น ๋ฌธ์๋ก ์์ฒญ์ ๋ณด๋ด๋ ๊ฒฝ๋ก๋ฅผ ์กฐ์ํ ์ ์๋ค. ํด๋น ์ทจ์ฝ์ ์ ๊ฒฝ๋ก๋ฅผ ๋ณ์กฐํ๋ค๋ ์๋ฏธ์์ Path Traversal์ด๋ผ๊ณ ๋ถ๋ฆฐ๋ค.
์ด ์ธ์๋, # ๋ฌธ์๋ฅผ ์ ๋ ฅํด ๊ฒฝ๋ก๋ฅผ ์กฐ์ํ ์ ์๋ค. ์๋ฅผ ๋ค์ด, user_search ํจ์์์ user_name์ secret&user_type=private#๋ฅผ ์ ๋ ฅํ ๊ฒฝ์ฐ ์น ์๋น์ค๋ ๋ค์๊ณผ ๊ฐ์ URL์ ์์ฒญ์ ๋ณด๋ธ๋ค.
http://api.internal/search?user_name=secret&user_type=private#&user_type=public
# ๋ฌธ์๋ Fragment Identifier ๊ตฌ๋ถ์๋ก, ๋ค์ ๋ถ๋ ๋ฌธ์์ด์ API ๊ฒฝ๋ก์์ ์๋ต๋๋ค.
๋ฐ๋ผ์ ํด๋น URL์ ์ค์ ๋ก ์๋์ ๊ฐ์ URL์ ๋ํ๋ธ๋ค.
http://api.internal/search?user_name=secret&user_type=private
4. ์น ์๋น์ค์ ์์ฒญ Body์ ์ด์ฉ์์ ์ ๋ ฅ๊ฐ์ด ํฌํจ๋๋ ๊ฒฝ์ฐ
# pip3 install flask
# python main.py
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 ํ์์ผ๋ก ๋ณํํ์ฌ ๋ฆฌํดํด์ค๋ค.
title /body ์ ๋ ฅ์ฐฝ
- title = hi / body = mnzy ์ ๋ ฅ
์์ฒญ์ ์ ์กํ ๋ ์ธ์ ์ ๋ณด๋ฅผ "guest"๋ก ์ค์ ํ๊ธฐ ๋๋ฌธ์ user๊ฐ "guest"์ธ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์์ ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด, ๋ด๋ถ API๋ก ์์ฒญ์ ๋ณด๋ด๊ธฐ ์ ์ ๋ค์๊ณผ ๊ฐ์ด ๋ฐ์ดํฐ๋ฅผ ๊ตฌ์ฑํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
data = f"title={title}&body={body}&user={session['idx']}
data = f"title={title}&body={body}&user={session['idx']}"
๋ฐ์ดํฐ๋ฅผ ๊ตฌ์ฑํ ๋ ์ด์ฉ์์ ์ ๋ ฅ๊ฐ์ธ title, body ๊ทธ๋ฆฌ๊ณ user์ ๊ฐ์ ํ๋ผ๋ฏธํฐ ํ์์ผ๋ก ์ค์ ํ๋ค.
์ด๋ก ์ธํด ์ด์ฉ์๊ฐ URL์์ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด ์ฌ์ฉํ๋ ๊ตฌ๋ถ ๋ฌธ์์ธ &๋ฅผ ํฌํจํ๋ฉด ์ค์ ๋๋ data์ ๊ฐ์ ๋ณ์กฐํ ์ ์๋ค.
title์์ title&user=admin๋ฅผ ์ฝ์ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด data๊ฐ ๊ตฌ์ฑ๋ฉ๋๋ค.
title=title&user=admin&body=body&user=guest
๋ด๋ถ API์์๋ ์ ๋ฌ๋ฐ์ ๊ฐ์ ํ์ฑํ ๋ ์์ ์กด์ฌํ๋ ํ๋ผ๋ฏธํฐ์ ๊ฐ์ ๊ฐ์ ธ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ user์ ๊ฐ์ ๋ณ์กฐํ ์ ์๋ค.๋ค. title&user=admin๋ฅผ ์ฝ์ ํ์ ๋์ ์คํ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํด๋ณด๋ฉด user๊ฐ "admin"์ผ๋ก ๋ณ์กฐ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
+)
- ์์ ๋๋ ํ ๋ฆฌ ์ฐํ : ../๋ก ๋ฃจํธ๋ก ์ด๋
- localhost services access
- ?url=http://localhost/server-status
- file system access
- ?url=file:///etc/passwd
- localhost services access
- ํ์ฅ์ ์ฐํ : %00 / ? / #
[์ฐธ๊ณ ]