Post

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.

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