웹 개발 언어는 HTTP 요청을 전송하는 라이브러리를 제공한다.
각 언어별 HTTP 라이브러리는 아래와 같다.
PHP : php-curl
NodeJS : http
Python : urllib, requests
이러한 라이브러리는 HTTP 요청을 보낼 클라이언트 뿐만 아니라 서버와 서버간 통신을 위해 사용되기도 한다.
일반적으로 다른 웹 애플리케이션에 존재하는 리소스를 사용하기 위한 목적으로 통신한다.
예를 들어, 마이크로서비스 간 통신, 외부 API 호출, 외부 웹 리소스 다운로드 등이 있다.
최근 웹 서비스들은 관리 및 코드 복잡도를 낮추기 위해서 마이크로서비스들로 웹 서비스들을 구현하는 추세이다.
이때, 각 마이크로서비스는 주로 HTTP, GRPC 등을 사용하여 API 통신을 한다.
서비스 간 HTTP 통신이 이뤄질 때 요청 내에 이용자 입력값이 포함될 수 있다.
이용자의 입력값으로 포함된다면 개발자가 의도하지 않은 요청이 전송 될 수 있다.
SSRF 취약점은 웹 서비스의 요청을 변조하는 취약점으로, 웹 서비스의 권한으로 변조된 요청을 보낼 수 있다.
💡 마이크로서비스란?
마이크로서비스는 소프트웨어가 잘 정의된 API 를 통해서 통신하는
소규모의 독립적인 서비스로 구성되어 있는 경우의 소프트웨어 개발을 위한 아키텍처 및 조직접 접근 방식
이다. 이러한 서비스는 독립적인 소규모 팀에서 보유한다.
Server-side Request Forgery(SSRF)
웹 서비스는 외부에서 접근할 수 없는 백오피스 서비스와 같은 내부망 기능을 사용할 때가 있다.
백오피스 서비스는 관리자 페이지라고도 하며,
이용자의 행위가 의심스러울 경우 해당 계정을 정지시키거나 삭제하는 등 관리자만이 수행할 수 있는 모든 기능을 구현한 서비스다.
이러한 서비스는 관리자만 이용할 수 있어야 하므로 외부에서 접근할 수 없는 내부망에 위치한다.
따라서, 이를 이용하여 공격자가 SSRF 취약점을 통해 웹 서비스의 권한으로 요청을 보낼 수 있다면
공격자는 외부에서 간접적으로 내부망 서비스를 이용할 수 있다.
웹 서비스가 보내는 요청을 변조하기 위해서는 요청 안에 이용자의 입력값이 포함되어야 한다.
입력 값이 포함되는 경우의 예시
1. 웹 서비스가 이용자가 입력한 URL 에 요청을 보내거나
2. 요청을 보낼 URL 에 이용자 번호와 같은 내용이 사용되거나
3. 이용자가 입력한 값이 HTTP Body 에 포함되거나
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)
return (
response.content,
200, # HTTP 응답 코드
{"Content-Type": response.headers.get("Content-Type", "")},
)
@app.route("/request_info")
def request_info():
return request.user_agent.string
app.run(host="127.0.0.1", port=8000)
- image_downloader
이용자가 입력한 image_url 을 requests.get 함수를 사용하여 GET 메소드로 HTTP 요청을 보낸 후 응답을 반환한다.
브라우저에 아래와 같은 URL 을 입력하면 드림핵 페이지에 요청을 보내고 응답을 반환한다.
https://dreamhack.io/assets/dreamhack_logo.png>
- request_info
웹 페이지에 접속한 브라우저의 정보(User-Agent)을 반환한다.
브라우저를 통해 해당 엔드포인트로 접속하면 그때 사용된 브라우저의 정보가 출력된다.
- image_downloader 의 image_url 에 request_info 엔드포인트 경로를 입력한다.
<http://127.0.0.1:8000/image_downloader?image_url=http://127.0.0.1:8000/request_info>
→ 위 경로에 접속하면
image_downloader 에서는 http://127.0.0.1:8000/request_info 의 URL 에 HTTP 요청을 보내고 응답을 반환한다.
반환값을 확인해보면 브라우저로 request_info 엔드포인트에 접속했을 때와는 다르게
브라우저 정보가 python-requests/2.11.1 이다.
접속한 브라우저 정보로 위와 같이 출력된 이유는 웹 서비스에서 HTTP 요청을 보냈기 때문이다.
이와같이 이용자가 웹 서비스에서 사용하는 마이크로서비스의 API 주소를 알아내고,
image_url 에 주소를 전달하면 외부에서 직접 접근 불가능한 마이크로서비스의 기능을 임의로 사용할 수 있다.
1. 웹 서비스의 요청 URL에 이용자의 입력값이 포함되는 경우
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}")
위의 코드는 이용자의 입력값이 포함된 URL에 요청을 보내는 코드다.
- user_info
<http://x.x.x.x/v1/api/user/information?user_idx=1>
이용자가 전달한 user_idx 값을 내부 API 의 URL 경로로 사용한다.
<http://api.internal/user/1>
이용자가 user_idx 를 1로 설정하고 요청을 보내면 웹 서비스는 위와 같은 주소에 요청을 보낸다.
- user_search
<http://x.x.x.x/v1/api/user/search?user_name=hello>
이용자가 전달한 user_name 값을 내부 API 의 쿼리로 사용한다.
이용자가 위와 같이 user_name을 “hello”로 설정하고 요청을 보내면 웹 서비스는 다음과 같은 주소에 요청을 보낸다.
<http://api.internal/user/search?user_name=hello&user_type=public>
이용자 입력 값중에 URL 구성 요소 문자를 삽입하면 API 경로를 조작할 수 있다. user_info 함수에서 user_idx 에 ../search 를 입력할 경우 아래와 같은 URL 에 요청을 보낸다. → Path Traversal 취약점
<http://api.internal/search>
또한 # 문자를 입력해 경로를 조작할 수 있다.
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 은 아래와 같이 된다.
<http://api.internal/search?user_name=secret&user_type=private>
2. 웹 서비스의 요청 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"
title = request.form.get("title", "")
body = request.form.get("body", "")
data = f"title={title}&body={body}&user={session['idx']}"
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():**
# 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를 구현한 기능이다.
- index
board_write 기능을 호출하기 위한 인덱스 페이지이다.
코드를 실행하고 다음 URL에 접속하면 title과 body를 입력하는 페이지가 표시된다.
입력창에 값을 입력하고 제출 버튼을 누르면 다음과 같은 응답을 확인할 수 있다.
{ "body": "body", "title": "title", "user": "guest" }
요청을 전송할 때 세션 정보를 “guest”로 설정했기 때문에 “user”가 “guest”인 것을 확인할 수 있다.
예시 코드를 살펴보면, 내부 API로 요청을 보내기 전에 다음과 같이 데이터를 구성하는 것을 확인할 수 있다.
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의 값을 변조할 수 있다.
{ "body": "body", "title": "title", "user": "admin" }
키워드 정리
SSRF :
웹 서비스의 요청을 변조하는 취약점으로,
브라우저가 변조된 요청을 보내는 CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청을 보낼 수 있다.
구분 문자(Delimiter) :
독립적 영역 사이의 경계를 지정하는 데 사용하는 하나의 문자 혹은 문자들의 배열을 말한다.
URL 에서 구분 문자는 “/”(Path identifier), “?” (Query identifier) 등이 있으며 구분 문자에 따라 URL의 해석이 달라질 수 있다.
(Exercise : SSRF )
[초기화면]
[Image Viewer 클릭하면 나오는 화면]
- ../static 을 입력해봤는데 아래와 같은 것이 떴다.
- 인코딩 된 걸 확인해봤지만 그저 잘못된 입력이라는 에러를 알려주는 문구였다.
- 개발자 도구를 이용하여 소스를 확인해봤지만 별 다른 점을 찾지 못했다.
- 소스 코드를 분석해보자.
[소스 코드 분석]
- 소스 코드 중 이 부분이 눈에 들어왔다.
- local_host, local_port 를 만족하면 될 것 같았다.
- 특히 포트 번호는 랜덤으로 1500 ~ 1800 이므로 이것도 패킷을 이용하여 브루트 포스하면 되지 않을까? 생각이 들었다.
- 하지만 브루트 포스 속도가 너무 느려서 파이썬 스크립트를 이용했다.
- 127.0.0.1 과 localhost 는 필터링에 걸리므로 Localhost 를 이용했다.
- 포트 번호가 나왔고 Image Viewer 에 http://Localhost:1729/flag.txt 를 입력했다.
- 이미지가 나왔고 base64 로 인코딩된 문자열이 나왔다.
- base64 로 디코딩을 했다.
- 플래그를 획득했다!
- 전에 풀었던 문제랑 매우 비슷한 유형이라서 어렵지 않게 풀었던 것 같다.
'TeamH4C' 카테고리의 다른 글
[빡공팟 5기] W5 : DVWA 실습 - Brute Force (3) | 2022.10.18 |
---|---|
[빡공팟 5기] W4 : Web Hacking 로드맵 총 정리 +) NoSQL Injection 포함 (0) | 2022.10.18 |
[빡공팟 5기] W4 : Web Hacking 로드맵 - STAGE 7 - File Vulnerability (0) | 2022.10.17 |
[빡공팟 5기] W4 : Web Hacking 로드맵 - STAGE 6 - Command Injection (0) | 2022.10.17 |
[빡공팟 5기] Web Hacking 로드맵 - STAGE 5 - Non-Relational DBMS (0) | 2022.10.14 |