Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
MiscMedium

Dynamic Paths

CTF writeup for Dynamic Paths from HTB CTF TRY OUT

//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

  1. Connect to remote TCP service.

  2. Read initial explanatory text (skip the example problem in the prompt).

  3. Wait for the "Test X/Y" header which signals real tests are starting.

  4. For each test, parse a dimensions line: two integers rows cols.

  5. Read rows*cols integers (they can be laid out across multiple whitespace-separated lines) and build the grid.

  6. Compute minimal path sum using a standard DP (O(rowscols) time, O(rowscols) memory).

  7. Send the numeric answer followed by a newline.

  8. 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/y header.

  • 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

bash

#!/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

python

#!/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

python

#!/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)

  1. Make sure you have Python 3 installed.

  2. Create the workspace files (they are already created in this repo).

  3. (Optional) create and activate a Python virtualenv:

bash

chmod +x 001_setup_virtualenv.sh

./001_setup_virtualenv.sh

source .venv/bin/activate
  1. Run the automated solver (this will show progress and print the flag at the end):
bash

python3 003_auto_solver.py
  1. If you prefer manual exploration, run:
bash

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/Y header.


>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}