JWT Attacks
Home | CheatSheets | Theory | About | Back |
JWT Background:
JWTs serve as an alternative to traditional session management and contain information about users. They are used for authentication, session handling and implementing access control mechanisms. Unlike session cookies, JWTs store all of the information about the user on the client side which is useful for highly distributed web applications.
A JWT consists of three parts:
-
Header - Contains metadata about the token itself.
-
Payload - Contains user data and information
-
Signature - A signature is generated by hashing the header and payload and may also be encrypted.
Each component is a base64url-encoded JSON object separated by dots ‘.’ and can be read by anybody with access to a token using a decoder. Since the data in a JWT can be easily read and modified by anyone who has access to it, the security of JWTs relies on the presence of a cryptographic signature.
When a server issues a JWT token a signature is typically generated from the header and payload values. This involves using a secret signing key that is known only by the server and allows the server to verify that the contents of the JWT has not been modified. Since the signature is generated from the header and payload, changing any part of the JWT will result in a signature mismatch, it should be impossible to generate the correct signature for a given header and payload without knowing the secret signing key.
A JWT is either implemented as a JWS (JSON Web Signature) or JWE (JSON Web Encryption), these specifications define how a JWT should be implemented in practice. In most cases, people refering to JWTs are actually refering to a JWS, a JWE is similar to a JWS except the header and payload contents are encrypted rather than encoded.
JWT Attack Methods:
JWT attacks are performed in an attempt to bypass authentication and access control mechanisms, if we are able to steal or forge JWTs, we might be able to perform actions on behalf of another application user and obtain sensitive information. In the worst case scenario, we might be able to elevate our privileges and take full control over user accounts.
JWT vulnerabilities typically arise due to a mishandling of the token in the application, many specifications for JWTs exist and it is often up to the developer to implement the functionality themselves. This can result in accidental vulnerabilities being introduced even if libraries are used to harden the application environment. JWT vulnerabilties are commonly found in the way the JWT signature is verified, if the server fails to properly verify a JWT signature, an attacker might be able to tamper with the values that are passed to the application via the payload field. In addition, the integrity of a JWT lies entirely on the assumption that the secret signing key remains secret, if this key is guessable via bruteforce or is leaked in some other way for example through a LFI attack, an attacker could generate a valid signature for any token.
Exploiting JWT Signature Verification:
The application server does not store any information about the JWTs that it issues by design, instead each token is a self contained entity. This design provides many benefits for developers and applications but introduces a different problem, how does the server know about the original contents of the token that it issued? If the server fails to verify the signature correctly, an attacker can make arbitrary changes to the token.
Consider this example JWT token:
{
"username": "john",
"userType": "user"
}
If the server uses the username name/value pair to identify which user account to display and signature verification is flawed, an attacker could change the value to any user they wish and take over their account. Similarly, if the value of the userType name/value pair was changed to “admin”, an attacker might be able to access administration functionality.
Accepting Arbitrary Signatures:
JWT code libraries typically provide two methods for handling tokens, one method will verify the token and the other will simply decode the token. The Node.js jsonwebtoken library provides the methods verify() for verifying the tokens signature and decode() for decoding the JWT, it could be the case that in the implementation of the JWT functionality, the developer(s) passed the token only to the decode() method and not to the verify() method beforehand. This means that the application does not perform any signature validation at all before returning application data to the user.
If the server fails to validate the signature in this way, we could try changing the values in the payload section to other users or administrative usernames in an attempt to access restricted parts of the application. To do this, modify the payload values in a JWT editor such as the one provided in burpsuite and replace your token with the one you modified. If the server does not perform any signature validation you might be able to access restricted parts of the application such as user other user accounts, administration portals, etc. Remember, if the server does not perform any signature validation you won’t need to change the signature of the JWT in anyway just the payload values.
Removing the Signature:
The JWT header contains a parameter called ‘alg’ which informs the server which algorithm was used to sign the token. This is used by the server when validating the signature since it needs to know which algorithm to use to regenerate the signature from the given token:
{
"alg": "HS256",
"typ": "JWT"
}
. . . . . . . . . . .
{
"alg": "none",
"typ": "JWT"
}
Since JWTs are stored on the client side we can also tamper with the values specified in the header component of the token. JWTs can use different signing algorithms such as HS256 but they can also be left unsigned. If this is the case, the value of the ‘alg’ name/value pair can be set to ‘none’ which indicates an insecure JWT. Typically servers will reject tokens with no signature but this is typically done through string validation, it is sometimes possible to bypass these validation checks by using character obfuscation techniques such as character encoding or random capitalization.
It is important to note that even if a JWT is unsigned it must still contain a trailing dot ‘.’
JWT Header Parameter Injections:
The JWS specification states that only the ‘alg’ header parameter is required but in practice, JWT headers also known as JOSE headers can contain several other parameters. From an attackers perspective, we are interesting in the following parameters:
-
jwk (JSON Web Key) - Provides an embedded JSON object representing the key.
-
jku (JSON Web Key Set URL) - Provides a URL from which servers can fetch a set of keys containing the correct key.
-
kid (Key ID) - Provides an ID that servers can use to identify the correct key in cases where there are multiple keys to choose from. Depending on the format of the key, this may have a matching kid parameter.
Each of these parameters are interesting to us since they tell the server which key to use when verifying the signature of a JWT, we can attempt to exploit these by injecting modified JWTs signed using our own arbitrary key rather than the servers.
Self Signing JWTs using the ‘jwk’ Parameter:
We can inject self signed JWTs via the “jwk” parameter, the jwk parameter can be used by servers to embed their own public key directly within the token itself in jwk format. “A JWK (JSON Web Key) is a standardized format for representing keys as a JSON object.” Below is an example of jwk in a JWT:
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}
In a perfect world, servers should only use a whitelist of public keys to verify the signatures in JWTs, misconfigured servers on the other hand sometimes use any key that is embedded in the jwk parameter. You can exploit this behavior by signing a modified JWT using your own RSA private key, then embedding the matching public key in the jwk header.
To perform this attack using BurpSuite we can follow the steps taken from the PortSwigger labs on JWTs:
-
In Burp, load the JWT Editor extension from the BApp store.
-
In the lab, log in to your own account and send the post-login GET /my-account request to Burp Repeater.
-
In Burp Repeater, change the path to /admin and send the request. Observe that the admin panel is only accessible when logged in as the administrator user.
-
Go to the JWT Editor Keys tab in Burp’s main tab bar.
-
Click New RSA Key.
-
In the dialog, click Generate to automatically generate a new key pair, then click OK to save the key. Note that you don’t need to select a key size as this will automatically be updated later.
-
Go back to the GET /admin request in Burp Repeater and switch to the extension-generated JSON Web Token tab.
-
In the payload, change the value of the sub claim to administrator.
-
At the bottom of the JSON Web Token tab, click Attack, then select Embedded JWK. When prompted, select your newly generated RSA key and click OK.
-
In the header of the JWT, observe that a jwk parameter has been added containing your public key.
-
Send the request. Observe that you have successfully accessed the admin panel.
Self Signing JWTs using the ‘jku’ Parameter:
Instead of embedding public keys directly using the ‘jwk’ parameter, some application servers will let you use the ‘jku’ header parameter to references a jwk set containing the public key. When verifiying the signature of a JWT, the server fetches the relevant key from the provided URL. A “JWK Set”, is a JSON object containing an array of JWKs representing different keys:
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
{
"kty": "RSA",
"e": "AQAB",
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
}
]
}
JWK sets such as the one illustrated above, are sometimes accessible at application endpoints such as /.well-known/jwks.json, secure web applications will only fetch keys from trusted domains and sources but it could be possible to use URL parsing discrepancies to bypass filtering.
To perform this attack using BurpSuite we can follow the steps taken from the PortSwigger labs on JWTs:
-
In Burp, load the JWT Editor extension from the BApp store.
-
In the lab, log in to your own account and send the post-login GET /my-account request to Burp Repeater.
-
In Burp Repeater, change the path to /admin and send the request. Observe that the admin panel is only accessible when logged in as the administrator user.
-
Go to the JWT Editor Keys tab in Burp’s main tab bar.
-
Click New RSA Key.
-
In the dialog, click Generate to automatically generate a new key pair, then click OK to save the key. Note that you don’t need to select a key size as this will automatically be updated later.
-
In the browser, go to the exploit server.
-
Replace the contents of the Body section with an empty JWK Set as follows:
{
"keys": [
]
}
-
Back on the JWT Editor Keys tab, right-click on the entry for the key that you just generated, then select Copy Public Key as JWK.
-
Paste the JWK into the keys array on the exploit server, then store the exploit.
-
Go back to the GET /admin request in Burp Repeater and switch to the extension-generated JSON Web Token message editor tab.
-
In the header of the JWT, replace the current value of the kid parameter with the kid of the JWK that you uploaded to the exploit server.
-
Add a new jku parameter to the header of the JWT. Set its value to the URL of your JWK Set on the exploit server.
-
In the payload, change the value of the sub claim to administrator.
-
At the bottom of the tab, click Sign, then select the RSA key that you generated in the previous section.
-
Make sure that the Don’t modify header option is selected, then click OK. The modified token is now signed with the correct signature.
-
Send the request. Observe that you have successfully accessed the admin panel.
Injecting Self-Signed JWTs via the KID (Key Identifier) Parameter:
Application servers often use many different keys to sign different kinds of data, JWTs are typically just one type of application data that needs signing by the server. For this reason, the header of a JWT may contain a KID parameter, this helps the server identify which key to use when verifying the signature of a JWT.
Verification keys are usually stored as a JWK Set, in this case the server may simply look for the JWK with the same KID as the token. The JWS specification does not specify a concrete structure for this ID and instead this is left to the developer, it is simply an arbitrary string. A developer might set the KID parameter to point to an entry in a database or even a file name. If the “kid” parameter is also vulnerable to directory traversal, an attacker could force the server to use an arbitrary file from the servers filesystem as the verification key.
{
"kid": "../../path/to/file",
"typ": "JWT",
"alg": "HS256",
"k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}
This attack vector is particularly dangerous if the server also supports JWT signing using a symmetric encryption algorithm. In this case, an attacker could point the ‘kid’ parameter to a predicatable static file and sign the JWT using a secret that matches the contents of the file. One of the most simplest methods do this is to use the /dev/null file on Linux based systems, since the file is empty fetching it returns a null value and we can sign the token with a Base64-encoded null byte, this will result in a valid signature.
If the server stores its verification keys in a database, the kid header parameter is also a potential vector for SQL injection attacks.
To perform this attack using BurpSuite we can follow the steps from the PortSwigger labs on JWTs:
-
In Burp, load the JWT Editor extension from the BApp store.
-
In the lab, log in to your own account and send the post-login GET /my-account request to Burp Repeater.
-
In Burp Repeater, change the path to /admin and send the request. Observe that the admin panel is only accessible when logged in as the administrator user.
-
Go to the JWT Editor Keys tab in Burp’s main tab bar.
-
Click New Symmetric Key.
-
In the dialog, click Generate to generate a new key in JWK format. Note that you don’t need to select a key size as this will automatically be updated later.
-
Replace the generated value for the k property with a Base64-encoded null byte (AA==).
-
Go back to the GET /admin request in Burp Repeater and switch to the extension-generated JSON Web Token message editor tab.
-
In the header of the JWT, change the value of the kid parameter to a path traversal sequence pointing to the /dev/null file:
../../../../../../../dev/null
-
In the JWT payload, change the value of the sub claim to administrator.
-
At the bottom of the tab, click Sign, then select the symmetric key that you generated in the previous section.
-
Make sure that the Don’t modify header option is selected, then click OK. The modified token is now signed using a null byte as the secret key.
-
Send the request and observe that you have successfully accessed the admin panel.
Defending JWT Attacks:
-
Use an up-to-date library for handling JWTs and make sure your developers fully understand how it works, along with any security implications. Modern libraries make it more difficult for you to inadvertently implement them insecurely, but this isn’t foolproof due to the inherent flexibility of the related specifications.
-
Make sure that you perform robust signature verification on any JWTs that you receive, and account for edge-cases such as JWTs signed using unexpected algorithms.
-
Enforce a strict whitelist of permitted hosts for the jku header.
-
Make sure that you’re not vulnerable to path traversal or SQL injection via the kid header parameter.
-
Always set an expiration date for any tokens that you issue.
-
Avoid sending tokens in URL parameters where possible.
-
Include the aud (audience) claim (or similar) to specify the intended recipient of the token. This prevents it from being used on different websites.
-
Enable the issuing server to revoke tokens (on logout, for example).
Advanced JWT Algorithm Confusion Attacks:
References for Content and Additional Attacks:
Algorithm confusion attacks commonly known as “key confusion attacks”, occur when an attacker is able to force the server into verifying the signature of a JWT using a different algorithm that is intended by the web application. If this case is not handled correctly, attackers could forge valid JWTs containing arbitrary values without requiring the secret signing key.
JWTs can be signed using a plethora of different algorithms, some such as HS256 use a symmetric key, meaning that the server uses a single key to sign and verify the token. This key needs to be kept secret much like a password would be. Other algorithms such as RS256 (RSA + SHA-256) utilise “asymmetric” cryptography, a private key is used to sign the token and the public key is used to verify the signature. The private much like the symmetric key must be kept secret!
Algorithm confusion vulnerabilities arise due to flawed implementation of JWT libraries. Actual verification differs on the algorithm used, many libraries provide a single, alogrithm-agnostic method for verifying signatures. These methods rely on the “alg” header parameter to determine the type of verification to perform, e.g:
function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader();
if(algorithm == "RS256"){
// Use the provided key as an RSA public key
} else if (algorithm == "HS256"){
// Use the provided key as an HMAC secret key
}
}
Issues arise when developers who uses these methods assume that it will exclusively handle JWTs signed using an asymmetric algorithm like RS256. Due to this assumption they may always pass a fixed public key to the method:
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);
In this scenario, the server recieves a token signed by a symmetric algorithm like HS256, the libraries verify() method will treat the public key as an HMAC secret. An attacker could sign the token using HS256 and the public key, and the server will use the same public key to verify the signature. The public key you use to sign the token must be absolutely identical to the public key stored on the server. This includes using the same format (such as X.509 PEM) and preserving any non-printing characters like newlines. In practice, you may need to experiment with different formatting in order for this attack to work.
An algorithm confusion attack typically follows these abstracted steps:
- Obtain the servers public key
- Convert the public key to a suitable format
- Create a malicious JWT with a modified payload and the alg header set to HS256
- Sign the token with HS256 using the public key as secret.
Perhaps the most complex part of this attack is step 2. Public keys are typically stored in JWT format but when the server verifies the signature of a token it will use its own copy of the key on the local filesystem or database. In order for the attack to work the version of the key used to sign the JWT must be identical to the servers local copy, it must be the same format and every byte must match including non-printing characters. To convert a key into X.509 PEM format using BurpSuite we can:
-
With the JWT Editor Extension, in Burp’s main tab bar, go to the JWT Editor Keys tab.
-
Click New RSA Key. In the dialog, paste the JWK that you obtained earlier.
-
Select the PEM radio button and copy the resulting PEM key.
-
Go to the Decoder tab and Base64-encode the PEM.
-
Go back to the JWT Editor Keys tab and click New Symmetric Key.
-
In the dialog, click Generate to generate a new key in JWK format.
-
Replace the generated value for the k parameter with a Base64-encoded PEM key that you just copied.
-
Save the key.
Once we have converted the key into suitable format, we can modify our JWT however we like, we must make sure however that the alg header parameter is set to HS256. We can then use BurpSuite again to sign the token using the HS256 algorithm and the RSA public key as a secret, we can do this using the attack function of the JSON Web Tokens extension and selecting “HMAC Key Confusion”.
Please see: https://portswigger.net/web-security/jwt/algorithm-confusion for more details :)
We can also attempt to derive public keys from existing tokens if we are unable to uncover them on the server. This process uses toolkits such as jwt_forgery.py and other tools from https://github.com/silentsignal/rsa_sign2n.
This uses the JWTs that you provide to calculate one or more potential values of n. Don’t worry too much about what this means - all you need to know is that only one of these matches the value of n used by the server’s key. For each potential value, our script outputs:
-
A Base64-encoded PEM key in both X.509 and PKCS1 format.
-
A forged JWT signed using each of these keys.
To identify the correct key, use Burp Repeater to send a request containing each of the forged JWTs. Only one of these will be accepted by the server. You can then use the matching key to construct an algorithm confusion attack.
References:
- https://portswigger.net/web-security/jwt
- https://portswigger.net/web-security/jwt/algorithm-confusion
- https://book.hacktricks.xyz/pentesting-web/hacking-jwt-json-web-tokens