YALM
hackthebox
RSA encryption server with e=3 and hidden modulus N. Option 1 encrypts a flag with known prefix. Option 2 provides a binary oracle that reveals whether input m < N or m >= N. Goal: recover N via binary search, then use Coppersmith's method to find the flag.
$ ls tags/ techniques/
YALM — HackTheBox
Description
"I created an encryption server with RSA, but I forgot to show the modulus. Can you help me recover it yet another time?"
Server at 154.57.164.75:30995
RSA encryption server with e=3 and hidden modulus N. Option 1 encrypts a flag with known prefix. Option 2 provides a binary oracle that reveals whether input m < N or m >= N. Goal: recover N via binary search, then use Coppersmith's method to find the flag.
Analysis
The server implements RSA with:
e = 3(very small public exponent)- Unknown modulus
N(2048-bit) - Two options:
- Option 1 (Get secret): Encrypts a message with known prefix
'Hey! This is my secret... it is secure because RSA is extremely strong and very hard to break... Here you go: '(110 bytes) concatenated with the FLAG, and returns the ciphertext. - Option 2 (Test encryption): Takes a hex plaintext
m, encrypts it in a loop dividing by N each iteration. Ifm >= N, the loop runs more than once and returns "Too many messages!". Ifm < N, the loop runs once and returns "Thanks for the message!".
- Option 1 (Get secret): Encrypts a message with known prefix
The key vulnerability is the combination of:
- Binary oracle in option 2 allows recovering N exactly via binary search
- e = 3 with known message prefix enables Coppersmith's stereotyped message attack
For Coppersmith's method to work, the unknown portion must be smaller than N^(1/e). With e=3 and N being 2048-bit, we can recover up to ~682 bits of unknown data. The flag is ~25 bytes = 200 bits, well within bounds.
Solution
Step 1: Recover N via Binary Search
The test_encryption oracle tells us whether any given integer m is less than N or not. We binary search between 2^2047 and 2^2048 to find the exact value of N. This requires ~2047 network queries.
from pwn import * HOST = "154.57.164.75" PORT = 30995 def connect(): r = remote(HOST, PORT) r.recvuntil(b"Option: ") return r def get_secret(r): r.sendline(b"1") line = r.recvuntil(b"Option: ").decode() for l in line.split('\n'): if 'Ciphertext:' in l: ct = l.split('Ciphertext: ')[1].strip() return int(ct, 16) return None def test_encryption(r, m_int): m_hex = hex(m_int)[2:] r.sendline(b"2") r.recvuntil(b"Plaintext: ") r.sendline(m_hex.encode()) resp = r.recvuntil(b"Option: ").decode() if "Thanks" in resp: return True # m < N elif "Too many" in resp: return False # m >= N return None def binary_search_n(r): low = 1 << 2047 high = 1 << 2048 iterations = 0 while high - low > 1: mid = (high + low) // 2 result = test_encryption(r, mid) if result: low = mid else: high = mid iterations += 1 if iterations % 200 == 0: log.info(f"Iteration {iterations}, remaining: {(high-low).bit_length()} bits") return high r = connect() ct = get_secret(r) log.info(f"Got ciphertext: {hex(ct)[:50]}...") N = binary_search_n(r) log.info(f"Recovered N: {hex(N)[:50]}...") r.close()
Step 2: Coppersmith's Stereotyped Message Attack
With e = 3 and known N, we apply Coppersmith's method:
- The message format is:
prefix || FLAGwhere the prefix is 110 bytes of known text. - The unknown FLAG is ~25 bytes ≈ 200 bits.
- Coppersmith's bound for
e=3isN^(1/e) = N^(1/3) ≈ 2^682bits. - Since 200 < 682, the unknown part is small enough for Coppersmith to recover.
# SageMath script for Coppersmith attack from Crypto.Util.number import long_to_bytes, bytes_to_long # Values from binary search N = <recovered_N> ct = <ciphertext> e = 3 prefix = b'Hey! This is my secret... it is secure because RSA is extremely strong and very hard to break... Here you go: ' def coppersmith_attack(N, e, ct, prefix, flag_len): """Coppersmith's stereotyped message attack using SageMath""" prefix_int = bytes_to_long(prefix) # Message = prefix * 256^flag_len + x where x is the unknown flag # c = (prefix * 256^flag_len + x)^e mod N P.<x> = PolynomialRing(Zmod(N)) shift = 256^flag_len f = (prefix_int * shift + x)^e - ct # Make monic f = f.monic() # Find small roots using Coppersmith's method # X is the upper bound on the root X = 256^flag_len roots = f.small_roots(X=X, beta=1.0, epsilon=0.05) return roots # Try different flag lengths for flag_len in range(20, 60): roots = coppersmith_attack(N, e, ct, prefix, flag_len) for root in roots: flag_bytes = long_to_bytes(int(root)) if b'HTB{' in flag_bytes: print(f"FLAG: {flag_bytes.decode()}") break
Full Solve Script
#!/usr/bin/env python3 from pwn import * from Crypto.Util.number import long_to_bytes, bytes_to_long HOST = "154.57.164.75" PORT = 30995 def connect(): r = remote(HOST, PORT) r.recvuntil(b"Option: ") return r def get_secret(r): r.sendline(b"1") line = r.recvuntil(b"Option: ").decode() for l in line.split('\n'): if 'Ciphertext:' in l: ct = l.split('Ciphertext: ')[1].strip() return int(ct, 16) return None def test_encryption(r, m_int): m_hex = hex(m_int)[2:] r.sendline(b"2") r.recvuntil(b"Plaintext: ") r.sendline(m_hex.encode()) resp = r.recvuntil(b"Option: ").decode() if "Thanks" in resp: return True elif "Too many" in resp: return False return None def binary_search_n(r): low = 1 << 2047 high = 1 << 2048 while high - low > 1: mid = (high + low) // 2 if test_encryption(r, mid): low = mid else: high = mid return high # Phase 1: Recover N r = connect() ct = get_secret(r) N = binary_search_n(r) r.close() print(f"N = {N}") print(f"ct = {ct}") # Phase 2: Coppersmith (run in SageMath) # Save N and ct, then run the SageMath script above
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [crypto][free]Mystery Box— hackthebox
- [crypto][free]Rhome— HackTheBox
- [crypto][Pro]RSA?— grodno_new_year_2026
- [crypto][Pro]ChristmasRSA— grodno_new_year_2026
- [crypto][Pro]Hidden Log Factoring— tamuctf