HTB Cyber Apocalypse 2024 Misc WriteUp

Lightfoe
12 min readMar 22, 2024

--

Lightfoe — Misc very easy to hard with the help of my collegue Jacopo

Hi Folks!
Welcome to the next part of my write-up series covering Cyber Apocalypse 2024: Hacker Royal, CTF event hosted by #HackTheBox.

As we transition from the Forensics segment, we now venture into the intriguing realm of Misc. In this series, I’ll embark on dissecting the Miscellaneous challenges… these tasks present a diverse array of puzzles and scenarios that test participants’ creativity, problem-solving skills, and adaptability across various cybersecurity domains, all requiring some level of programming skills.

Stay tuned as we delve into the fascinating world of Miscellaneous challenges in Cyber Apocalypse 2024! 🎩✨

Easy Misc

Challenge: Character

The flag is HTB{tH15_1s_4_r3aLly_l0nG_fL4g_i_h0p3_f0r_y0Ur_s4k3_tH4t_y0U_sCr1pTEd_tH1s_oR_els3_iT_t0oK_qU1t3_l0ng!!}

Description

The server asks us to specify the index of the flag we desire. Upon our request, say for index 3, 4, or 5, it promptly responds with the corresponding letter. A conventional iteration with a for loop would seamlessly reveal the entire flag.

This challenge serves as a tutorial within its category, encouraging participants to automate processes using specific tools. While it’s possible to obtain the flag through manual interaction, this approach isn’t ideal for two reasons:

  1. Manual interaction consumes significant time.
  2. Crafting a simple script not only aids in obtaining the flag but also establishes a foundation in the basics of this category, offering long-term benefits.

In this challenge, as I said before, scripting isn’t an absolute necessity, as it can be accomplished without writing a single line of code, although it’s strongly recommended, especially for subsequent challenges.

With the awareness of the flag’s syntax, detecting if the character is the closing brace signifies the culmination of the flag. Subsequently, we could gracefully conclude the program using a Python invocation such as sys.exit(0), indicative of the program’s successful execution.

Ordinarily, in such challenges, this step isn’t usually done since the server typically terminates the connection upon getting the flag. Nevertheless, “I have sinned, Father, please forgive me”—since I did not do that way but relied on the except clause to spit it out the indexes found as soon as I got some irregularity like it closes the connection🤣

For this task, I relied on pwn-tools, my trusty companion in such scenarios. I initiated the docker instance and configured a remote server from it.

from pwn import *

def get_index(p, answer,x):
p.sendline(f"{x}")
answers = []
answer = str(p.recvuntil(b'Which character (index) of the flag do you want? Enter an index: '))
answer = answer.split("\\n")[0].split(": ")[1]
answers.append(answer)
return "".join(answers)

p = remote('94.237.58.148', '52284')
rcv = str(p.recvuntil(b'Which character (index) of the flag do you want? Enter an index: '))
answers = []
p.sendline(b'1')
answer = str(p.recvuntil(b'Which character (index) of the flag do you want? Enter an index: '))
answer = answer.split("\\n")[0].split(": ")[1]
answers.append(answer)
answer=''
try:
for x in range(180):
answer+=get_index(p, answer, x)

except:
success(f"Flag is : {answer}")
sys.exit(0)

While the initial iteration could be hard-coded, I opted for a recursive approach for subsequent steps. Each iteration brought me closer to unraveling the answer bit by bit. 🔍

Finally, employing the try-catch syntax, I detected any errors. Upon realizing the absence of further errors, I, then, transitioned to the success syntax. 🎉

And voila! We’ve snagged the first flag of this category! 🚩

I’ll provide some more insight in the next challenge since this stage would suffice a single iteration with no regard for what the server is answering, only that we give the response and join it later.

Challenge: Stop Drop and Roll

The flag is HTB{1_wiLl_sT0p_dR0p_4nD_r0Ll_mY_w4Y_oUt!}

In this task, I’ll provide a more comprehensive explanation of the code, considering it slightly differs from the previous version since it now requires some manipulation of responses at a rudimentary level.

The rules are simple, there is a syntax of three words that are like attacks gorge phreak and fire, and to a specific attack it corresponds to a specific action: stop, drop, and roll.

First and foremost, our initial step entails crafting a dictionary that maps actions to their respective responses.

Next, by invoking the Python script with “DEBUG” as an argument, we gain access to more detailed insights into the process. Through scrutinizing the pattern utilizing the provided technique, we gradually get the precise values necessary for the recvuntil functions.

By referencing the dictionary as mentioned earlier, we can easily evaluate the responses and overcome the challenge. It’s worth noting that typically, we iterate a high number of times until we obtain the flag, as per the standard procedure for such challenges. In the event of an unexpected occurrence, the script will transition into interactive mode, breaking out of the loop. While it would be prudent to include a timeout value for these functions, I omitted it this time, given the simplicity of the task and its successful completion on the first attempt. 😇

from pwn import *

def repeat_test(p):
try:
rcv = str(p.recvuntil(b'What do you do? '))
question = rcv[2:].split("\\n")[0]
moves = question.split(",")
answer = []
for move in moves:
answer.append(actions[move.strip()])
info(f"Question is {question}")
answer="-".join(answer)
info(f"Answered with {answer}")
p.sendline(answer)
except:
rcv = str(p.recvuntil(b'}'))
success("Flag is : %s", rcv)
sys.exit(0)


actions = {"GORGE":"STOP","PHREAK":"DROP","FIRE":"ROLL"}
p = remote('94.237.58.148', '37170')
print(p.recvuntil(b'Are you ready? (y/n)'))
p.sendline("y")
rcv = str(p.recvuntil(b'What do you do? '))
question = rcv.split("\\n")[1]
moves = question.split(",")
answer = []
for move in moves:
answer.append(actions[move.strip()])
info(f"Question is {question}")
answer="-".join(answer)
info(f"Answered with {answer}")
p.sendline(answer)
for i in range(1000):
repeat_test(p)

p.interactive()

This code is pretty self-explanatory, however, if you would like some more explanations, please feel free to reach out and DM me

There were 498 iterations, if someone would go manually on the first challenge, he would die before completing this one 😆💀

Hard Misc

Challenge: Path of Survival

This is the hard challenge of this category, it was resolved by my colleague Jacopo, we thought it together but he wrote the code by himself and I thank him for his resolve in doing so!

The flag is HTB{i_h4v3_mY_w3ap0n_n0w_dIjKStr4!!!

We are in front of a web docker instance:

The rules

The game requires players to navigate through various terrain tiles to reach a weapon quickly. Players can use WASD or arrow keys for movement or script their actions. Each tile has terrain, affecting movement time. The objective is to reach a weapon before time runs out, with 100 completions granting the flag. Terrains include Cliff, Geyser, Mountain, Plains, River, and Sand, with an Empty tile representing gaps. Tiles may depict players or weapons. Movement within the same terrain costs 1 time point, as does movement to or from Cliffs or Geysers. Special entry rules apply to Cliffs and Geysers. Timepoint-costs vary based on terrain type.

The API

There are 6 endpoints:

  • /: Loads the UI.
  • /rules: Loads the Rules.
  • /api: Loads information about the API (here).
  • /regenerate: Regenerates the map, resetting solved games to 0. If something goes somehow “wrong”
  • /map: A POST request (with no data) returns game information in JSON format.
  • /update: Takes a POST request with JSON data. The “direction” field should be set to one of UDLR to move the player Up, Down, Left, or Right. This endpoint returns information for updating the front end after each move, along with any errors due to invalid choices. If the player reaches the weapon before time runs out, a “solved” field is set to True. If all games are solved without errors, the “flag” field contains the flag, completing the challenge.

What options did we have to solve the problem?

The first approach that came to mind was the brute force one; try every possible move and look for the less expensive one. It is trivial to understand that with this kind of solution, the complexity increases a lot when dealing with large grids.

So, what is the right way then?

The Dijkstra algorithm.

The Dijkstra algorithm is a popular method used to find the shortest path in a graph or network or, in this case, a grid. It works by starting at a specific node — our initial position — and then exploring all possible paths, selecting the one with the lowest Time cost at each step.

I first implemented the Dijkstra algorithm in Python, creating some utility functions:

  • A function to look for every possible move, considering obstacles and unallowed moves.
  • A function to calculate the Time cost of a given move.

The Dijkstra algorithm is structured as follows:

For every node in the open_list — the list containing every node we need to explore — let’s take the first one, the one with a minor Time cost.

Then, we need to look for every neighbor of the current node — that is, the list of possible moves — and calculate the Time cost for moving from our current node to a specific neighbor.

After that, if we have not explored the neighbor yet or if we find a move less expensive than another move previously found, we add the node to the open_list and we keep track of the path to reach that node.

At the end, we sort the open_list by the total cost and we start the loop again.

When we reach one of the destinations, we build our path and return it.

As the server accepted some API calls to make our moves effective, we needed to convert our path to a list of “Right”, “Left”, “Down” or “Up” moves:

Making the right API calls 100 times, as the game states in the description, the game is successfully completed.

import os, sys
import requests
from pathlib import Path
from enum import Enum
import json


class Terrain(Enum):
EMPTY = 0
CLIFF = 1
GEYSER = 2
MOUNTAIN = 3
PLAINS = 4
RIVER = 5
SAND = 6


terrain_dict = dict()
terrain_dict["P"] = Terrain.PLAINS
terrain_dict["E"] = Terrain.EMPTY
terrain_dict["C"] = Terrain.CLIFF
terrain_dict["M"] = Terrain.MOUNTAIN
terrain_dict["R"] = Terrain.RIVER
terrain_dict["G"] = Terrain.GEYSER
terrain_dict["S"] = Terrain.SAND

board = {}


def updateboard(address: str):
global board
game_board = requests.post(f"{address}/map", timeout=10)
map_file_path = os.path.join(Path(__file__).parent.absolute(), "map.json")
if not os.path.exists(map_file_path):
# if not exist, do create a file
with open(map_file_path, "w") as map_file:
pass
with open(map_file_path, "r+", encoding="utf-8") as file:
file.write(game_board.text)
board = json.loads(game_board.text)


class Position:

def __init__(self, x, y) -> None:
self.x = x
self.y = y

@staticmethod
def is_position_near(x_src: int, y_src: int, x_dst: int, y_dst: int, x_limit: int, y_limit: int):
# Check if we are inside the grid
if x_dst >= x_limit or y_dst >= y_limit or x_dst < 0 or y_dst < 0:
return False
# Moving on the x axis
elif abs(x_src - x_dst) == 1 and y_src == y_dst:
return True
# Moving on the y axis
elif abs(y_src - y_dst) == 1 and x_src == x_dst:
return True
else:
return False


class Table:
@staticmethod
def get_move_cost(source: Terrain, dest: Terrain) -> int:
if source == dest:
return 1
elif source == Terrain.CLIFF or source == Terrain.GEYSER or dest == Terrain.CLIFF or dest == Terrain.GEYSER:
return 1

elif (source == Terrain.PLAINS and dest == Terrain.MOUNTAIN) or (source == Terrain.PLAINS and dest == Terrain.RIVER) or (source == Terrain.RIVER and dest == Terrain.PLAINS) or (source == Terrain.MOUNTAIN and dest == Terrain.SAND):
return 5

elif (source == Terrain.MOUNTAIN and dest == Terrain.PLAINS) or (source == Terrain.PLAINS and dest == Terrain.SAND) or (source == Terrain.SAND and dest == Terrain.PLAINS):
return 2

elif source == Terrain.SAND and dest == Terrain.MOUNTAIN:
return 7

elif (source == Terrain.MOUNTAIN and dest == Terrain.RIVER) or (source == Terrain.SAND and dest == Terrain.RIVER):
return 8

elif source == Terrain.RIVER and dest == Terrain.MOUNTAIN:
return 10

elif source == Terrain.RIVER and dest == Terrain.SAND:
return 6

@staticmethod
def is_move_possible(x_src, y_src, x_dst, y_dst, max_width, max_height):
if not Position.is_position_near(x_src, y_src, x_dst, y_dst, max_width, max_height):
return False
# Can't go on EMPTY tile
dst_terrain = Table.get_type_of_terrain_from_pos(x_dst, y_dst)
if dst_terrain == Terrain.EMPTY:
return False
# Can reach a CLIFF only from up or the left
if dst_terrain == Terrain.CLIFF and (y_src == y_dst + 1 or x_src == x_dst + 1):
return False
# Can reach a GEYSER only from the bottom or the right
if dst_terrain == Terrain.GEYSER and (y_src == y_dst - 1 or x_src == x_dst - 1):
return False

return True

@staticmethod
def get_type_of_terrain_from_pos(x, y) -> Terrain:
letter = board["tiles"][f"({x}, {y})"]["terrain"]
return terrain_dict.get(letter)

@staticmethod
def get_max_width():
return board["width"]

@staticmethod
def get_max_height():
return board["height"]

@staticmethod
def get_remaining_time():
return board["player"]["time"]

@staticmethod
def get_current_x():
return board["player"]["position"][0]

@staticmethod
def get_current_y():
return board["player"]["position"][1]

@staticmethod
def get_dest():
result = []
for key, value in board["tiles"].items():
if value["has_weapon"] == True:
key = key.strip("()")
array = key.split(",")
result.append([int(x) for x in array])
return result

@staticmethod
def get_neighbors(src):
"""Src nel formato [0, 1]"""
x_source = src[0]
y_source = src[1]

result = []

if Table.is_move_possible(x_source, y_source, x_source+1, y_source, Table.get_max_width(), Table.get_max_height()):
result.append([x_source+1, y_source])
if Table.is_move_possible(x_source, y_source, x_source-1, y_source, Table.get_max_width(), Table.get_max_height()):
result.append([x_source-1, y_source])
if Table.is_move_possible(x_source, y_source, x_source, y_source+1, Table.get_max_width(), Table.get_max_height()):
result.append([x_source, y_source+1])
if Table.is_move_possible(x_source, y_source, x_source, y_source-1, Table.get_max_width(), Table.get_max_height()):
result.append([x_source, y_source-1])

return result


def a_star_search(board, src, dest):
open_list = [(src, 0)]
closed_list = []
came_from = {}
cost_dict = {(src[0], src[1]): 0}

while open_list:
current, cost = open_list.pop(0)

if current in dest:
# Trace our path
my_path = [current]
while (current[0], current[1]) in came_from:
current = came_from[(current[0], current[1])]
my_path.append(current)

my_path.reverse()
return my_path

closed_list.append(current)

for neighbor in Table.get_neighbors(current):
new_cost = cost + Table.get_move_cost(Table.get_type_of_terrain_from_pos(
current[0], current[1]), Table.get_type_of_terrain_from_pos(neighbor[0], neighbor[1]))

if (neighbor[0], neighbor[1]) not in cost_dict or new_cost < cost_dict[(neighbor[0], neighbor[1])]:
cost_dict[(neighbor[0], neighbor[1])] = new_cost
open_list.append((neighbor, new_cost))
came_from[(neighbor[0], neighbor[1])] = current

# sort by cost
open_list.sort(key=lambda x: x[1])

# if not found
return float('inf')


def get_direction(point1, point2):
"""Convert a move to a direction"""
x1, y1 = point1
x2, y2 = point2

if x2 == x1+1:
return "R"
elif x2 == x1-1:
return "L"
elif y2 == y1 + 1:
return "D"
elif y2 == y1-1:
return "U"
return None


if __name__ == "__main__":
address = "http://94.237.58.148:39120"
for i in range(0, 100):
updateboard(address)
x_src = Table.get_current_x()
y_src = Table.get_current_y()
dest = Table.get_dest()
path = a_star_search(board, [x_src, y_src], dest)
directions = []
for i in range(len(path)-1):
directions.append(get_direction(path[i], path[i+1]))

for direction in directions:
resp = requests.post(
f"{address}/update", json={"direction": direction}, timeout=10)
# Check if "flag" or "HTB" are present in resp.text
if "flag" in resp.text or "HTB" in resp.text:
print(f"Got Flag: {resp.text}")
sys.exit(1)
else:
print(f"Pos updated: {resp.text}")

After running the script, we finally got our flag🚩🎉

Jacopo’s conclusion: Using Dijkstra’s algorithm in Python was rewarding, demonstrating the power of algorithms in solving complex problems and testing my programming and problem-solving skills.

I’ve made some small corrections, like checking if we did not have the “map.json” file before accessing it and the flag-capture event.

Conclusion

The HTB Cyber Apocalypse 2024 Misc challenges presented a mix of difficulty levels, from easy to hard, each requiring a different approach and problem-solving strategy.

🔍 For the “Character” challenge, automation played a key role in efficiently obtaining the flag. While manual interaction was possible, automating the process not only saved time but also provided a foundational understanding of the category, setting a precedent for future challenges.

⚙️ The “Stop Drop and Roll” challenge showcased the importance of strategic thinking and improved our knowledge of automation since it was now essential.

💡 In the hard challenge, “Path of Survival,” the application of Dijkstra’s algorithm proved to be a game-changer. By efficiently navigating through terrain tiles and optimizing moves, participants could reach the weapon before time ran out, demonstrating the power of algorithms in solving complex problems.

Overall, the Misc challenges in the HTB Cyber Apocalypse 2024 event tested participants’ creativity, adaptability, and problem-solving skills across different cybersecurity domains. Whether through automation, strategic thinking, or algorithmic optimization, each challenge provided a unique opportunity for learning and skill development.
Collaboration and knowledge sharing remain vital in our ongoing battle against “the Fray”, so I am inviting you to provide your resoning for these challenges.

Stay tuned for more in-depth blue team content, and until then, happy defending! 💙

--

--

Lightfoe

I'm a cybersecurity analyst that love to explore this field through challenges and ctfs!