Practical Approaches for Testing and Breaking JWT Authentication- 10 mins
JWT (JSON Web Token) is a popular authentication/authorization protocol. It integrates cryptographic signatures into JSON objects to verify the integrity of the object.
The approach of JWT is systematic and fairly simple. There were several pieces of research done to cover the security aspects of JWT authentication.
Several tools were also previously developed. However, after assessing the quality of the public tools, I concluded that we need better tools to perform testing against JWT. I have developed a set of scripts I call it “jwt-pwn” that aims to test JWT authentication with stability, simplicity, and efficiency in mind. The scripts set are very simple, as it integrates directly with the JWT Python library.
This article will cover the background of JWT, wrong implementations, and practical approaches to test and break JWT. It will also cover a section of constructive criticism, where I will be discussing each known public tool for testing JWT, and the issues I faced with it where I had come to the idea of developing jwt-pwn.
The following figure explains the fundamental approach in JWT generation
JWT is structured by three parts:
- Header: Acts as a guide on how to operate. The header holds the JWT algorithm used in generating the signature.
- Payload: Holds the JSON object, along with the preserved claims if any.
- Signature: The Base64 encoded representation of the header and the payload. The secret is optionally added to the signature.
Testing the Implementation
1. Brute-forcing secret keys
JSON web tokens are typically signed with a secret key. A secret key is simply a password or digital key. If we were able to find the secret key, then we will be able to generate valid tokens with any value we specify.
This test is done for any token generated with a password; the attack does not apply to the key-based signing of the token.
An interesting point to mention that it’s an offline password attack, so no noise would be made to the target’s backend. We only require a valid JWT provided by the backend when authentication or similar situations.
2. Signing a new token with the “none” algorithm
This test is quite straightforward. We would decode the value of the JWT without validating the signature. From there, we will generate a new token with the “none” algorithm.
After this, we will replace the original JWT in the authentication with the newly generated token. If it’s accepted, and the same response of the original token is given, then the authentication functionality is arbitrarily accepting the “none” algorithm, where we explicitly “ask” the JWT validator on the backend to not validate the token.
If it works, replay attacks can be done. I’m referring to the “replay attacks” term by the act of replaying a valid token to the authentication controller, without focusing on the source of obtaining the token. In our case here, it will be signing a new token that is valid for the another user (or admin).
3. Changing the signing algorithm of the token (for fuzzing purposes)
Let’s say a backend returns a JSON Web Token signed with RSA256. The validation test would aim to check whether the JWT validator enforces RSA256 in this case.
We will decode the value of the original JWT, then we will generate a newly signed token with our secret key, using HS256 for example.
This test may not fully break the JWT authentication system in typical cases. However, it can be a good practice to test it against and inspect its response for fuzzing purposes.
4. Signing the asymmetrically-signed token to its symmetric algorithm match (when you have the original public key)
When a token is asymmetrically signed (e.g. RSA256), and you obtained the public key that is used for verifying the signature, then an attack that can be applied is to generate a new token with the same payload, and with symmetric signing algorithm (e.g. HS256), while using the public key as the password.
If it works, then this means that the validator is not whitelisting the signing algorithm for the JWT, and we can generate arbitrary tokens that will be accepted by the JWT validator.
Several tools aim to assess JWT authentication. The tested tools for doing JWT assessments faces different engineering issues.
jwt_tool - (https://github.com/ticarpi/jwt_tool)
jwt_tool does various tests for JWT. However, the custom implementation of parsing and generating JWT causes errors and false-positives. Jwt_tool uses a custom method of decoding and encoding Base64 instead of using the default JWT library. Although the JWT protocol is public and documented, Base64 varies from implementation to another. For instance, multiple Base64 encoded strings can represent the same value.
Note: Jwt_tool released a new update (as of October 2019) that should be fixing the majority of the issues that was encountered.
c-jwt-cracker - (https://github.com/brendan-rius/c-jwt-cracker)
C-jwt-cracker is a tool to brute-force the private key of JWT. Besides it uses its implementation of JWT, the Base64 library used by c-jwt-cracker is proven to be buggy and delivers invalid results.
Quoting from the c-jwt-cracker Github page:
“The base64 implementation I use (from Apple) is sometimes buggy because not every Base64 implementation is the same. So sometimes, decrypting of your Base64 token will only work partially and thus you will be able to find a secret to your token that is not the correct one”.
- Source: https://github.com/brendan-rius/c-jwt-cracker
jwt-cracker - https://github.com/lmammino/jwt-cracker
This tool is limited to a single signing algorithm (HS256), which makes it unable to operate if a different signing algorithm is provided.
Besides, all tools discussed in the experiment are using a linear approach to crack the password.
The scripts take every test and approach to break JSON web tokens and apply it systematically. Everything is also done using the primary JWT library for Python, “pyjwt”. Pyjwt is heavily used in real-world applications. It’s stable, tested, and maintained.
I find using Pyjwt instead of writing a custom library as many did is much better for stability wise. There are known tools that give wrong results due to the failure of parsing JWT tokens correctly. Usage of Pyjwt would generate the same results the backend parser would get in most cases.
First script: jwt-cracker.py
This script performs brute-force attacks via a provided wordlist against a JSON web token.
I had two prototypes for the brute-forcing algorithm part.
1- Load all the wordlist in a queue
2- spawn a new thread once an available thread is available (there is a thread lock that controls this part).
After analysis, I found this to better not as good as needed. The reason is: Besides the actual brute-forcing, the problems are:
1- There should be a thread constantly unloading from the queue
2- Checker for threads availability.
3- Locker for threads from being spawned until an available thread is there.
4- Have the queue unloaded linearly.
Many factors are involved in this part that approached the algorithm not the most efficient one to use.
I find this to be the correct method.
1- Split the entire wordlist into smaller queues.
2- Spawn new threads, and then feed every queue to a unique thread.
3- The thread is treated as a “worker”, and all the functions are synchronized with a global checker that checks if the key is found by other workers.
4- While the key is not found, all threads test values in the assigned queue.
5- If the key is found, all threads are joined, and the application terminates.
6- The application terminates when all values in every child queue are tested.
After implementing this design in Python, The GIL (Global Interpreter Lock) in Python has caused the app to be relatively slower than needed.
Second Script: jwt-cracker-go
The script is a linear implementation of the jwt-cracker in Golang. It’s really fast compared to other tools, although it’s using a linear approach for password cracking, and reliable as it’s using the official Golang library for JWT in the process of validating tokens. This library should be relatively the same library being used by developers in real-world environments.
I have also written another variant that uses goroutines which should be faster. The code for the multi-threaded variant is available in the project repository.
Third script: jwt-decoder.py
This is a simple script that decodes the values of JSON web tokens. It should be handy for testing JWT.
Fourth script: jwt-mimicker.py
This script creates an unsigned token from the JSON web tokens. This script applies the “none” attack discussed previously.
Fifth script: jwt-key_based_token-to-hs256.py
This script creates a signed JWT from a key-based JWT. This applies to the attack mentioned previously.
The script works on every key-based signing algorithm, thanks to the powerful pyjwt library.
What to Do?
Penetration Testers and Security Researchers
Test your organization’s JWT implementation via jwt-pwn, and report any weaknesses identified.
Developers and Defenders
- Make sure you’re enforcing the algorithm used in the JWT validator.
- Disallow unused algorithm (via whitelisting approach).
- Always verify the JWT header, and verify the JWT “alg” key in the JWT header.
- Never trust the “none” algorithm for signing.
- Use a long and extremely difficult to recover secret keys. If the secret key is identified, the entire authentication will be broken.
- Rotate your signing keys periodically.
- Don’t expose important client-data in JWT; it can be decoded. If there is sensitive data shared in the payload, any party that obtains the token would be able to see it.
- Add a claim for “Expiration” to overcome the non-expiration issue in the stateless protocol.