← Back to Writeups
CUSTOMMediumCrypto

Protocol Analysis 8: Reflection

XESXOR9/10/20265 min read
#crypto#custom#medium#identity_reflection#nonce_reuse_abuse#reflection_attack#role_confusion#signature_relay

Protocol Analysis 8: Reflection

Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: identity_reflection, nonce_reuse_abuse, reflection_attack, role_confusion, signature_relay

Summary

Walkthrough of the CUSTOM Crypto Protocol Analysis 8: Reflection. Task: analyze a certificate-based Alice/Bob protocol exposed over HTTP and recover Bob's flag. Solution: exploit a reflection and role-confusion flaw by feeding Bob's identity and nonce back through A


title: "Protocol Analysis 8: Reflection" id: 20260410_metactf_protocol_analysis_8_reflection category: crypto difficulty: medium event: metactf tags: ["rsa", "authentication", "certificate", "protocol_analysis", "dawgctf", "nonce_exchange", "signature_relay", "reflection_attack", "role_confusion"] techniques: ["identity_reflection", "nonce_reuse_abuse", "reflection_attack", "role_confusion", "signature_relay"] tools: ["python3", "requests"] indicators: ["both parties start in the same protocol role and send symmetric opener messages", "Bob reveals a nonce and signature before receiving Alice's final proof", "Alice accepts Bob's certified identity as the peer in her recv step", "util/asym_verify only succeeds over literal typed text such as t:n:bob|d:<nB>|d:<nA2>", "Bob accepts Alice's signature only when the signed identity field is bob rather than alice"]

Protocol Analysis 8: Reflection — metactf

Description

Bob and Alice are talking again, but this time they messed up and are both in the same roles of the protocol. Bob still has a flag though, can you get it?

English summary: the challenge provides a live Alice/Bob authentication protocol at https://protocols.live plus a PDF transcript. We need to drive both state machines, understand what each side actually signs, and make Bob accept a forged-looking but valid reflected authentication flow.

Analysis

The PDF excerpt says both Alice and Bob begin by sending their own public key, identity, and certificate:

text
Alice: send pubA, A, certA
Bob:   send pubB, B, certB

Then Bob expects:

text
pubX, X, certX, nX1

and responds with:

text
nA, {X, nX1, nA}priv

Later Alice expects a similar structure and signs another tuple, after which Bob checks the final message and releases the flag.

On the live service, the interaction details are:

  • POST /model/8 creates a fresh instance and returns conn_id.
  • POST /alice and POST /bob are stateful endpoints that must share that same conn_id.
  • Sending JSON {"conn_id": ..., "content": ""} triggers the participant's initial send.
  • Messages are pipe-delimited typed fields such as k:<hex>, n:<name>, d:<hex>, t:<text>.

Observed openers:

text
Alice: k:<pubA>|n:alice|d:<certA>
Bob:   k:<pubB>|n:bob|d:<certB>

If we send Bob Alice's opener plus our own chosen nonce n1:

text
k:<pubA>|n:alice|d:<certA>|d:<n1>

Bob replies with two data fields:

text
d:<nB>|d:<sigB>

At this point the obvious idea is to reflect Alice's identity back to Alice. That fails. The important implementation quirk came from testing /util/asym_verify: Alice's reflected signature is not accepted by Bob unless the signed literal text uses Bob's identity in the first field.

The winning message to Alice is therefore:

text
k:<pubB>|n:bob|d:<certB>|d:<nB>

Alice answers with:

text
d:<nA2>|d:<sigA>

Verification probes showed that sigA is over the literal text:

text
t:n:bob|d:<nB>|d:<nA2>

not the PDF-looking variant with alice in the first field. Bob then accepts the final relay:

text
d:<nA2>|d:<sigA>

and returns:

text
t:DawgCTF{REDACTED}

So the core bug is a reflection / role-confusion vulnerability. Because both parties operate in the same role family, Bob can be tricked into accepting Alice's valid signature on a tuple containing Bob's own identity and Bob's own nonce, even though that signature was produced in a different peer context.

Solution

  1. Create a fresh protocol instance with POST /model/8.
  2. Trigger Alice and Bob with empty content to collect their opener messages.
  3. Send Bob Alice's opener plus an attacker-chosen nonce n1.
  4. Parse Bob's response to recover Bob's nonce nB.
  5. Send Alice Bob's opener plus Bob's nonce nB.
  6. Alice returns nA2 and a signature over the literal text t:n:bob|d:<nB>|d:<nA2>.
  7. Relay d:<nA2>|d:<sigA> to Bob.
  8. Bob accepts and returns the flag.

The main trap is step 5: reflecting Alice's own identity back to Alice looks natural from the PDF, but the real service only works when the identity field is bob. This is exactly what makes the exploit a role-confusion bug rather than a simple nonce reflection.

python
#!/usr/bin/env python3
import json
import os
import time

import requests


BASE = "https://protocols.live"
TIMEOUT = 60


def raw_post(path, body):
    try:
        return requests.post(
            f"{BASE}{path}",
            headers={"Content-Type": "application/json"},
            data=json.dumps(body),
            timeout=TIMEOUT,
        )
    except requests.exceptions.RequestException:
        return None


def stateful(path, conn_id, content, retries=15):
    for _ in range(retries):
        r = raw_post(path, {"conn_id": conn_id, "content": content})
        if r is None:
            time.sleep(2)
            continue
        if r.status_code == 200:
            return r.json()["content"]
        if "DB access error" in r.text or r.status_code >= 500:
            time.sleep(2)
            continue
        raise RuntimeError(f"{path} failed: HTTP {r.status_code} {r.text}")
    raise RuntimeError(f"{path} retries exhausted")


def new_conn(retries=10):
    for _ in range(retries):
        r = raw_post("/model/8", {})
        if r is not None and r.status_code == 200:
            return r.json()["conn_id"]
        time.sleep(2)
    raise RuntimeError("failed to create conn_id")


def parse(content):
    return [tuple(x.split(":", 1)) for x in content.split("|")]


def first(items, t):
    return next(v for tt, v in items if tt == t)


def all_of(items, t):
    return [v for tt, v in items if tt == t]


def attempt():
    conn = new_conn()
    time.sleep(2)

    a0 = stateful("/alice", conn, "")
    pa0 = parse(a0)
    pub_a = first(pa0, "k")
    name_a = first(pa0, "n")
    cert_a = first(pa0, "d")
    print(f"[+] alice opener: {a0}")

    b0 = stateful("/bob", conn, "")
    pb0 = parse(b0)
    pub_b = first(pb0, "k")
    name_b = first(pb0, "n")
    cert_b = first(pb0, "d")
    print(f"[+] bob opener:   {b0}")

    n1 = os.urandom(32).hex()
    b1 = stateful("/bob", conn, f"k:{pub_a}|n:{name_a}|d:{cert_a}|d:{n1}")
    print(f"[+] bob challenge: {b1}")
    pb1 = parse(b1)
    n_b, sig_b = all_of(pb1, "d")
    print(f"[+] bob nonce: {n_b}")
    print(f"[+] bob sig:   {sig_b[:32]}...")

    a1 = stateful("/alice", conn, f"k:{pub_b}|n:{name_b}|d:{cert_b}|d:{n_b}")
    print(f"[+] alice reflected reply: {a1}")
    pa1 = parse(a1)
    n_a2, sig_a = all_of(pa1, "d")
    print(f"[+] reflected nonce from alice: {n_a2}")
    print(f"[+] reflected sig from alice:   {sig_a[:32]}...")

    out = stateful("/bob", conn, f"d:{n_a2}|d:{sig_a}")
    print(f"[+] final bob response: {out}")
    return first(parse(out), "t")


def main():
    for i in range(8):
        print(f"=== Attempt {i + 1} ===")
        try:
            flag = attempt()
            print(f"FLAG: {flag}")
            return
        except Exception as e:
            print(f"[!] retrying after error: {e}")
            time.sleep(3)
    raise SystemExit("failed")


if __name__ == "__main__":
    main()

Key Takeaways / Lessons

  • Mastering identity_reflection, nonce_reuse_abuse, reflection_attack, role_confusion, signature_relay was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-10 • CUSTOM • Protocol Analysis 8: Reflection • Difficulty: Medium