HackTheBox Cyber Apocalypse 2025 — Twin Oracle CTF Writeup
Welcome back to my WriteUp! Today, I’m excited to take you through my journey of solving Twin Oracles, the most challenging cryptography challenge in HackTheBox’s Cyber Apocalypse CTF 2025. I’ll break down my thought process, the techniques I used, and the obstacles I overcame to crack this tough puzzle. Let’s dive in!
Description
A powerful artifact — meant to generate chaos yet uphold order — has revealed its flaw. A misplaced rune, an unintended pattern, an oversight in the design. The one who understands the rhythm of its magic may predict its every move and use it against its creators. Will you be the one to claim its secrets?
Let’s start analyzing the provided file
This is the provided server.py:
from Crypto.Util.number import *
FLAG = bytes_to_long(open('flag.txt', 'rb').read())
MENU = '''
The Seers await your command:
1. Request Knowledge from the Elders
2. Consult the Seers of the Obsidian Tower
3. Depart from the Sanctum
'''
class ChaosRelic:
def __init__(self):
self.p = getPrime(8)
self.q = getPrime(8)
self.M = self.p * self.q
self.x0 = getPrime(15)
self.x = self.x0
print(f"The Ancient Chaos Relic fuels the Seers' wisdom. Behold its power: M = {self.M}")
def next_state(self):
self.x = pow(self.x, 2, self.M)
def get_bit(self):
self.next_state()
return self.extract_bit_from_state()
def extract_bit_from_state(self):
return self.x % 2
class ObsidianSeers:
def __init__(self, relic):
self.relic = relic
self.p = getPrime(512)
self.q = getPrime(512)
self.n = self.p * self.q
self.e = 65537
self.phi = (self.p - 1) * (self.q - 1)
self.d = pow(self.e, -1, self.phi)
def sacred_encryption(self, m):
return pow(m, self.e, self.n)
def sacred_decryption(self, c):
return pow(c, self.d, self.n)
def HighSeerVision(self, c):
return int(self.sacred_decryption(c) > self.n//2)
def FateSeerWhisper(self, c):
return self.sacred_decryption(c) % 2
def divine_prophecy(self, a_bit, c):
return self.FateSeerWhisper(c) if a_bit == 0 else self.HighSeerVision(c)
def consult_seers(self, c):
next_bit = self.relic.get_bit()
response = self.divine_prophecy(next_bit, c)
return response
def main():
print("You stand before the Seers of the Obsidian Tower. They alone hold the knowledge you seek.")
print("But be warned—no force in Eldoria can break their will, and their wisdom is safeguarded by the power of the Chaos Relic.")
my_relic = ChaosRelic()
my_seers = ObsidianSeers(my_relic)
counter = 0
while counter <= 1500:
print(MENU)
option = input('> ')
if option == '1':
print(f"The Elders grant you insight: n = {my_seers.n}")
print(f"The ancient script has been sealed: {my_seers.sacred_encryption(FLAG)}")
elif option == '2':
ciphertext = int(input("Submit your encrypted scripture for the Seers' judgement: "), 16)
print(f'The Seers whisper their answer: {my_seers.consult_seers(ciphertext)}')
elif option == '3':
print("The doors of the Sanctum close behind you. The Seers watch in silence as you depart.")
break
else:
print("The Seers do not acknowledge your request.")
continue
counter += 1
print("The stars fade, and the Seers retreat into silence. They shall speak no more tonight.")
if __name__ == '__main__':
main()Analyzation between the 2 classes
ChaosRelic Class
The ChaosRelic class functions as a pseudo-random bit generator (PRNG) using a Blum-Blum-Shub (BBS)-like method. It initializes with two randomly chosen 8-bit prime numbers (p and q), computing M = p * q, and selects a 15-bit prime (x0) as the initial state. The generator updates its state iteratively by squaring the previous value modulo M, ensuring a level of unpredictability. The least significant bit (LSB) of x is extracted at each step to produce a sequence of random bits, which are later used to control oracle selection. This method relies on quadratic residues and modular arithmetic, fundamental to cryptographic PRNGs.
ObsidianSeers Class
The ObsidianSeers class implements a 1024-bit RSA encryption system, generating two 512-bit primes (p, q) to compute n = p * q. Using the standard public exponent 65537, it derives the private exponent via modular inversion. Messages are encrypted using the RSA formula c=memod nc = m^e \mod nc=memodn and decrypted with m=cdmod nm = c^d \mod nm=cdmodn. The class also provides two distinct oracle functions. The parity oracle (FateSeerWhisper) reveals whether the decrypted plaintext is even or odd, which can be exploited in an LSB Oracle Attack. The location oracle (HighSeerVision) determines whether the plaintext is in the lower or upper half of [0, n-1], aiding in a binary search attack. These two oracles are combined within the twin_Oracles function, which dynamically selects an oracle based on an input bit—querying the parity oracle when the bit is 0 and the location oracle when the bit is 1.
To add complexity, the Ask_twins function integrates the BBS PRNG, using its output bits to decide which oracle will be queried at each step, making the decryption process non-deterministic. Additionally, to prevent excessive interaction, the challenge imposes a query limit, if the player exceeds 1500 queries, the program automatically terminates.
Solution
What is the vulnerability here??? There’s 2, let me explain.
It is well known that RSA becomes vulnerable when a location oracle is available.
The security of BBS relies on the difficulty of factoring MM, but in this case, factoring MM is trivial.
How to solve it? Here’s our plan
We will factor M.
2. We will determine the cycle length of the BBS generator.
3. We will construct an oracle distinguisher to recover all bits within the BBS cycle.
4. We will design a method to convert the parity oracle into a location oracle.
5. We will apply this conversion strategically, guided by predicted BBS outputs.
6. We will utilize the location oracle to conduct a binary search and uncover the plaintext.
This is the decode script that I’ve made to automate the process and crack this challenge
When we run the script, this is the output (Take note that the cracking process takes a lot of time).

As you can see here, the wait is worth it! It gives me the flag!!!
Conclusion
This attack highlights critical vulnerabilities in RSA when combined with a weak PRNG like Blum-Blum-Shub (BBS). By factoring MM, we can determine the BBS period, allowing us to predict its future outputs. With access to both a parity oracle and a location oracle, we can progressively narrow down the possible plaintext values based on the oracle responses. By leveraging this information, we apply a binary search approach, systematically refining the search space until the plaintext is fully recovered. This demonstrates how even minor information leaks in cryptographic implementations can be exploited to completely break encryption. The challenge reinforces the importance of using cryptographically secure PRNGs and ensuring that no unintended information is leaked, as attackers can use even small hints to compromise an otherwise secure system.
Last updated