Skip to content

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

BACK TO INTEL
CryptoMedium

Bingo

CTF writeup for Bingo from TSGctf

>Bingo

Flag format: TSGCTF{...}


>TL;DR

A logic bug in the server lets you get the flag simply by sending the message Get Flag. (hex: 47657420466c61672e) followed by any integer as the "signature". The server prints "Signature verification failed." but still continues to check the message and prints the flag when the message starts with Get Flag..

I provide a short explanation, the exact steps I used locally and remotely, and two tiny solver scripts (exploit_local.py and exploit_remote.py).


>Files provided

  • server.py — the challenge server (Python)

  • Dockerfile, compose.yaml, and requirements.txt — how the service was packaged


>Static analysis / understanding the server

Important excerpts from server.py (simplified):

python

# Hash function (custom)

def cvhp_hash(message_bytes):

    m = bytes_to_long(message_bytes)

    m_1 = m % (hash_p - 1)

    m_2 = m // (hash_p - 1)

    h = pow(alpha, m_1, hash_p) * pow(beta, m_2, hash_p) % hash_p

    return h

  

# Signature verification (RSA-like check)

def verify(message_bytes, signature, N, e):

    h = cvhp_hash(message_bytes)

    if pow(signature, e, N) != h:

        return False

    return True

  

# Check message and reveal flag if message starts with b"Get Flag."

def check_message(message_bytes, signature, N, e):

    if not verify(message_bytes, signature, N, e):

        print("Signature verification failed.")

  

    if message_bytes.startswith(b"Get Flag."):

        print(FLAG)

    else:

        print("We have nothing to give you.")

Why this is exploitable

  • The verification returns False for incorrect signatures and prints a warning, but it does not terminate the flow. The code always proceeds to the next if which checks whether the message begins with b"Get Flag." and prints FLAG if so.

  • Therefore an attacker does not need to compute any valid signature — sending any integer (e.g. 0) as the signature is sufficient.

This is purely a logic/flow bug (not a complicated crypto break), and can be exploited trivially.


>Local exploitation

Steps I used locally:

  1. Extract the archive and inspect server.py (already done).

  2. Run the server locally and provide inputs.

Example (manual):

$ python3 server.py

N = ...

e = 65537

hash_p = ...

alpha = ...

beta = ...

input message (hex): 47657420466c61672e

input signature (int): 0

Signature verification failed.

TSGCTF{THIS_IS_FAKE}

Explanation: The program printed Signature verification failed. (because 0^e mod N != H) and then printed the flag because the message started with Get Flag..

Local helper script

I included exploit_local.py in the repo (in the same folder). It spawns server.py, sends the payload and prints server output.

Usage:

$ python3 exploit_local.py

It prints the server's startup info and then the flag.


>Remote exploitation

Because the behavior is the same over the network, we can connect to the remote service and send the same message hex + any signature pair.

The exact message we sent (hex for Get Flag.):

  • 47657420466c61672e (this is ASCII Get Flag. in hex)

  • Signature: 0 (an arbitrary integer)

I tested with a short Python client (see exploit_remote.py) that connects and sends these two inputs.

Remote terminal output (captured):

N = 94788024375365760853147670634494760403756975343917897489131981615355733465218461171045... e = 65537 hash_p = 15296403113196288063295238350364867242230645195171379905624... alpha = 1438923097156689521297368618465847410950034118726488957525259... beta = 1359000061564595656493009060536669352970860547185611396242556... input message (hex): input signature (int): Signature verification failed. TSGCTF{+he|23_was_4_f4rm3|2_had_a_|)o6_a|^_||)_8i|^_|6o_wa5_his_nam3}

That final line above is the real flag returned by the remote service.


>All solver code

I included both scripts in the repository so you can reproduce the exploit easily.

exploit_remote.py

python

#!/usr/bin/env python3

import socket, sys

  

host = sys.argv[1] if len(sys.argv) > 1 else '35.194.98.181'

port = int(sys.argv[2]) if len(sys.argv) > 2 else 10961

  

payload = bytes.fromhex('47657420466c61672e') + b'\n'  # "Get Flag."

sig = b'0\n'  # any integer

  

with socket.create_connection((host, port), timeout=10) as s:

    s.sendall(payload + sig)

    data = b''

    while True:

        try:

            chunk = s.recv(4096)

            if not chunk:

                break

            data += chunk

        except socket.timeout:

            break

  

print(data.decode(errors='replace'))

Usage:

$ python3 exploit_remote.py

Or provide host/port: python3 exploit_remote.py 35.194.98.181 10961.


exploit_local.py

python

#!/usr/bin/env python3

import subprocess, time

  

# Start server.py locally (it prints N,e,hash_p,alpha,beta first)

p = subprocess.Popen(['python3', 'server.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

  

time.sleep(0.1)

# Send hex for "Get Flag." and an arbitrary integer for the signature

payload = '47657420466c61672e\n0\n'

out, _ = p.communicate(input=payload.encode(), timeout=5)

print(out.decode(errors='replace'))

Usage:

$ python3 exploit_local.py


>Fixes and lessons learned

  • The correct behavior after verify(...) fails is to stop the request (e.g., return or exit) and not proceed to reveal sensitive information.

  • Avoid printing flags or secret values under an if that is reachable regardless of verification outcome.

Suggested patch (simple):

python

if not verify(...):

    print("Signature verification failed.")

    return  # <-- important, stop here

>References


>Conclusion

This challenge was a classic logic/flow vulnerability: the cryptography looked complicated at first (custom hash, RSA-style verification) but the actual flaw was trivial once you read the code carefully. Sending Get Flag. and any integer as signature is enough to retrieve the flag.