1. ServerSide: SSRF
웹 개발 언어는 HTTP 요청을 전송하는 라이브러리를 제공한다. PHP는 php-curl, NodeJS는 http, 파이썬은 urllib, request를 예시로 들 수 있다. 이러한 라이브러리는 HTTP 요청을 보낼 클라이언트뿐만 아니라 서버와 서버간의 통신을 위해 사용되기도 한다. 일반적으로 다른 웹 애플리케이션에 존재하는 리소스를 사용하기 위한 목적으로 통신한다. 예를 들어, 마이크로 서비스 간 통신, 외부 API 호출, 외부 웹 리소스 다운로드 등이 있다.
• 마이크로서비스: 소프트웨어가 잘 정의된 API를 통해 통신하는 소규모의 독립적인 서비스로 구성되어 있는 경우의 소프트웨어 개발을 위한 아키텍처 및 조직적 접근 방식
기존의 웹 서비스는 단일 서비스로 구현했지만 최근의 웹 서비스는 지원하는 기능이 증가함에 따라 구성요소가 증가했다. 이에따라 관리 및 복잡도를 낮추기위해 마이크로 서비스들로 웹 서비스를 구현하는 추세이다. 각 마이크로 서비스는 주로 HTTP, GRPC 등을 사용해 API 통신을 한다.
서비스 간의 HTTP 통신에서 이용자의 입력 값이 포함될 수 있는데 SSRF는 웹 서비스의 요청을 변조하는 취약점으로 웹 서비스의 권한으로 변조된 요청을 보낸다.
1.1 Server Side Request Forgery (SSRF)
웹 서비스는 외부에서 접근할 수 없는 내부망의 기능을 사용할 때가 있다. 내부망의 기능은 백오피스 서비스를 예로 들 수 있다. 백오피스 서비스는 관리자 페이지와 같이 외부에서 접근할 수 없는 내부망에 위치하는데 입력 받는 부분이 취약할 경우 공격자는 SSRF 취약점을 통해 웹 서비스의 권한으로 요청을 보내 간접적으로 내부망을 사용할 수 있다.
1.1.1. 이용자가 입력한 URL에 요청을 보내는 경우
예를 들어 이미지를 불러오는 코드와 브라우저 정보를 반환하는 코드가 다음과 같을 때 발생할 수 있는 취약점이다. 공격자는 image_url에 웹 브라우저 정보를 요청하는 엔드포인트 경로를 입력할 수 있다. image_url=http://127.0.0.1:8000/request_info
1. image_downloader
https://dreamhack.io/assets/dreamhack_logo.png>
2. request_info
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
1.1.2 웹 서비스의 요청 URL에 이용자의 입력값이 포함되는 경우
예를 들어 이용자가 전달한 user_idx, user_name의 값을 내부의 API의 URL 경로로 사용한다면 요청을 변조할 수 있다. 이용자의 입력값 중 URL의 구성요소 문자를 사용하거나 ../를 이용한 Path Traversal으로 경로를 조작할 수 잇다. 혹은 user_name에 secret&user_type=private#를 입력해 뒤의 문자열을 생략해 다음과 같은 내용을 나타낼 수 있다.
# 변경 전
<http://api.internal/search?user_name=secret&user_type=private#&user_type=public>
# 변경 후
<http://api.internal/search?user_name=secret&user_type=private>
1.1.3 웹 서비스의 요청 Body에 이용자의 입력값이 포함되는 경우
다음은 이용자의 입력값이 요청의 Body에 입력되는 경우이다. 사용자가 & 구분자를 포함해 user 파라미터를 추가하는 경우 내부 API에서는 전달받은 값을 파싱할 댸 앞에 존재하는 파라미터 값을 사용하기 때문에 User의 값을 변조할 수 있다.
Figure 3. 웹 서비스의 요청 Body에 이용자의 입력값이 포함되는 경우 예시 코드
# pip3 install flask
# python main.py
from flask import Flask, request, sessionimport 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)
실행 결과
{ "body": "body", "title": "title", "user": "admin" }
2. 실습
2.1 예제 코드
@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
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, ()) # 다른 쓰레드로 `local_server`를 실행합니다.
127.0.0.1과 localhost 가 막혀있기 때문에 우회는 다음과 같은 방식으로 수행한다.
- 127.0.0.1과 매핑된 도메인 이름 사용: 도매인 이름을 구매해 127.0.0.1과 연결해 사용
- 127.0.0.1의 alias 이용: 여러가지 IP 표현 식 사용
- 각 자릿수를 16진수로 변환: 0x7f.0x00.0x00.0x01
- .를 제거해 사용: 0x7f000001
- 0x7f000001를 십진수로 변환한 2130706433
- 각 자리에 0을 생략한 127.1, 127.0.1 등
- localhost의 alias 이용: LoCalHost 등
<http://vcap.me:8000/>
2.2 풀이
필터링 우회 이후 버프로 내부 서버 포트 찾기
'Web hacking > 개념 정리 & 심화' 카테고리의 다른 글
[Dreamhack] W - STAGE 10 Epilogue : blind-command (0) | 2023.05.29 |
---|---|
[Dreamhack] W - STAGE 10 Epilogue: carve party (0) | 2023.05.04 |
[Dreamhack] W - STAGE 8 File Vulnerability (0) | 2023.05.04 |
[Dreamhack] W - STAGE 7 Command Injection (0) | 2023.05.04 |
[Dreamhack] W - STAGE 6 SQL Injection (0) | 2023.05.04 |