Thinking as an hacker: JWT JKU exploit
Don’t forget to put on your black hoodie, it’s time to hack! I recently did a presentation about looking at code from the perspective of a hacker and I thought it would be interesting to also share here. In my opinion developers can write better and more secure code when they put of their developer cap and put on the black hoodie of a hacker (metaphorically of course). To illustrate what I mean, I’ll be taking a look at a technique that most developers probably know: JWT.
Developer perspective vs hacker perspective
Developers have a different goal for an application in comparison to the goal of a hacker. The developer wants to build an application with fulfills the requirements, while a hacker often simply has the goal to break in and steal something. A developer has thoughts like ‘Which solution is the cheapest?’, ‘Which solution is the most maintainable?’ and ‘Will this StackOverflow code? Let’s just copy it and see whether the tests are still green.’. Often the question the developer asks is ‘How can I get it working?’. A hacker has different thoughts, like ‘What technique is used?’, ‘Do I understand the technique that’s used?’ and ‘Is the technique applied properly everywhere?’. For a hacker it boils down to asking him/herself ‘Can I find a weakness?’ and ‘Do I understand how the application works?’. Resulting from this different mindset comes a different perspective at an application. I think by looking at an application from the perspective of the other, can be benefitial. If a developer looks at a piece of code from the perspective of an hacker, he/she might ask: ‘Do I actually understand what this piece does that I copied from StackOverflow?’. To give an example of this, I’ll dive into the JWT JKU exploit.
Authentication with JWT
A widely used way for authenticating is JSON Web Tokens (JWTs). Since implementing authentication can get pretty complex, it’s not uncommon to look online for examples of implementations. In the following repository there is a setup with an authentication service and greeting service: jwt-jku-attack. The user can register and login via the authentication service and will get back a JWT. The JWT can be used to get a greeting fromt the /greeting
endpoint, but will get an ‘unauthenticated’ error when accessing the /secret_greeting
endpoint. To make it a bit easier, here is some background information:
- To access the
/secret_greeting
endpoint the roladmin
is necessary - There is already an account named
admin
registered - Authentication is done via a JWT token in the
Authorization
header
Let’s take a look at some questions the hacker could think and which action it would lead to:
- Is the authentication of
/secret_greeting
secured properly?- Send empty value for
Authorization
header - Don’t send
Authorization
header at all
- Send empty value for
- Is the login secured properly? We could try to login as
admin
- Try empty password
- Bruteforce password by trying common password (e.g. rockyou list)
- Try social engineering (e.g. phishing)
- Is the registration secured properly?
- Try to register new account with name
admin
- Try to register new account with name
All of the above are good things to try, but unluckily these won’t get us the secret greeting. It seems that everything is implemented properly, but of course we don’t give up that easily. There’s still the following question to answer: Is the JWT token secured properly? Let’s dive deeper into JWT.
The returned JWT looks as follows:
eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9wdWJsaWNfa2V5In0.eyJyb2xlIjoidXNlciJ9.GaUUwQR3RpXVFimlhxKdya1NCzORTpz0ytnTXyczNX4NLHx8HAGOzUwr3L9kcUFkQufweiyfgQ0dcfkH1Iu19xES8jj0NQbOf6Zs50PvNaDAny6KPhnofQw_cH3Et9fieec_zmF0ucXH9ztuQ2D0oCUSE9STU1TeCFR32QRl8t706mLIKXnfiAWHSEdAS5c-QL8oMs15jMY069Z2X3gDmYsWjh3DJVaT2tQ5-FVMrq19tbOvYLLSrJTDPgkJsrJjy9qV-J43dNm3DWej-Hoiv6T-M1PbMmEEKVyNfvoIZlz8zht9kbISbTiGRk4WYU4d7PFI6z11AeCkjX70X7YAGQ
Decoded from base64 we get three segments separated by dots:
// Header
{
"alg": "RS256",
"jku": "http://localhost:5000/public_key"
}
// Payload
{
"role": "user"
}
// Signature
...
The first part, the header, contains metadata, like the algorithm used for the signature (in this case RS256). The second part, the payload, is a free format JSON object, where in this case a role property is set. The third part, the signature, uses the private key of the server to create a signature of the payload using the indicated algorithm. Now that we know this, there are some thoughts a hacker might have:
- Is the algorithm implemented properly?
- Use a different, less secure algorithm instead of
RS256
- Use a different, less secure algorithm instead of
- Is the signature implemented properly?
- Change the
role
in the payload segment toadmin
without changing the signature - Same as previous, but with empty signature
- Change the
Nice try again, but we still get a 401…
How about: What is this JKU property?
Exploiting JWT using JKU
When in doubt, read the docs here:
The "jku" (JWK Set URL) Header Parameter is a URI [RFC3986] that
refers to a resource for a set of JSON-encoded public keys, one of
which corresponds to the key used to digitally sign the JWS. The
keys MUST be encoded as a JWK Set [JWK]. The protocol used to
acquire the resource MUST provide integrity protection; an HTTP GET
request to retrieve the JWK Set MUST use Transport Layer Security
(TLS) [RFC2818] [RFC5246]; and the identity of the server MUST be
validated, as per Section 6 of RFC 6125 [RFC6125]. Also, see
Section 8 on TLS requirements. Use of this Header Parameter is
OPTIONAL.
Since the ‘Header’ part of the token is not part of the signature, we can change the value of the jku
property to anything we want. This means we can let it point to our own hosted service with our generated public key. Under the hood the server should retrieve the public key from the specified jku location and validate the signature in the token using the public key. Since we also control the signature included in the token we have control over the the signature and validation of the signature. This means we could do the following:
- Create our own public/private key pair
- Craft our own payload for the token
- Create a signature of the payload using our own private key pair
- Host a server which returns our public key
- Set the
jku
field in the header of our token to the url where we host our public key - Combine all of these into a token and send this token to the server
- The server will retrieve our public key
- The server will validate the signature signed using our private key with our public key
- The signature should be valid and the server should think this token is valid and use what is in the payload
The generated token would look as follows:
eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTMzNy9wdWJsaWNfa2V5In0.eyJyb2xlIjoiYWRtaW4ifQ.n9iH3wVkOJrA6TcW35AVP_rLmUn-IIR9FK8mlKEB7zdbCTTnqfMSjEjQDYqg1xhxNCwesSIzerylU4U1IX4lH-Krm54NYZd79x7j-Bae5TR9X2iPJlmCpgV60ZKk9uKiwRwfH0Pj4hSWT9NYcLzHHjdrE6wSmrMLUJADXWGPq7L28MWrOlUdZ65bDEH_Ju682a-sl_5G91hkNHas7IPhnJ7uC2kiM-t04W2ePSqWl5x1as7v6ywliFKrBc0L_UAyZlnClVvv5NB8ZUqaVz9IPdrl5Wc5rmE3ww1CA2n4e8isX89s1AjKOEi5IT8qoLvPWYPL3cSN818DimF8LNKauw
Decoded:
// Header
{
"alg": "RS256",
"jku": "http://localhost:1337/public_key"
}
// Payload
{
"role": "admin"
}
// Signature
...
And… this works! With this token we are an admin and we can access to the /secret_greeting
endpoint. Thus the server allows the user to supply their own jku
value and doesn’t check the value.
Possible mitigations
For this specific there are multiple possible mitigations:
- Don’t allow the user to specify the
jku
value, but set it within the service - Check the
jku
value based on a regex or whitelist - Don’t allow traffic from within the network of the company to addresses outside the network
I think also knowing how to prevent this issue and similar issues in the future is valuable:
- Ask help of senior/security engineers
- Add code reviews and when doing those don’t just look at the syntax of code, but ask questions about what’s in between the lines of code
- For each service/component decide the impact if there are security issues with it, for critical ones spend extra attention to them when reviewing
And of course the point I’m trying to make:
- Try to understand how the code you write works
- When in doubt whether the code does what you think, don’t make assumptions
- When you’re not sure, try to find out what it does or ask for help
- Don’t think about just the happy-flow, but also other flows the code can go through (in the above JKU example: what if someone provides their own URL?)
Conclusion
I hope this post showed an interesting exploit, but also a different way to look at software. Understanding the code you write instead of just assuming you do can be very beneficial to the security, but also to your own technical development.