프롤로그
이건 또 뭘까..;;
서론
웹 개발 언어는 HTTP 요청을 전송하는 라이브러리를 제공한다. 파이썬같은 경우 urllib, requests 가 있다.
이 라이브러리는 HTTP 요청을 보낼 클라이언트 뿐만 아니라 서버와 서버간 통신을 위해 사용되기도 한다. 일반적으로 다른 웹 애플리케이션에 존재하는 리소스를 사용하기 위한 목적으로 통신한다.
기존에는 단일 서비스로 웹 서비스를 구현하였지만, 이제는 관리 및 코드의 복잡도를 낮추기 위해 마이크로서비스들로 웹 서비스를 구현하는 추세인데, 각 마이크로서비스는 주로 HTTP, GRPC 등을 사용해 API 통신을 한다.
서비스간 HTTP 통신이 이루어질때 요청 내에 이용자의 입력값이 포함될 수 있는데, 이용자의 입력값으로 포함되면 개발자가 의도하지 않은 요청이 전송될 수 있고 이를 Server-side Request Forgery(SSRF) 라고 한다. SSRF는 웹 서비스의 요청을 변조하는 취약점으로, CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청을 보낼 수 있다.
용어 정리
마이크로 서비스 : 소프트웨어가 잘 정의된 API 를 통해 통신하는 소규모의 독립적인 서비스로 구성되어 있는 경우의 소프트웨어 개발을 위한 아키텍처 및 조직적 접근 방식. 이러한 서비스는 독립적인 소규모 팀에서 보유.
API : Application Programming Interface 의 약자, 응용프로그램과 운영체제 사이의 통신에 사용되는 언어나 메시지 형식
좋은 API는 모든 building block 을 제공함으로써 프로그램 개발을 쉽게 해준다. 프로그래머는 그 block 를 함께 합치기만 하면 된다.
즉, API는 개발자들이 다른 애플리케이션이나 플랫폼에서 제공하는 데이터와 서비스에 접근하고 조작할 수 있는 방법을 제공한다. 이를 통해 개발자들은 복잡한 코딩 작업 없이도 원하는 기능을 구현할 수 있게 된다.
Server-side Request Forgery (SSRF)
백 오피스 서비스(관리자페이지)와 같이 관리자만 이용할 수 있고 외부에서 접근할 수 없게 해야하는 경우 내부망에 위치하는 경우가 있다. 웹 서비스는 외부에서 직접 접근할 수 없는 내부망 서비스와 통신할 수 있는데, 공격자가 SSRF 취약점을 통해 웹 서비스의 권한으로 요청을 보낼 수 있다면 공격자는 외부에서 간접적으로 내부망 서비스를 이용하게 되면 큰 피해가 생길 수 있다.
(1) 이용자가 입력한 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_url = request.args.get("image_url", "") # URL 파라미터에서 image_url 값을 가져옵니다.
이 부분을 보면, 이용자가 입력한 image_url 을 가져오는데, 저기다가
http://127.0.0.1:8000/image_downloader?image_url=https://dreamhack.io/assets/dreamhack_logo.png
이런식으로 입력을 하게 되면, 드림핵 페이지에 요청을 보내고 응답을 반환한다.
그리고 다른 취약점을 살펴보면
@app.route("/request_info")
def request_info():
# 접속한 브라우저(User-Agent)의 정보를 출력하는 페이지 입니다.
return request.user_agent.string
이 코드는 웹 페이지에 접속한 브라우저의 정보(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
이런식으로 사용된 브라우저의 정보가 출력된다. 그러나 이 부분에
http://127.0.0.1:8000/image_downloader?image_url=http://127.0.0.1:8000/request_info
이런식으로 입력을 하게 되면
웹 브라우저 정보가
python-requests/<LIBRARY_VERSION>
로 나온다. 이는 웹 서비스에서 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 를 받아들인다. 즉
http://x.x.x.x/v1/api/user/information?user_idx=1
이렇게 요청을 보내면
http://api.internal/user/1
웹 서비스는 다음과 같은 주소에 요청을 보낸다.
그리고 user_search 함수를 보면, 이용자가 전달한 user_name 값을 내부 API의 쿼리로 사용한다.
http://x.x.x.x/v1/api/user/search?user_name=hello
이용자가 user_name 을 hello로 설정하고 요청을 보내면 웹 서비스는 다음과 같은 주소에 요청을 보낸다.
http://api.internal/user/search?user_name=hello&user_type=public
이제 여기서 문제점을 확인하자면.... 웹 서비스가 요청하는 URL에 이용자의 입력값이 포함되면 요청을 변조할 수 있다. URL의 구성 요소 문자를 삽입하면 API 경로를 조작할 수 있는데, user_info 에서 user_idx 에 ../serarch 를 입력할 경우
http://api.internal/search
위 URL 에 요청을 보낸다.
해당 취약점은 경로를 변조한다는 의미에서 Path Traversal 이라고 불리는데, 지난 글에서 배웠었다.
user_search 에서도 user_name 부분에 secret&user_type=private#을 입력할 경우 아래와 같이 요청이 된다.
http://api.internal/search?user_name=secret&user_type=private#&user_type=public
#은 주석처리라고 생각하면 된다. 그러니까 user_type= public 를 지워버리고, private로 바꾼 것이다.
(3) 웹 서비스의 요청 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 형식으로 변환하고 반환한다. 그냥 JSON으로 바꾼다.
index 는 그냥 board_write 기능을 호출하기 위한 폼 이라고 보면 될 것 같다.
이제 문제점을 확인하면, board_wrtie에서 internal_board_write 로 넘어갈때 data 형식을
data = f"title={title}&body={body}&user={session['idx']}"
로 데이터를 구성하는 것을 확인할 수 있다.
title, body, user이 값을 파라미터 형식으로 설정한다. 이때 title 부분에다가 &를 붙여보자
title=title&user=admin&body=body&user=guest
user파라미터를 추가하면, 내부 API에서는 전달받은 값을 파싱할 때 앞에 존재하는 파라미터의 값을 가져와 사용하기 때문에 user의 값을 변조할 수 있다.
{ "body": "body", "title": "title", "user": "admin" }
이렇게 값이 변조된다.
에필로그
약간 이쪽도 URL 값에 파라미터 부분에다가 변조를 일으키는 느낌인 것 같다.
'보안 스터디 > 웹 해킹' 카테고리의 다른 글
[드림핵/워게임] phpreg (웹 해킹) (1) | 2024.01.26 |
---|---|
[드림핵/워게임] web-ssrf (웹 해킹) (0) | 2024.01.16 |
[드림핵/워게임] image-storage (웹 해킹) (1) | 2024.01.15 |
[드림핵/워게임] file-download-1 (웹해킹) (0) | 2024.01.15 |
[드림핵/웹해킹] ServerSide: File Vulnerability (0) | 2024.01.15 |