[HackTheBox] Unicode: Poisoning JWT Sessions with JKU Claims

7 minute read

HackTheBox is a hacking playground. Often I spent time working on their challenges and boxes but I rarely do any public write-ups about it. The goal of these posts is to highlight something I learnt when working on them and what could have be done from a blue teamer’s perspective.

In September 2021, a medium HackTheBox machine came out called Unicode1 and it contained a very interesting exploit to poison the sessions in the application. I had to wait until it got retired before writing this so I would not be spoiling details about this. Regardless, feels like this is an interesting topic to discuss and chat about.

The session for the lab’s web application was generated using JSON Web Tokens(JWT) with an added specification: The JWT contained a JKU parameter in the header. The goal was to somehow trick the server to use an attacker controlled JKU instead of the originally intended way, therefore creating an admin session.

In this post, I want to discuss about this setup, how to secure it and the few ways someone could exploit this (including two possibilities for this challenge)

JWTs and JKUs

JWTs are quite common nowadays and they are used to authenticate and authorize users in web applications. They represent claims that are transferred between services and they use a standard JSON object format. These tokens are digitally signed using a JSON Web Signature (JWS) and/or are encrypted using JSON Web Encryption (JWE).

These tokens have a common structure: a header, a payload and the signature. The header typically describes the cryptographic components of the token, like which cryptographic algorithm has been used to sign the tokens but more metadata can be included in the header like the jku property that we are about to discuss.

You can read more about these tokens in the official RFC75152.

In this post we will be exclusively focused on alg and jku but keep in mind there could be a lot more of these just in the header.

The alg property specifies which cryptographic algorithm was used to sign the token, while the jku (which stands for JSON Web Key Set URL) is a URI for a resource with a set of JSON-encoded public keys, one of which was used to sign this token. In this challenge, the token specified using RS256 which is an asymmetric cryptographic algorithm, meaning it make use of a private and public key for signing and verifying the token. This is common when services from different domains use the same token. jku can be used to verify the public key used to sign the token, as an added layer of security - although this assumes the integrity of the resource is checked. If we observe two services that know each other’s public keys, then one of the services can validate and trust the provided auth token. A simple use case would be using the same token between multiple sites. For that to be done securely, the public key used from the asymmetric key3 has to be exposed online for other services to validate its legitimacy.

After watching IppSec’s instructional of this box I see we followed different paths on this, although his is way simpler and more straightforward. I made use of some scripts and exploits we can easily find online but using native Unix command-line tools is simpler and for sure works everywhere the same.

To exploit this part to forge a session, in a nutshell, an attacker can create a key pair using the RSA algorithm (which is the one used), and then we force the server to check for the JWKS.json file on a server we control.

We can make use of ssh-keygen, which comes natively in a Unix machine, to create an RSA key pair and then use openssl to extract the public part out of the key. Again, IppSec video demonstrates this really well, so I am not going to go over this again.

The final JWT payload sent to the server will look something like this:

2    "typ": "JWT",
3    "alg": "RS256",
4    "jku": "<server we control>/jwks.json",
8    "user": "admin"

When the server processes the above payload, it will essentially search for our jwks.json and consider it valid, even though we are using a key we (imposing as an attacker) control.

The following sections will discuss how this process could have been secured to avoid this vulnerability (that in fact is two vulnerabilities chained together).

Securing a JKU

Some servers, like the one in this challenge, verify the location provided in the jku property. They extract the URI in the header and check if it’s allowed. In this challenge, anything other than one expected single point would be rejected.

As mentioned before, the key to use this feature securely is integrity. Being an optional header, its usage has to be thoroughly planned to avoid falling short and have malicious actors forging sessions as they please. The next sessions discuss a few mechanisms I believe would suffice to secure this approach to session management.

Drop jku if symmetric key is used

Often we see JWTs signed using HS256, which is a symmetric cryptographic algorithm. In simpler terms, this means the same key to sign and verify the token.

If you expose the single key that has been used to sign and verify tokens then nothing is stopping a malicious actor from taking that key and create tokens at will. You would be opening up your authentication service to the world and badly misusing this header.

This holds true for any symmetric algorithm.

Source Validation

A key assurance we need to implement is that the URL in the JWT jku property is trusted. If we do not implement anything there, an attacker can simply forge sessions with ease by pointing the server to an untrusted resource.

To do this correctly, this check must not contain any wildcards or similar partial checks, as those can be easily bypassed.

Even though we did not have access to this server’s source code, I am confident the check in there is something like url contains https://<my trusted source>/*, which basically means any resource on its main subdomain would be accepted.

This creates holes in our posture. Whether you are serving multiple jwks or just one, then your implementation should be a single allow list containing expected and trusted URIs, where the individual check is url == "https://<my trusted source>/jwks.json.

This list should never be manipulated by user input to close attack opportunities from other places of our services.

Fix Code Vulnerabilities

Although probably other forms of attack can be used to achieve the same result, this challenge had us chaining another existing vulnerability in the application to forge the server to trust us. In this case, it was an open redirect.

Anything that can help attackers forge trust into resources they control has to be mitigated. Open redirects, in addition to improper logic checks as discussed in the previous check, are the perfect combination to forge these requests and go unnoticed.

On top of mind, an additional vulnerability that could still be used to spoof the server to trust an external resource would be SSRF4. Say an endpoint your source validation would approve is vulnerable to server-side request forgery, then an attacker could forge a request to retrieve an external jwks and have the server trust it, even though it is coming from an external source.

Moreover, as briefly discussed in the last section, if a user can manipulate the trusted sources list from the outside, then it’s easy to still spoof the service to the wrong resource.

The main lesson here is that developers should try to reduce the attack surface and always adopt secure coding practices. A minor vulnerability like an open redirect would be a sufficient trigger to make such a devastating attack work.

[Bonus] Single-purpose Service

Speaking of adopting secure coding practices and reducing attack surface, the server serving the key set should be its own service and, preferably, isolated from other business functions to reduce the attack service. You are not vulnerable if you don’t have anything to exploit (insert smart thinking meme).

A single purposed service would facilitate the job of hardening it, controlling it and ensuring it is secure from many forms of attack. Complex ones have larger surfaces of attack and we are just increasing our risk. Additionally, monitoring should also be easy to do on a single, simple service instead of a giant more complex one.

Which leads me to another bonus topic:

[Bonus] Monitoring and Logging

An added layer of security that could be added to this service would be proper monitoring and logging. You need systems in place to detect external, unexpected outbound calls. That should suffice to trigger something in your systems and have someone following up with what could happen.

To have proper monitor, naturally, we need proper logging. OWASP Logging Cheat Sheet is a great start to tackle this topic.

What would you do differently if you had the chance?

Thanks for reading,

comments powered by Disqus