-
Notifications
You must be signed in to change notification settings - Fork 672
Attack Methodology
Below is presented a workflow for checking JWTs on a web application or an API.
Testing tokens can be done in a variety of ways, but the crucial part is that you'll need a way to send the HTTP request and read the full, explicit response.
It might be tempting to just adjust the JWT cookie in your browser and reload the page, but this is prone to a number of potential issues and you're likely to miss something that could prove useful.
My recommendation would be to use an "intercepting proxy" tool such as Burp Suite (the free version will suffice) or ZAP, or for APIs you could use POSTMan or an equivalent.
In a pinch I've used curl, but to do that you'll need to reconstruct all the headers of the original HTTP request, so this can be prone to false positives/negatives unless you're really careful.
- Install your intercepting proxy
- Set your browser to use the proxy for all HTTP(S) requests
- Install the proxy's SSL certificate (if using an HTTPS connection)
- Use the target site as required, making sure the traffic is being seen in the intercepting proxy's history.
The below steps are roughly in order, with some tests building on the results of previous tests. So follow along in order.
Your first goal is to identify that the application is using JWT. The easiest way to do this is to search the proxy tool's history for some JWT regexes:
[= ]ey[A-Za-z0-9_-]*\.[A-Za-z0-9._-]*
- url-safe JWT version
[= ]ey[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*
- all JWT versions (higher possibility of false positives)
From this point on we will be replaying tokens and looking for variations in the responses in order to identify issues. It is important to find a base request to use that provides a useful response that is clear whether a token is still valid.
A good example of this is a "profile" page on a website, as we can only access this when authorised to do so via the valid JWT.
After capturing a JWT we can replay it back to the application in the same context.
This should yield the same result as the original token.
To do this select the page load request containing the token and choose to replay it (in Burp right-click and choose Send to Repeater, in ZAP it is Open/Resend with Request Editor)
If the response is not the same then the token may have expired, or some other condition may have caused the token to become invalid. Have a dig to identify the issue. You'll need a repeatable, verifiable response in order to continue.
Remove the token from the request and observe the result - has the result changed?
Was the token required?
- Yes - Good! on to the next step
- No - perhaps the JWT isn't the means of authorisation on this app. Check for other headers, cookies or POST data that might be persisting the session. You may still be able to something with the token, so keep going.
Delete the last few characters of the signature. Does it: return an error, fail, or succeed?
Is the token checked?
- If an error message occurs the signature is being checked - read any verbose error info that might leak something sensitive.
- If the page returned is different the signature is being checked.
- If the page is the same then the signature is not being checked - time to start tampering the Payload claims to see what you can do!
Resend the same token multiple times, interspersed with sending no token, or one with an invalid signature (delete a character or two from the end of the token). Does it continue to work each time the valid token is sent?
Is the token persistent?
- Yes - the token stays static, which is common behaviour. However, this may indicate an immortal token if the same JWT is valid after logout, or after a very long duration. Be sure to retest this same token in ~24 hours and report it if it never expires.
- No - the token has either expired, or has been invalidated by the application. Some systems invalidate tokens every so often and either just send you a new token in a normal HTTP response, or may programmatically call a "refresh token" API endpoint to retrieve a new token. This may mean you need to switch out your base test token every so often, so keep re-checking it before sending a tampered token.
Check where the token originated in your proxy's request history. It should be created on the server, not the client.
- If it was first seen coming from the client-side then the key is accessible to client-side code - seek it out!
- If it was first seen coming from the server then all is well.
Alter any Payload claims that are directly reflected or processed on the page, but leave the signature the same. Did the altered values get processed?
Example:
If the Payload contains a profile image URL or some text
(e.g. {"login": "ticarpi", "image": "https://ticarpi.com/profile.jpg", "about": "Hello this is my profile page."})
then tweak the address to see if a new image is reflected in the page, or the text to see if that is altered in the response.
Tampering in jwt_tool: Enter tamper mode:
-
python3 jwt_tool.py [token] -T
-
Follow the menu to tamper various claims
-
Finally, choose to keep the original signature for this token
-
If the changes are accepted then the application is processing these before (or regardless of) signature verification. Look to see if you can tamper anything crucial.
-
If the changes aren't reflected then the JWT claims are being processed in the correct order.
HMAC signed keys (algs HS256/HS384/HS512) use symmetric encryption, meaning the key that signs the token is also used to verify it. Often these are set to simple passphrases/passwords.
As signature verifying is a self-contained process the token itself can be tested for valid passwords without having to send it back to the application to verify it.
HMAC JWT cracking is therefore an entirely offline affair and can be performed at GREAT SCALE by an attacker.
Many tools for JWT cracking exist, and jwt_tool
is no exception. This is useful for a quick check against known leaked password lists, or default passwords.
Use jwt_tool's -C flag alongside the -d dictionary.txt argument to attempt to verify the key against all the words in the dictionary file
This approach is useful, however due to the fact that a JWT is often the very KEY to the application, then a more robust testing strategy is recommended by using hashcat to harness highly-parallelized cracking attempts with a wide range of techniques available. Indeed, if you have a compatible GPU you can harness cracking JWTs with hashcat at the rate of hundreds of millions of guesses per second! With that in mind it is worth kicking off various tests in hashcat while you continue working through the rest of this methodology.
Hashcat commands:
- Dictionary attack:
hashcat -a 0 -m 16500 jwt.txt wordlist.txt
- Rule-based attack:
hashcat -a 0 -m 16500 jwt.txt passlist.txt -r rules/best64.rule
- Brute force attack:
hashcat -a 3 -m 16500 jwt.txt ?u?l?l?l?l?l?l?l -i --increment-min=6
A useful cracking approach:
- Dictionary attack with a common default password list
- Dictionary attack with a ‘leaked passwords’ wordlist
- Targeted Dictionary attack with words scraped from the target website (and related wiki pages?)
- Rules attack using the Targeted Dictionary
- Brute force attack using a narrow focus (e.g. ?u?l?l?l?l?l?l?l?l --incremental)
- Rules attack using a massive wordlist
- Long-running Brute force attack using a broad focus (e.g. ?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a -i --increment-min=6)
If you can crack the HMAC secret then you can forge anything you like in the token. This could be a critical vulnerability.
Something to consider about known vulnerabilities in JWTs libraries is that they are an attempt to get a forged token to be accepted by the application. As our attack changes the content that gets signed it also invalidates the existing signature.
Because of this we test by using the existing token Payload values alongside the relevant attack method.
If the attack is successful our forged token will be validated and the original value used.
To put it another way, if you are logged in as "user1" with a valid token and you try the alg:none attack with all the Payload data left intact and are logged out then the attack didn't work.
If you stay logged in then your attack was successful and you can start playing with the Payload values to see what you can now adjust to gain privileges or other beneficial results.
For more details on Known JWT Vulnerabilities and how to exploit them jump to Known Exploits
All the commands for performing these attacks in jwt_tool can be found here: Using jwt_tool
Set "alg": "none" with no signature, but don't change the payload - does page still return valid?
Use jwt_tool's -A flag to switch the algorithm and ditch the signature
- If the page returns valid then you have a bypass - go tampering.
You'll need an RSA Public Key to test this. Sometimes the application provides this via an API, or something hidden in the docs. Check out Finding Public Keys for more tips.
You will also need to use the right format of the Public Key. If the key is provided then you're fine, if not then best guess is PEM format. Note that PEM should contain a single newline character at the end, however some tools may miss this off when exporting a key.
Use jwt_tool's -V flag alongside the -pk public.pem argument to verify that the Public Key you found matches the key used to sign the token
Use jwt_tool's -K flag alongside the -pk public.pem argument to forge a new attack token
- If page returns valid then you have a bypass - go tampering.
Create a new RSA certificate pair, inject a JWKS file with the details of the Public Key in it, then sign the data with the Private Key. If successful the application should use your provided key data to verify it.
Use jwt_tool's -I flag to generate a new key pair, inject the JWKS containing the Public Key, and sign the token with the Private Key
- If page returns valid then you have a bypass - go tampering.
If the claim "kid" is used in the header, check the web directory for that file or a variation of it. For example if "kid":"key/12345” then look for /key/12345 and /key/12345.pem on the web root.
If the claim "kid" is used in the header, check if you can use a different file in the file system. Pick a file you might be able to predict the content of, or maybe try "kid":"/dev/tcp/yourIP/yourPort to test connectivity, or even some SSRF payloads...
Use jwt_tool's -T flag to tamper the JWT and change the value of the kid claim, then choose to keep the original signature
For the following attacks you will need to collect responses from the server. The idea is that you are trying to get the server to follow your links. An easy way to do this is with Burp Collaborator, however a number of other options exist depending on the tools you have available:
If you have an internet routable host (e.g. a web server) with terminal access you can use tcpdump
to collect interactions:
sudo tcpdump -A 'tcp dst port 80' -i eth0
Or you could watch the realtime HTTP traffic logging on your webserver. e.g.:
tail -f /var/log/apache2/access.log
You could even use a real-time logging service like RequestBin.
However for Burp users, Burp Collaborator is a simple option, just click on Burp Collaborator client in the Burp menu. Click Copy to clipboard to copy the current hostname to inject into your tests.
If you find you get interactions in Burp Collaborator you should check if it is just DNS or actually HTTP traffic.
- DNS may indicate a proxy or a WAF checking traffic for malicious payloads, or it could be a precursor to later HTTP traffic from the server.
- HTTP access typically suggests the service is attempting to interact - usually either to gather or load a resource, or to send data.
If the token uses a “jku” Header claim then check out the provided URL. This should point to a URL containing the JWKS file that holds the Public Key for verifying the token. Tamper the token to point the jku value to a web service you can monitor traffic for.
If you get an HTTP interaction you now know that the server is trying to load keys from the URL you are supplying. Use jwt_tool's -S flag alongside the -u http://example.com argument to generate a new key pair, inject your provided URL, generate a JWKS containing the Public Key, and sign the token with the Private Key
The following are known weaknesses that should be tested for.
Some web applications use a trusted JWT ‘service’ to generate and manage tokens for them. In the past some instances have occurred where a token generated for one of the JWT services’ clients can actually be accepted by another of the JWT services’ clients.
If you observe the JWT being issued or renewed via a third-party service then it is worth identifying if you can # for an account on another of that service’s clients with your same username/email. If so try taking that token and replaying it in a request to your target. Is it accepted?
- If your token is accepted then you may have a critical issue allowing you to spoof any user’s account. HOWEVER, be aware that if you are signing up on a third party application you may need to seek permission for wider testing permissions in case it enters a legal grey-area!
The “exp” Payload claim is used to check the expiry of a token. As JWTs are often used in the absence of session information, so they do need to be handled with care - in many cases capturing and replaying someone else’s JWT will allow you to masquerade as that user.
One mitigation against JWT replay attacks (that is advised by the JWT RFC) is to use the “exp” claim to set an expiry time for the token. It is also important to set the relevant checks in place in the application to make sure this value is processed and the token rejected where it is expired.
If the token contains an “exp” claim and test time limits permit it - try storing the token and replaying it after the expiry time has passed.
Use jwt_tool's -R flag to read the content of the token, which includes timestamp parsing and expiry checking (timestamp in UTC)
- If the token still validates in the application then this may be a security risk as the token may NEVER expire.
Now you’ve run through the known and common attacks, but wait - this is not the end!
As every good app tester knows: every piece of user-submitted data that is processed by the application could be an attack vector. As we know, we have full control of the contents of the token claims, so it’s time to get creative…
Fuzzing capabilities are coming soon to jwt_tool
, but until then, and for suggestions of techniques and approaches, check out the Tampering and Fuzzing page.