드림이는 갤러리 사이트를 구축했습니다.
그런데 외부로 요청하는 기능이 안전한 건지 모르겠다고 하네요...
갤러리 사이트에서 취약점을 찾고 flag를 획득하세요!
flag는 /flag.txt에 있습니다.
우선 요청하는 기능이 힌트인 것 같고 유심히 보도록 할 것이다.
from flask import Flask, request, render_template, url_for, redirect
from urllib.request import urlopen
import base64, os
# Flask 애플리케이션 인스턴스 생성
app = Flask(__name__)
# 애플리케이션의 비밀 키 설정 (세션 관리에 사용)
app.secret_key = os.urandom(32)
# 이미지 데이터를 저장할 간단한 데이터베이스 역할을 하는 리스트
mini_database = []
# 기본 경로로 접근하면 view 함수로 리디렉션
@app.route('/')
def index():
return redirect(url_for('view'))
# URL에서 이미지를 요청하는 경로
@app.route('/request')
def url_request():
# URL과 제목을 요청 매개변수로부터 가져오기
url = request.args.get('url', '').lower()
title = request.args.get('title', '')
# URL이 비어있거나 파일 URL 또는 'flag' 문자열이 포함된 경우, 또는 제목이 비어있는 경우
if url == '' or url.startswith("file://") or "flag" in url or title == '':
# 요청 페이지를 다시 렌더링 (에러 방지)
return render_template('request.html')
try:
# 주어진 URL에서 데이터를 읽기
data = urlopen(url).read()
# 데이터를 base64로 인코딩하여 mini_database에 저장
mini_database.append({title: base64.b64encode(data).decode('utf-8')})
# view 페이지로 리디렉션
return redirect(url_for('view'))
except:
# URL에서 데이터를 읽는 데 실패한 경우 다시 요청 페이지 렌더링
return render_template("request.html")
# 이미지를 보는 경로
@app.route('/view')
def view():
# 이미지 리스트를 템플릿에 전달하여 렌더링
return render_template('view.html', img_list=mini_database)
# 파일 업로드 경로
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == 'POST':
# 업로드된 파일과 제목을 가져오기
f = request.files['file']
title = request.form.get('title', '')
# 파일이 없거나 제목이 비어있는 경우
if not f or title == '':
# 업로드 페이지를 다시 렌더링 (에러 방지)
return render_template('upload.html')
# 파일 데이터를 base64로 인코딩하여 mini_database에 저장
en_data = base64.b64encode(f.read()).decode('utf-8')
mini_database.append({title: en_data})
# view 페이지로 리디렉션
return redirect(url_for('view'))
else:
# GET 요청 시 업로드 페이지 렌더링
return render_template('upload.html')
# 메인 함수
if __name__ == "__main__":
# 초기 이미지 리스트 정의
img_list = [
{'초록색 선글라스': "static/assetA#03.jpg"},
{'분홍색 선글라스': "static/assetB#03.jpg"},
{'보라색 선글라스': "static/assetC#03.jpg"},
{'파란색 선글라스': "static/assetD#03.jpg"}
]
# img_list의 각 이미지 파일을 읽어 mini_database에 저장
for img in img_list:
for k, v in img.items():
data = open(v, 'rb').read()
mini_database.append({k: base64.b64encode(data).decode('utf-8')})
# Flask 애플리케이션 실행
app.run(host="0.0.0.0", port=80, debug=False)
우선 문제 힌트가 요청이므로 /request 부터 보면
@app.route('/request')
def url_request():
# URL과 제목을 요청 매개변수로부터 가져오기
url = request.args.get('url', '').lower()
title = request.args.get('title', '')
# URL이 비어있거나 파일 URL 또는 'flag' 문자열이 포함된 경우, 또는 제목이 비어있는 경우
if url == '' or url.startswith("file://") or "flag" in url or title == '':
# 요청 페이지를 다시 렌더링 (에러 방지)
return render_template('request.html')
try:
# 주어진 URL에서 데이터를 읽기
data = urlopen(url).read()
# 데이터를 base64로 인코딩하여 mini_database에 저장
mini_database.append({title: base64.b64encode(data).decode('utf-8')})
# view 페이지로 리디렉션
return redirect(url_for('view'))
except:
# URL에서 데이터를 읽는 데 실패한 경우 다시 요청 페이지 렌더링
return render_template("request.html")
필터링
URL이 비어있거나 시작할때 "file://" 로 시작 또는 'flag' 문자열이 포함된 경우, 또는 제목이 비어있는 경우
URL 필터링을 통과하면 base64 인코딩을 통해 저장한다. 따로 파일 형식을 걸러내지는 않는것으로 보인다.
우선 예시로 이미지를 올려보고 이미지가 어떤 방식으로 저장되는지 확인 해보았다.
개발자 도구를 통해 확인해보면
data:image/png;base64,[base64인코딩영역]
위와 같은 형식으로 저장되는것을 확인할 수 있었다. 파일 내용을 base 64 인코딩을 하는것을 확인할 수 있다.
필터링을 통과하여 flag.txt의 값을 저장하면 base64 인코딩영역에 우리가 얻을 flag값이 인코딩되어 있을 것이다.
메인함수에서
data = open(v, 'rb').read()
를 통해 열람하므로 v에 flag.txt를 전달하면 될 것 같다. 그렇다면 필터링을 뚫어야 하는것이 우리의 목표이다.
그렇다면 file:// 뭔데 필터링으로 걸러내고 있을지 확인해보자
이를 위해 URL에 대한 공부를 진행할 필요가 있다.
URL은 아래와 같은 구조를 가진다.
<스킴>://<사용자 정보>@<호스트>:<포트>/<경로>?<쿼리>#<프래그먼트>
여기서 필요한 정보는 스킴이다
URL 스킴
인터넷 리소스의 위치를 식별하고 해당 리소스에 접근하는 방법을 정의하는 URL의 첫 번째 부분입니다. 스킴은 URL의 구조에서 : 기호 앞에 위치하며, 리소스에 접근하는 프로토콜이나 방법을 나타낸다.
- <http,https>: 웹 페이지에 접근하기 위한 표준 프로토콜
- <ftp>:파일 전송 프로토콜
- <mailto>: 이메일 주소를 열때 사용
- <file> :로컬 파일 시스템에 있는 파일을 참조할 때 사용
- <data> : 임베디드 데이터를 포함할 수 있으며, 주로 브라우저에서 이미지나 파일 데이터를 직접 포함할 때 사용
그렇다면 file://flag.txt를 전달하면 로컬 파일 flag.txt를 읽을 수 있을것이다.
bypass
- ' 'file://
공백 한칸 삽입- <file:// >
<>태그 사용- file:/
/ 하나만 사용
flag우회 방법은 아무문자나 url 인코딩하면 된다.
만약 file:/ 만 쓸 경우 flag.txt만 입력해도 괜찮지만
공백이나 <>태그를 쓸때에는 /flag.txt를 넣어줘야지 반환해준다.
이유는 다음과 같다.
- file:/%66lag.txt처럼 단순히 파일 경로로 접근하는 경우
시스템의 기본 파싱 로직에 따라 바로 접근이 가능- 공백이나 <태그>와 같이 경로 앞에 특수 기호가 포함된 경우
시스템이 이를 보안 위험으로 인식하고 경로의 정확성을 추가적으로 확인하려고 하기 때문에 /flag.txt와 같은 명시적 경로 지정이 요구
따라서 아래와 같이 사용해야한다.
- (공백)file:///%66lag.txt
- <file:///%66lag.txt
- file:/%66lag.txt
위 url을 입력하면 view에서 주소를 보면 아래와 같다.
REh7YjIwMzdhMDI2YjQwY2M5ODgwNGU5MWI1YTJhMDdmNTR9
이 문자열을 base64 디코딩하면 flag를 얻을 수 있다.