HackTheBox Cubicle Riddle Challenge
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.
#!/usr/bin/python3
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
>>> 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
>>> 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:
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
#!/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
Cubicle Riddle: reduce the custom rules to a scriptable check and use the smallest reliable path to the flag.