HackTheBox Crubicle Riddle Writeup
Explore the basics of cybersecurity in the Crubicle 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.
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)
Proof of Concept
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.