Created
September 6, 2019 09:44
-
-
Save GioBonvi/b94278d0519dfa46f96f3de354efe269 to your computer and use it in GitHub Desktop.
A simple tool to decode, verify or generate a signed Flask session cookie. Useful during CTFs.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# encoding: utf-8 | |
""" | |
Flask session cookie toolkit. | |
Decode, verify or generate a signed Flask session cookie. | |
Credits to Terry Vogelsang for the original script (https://terryvogelsang.tech/MITRECTF2018-my-flask-app/) | |
which I just slightly modified for my personal use. | |
""" | |
from hashlib import sha512 | |
from flask.sessions import session_json_serializer | |
from itsdangerous import URLSafeTimedSerializer, BadTimeSignature | |
import argparse | |
import base64 | |
from zlib import decompress | |
import sys | |
import json | |
# GENERAL FUNCTIONS. | |
def debug(msg): | |
if VERBOSE_OUTPUT: | |
print("[DEBUG] " + msg) | |
def pretty_print_json_data(json_data): | |
json_pretty_str = json.dumps(json_data, indent=4) | |
print(json_pretty_str) | |
# COOKIE DECODER. | |
def decode_cookie_payload(cookie): | |
debug(f"Cookie:\n{cookie}") | |
# If the cookie starts with a dot the paylod is base64 encoded and GZIP compressed. | |
if cookie[0] == ".": | |
b64_gzip_payload = cookie[1:].split(".")[0] | |
# Python needs the padding which is stripped in the base64_URLsafe version (see https://stackoverflow.com/questions/2941995/python-ignore-incorrect-padding-error-when-base64-decoding#comment12174484_2942039) | |
b64_gzip_payload += "=" * (-len(b64_gzip_payload) % 4) | |
debug(f"Encoded and compressed payload:\n{b64_gzip_payload}") | |
gzip_payload = base64.urlsafe_b64decode(b64_gzip_payload) | |
payload = decompress(gzip_payload) | |
debug(f"Decoded and decompressed payload:\n{payload}") | |
else: | |
# If the cookie does not start with a dot the payload is just base64 encoded. | |
b64_payload = cookie.split(".")[0] | |
# Python needs the padding which is stripped in the base64_URLsafe version (see https://stackoverflow.com/questions/2941995/python-ignore-incorrect-padding-error-when-base64-decoding#comment12174484_2942039) | |
b64_payload += "=" * (-len(b64_payload) % 4) | |
debug(f"Encoded payload:\n{b64_payload}") | |
payload = base64.urlsafe_b64decode(b64_payload) | |
debug(f"Decoded payload:\n{payload}") | |
return payload | |
def output_decoded_payload(payload, json_pretty, str_encoding): | |
str_payload = payload.decode(str_encoding) | |
if json_pretty: | |
# Pretty print the JSON data. | |
try: | |
debug(f"Payload string (encoding=\"{str_encoding}\"):\n{str_payload}") | |
pretty_print_json_data(json.loads(str_payload)) | |
except: | |
print("The payload is not valid JSON!", file=sys.stderr) | |
sys.exit(1) | |
else: | |
# Do not pretty print the JSON data. | |
print(str_payload) | |
sys.exit(0) | |
# COOKIE VERIFIER. | |
def readAndVerifyCookie(cookie, secret_key): | |
debug(f"Cookie:\n{cookie}") | |
signer = URLSafeTimedSerializer( | |
secret_key, salt="cookie-session", | |
serializer=session_json_serializer, | |
signer_kwargs={"key_derivation": "hmac", "digest_method": sha512} | |
) | |
try: | |
session_data = signer.loads(cookie) | |
print("The signature is correct!") | |
return session_data | |
except BadTimeSignature: | |
print(f"The signature is not correct!") | |
sys.exit(1) | |
# COOKIE GENERATOR. | |
def generate_cookie(json_str_payload, key): | |
try: | |
payload = json.loads(json_str_payload) | |
except: | |
print("Your payload is not a valid JSON string!") | |
sys.exit(1) | |
signer = URLSafeTimedSerializer( | |
key, salt="cookie-session", | |
serializer=session_json_serializer, | |
signer_kwargs={"key_derivation": "hmac", "digest_method": sha512} | |
) | |
cookie = signer.dumps(payload) | |
return cookie | |
# MAIN. | |
if __name__ == "__main__": | |
# Argparse setup. | |
argparser = argparse.ArgumentParser(description="Pefroma various actions regarding a Flask session COOKIE.") | |
argparser.add_argument("command", metavar="COMMAND", choices=["decode", "verify", "generate"], help="the command to execute") | |
argparser.add_argument("-c", "--cookie", metavar="COOKIE", help="the COOKIE to decode or verify") | |
argparser.add_argument("-k", "--key", metavar="SECRET_KEY", help="the SECRET_KEY to sign or verify the cookie with") | |
argparser.add_argument("-p", "--payload", metavar="PAYLOAD", help="the PAYLOAD to encode in teh cookie") | |
argparser.add_argument("-v", "--verbose", action="store_true", help="enable verbose output") | |
argparser.add_argument("--pretty-json", action="store_true", help="whether to pretty print the JSON data") | |
argparser.add_argument("--encoding", default="UTF-8", help="the ENCODING to use when parsing data as a string") | |
# Parse arguments. | |
args = argparser.parse_args() | |
command = args.command | |
VERBOSE_OUTPUT = args.verbose | |
# Choose command. | |
if command == "decode": | |
# Check arguments. | |
if args.cookie is None: | |
argparser.error("The 'decode' command requires the --cookie argument.") | |
cookie = args.cookie | |
json_pretty = args.pretty_json | |
str_encoding = args.encoding | |
# Decode. | |
payload = decode_cookie_payload(cookie) | |
# Output. | |
output_decoded_payload(payload, json_pretty, str_encoding) | |
elif command == "verify": | |
# Check arguments. | |
if args.cookie is None or args.key is None: | |
argparser.error("The 'verify' command requires both the --cookie and the --key arguments.") | |
cookie = args.cookie | |
key = args.key | |
pretty_json = args.pretty_json | |
# Verify. | |
session_data = readAndVerifyCookie(cookie, key) | |
# Output. | |
print('') | |
if pretty_json: | |
debug(f"Session data:\n{session_data}") | |
pretty_print_json_data(session_data) | |
else: | |
print(json.dumps(session_data)) | |
elif command == "generate": | |
# Check arguments. | |
if args.payload is None or args.key is None: | |
argparser.error("The 'generate' command requires both the --payload and the --key arguments.") | |
payload = args.payload | |
key = args.key | |
# Generate. | |
cookie = generate_cookie(payload, key) | |
# Output. | |
print(cookie) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Flask>=1.1.1 | |
itsdangerous>=1.1.0 |
If this doesn't work try SHA1. I think it's the default for flask as of right now.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thanks