Post

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.

This post is licensed under CC BY 4.0 by the author.