Tags: crytography web 


# Run this script in python 3.8 or newer

import base64
import hashlib

Disclaimer: I did not solve this challenge under the CTF (although I was close). This was solved after the CTF had finished with some help from the challenge author.
I am sharing this as a writeup to help others who like me were curious about the solution to this challenge, yet since so few actually solved it there has been
no solution made public until now.

Since the algorithm as described in the header is ES256, the curve is P-256 (see: https://ldapwiki.com/wiki/ES256). Logging in with different usernames gives a signature with the same prefix,
so there's likely a reuse of k-values. The last 32 bytes of the signature is a SHA256 hash of the token header and payload.

I recommend you to read https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Signature_generation_algorithm,
which details an attack on repeated k-values, and is implemented in this python script. Reading this will give you a better understanding
of this script.

Suprisingly, you didn't have to mess with curves of any kind to solve this challenge, just some basic modular artihmetic, web stuff, and perhaps some research

# curve parameters of P-256 (see: https://ldapwiki.com/wiki/P-256)
n = 115792089210356248762697446949407573529996955224135760342422259061068512044369
L_n = len(bin(n)[2:])

def parse_signature(token):
t = token.split(".")

sig = base64.urlsafe_b64decode(t[2]+"===")
m = t[0]+"."+t[1]

e = hashlib.sha256(m.encode()).digest()

z = int(bin(int(e.hex(),16))[2:L_n+2],2)

r = int(sig[:-32].hex(),16) # start of the signature is r, which is defined as the x-component of the point k*G on the curve
s = int(sig[-32:].hex(),16) # the last 32 bytes of the signature is the s value
return r,s,z

# two tokes generated for the usernames "admim" and "admio" respectively
token1 = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VybmFtZSI6ICJhZG1pbSJ9.wN0kGlDUj5n8x6GGptROB2PskEeOHe-ONvXE6VDWevuMm5QA7M6_aA7V3oa0ohEVhggmzaDWBArvODBI1in4IQ"
token2 = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VybmFtZSI6ICJhZG1pbyJ9.wN0kGlDUj5n8x6GGptROB2PskEeOHe-ONvXE6VDWevucC-db4xgw8VCtuYuKyMUSvvJ7qI8dKwOmWdBDj8dy2g"

# calculate the signature parameters from the tokens
r1, s1, z1 = parse_signature(token1)
r2, s2, z2 = parse_signature(token2)

assert r1==r2 # checks if the r-values match for the two signatures, if they do (spoiler alert: they do), then the same k-value is used and a signature can be forged

# recover the private key d from the two signatures using modular arithmetic (read the wikipedia article)
k = ((z1-z2)*pow((s1-s2)%n,-1,n))%n # k = (z-z')/(s-s') (mod n)
d = ((s1*k-z1)*pow(r1,-1,n))%n

# header and payload of the JWT
admin_token = b"eyJhbGciOiAiRVMyNTYiLCAidHlwIjogIkpXVCJ9." + base64.urlsafe_b64encode(b'{"username": "admin"}')
admin_token = admin_token.replace(b"=",b"")

# calculate the signature using the recovered private key (again, read the wikipedia article)
e = hashlib.sha256(admin_token).digest() # e = HASH(m)
z = int(bin(int(e.hex(),16))[2:L_n+2],2) # z = e[:L_n] (in bits)
r = r1
s = (pow(k,-1,n)*(z+r*d))%n # s = (z+rd)/k (mod n)

# putting r and s together and encoding
signature = bytes.fromhex(hex(r)[2:]) + bytes.fromhex(hex(s)[2:])
admin_token += b"." + base64.urlsafe_b64encode(signature)

# print forged token

Original writeup (https://github.com/williamsolem/writeups/blob/main/NahamCon%202021/Elliptical/solve.py).