$ cat writeup.md…
$ cat writeup.md…
metactf
Task: a DawgCTF protocol analysis challenge where Alice is supposed to tell Bob to encrypt the flag under her public key, but Bob's recv template accepts any public key alongside Alice's name. Solution: generate our own asymmetric key pair, send Bob the hello message with our public key substituted for Alice's, then decrypt the returned ciphertext with our private key using the service's asym_decrypt helper.
Alice and Bob are using this cool new thing called asymmetric cryptography! Alice isn't gonna leak her private key, so how could anyone intercept the flag?
Source: https://github.com/UMBCCyberDawgs/dawgctf-sp26/blob/main/Protocol%20Analysis%20(1-9)/Protocol_Analysis_chals.pdf
Challenge 5 of the DawgCTF Protocol Analysis series. All Protocol Analysis tasks share the same PDF manual and live server at https://protocols.live.
From the manual, Challenge 5 is specified as:
Protocol key: A = alice B = bob X = any name Alice Bob send: "Hello",B,"this is",A,"send the flag encrypted with this asymmetric key",pubA recv: "Hello",B,"this is",A,"send the flag encrypted with this asymmetric key",pubX send: "here it is",{[FLAG]}pubX recv: "here it is",{[FLAG]}pubA
The critical notation detail is X = any name in the protocol key. Bob's recv template uses pub_X where the X subscript means "any public key is accepted". Bob will then use that same key in his send to encrypt the flag, so whoever supplies the key also gets to decrypt what Bob sends back.
https://protocols.live.POST /model/5 creates a fresh instance and returns {"conn_id": ...}.POST /alice and POST /bob take {"conn_id": ..., "content": ...} and advance the corresponding participant's script./util/ ignore conn_id and provide cryptographic helpers:
/util/gen_asym_key_pair returns a fresh public and private key./util/asym_decrypt takes k:<priv>|d:<ciphertext> and returns plaintext.content field is a pipe-separated list of typed items type:value. Types are t (text), n (name), k (key hex), d (data / hex blob).There is no binding between Alice's name and her public key. The protocol transmits pubA inline, with no certificate, no signature, and no prior trusted distribution. Bob's recv template only checks:
Hellobobthis isalicesend the flag encrypted under this asymetric key (note the server's typo — single m)Nothing ties the key to Alice's identity, and Bob will encrypt the flag under whatever key was provided. This is the classic "public key substitution" weakness that certificates (cert_E) solve in later challenges of the series.
Because step 6 accepts any key, we can act as the network adversary, supply our own public key, and let Bob encrypt the flag under a key we control. Alice's private key is never touched — the challenge text about Alice "not leaking her private key" is a red herring.
Steps:
/util/gen_asym_key_pair to produce (pubX, privX).POST /model/5.pubX substituted for pubA:
t:Hello|n:bob|t:this is|n:alice|t:send the flag encrypted under this asymetric key|k:<pubX>POST /bob with that content. Bob replies with t:here it is|d:<{FLAG}pubX>.privX to /util/asym_decrypt to recover the flag plaintext.No interaction with /alice is required at all — we know Bob's exact expected input from the protocol definition, and we never need Alice's key material.
#!/usr/bin/env python3 """Solve DawgCTF Protocol Analysis 5: Is This Real?""" import json import sys import requests BASE = "https://protocols.live" def post(path, conn_id, content): r = requests.post( f"{BASE}{path}", headers={"Content-Type": "application/json"}, data=json.dumps({"conn_id": conn_id, "content": content}), ) if r.status_code != 200: sys.exit(f"Request failed: {r.status_code} {r.text}") return r.json()["content"] def util(name, content): return post(f"/util/{name}", 0, content) def parse_content(content): return [tuple(item.split(":", 1)) for item in content.split("|")] def main(): # 1. Generate our own RSA keypair (attacker-controlled) kp = parse_content(util("gen_asym_key_pair", "")) pub_x = kp[1][1] # first k: priv_x = kp[3][1] # second k: # 2. Fresh protocol instance conn = requests.post(f"{BASE}/model/5").json()["conn_id"] # 3. Craft Bob's expected hello with OUR public key in place of Alice's mitm = ( "t:Hello|n:bob|t:this is|n:alice|" "t:send the flag encrypted under this asymetric key|" f"k:{pub_x}" ) # 4. Bob encrypts the flag under pub_x and returns it bob_resp = post("/bob", conn, mitm) enc_flag = dict(parse_content(bob_resp))["d"] # 5. Decrypt with our private key via the utility oracle dec = util("asym_decrypt", f"k:{priv_x}|d:{enc_flag}") print("FLAG =", dict(parse_content(dec))["t"]) if __name__ == "__main__": main()
Example run:
[+] pubX = 30818902818100c18c2ee92eb31ad31e788244edf53e108c99c8d998d473... [+] privX = 30820276020100300d06092a864886f70d0101010500048202603082025c... [+] conn_id = 14441278294909 [+] MITM -> Bob [+] Bob -> t:here it is|d:6facd7a293974343775351b345826b5c8a59bb052913e52f56a8336cb00cfefdd... [+] FLAG = DawgCTF{REDACTED}
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar