HackTheBox Path of Survival Challenge
https://app.hackthebox.com/challenges/671
Description
Far off in the distance, you hear a howl. Your blood runs cold as you realise the Kara’ka-ran have been released - vicious animals tortured beyond all recognition, starved to provide a natural savagery. They will chase you until the ends of the earth; your only chance of survival lies in a fight. Strong but not stupid, they will back off if they see you take down some of their number - briefly, anyway…
Exploitation
#!/usr/bin/env python3
from collections import deque
from pwn import log, sys
import requests
import math
import ast
if len(sys.argv) != 2 or ':' not in sys.argv[1]:
exit(f'Usage: python {sys.argv[0]} <ip:port>')
URL = f'http://{sys.argv[1]}'
terrain_costs = {
'PM': 5, 'MP': 2,
'PS': 2, 'SP': 2,
'PR': 5, 'RP': 5,
'MS': 5, 'SM': 7,
'MR': 8, 'RM': 10,
'SR': 8, 'RS': 6,
'PP': 1, 'MM': 1,
'SS': 1, 'RR': 1,
'CC': 1, 'GG': 1,
'PC': 1, 'CP': 1,
'MC': 1, 'CM': 1,
'SC': 1, 'CS': 1,
'RC': 1, 'CR': 1,
'GC': 1, 'CG': 1,
'PG': 1, 'GP': 1,
'MG': 1, 'GM': 1,
'SG': 1, 'GS': 1,
'RG': 1, 'GR': 1,
}
DIRECTIONS = {-1: 'L', 1: 'R', -1j: 'U', 1j: 'D'}
def get_map():
map_data = requests.post(f'{URL}/map').json()
time = map_data.get('player').get('time')
orig_coords = tuple(map_data.get('player').get('position'))
orig = orig_coords[0] + 1j * orig_coords[1]
weapons = set()
map_tiles = {}
for coord, tile_data in map_data.get('tiles').items():
if tile_data.get('has_weapon'):
dest_coords = ast.literal_eval(coord)
weapons.add(dest_coords[0] + 1j * dest_coords[1])
x, y = ast.literal_eval(coord)
map_tiles[x + 1j * y] = tile_data.get('terrain')
return map_tiles, orig, weapons, time
def update(direction):
return requests.post(f'{URL}/update', json={'direction': direction}).json()
def regenerate():
requests.get(f'{URL}/regenerate')
def bfs(root, map_tiles):
queue = deque([root])
visited_states = {(root, time)}
while len(queue):
pos, time_left, path = queue.popleft()
if time_left < 0:
continue
if (current_tile := map_tiles.get(pos)) is None:
continue
next_pos = [pos - 1, pos + 1, pos - 1j, pos + 1j]
for n in next_pos:
tile = map_tiles.get(n, 'E')
if tile == 'E':
continue
if tile == 'G' and n - pos in {1, 1j}:
continue
if tile == 'C' and n - pos in {-1, -1j}:
continue
new_time = time_left - terrain_costs.get(current_tile + tile, math.inf)
if new_time >= 0:
if n in weapons:
return path + (n, ), time_left
if (n, new_time) not in visited_states:
queue.append((n, new_time, path + (n, )))
visited_states.add((n, new_time))
return (), 0
regenerate()
rounds = 1
round_prog = log.progress('Round')
while rounds <= 100:
map_tiles, orig, weapons, time = get_map()
root = (orig, time, (orig, ))
path, time_left = bfs(root, map_tiles)
round_prog.status(f'{rounds} / 100')
if not path and not time_left:
regenerate()
round_prog.failure('No path found')
round_prog = log.progress('Round')
rounds = 1
continue
path_tiles = list(map(map_tiles.get, path))
prev = orig
for coord in path[1:]:
direction = DIRECTIONS.get(coord - prev, '?')
data = update(direction)
prev = coord
if data.get('error'):
round_prog.failure(data)
round_prog = log.progress('Round')
rounds = 1
break
if (flag := data.get('flag')):
log.success(f'Flag: {flag}')
round_prog.success('100 / 100')
else:
rounds += 1
Summary
Path of Survival: reduce the custom rules to a scriptable check and use the smallest reliable path to the flag.