Énoncé
My Dearest Hacker,
LoveNote built it’s reputation on trust. Every message, every action, signed and verified by the system itself. LoveNoe claims that no message can be forged, no identity faked.
Yet an internal leak suggests the platform may be trusting something it shouldn’t. With Valentine’s Day fast approaching, the consequences of a broken trust system could be disastrous.
You can find the web application here:
http://MACHINE_IP:5000
Retex
Cette fois-ci c’est un site de note qui signe et chiffre chaque action par le système.

Mais d’après l’énoncé le système de confiance n’est pas top.

Déjà, on peut voir que même sans se créer de compte, on peut se connecter au compte admin sans mdp.
Et le message envoyé plus tôt par ce compte est :
Welcome to LoveNote
Welcome to LoveNote! Send encrypted love messages this Valentine's Day. Your communications are secured with industry-standard RSA-2048 digital signatures.
On y comprend donc que ce chall sera consacré au RSA et donc le jeu de public key / private key.

Après un rapide gobuster on peut voir qu’il y a pleins de pages existantes, mais plus particulièrement une page “debug”.

On peut y accéder même sans se connecter à un compte donc elle est accessible de l’extérieur.
On y apprend aussi qu’au lieu d’utiliser de la vraie randomisation, les clés sont générées à partir d’un seed prévisible basé sur un username.
La seed est toujours de la forme :
seed = "{username}_lovenote_2026_valentine"Donc si on a la seed, on peut recréer la clé privée d’un compte, admin à tout hasard.
Le site nous permet de vérifier si un message a bien été fait par une personne à partir :
- username
- message
- Digital Signature (Hex)
On va utiliser le script suivant, qui se base sur cette page de debug.
import hashlib
from sympy import nextprime
from Crypto.PublicKey import RSA
from Crypto.Signature import pss
from Crypto.Hash import SHA256
from Crypto.Util.number import inverse
TARGET_USER = "grostest"
TARGET_MESSAGE = "pitit_message"
def generate_admin_key():
print(f"[*] Generating deterministic 512-bit key for: {TARGET_USER}")
# Deterministic seed
seed_str = f"{TARGET_USER}_lovenote_2026_valentine"
seed_bytes = seed_str.encode()
# Generate p
sha256_p = hashlib.sha256(seed_bytes).hexdigest()
p = nextprime(int(sha256_p, 16))
# Generate q
sha256_q = hashlib.sha256(seed_bytes + b"pki").hexdigest()
q = nextprime(int(sha256_q, 16))
n = p * q
e = 65537
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
key = RSA.construct((n, e, d, p, q))
modBits = key.size_in_bits()
h = SHA256.new(TARGET_MESSAGE.encode())
emLen = (modBits - 1 + 7) // 8
maxSalt = emLen - h.digest_size - 2
print(f"[*] Key size: {modBits} bits")
print(f"[*] EM Length: {emLen} bytes")
print(f"[*] Calculated Max Salt: {maxSalt} bytes")
if maxSalt < 0:
raise ValueError("Key too small for PSS padding.")
signer = pss.new(key, salt_bytes=maxSalt)
signature = signer.sign(h)
return signature.hex()
if __name__ == "__main__":
try:
signature = generate_admin_key()
print("\n" + "="*60)
print(" >>> FINAL ZEUS SIGNATURE <<< ")
print("="*60)
print(signature)
print("="*60)
print("1. Go to Verify page.")
print("2. User: zeus")
except Exception as e:
print(f"[!] Error: {e}")Afin de vérifier que le script fonctionne bien, on peut déjà comparer ce qu’il donne avec ce que nous donne le site lors de la création d’un compte :


Et en vérifiant la signature avec l’ancien message et le compte admin, on a accès au flag : THM{PR3D1CT4BL3_S33D5_BR34K_H34RT5}
Explication de la faille
Les clés RSA sont générées avec une seed prévisible ({username}_lovenote_2026_valentine) au lieu d’entropie aléatoire. Avec la seed d’admin, on recrée sa clé privée et signe n’importe quel message → le site valide la signature comme légitime.
Pour s’en protéger
- Random vrai : utiliser un vrai random ou une seed vraiment secrète ou changeante pour éviter de la retrouver.
- Pas d’info leak : La page
/debugne doit jamais exister en prod