When you log into a website, many web frameworks will issue you a cookie as proof that you have correctly authenticated. In the past, this token was just a random identifier that was assigned to the user in the application's database. However, many modern applications now use cryptography to embed the user information into the cookie itself, with a signature around the data to prevent it from being modified. This can take the form of JSON Web Tokens (JWTs), but many web frameworks like Flask and Laravel also expose their own schemes for this.
Using stateless cryptography like this is precarious and requires care from developers that they do not expect. Many frameworks require developers to specify a secret key before they will work correctly, and many developers then pick quick, unsafe values like changeme
to unblock themselves. Unfortunately, this key is critical to the security of these applications.
When this key is known, an attacker is able to change any of the data inside the session. For example, if an application uses Flask-Login
, it stores a user_id
attribute inside of the Flask session. If an attacker changes this value, not only will they be logged into someone else's account, but the application will have no idea (and no logs) that this occurred! In some cases, like with Django configured to use a Pickle serializer, this misconfiguration could even lead to RCE via deserialization.
A prime example of Flask being misconfigured would be CVE-2020-17526, where Apache Airflow defaulted to using temporary_key
as the Flask signing key for quite some time, before someone reported that this was happening.
None of this is a new type of issue and is often manually exploited by attackers, but it is taking security researchers too long to broadly find these types of issues. This is likely because there is simply not much offensive tooling on these systems. Some tools do exist like Flask-Unsign and jwt-pwn, however they are only oriented towards one implementation of these schemes, even though they can be very similar. Additionally, many are written in languages which add too much CPU overhead to run them at scale and within vulnerability scanners.
Introducing CookieMonster
CookieMonster is a tool I wrote to overcome some of these challenges. CookieMonster is written in Go and designed to be a high-performance tool ā with both an API and a CLI ā for detecting these types of issues. It's able to decode and unsign cookies from many different applications; while it supports JWTs, it also handles session cookies from Django, Flask, Laravel, and other frameworks. It's open-source with an MIT license and can be easily extended to support other cookies.
You can expect to use CookieMonster in a couple different scenarios:
- In an automation pipeline, you can use its Go API to audit cookies you receive from servers that you are scanning. It's fast enough to run it inline with typical vulnerability scanning and with high parallelism, and handles arbitrary input well.
- When manually testing an application, you can use its command-line interface to easily check session cookies you get from the application.
Findings from CookieMonster are unlikely to be false positives, because it generates collision-proof digests and looks for them in the cookies you provide. Additionally, CookieMonster provides a useful -resign
option for certain decoders, which allow you to change the contents of the cookie at the same time as you unsign it. (Otherwise, you'll have to use the secret key yourself to modify the contents.)
Using CookieMonster on a test application
Martin Fowler and Jack Singleton detail a simple Rack application which uses a misconfigured Rack secret key of super secret
. They show a simple gen-cookie.rb
script to generate a Rack cookie, signed with the secret key. Instead of manually configuring and running hashcat like they did, let's use CookieMonster to get the secret key for their script:
~ % go install github.com/iangcarroll/cookiemonster/cmd/cookiemonster@latest
~ % cookiemonster -cookie BAhJIgl0ZXN0BjoGRVQ=--8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2
šŖ CookieMonster 1.0.0
ā¹ļø CookieMonster loaded the default wordlist; it has 38919 entries.
ā
Success! I discovered the key for this cookie with the rack decoder; it is: super secret
Nearly immediately, CookieMonster is able to find the secret key for this cookie. It doesn't need to be told that it's from a Rack application, or need any further information. Later in the article, they demonstrate how to use this key to achieve RCE.
What I've found so far
Using CookieMonster in my automated scanning, I've already submitted several bug bounty reports to large companies about broken session authentication and other unsafe implementations of things like JWTs. I hope to be able to share more on this front soon, as some of the findings still need to be fixed or have advisories issued.
Best of all, I was able to move my cookie scanning further up into my pipeline. When I wrote about my findings with Airflow, I detailed my scanning implementation which wrote cookies from responses into a database table, and then a background process asynchronously fetched entries from this table and ran individual tools to detect weak cookie signing. Due to the CPU usage of these tools, I was only able to try 5 cookies at a time. Now that CookieMonster is available, I'm able to scan cookies and alert in real-time for over 5,000 requests/second, and for even more formats.
In the future, I think there's a lot of promise in optimizing wordlists for common keys. CookieMonster uses the wordlist from Flask-Unsign by default, which results in many findings but only contains ~40,000 entries. Brute forcing these tokens can be difficult, but with a larger wordlist you may still be able to go quickly and find more things!