HackTheBox Cubicle Riddle Writeup
Explore the basics of cybersecurity in the Cubicle Riddle Challenge on Hack The Box. This easy-level Challenge introduces encryption reversal and file handling concepts in a clear and accessible way, perfect for beginners.
https://app.hackthebox.com/challenges/687
Description
Navigate the haunting riddles that echo through the forest, for the Cubicle Riddle is no ordinary obstacle. The answers you seek lie within the whispers of the ancient trees and the unseen forces that govern this mystical forest. Will your faction decipher the enigma and claim the knowledge concealed within this challenge, or will the forest consume those who dare to unravel its secrets? The fate of your faction rests in you.
Analysis
The key exploitable section resides in riddler.py
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import types
from random import randint
class Riddler:
max_int: int
min_int: int
co_code_start: bytes
co_code_end: bytes
num_list: list[int]
def __init__(self) -> None:
self.max_int = 1000
self.min_int = -1000
self.co_code_start = b"d\x01}\x01d\x02}\x02"
self.co_code_end = b"|\x01|\x02f\x02S\x00"
self.num_list = [randint(self.min_int, self.max_int) for _ in range(10)]
def ask_riddle(self) -> str:
return """ 'In arrays deep, where numbers sprawl,
I lurk unseen, both short and tall.
Seek me out, in ranks I stand,
The lowest low, the highest grand.
What am i?'
"""
def check_answer(self, answer: bytes) -> bool:
_answer_func: types.FunctionType = types.FunctionType(
self._construct_answer(answer), {}
)
return _answer_func(self.num_list) == (min(self.num_list), max(self.num_list))
def _construct_answer(self, answer: bytes) -> types.CodeType:
co_code: bytearray = bytearray(self.co_code_start)
co_code.extend(answer)
co_code.extend(self.co_code_end)
code_obj: types.CodeType = types.CodeType(
1,
0,
0,
4,
3,
3,
bytes(co_code),
(None, self.max_int, self.min_int),
(),
("num_list", "min", "max", "num"),
__file__,
"_answer_func",
"_answer_func",
1,
b"",
b"",
(),
(),
)
return code_obj
The user input is processed as Python bytecode to construct a Code
object, which subsequently generates a Function
object _answer_func
. The Code
object is supplied with:
co_consts
:(None, self.max_int, self.min_int)
co_varnames
:("num_list", "min", "max", "num")
To enforce specific functionality, additional bytecode is prepended and appended to the user’s bytecode.
Prepended Bytecode
1
2
3
4
5
>>> dis.dis(b"d\x01}\x01d\x02}\x02")
0 LOAD_CONST 1 # Load self.max_int onto the stack
2 STORE_FAST 1 # Store in variable "min"
4 LOAD_CONST 2 # Load self.min_int onto the stack
6 STORE_FAST 2 # Store in variable "max"
Purpose: Initializes min
and max
with values self.max_int
(e.g., 1000
) and self.min_int
(e.g., -1000
).
Appended Bytecode
1
2
3
4
5
>>> dis.dis(b"|\x01|\x02f\x02S\x00")
0 LOAD_FAST 1 # Load variable "min"
2 LOAD_FAST 2 # Load variable "max"
4 BUILD_TUPLE 2 # Create a tuple (min, max)
6 RETURN_VALUE # Return the tuple
Purpose: Ensures the function output is always (min, max)
.
The resulting function behaves like:
1
2
3
4
5
6
7
8
9
def _answer_func(num_list: list[int]) -> tuple[int, int]:
min: int = 1000 # self.max_int
max: int = -1000 # self.min_int
for num in num_list:
if num < min:
min = num
if num > max:
max = num
return (min, max)
Exploitation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python3
from pwn import *
import sys
def _answer_func(num_list: int):
min: int = 1000
max: int = -1000
for num in num_list:
if num < min:
min = num
if num > max:
max = num
return (min, max)
def send_bytecode(target):
ip, port = target.split(":")
port = int(port)
print(f"[+] Connecting to {ip}:{port}...", flush=True)
conn = remote(ip, port)
conn.recvuntil(b"(Choose wisely) > ")
conn.sendline(b"1")
conn.recvuntil(b"(Answer wisely) > ")
print(f"[*] ByteCode : {_answer_func.__code__.co_code}")
bytecode = list(b'\x97\x00d\x01}\x01d\x02}\x02|\x00D\x00]\x12}\x03|\x03|\x01k\x00\x00\x00\x00\x00r\x02|\x03}\x01|\x03|\x02k\x04\x00\x00\x00\x00r\x02|\x03}\x02\x8c\x13|\x01|\x02f\x02S\x00')
bytecode_str = ",".join(map(str, bytecode))
print(f"[+] Decimal Bytecode to Send: {bytecode_str}", flush=True)
conn.sendline(bytecode_str.encode())
print("[+] Receiving response...", flush=True)
response = conn.recvall(timeout=5).decode()
print("[*] Server Response:\n" + response, flush=True)
conn.close()
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <ip:port>", file=sys.stderr)
sys.exit(1)
target = sys.argv[1]
send_bytecode(target)
Summary
The Crubicle Riddle Challenge introduces basic Python bytecode exploitation and dynamic function construction. By crafting and injecting valid bytecode, you compute the minimum and maximum values from a list to solve the challenge. This is an excellent starting point for beginners exploring Python internals and exploitation techniques.