//Dynamic Paths
Challenge: Dynamic Paths (Category: Coding)
Remote service: nc 94.237.51.6 40726
Flag format: HTB{...}
>Summary
On connecting to the remote service you are presented with 100 grid puzzles. For each grid you start at the top-left cell and may move only right or down. Each cell contains a positive integer cost. The goal is to output the minimal sum of numbers along a path from the top-left to the bottom-right.
I automated the solving by writing a small Python client that parses the remote service stream, extracts each grid (dimensions and numbers), computes the minimal path sum using dynamic programming, and replies with the answer. After solving all 100 tests the service returned the flag.
Final flag:
HTB{b3h3M07H_5h0uld_H4v3_57ud13D_dYM4m1C_pr09r4mm1n9_70_C47ch_y0u_68024634f6fdfd1b42c230d19b5fc668}
>Plan / Approach
-
Connect to remote TCP service.
-
Read initial explanatory text (skip the example problem in the prompt).
-
Wait for the "Test X/Y" header which signals real tests are starting.
-
For each test, parse a dimensions line: two integers
rows cols. -
Read rows*cols integers (they can be laid out across multiple whitespace-separated lines) and build the grid.
-
Compute minimal path sum using a standard DP (O(rowscols) time, O(rowscols) memory).
-
Send the numeric answer followed by a newline.
-
Repeat until the remote sends the final flag.
Edge cases handled
-
The numeric grid data can be split across many lines; the parser accumulates tokens across incoming chunks.
-
Example problem(s) at the top of the prompt are ignored by waiting for the explicit
Test x/yheader. -
Large grids up to 100x100 handled by DP efficiently.
>Files I created
-
001_setup_virtualenv.sh— tiny helper to create a Python virtualenv. -
002_connect_remote.py— interactive TCP client for manual exploration. -
003_auto_solver.py— automated solver that solved all 100 tests and obtained the flag. -
004_writeup_Dynamic_Paths.md— this writeup.
>Full code listings
Below are the exact source files used (copied verbatim).
001_setup_virtualenv.sh
#!/usr/bin/env bash
# 001_setup_virtualenv.sh
# Creates a Python virtualenv in .venv and installs requirements.txt if present.
set -euo pipefail
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
echo "✅ Virtualenv .venv created and activated. Use 'source .venv/bin/activate' to activate."
002_connect_remote.py
#!/usr/bin/env python3
# 002_connect_remote.py
# Connect to the remote CTF service and provide a simple interactive client.
# Shows simple progress messages and forwards stdin/stdout to the socket.
import socket
import sys
import threading
import time
HOST = '94.237.51.6'
PORT = 40726
def receive_loop(s):
try:
while True:
data = s.recv(4096)
if not data:
print('\n⚠️ Connection closed by remote')
break
sys.stdout.write(data.decode(errors='replace'))
sys.stdout.flush()
except Exception as e:
print('\n❌ receive error:', e)
if __name__ == '__main__':
print('🔌 Attempting to connect to', HOST, PORT)
try:
s = socket.create_connection((HOST, PORT), timeout=10)
except Exception as e:
print('❌ Could not connect:', e)
sys.exit(1)
print('✅ Connected. Starting interactive session. Type Ctrl-C to exit.')
t = threading.Thread(target=receive_loop, args=(s,), daemon=True)
t.start()
try:
while True:
line = sys.stdin.readline()
if not line:
break
s.sendall(line.encode())
except KeyboardInterrupt:
print('\n✋ Exiting...')
finally:
s.close()
print('👋 Goodbye')
003_auto_solver.py
#!/usr/bin/env python3
"""
003_auto_solver.py
Automated solver for the "Dynamic Paths" challenge.
Connects to the remote service, parses grid descriptions from the text stream,
computes minimal path sum (only down/right moves), and sends answers back.
Shows real-time progress with emojis and separators.
"""
import socket
import re
import sys
import time
HOST = '94.237.51.6'
PORT = 40726
BUFFER_SIZE = 4096
# Regex to detect a line with two integers (dimensions)
dim_line_re = re.compile(r'^\s*(\d{1,3})\s+(\d{1,3})\s*$')
int_token_re = re.compile(r'-?\d+')
def compute_min_path(grid, rows, cols):
# classic DP in-place on grid copy
dp = [[0]*cols for _ in range(rows)]
dp[0][0] = grid[0][0]
for j in range(1, cols):
dp[0][j] = dp[0][j-1] + grid[0][j]
for i in range(1, rows):
dp[i][0] = dp[i-1][0] + grid[i][0]
for i in range(1, rows):
for j in range(1, cols):
a = dp[i-1][j]
b = dp[i][j-1]
dp[i][j] = grid[i][j] + (a if a < b else b)
return dp[rows-1][cols-1]
class StreamParser:
def __init__(self):
self.lines = []
self.partial = ''
self.tests_done = 0
self.tests_started = False
def feed(self, data):
# Append incoming bytes (decoded) to partial and split into lines
s = data.decode(errors='replace')
self.partial += s
if '\n' in self.partial:
parts = self.partial.split('\n')
self.lines.extend(parts[:-1])
self.partial = parts[-1]
# Detect the beginning of the real tests to avoid answering the example
if not self.tests_started:
for idx, l in enumerate(self.lines):
if re.search(r"Test\s*\d+\s*/\s*\d+", l):
# drop everything up to and including this header line
self.tests_started = True
self.lines = self.lines[idx+1:]
# clear partial to avoid leftover example tokens
self.partial = ''
break
def next_test(self):
# Only process dims once the real tests have started (skip example)
if not self.tests_started:
return None
# Try to find a dims line among buffered lines. If found, consume it and
# collect next rows*cols integers (possibly across multiple following lines).
# Returns (rows, cols, grid) or None if not enough data yet.
# First, scan lines for a dims line
for idx, line in enumerate(self.lines):
m = dim_line_re.match(line)
if m:
rows = int(m.group(1))
cols = int(m.group(2))
# Remove lines up to and including idx
remaining_lines = self.lines[idx+1:]
# Gather integer tokens from remaining_lines and partial
tokens = []
for l in remaining_lines:
tokens.extend(int_token_re.findall(l))
tokens.extend(int_token_re.findall(self.partial))
need = rows*cols
if len(tokens) < need:
return None # wait for more data
# consume required tokens and update self.lines/self.partial
needed_tokens = tokens[:need]
# Now reconstruct what remains after consuming those tokens
# We'll rebuild buffer by joining remaining tokens back into text
# Easiest is to consume the lines: we will remove all lines up to idx,
# then we need to compute how many full lines we consumed from remaining_lines
consumed_tokens = 0
new_remaining = []
for l in remaining_lines:
toks = int_token_re.findall(l)
if consumed_tokens + len(toks) <= need:
consumed_tokens += len(toks)
continue
else:
# this line contains the split point
# remove the tokens consumed from this line
remain_in_line = toks[need - consumed_tokens:]
# rebuild the remainder of the line by finding their positions is complex; instead,
# put back the tokens as a simple space-joined string (parser is lenient)
new_remaining.append(' '.join(remain_in_line))
# append the rest of the following lines unchanged
idx_after = remaining_lines.index(l) + 1
new_remaining.extend(remaining_lines[idx_after:])
break
else:
# consumed all remaining_lines, now we need to consume from partial
# compute tokens left in partial after consumption
partial_tokens = int_token_re.findall(self.partial)
rest_tokens = partial_tokens[need - consumed_tokens:]
self.partial = ' '.join(rest_tokens)
new_remaining = []
# replace self.lines with lines before idx (already removed) plus new_remaining
self.lines = new_remaining
# build grid
int_vals = list(map(int, needed_tokens))
grid = [int_vals[i*cols:(i+1)*cols] for i in range(rows)]
return rows, cols, grid
return None
def main():
print('🔎 Connecting to', HOST, PORT)
try:
s = socket.create_connection((HOST, PORT), timeout=10)
except Exception as e:
print('❌ Could not connect:', e)
return
print('✅ Connected — starting automated solver. Press Ctrl-C to stop.')
parser = StreamParser()
s.settimeout(0.5)
try:
while True:
try:
data = s.recv(BUFFER_SIZE)
if not data:
print('\n⚠️ Remote closed the connection')
break
sys.stdout.write(data.decode(errors='replace'))
sys.stdout.flush()
parser.feed(data)
# Try to extract as many tests as we can
while True:
nt = parser.next_test()
if not nt:
break
rows, cols, grid = nt
parser.tests_done += 1
print('\n' + '='*40)
print(f'🧭 Test {parser.tests_done}: dimensions {rows}x{cols} — computing...')
ans = compute_min_path(grid, rows, cols)
print(f'✅ Answer computed: {ans}')
# send answer
msg = str(ans) + '\n'
s.sendall(msg.encode())
print('📤 Sent answer to remote')
print('='*40 + '\n')
except socket.timeout:
# just continue to check for parser progress
continue
except KeyboardInterrupt:
print('\n✋ Interrupted by user')
finally:
try:
s.close()
except:
pass
print('👋 Connection closed — tests done:', parser.tests_done)
if __name__ == '__main__':
main()
>How to run (reproducible steps)
-
Make sure you have Python 3 installed.
-
Create the workspace files (they are already created in this repo).
-
(Optional) create and activate a Python virtualenv:
chmod +x 001_setup_virtualenv.sh
./001_setup_virtualenv.sh
source .venv/bin/activate
- Run the automated solver (this will show progress and print the flag at the end):
python3 003_auto_solver.py
- If you prefer manual exploration, run:
python3 002_connect_remote.py
Notes: the automated solver prints per-test progress and sends answers automatically. It was used to collect the flag.
>Implementation notes
-
The DP uses O(rows*cols) memory which is safe since constraints are <= 100 for each dimension.
-
I kept the parser robust to line breaks and split tokens by using regular expressions that find integers across lines.
-
The solver avoids replying to the example in the prompt by waiting until the remote prints a
Test X/Yheader.
>Transcript (short)
I connected to the remote service, solved 100 tests, and the remote returned:
You managed to traverse the maze of the underground and escape the behemoth. Here is your reward: HTB{b3h3M07H_5h0uld_H4v3_57ud13D_dYM4m1C_pr09r4mm1n9_70_C47ch_y0u_68024634f6fdfd1b42c230d19b5fc668}