As per jwt.io:
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
JWT (JSON Web Token) is widely used for authentication and authorization purposes. When a user enters their credential to sign in, the application verifies the credential, and if matched, issues a JWT to the user. Browser then sends the JWT to the server (usually through the Authorization
header or cookies) for accessing protected resources.
There are 3 parts of a JSON Web Token which are separated by dots (.
):
Let's understand each of them one by one,
1{ 2 "alg": "HS256", 3 "typ": "JWT" 4} 5
This section is then encoded using base64Url which results in eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
Mostly, there are two algorithms used in JWT: RS256 (uses the private key to sign the JWT and public key for verifying) and HS256 (uses a secret key to sign and verify).
1{ 2 "iat":"1695023887", 3 "exp":"1695025687", 4 "email":"[email protected]", 5 "isAdmin":true 6} 7
Here iat
and exp
are issued at time (when the JWT is issued) and expiration time (when the JWT will expire) respectively.
This section is then encoded using base64Url to form the second part of the JWT. This results in eyJpYXQiOiIxNjk1MDIzODg3IiwiZXhwIjoiMTY5NTAyNTY4NyIsImVtYWlsIjoidHVoaW4uYm9zZUBidWdiYXNlLmluIiwiaXNBZG1pbiI6dHJ1ZX0
1$header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; 2$payload = "eyJpYXQiOiIxNjk1MDIzODg3IiwiZXhwIjoiMTY5NTAyNTY4NyIsImVtYWlsIjoidHVoaW4uYm9zZUBidWdiYXNlLmluIiwiaXNBZG1pbiI6dHJ1ZX0"; 3$secretKey = "BugBase@$3cur!7y"; 4$signature = hash_hmac('sha256', "$header.$payload", $secretKey, true); 5
This results in W7F89xlBibTOQ_fVxUzm0KJDZwcIXbSkNDBYKcffAb0
.
So the final JWT would be: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNjk1MDIzODg3IiwiZXhwIjoiMTY5NTAyNTY4NyIsImVtYWlsIjoidHVoaW4uYm9zZUBidWdiYXNlLmluIiwiaXNBZG1pbiI6dHJ1ZX0.W7F89xlBibTOQ_fVxUzm0KJDZwcIXbSkNDBYKcffAb0
.
You can decode a JWT using jwt.io:
Before proceeding with the common vulnerabilities present in JWT, we need a burp extension called "JSON Web Tokens". You can download it by navigating to the extender tab and searching for the name in the BApp Store.
none
algorithm where any token would be considered valid if the signature is empty. For example, the token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpYXQiOiIxNjk1MDIzODg3IiwiZXhwIjoiMTY5NTAyNTY4NyIsImVtYWlsIjoidHVoaW4uYm9zZUBidWdiYXNlLmluIiwiaXNBZG1pbiI6dHJ1ZX0.
will be considered as valid. If you notice carefully, there is no signature part here and the header and body are just base64UrlEncoded. You can also try changing the algorithm to None
/noNe
/nOnE
/NONE
etc.hashcat -a 0 -m 16500 <JWT-Token> <Path-to-Wordlist>
We can use rockyou.txt
or any other popular wordlist for this.
RS256
to HS256
(in the header) and use their public key to verify the token. You can get the certificate of the web server using the following command:openssl s_client -connect example.com:443 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > certificatechain.pem
openssl x509 -pubkey -in certificatechain.pem -noout > pubkey.pem
1{ 2 "alg": "HS256", 3 "typ": "JWT", 4 "kid": "68bf80e1-ecbb-4e36-a976-5eeb0d4d50fb" 5} 6
As you can imagine, the database must have a secret key corresponding to the id 68bf80e1-ecbb-4e36-a976-5eeb0d4d50fb
. The application will fetch the secret key from the database and use it to verify the JWT token. Another example would be,
1{ 2 "alg": "HS256", 3 "typ": "JWT", 4 "kid": "http://localhost:8080/key1.txt" 5} 6
After receiving the JWT, the backend will fetch the key from the above URL and then use the key to sign/verify the token.
Now since the KID can be controlled by the user, there can be multiple vulnerabilities:
1{ 2 "alg": "HS256", 3 "typ": "JWT", 4 "kid": "/var/keys/key1.txt" 5} 6
It may possible that in the backend the application is passing the KID value directly into a command without proper sanitization:
1header = jwt.get_unverified_header(token) 2kid = header.get("kid") 3secret_key = subprocess.check_output([f"cat {kid}"], shell=True).decode().strip() 4decoded_payload = jwt.decode(token, secret_key, algorithms=["HS256"]) 5
Now, in this case, an attacker can simply achieve RCE by the following payload:
We can also try directory traversal attack if the KID contains path of the secret key. In this case, we will use a publicly available file to verify the token.
1{ 2 "alg": "HS256", 3 "typ": "JWT", 4 "kid": "../../../robots.txt" 5} 6
3. If the KID contains the URL of the secret key like this:
1{ 2 "alg": "HS256", 3 "typ": "JWT", 4 "kid": "http://localhost:8080/key1.txt" 5} 6
We can try changing the URL to our controlled domain so that the secret key is fetched from our server to verify the JWT.
If the application allows only RS256
algorithm, we can try to generate the public and private key by the following command:
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
How to Hunt These Vulnerabilities?
i. Look for an endpoint that returns some data about the user if the given JWT is correct, otherwise returns 401 or a similar response. Usually profile page is a good start.
ii. Manipulate JWT in the request and try each vulnerability (if applicable) one by one.
There are many more vulnerabilities related to JWT. We will discuss it in part 2 of this blog.
For automating most of the attacks, you can use jwt_tool. Check their README.md
for installation and usage instructions.
[1] Hacktricks: https://book.hacktricks.xyz/pentesting-web/hacking-jwt-json-web-tokens
[2] Payloads All The Things: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/JSON%20Web%20Token
[3] JWT.io
by Auth0: https://jwt.io/introduction
All About JWT Vulnerabilities